@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.
package/dist/index.js CHANGED
@@ -2005,6 +2005,12 @@ function pointsToEighthPoints(points) {
2005
2005
  function inchesToTwips(inches) {
2006
2006
  return Math.round(inches * TWIPS_PER_INCH);
2007
2007
  }
2008
+ function inchesToEmu(inches) {
2009
+ return Math.round(inches * EMU_PER_INCH);
2010
+ }
2011
+ function pointsToEmu(points) {
2012
+ return Math.round(points * EMU_PER_INCH / 72);
2013
+ }
2008
2014
  function twipsToEmu(twips) {
2009
2015
  return Math.round(twips * EMU_PER_TWIP);
2010
2016
  }
@@ -2207,6 +2213,10 @@ function inlineChildren(children, resources = /* @__PURE__ */ new Map()) {
2207
2213
  case "shape":
2208
2214
  out.push(emitShape(child, resources));
2209
2215
  break;
2216
+ case "drawingGroup":
2217
+ throw new Error(
2218
+ "the docxjs renderer has no emitter for a drawing group; this document should have been refused by the capability check"
2219
+ );
2210
2220
  case "noteReference":
2211
2221
  out.push(
2212
2222
  child.noteKind === "endnote" ? new EndnoteReferenceRun(child.id) : new FootnoteReferenceRun(child.id)
@@ -2308,7 +2318,7 @@ function paragraphOptions(formatting) {
2308
2318
  const options = {};
2309
2319
  if (!formatting) return options;
2310
2320
  if (formatting.alignment) {
2311
- options.alignment = ALIGNMENT[formatting.alignment];
2321
+ options.alignment = ALIGNMENT2[formatting.alignment];
2312
2322
  }
2313
2323
  if (formatting.spacing) {
2314
2324
  const spacing = {};
@@ -2542,11 +2552,11 @@ function emitBorder(border3) {
2542
2552
  color: border3.color?.hex ?? "000000"
2543
2553
  };
2544
2554
  }
2545
- var ALIGNMENT, PAGE_FIELD, WRAP_TYPE, VERTICAL_ALIGN, TABLE_ANCHOR, HORIZONTAL_POSITION, VERTICAL_POSITION, BORDER_STYLE;
2555
+ var ALIGNMENT2, PAGE_FIELD, WRAP_TYPE, VERTICAL_ALIGN, TABLE_ANCHOR, HORIZONTAL_POSITION, VERTICAL_POSITION, BORDER_STYLE;
2546
2556
  var init_emit = __esm({
2547
2557
  "src/renderers/docxjs/emit.ts"() {
2548
2558
  "use strict";
2549
- ALIGNMENT = {
2559
+ ALIGNMENT2 = {
2550
2560
  left: AlignmentType.LEFT,
2551
2561
  center: AlignmentType.CENTER,
2552
2562
  right: AlignmentType.RIGHT,
@@ -2669,7 +2679,7 @@ function paragraphProperties(formatting) {
2669
2679
  out.spacing = spacing;
2670
2680
  }
2671
2681
  if (formatting.alignment !== void 0) {
2672
- out.alignment = ALIGNMENT[formatting.alignment] ?? formatting.alignment;
2682
+ out.alignment = ALIGNMENT2[formatting.alignment] ?? formatting.alignment;
2673
2683
  }
2674
2684
  if (formatting.keepNext !== void 0) out.keepNext = formatting.keepNext;
2675
2685
  if (formatting.keepLines !== void 0) out.keepLines = formatting.keepLines;
@@ -2798,6 +2808,15 @@ var init_features = __esm({
2798
2808
  "text-frames",
2799
2809
  /** Native shape text boxes. */
2800
2810
  "text-boxes",
2811
+ /**
2812
+ * A DrawingML group: shapes, text boxes and pictures sharing one child
2813
+ * coordinate space, drawn as a single anchored or inline object.
2814
+ *
2815
+ * Separate from `text-boxes` because a lone `wps:wsp` run is a far smaller
2816
+ * ask than `wpg:wgp` with child transforms, preset geometry and grouped
2817
+ * pictures — a backend can plausibly have the first and not the second.
2818
+ */
2819
+ "drawing-groups",
2801
2820
  /** A table-of-contents field. */
2802
2821
  "toc",
2803
2822
  /** TOC entries baked in so an unrefreshed reader still shows them. */
@@ -3044,7 +3063,7 @@ function numberingConfig(numbering) {
3044
3063
  level: level.level,
3045
3064
  format: level.format,
3046
3065
  text: level.text,
3047
- alignment: level.alignment ? ALIGNMENT[level.alignment] : void 0,
3066
+ alignment: level.alignment ? ALIGNMENT2[level.alignment] : void 0,
3048
3067
  ...level.suffix ? { suffix: level.suffix } : {},
3049
3068
  style: {
3050
3069
  ...level.indent ? {
@@ -3262,7 +3281,12 @@ var init_docxjs = __esm({
3262
3281
  "cached-fields",
3263
3282
  "shading",
3264
3283
  "borders",
3265
- "rtl"
3284
+ "rtl",
3285
+ // `drawing-groups` is the one exclusion that is a real backend gap rather
3286
+ // than a slice boundary: docx.js has no `wpg:wgp`. Leaving it out is what
3287
+ // turns a native `visual` sent to this backend into a named capability
3288
+ // error instead of a document with the graphic missing.
3289
+ "drawing-groups"
3266
3290
  ]);
3267
3291
  DOCXJS_CAPABILITIES = new Set(
3268
3292
  [...ALL_DOCX_FEATURES].filter((feature) => !NOT_YET_EMITTED.has(feature))
@@ -3373,15 +3397,15 @@ function inlineChildren2(children, ctx = emptyContext()) {
3373
3397
  out.push({ bookmarkEnd: { id: child.id } });
3374
3398
  break;
3375
3399
  case "image": {
3376
- const build = ctx.pictures.get(child.resourceId);
3377
- if (!build) {
3378
- throw new Error(
3379
- `no image was prepared for resource "${child.resourceId}"`
3380
- );
3381
- }
3382
3400
  const pending = breakOption();
3383
3401
  if (pending.break) out.push(pending);
3384
- out.push({ picture: build(child, ctx.nextDrawingId()) });
3402
+ out.push({ picture: pictureOptions(child, ctx) });
3403
+ break;
3404
+ }
3405
+ case "drawingGroup": {
3406
+ const pending = breakOption();
3407
+ if (pending.break) out.push(pending);
3408
+ out.push({ wpgGroup: drawingGroupOptions(child, ctx) });
3385
3409
  break;
3386
3410
  }
3387
3411
  case "hyperlink":
@@ -3476,6 +3500,166 @@ function floatingOptions2(floating) {
3476
3500
  zIndex: floating.zIndex
3477
3501
  };
3478
3502
  }
3503
+ function imageMedia(ctx, resourceId, placement) {
3504
+ const build = ctx.pictures.get(resourceId);
3505
+ if (!build) {
3506
+ throw new Error(`no image was prepared for resource "${resourceId}"`);
3507
+ }
3508
+ return build(placement);
3509
+ }
3510
+ function pictureOptions(image, ctx) {
3511
+ const media = imageMedia(ctx, image.resourceId, image);
3512
+ return {
3513
+ type: media.type,
3514
+ data: media.data,
3515
+ // No `fileName`: at run level the backend allocates one, and stating our
3516
+ // own would only fight it.
3517
+ ...media.fallback ? { fallback: media.fallback } : {},
3518
+ // Raw numbers are EMUs in @office-open/docx. Passing pixels here makes a
3519
+ // normal image only a few hundred EMUs wide, effectively a dot.
3520
+ transformation: { width: image.widthEmu, height: image.heightEmu },
3521
+ // The id is stated rather than left to the backend's process-global
3522
+ // counter. `name` stays empty, which is what it was before and what the
3523
+ // backend falls back to.
3524
+ altText: { id: String(ctx.nextDrawingId()) },
3525
+ ...image.floating ? { floating: floatingOptions2(image.floating) } : {}
3526
+ // No `description` or `title`: no DOCX this pipeline has produced carries
3527
+ // `wp:docPr` alt text, and the compiler warns so the gap is visible rather
3528
+ // than silent.
3529
+ };
3530
+ }
3531
+ function drawingGroupOptions(group, ctx) {
3532
+ const id = ctx.nextDrawingId();
3533
+ return {
3534
+ altText: {
3535
+ id: String(id),
3536
+ ...group.altText ? { description: group.altText } : {}
3537
+ },
3538
+ transformation: { width: group.widthEmu, height: group.heightEmu },
3539
+ childOffset: { x: 0, y: 0 },
3540
+ childExtent: { cx: group.canvasWidthEmu, cy: group.canvasHeightEmu },
3541
+ children: group.children.map((child) => groupChild(child, ctx)),
3542
+ ...group.floating ? { floating: floatingOptions2(group.floating) } : {}
3543
+ };
3544
+ }
3545
+ function groupChild(child, ctx) {
3546
+ return child.kind === "shape" ? groupShape(child, ctx) : groupPicture(child, ctx);
3547
+ }
3548
+ function groupShape(shape, ctx) {
3549
+ const id = ctx.nextDrawingId();
3550
+ return {
3551
+ type: "wps",
3552
+ transformation: childTransformation(shape.frame),
3553
+ data: {
3554
+ nonVisualProperties: {
3555
+ id,
3556
+ ...shape.name ? { name: shape.name } : {},
3557
+ // `txBox="1"` is how Word tells a text box from a shape that happens
3558
+ // to hold text, and it changes how the object behaves on selection.
3559
+ ...shape.isTextBox ? { textBox: "1" } : {}
3560
+ },
3561
+ presetGeometry: { preset: shape.geometry },
3562
+ ...shape.fill ? { fill: drawingFill(shape.fill) } : {},
3563
+ ...shape.outline ? { outline: drawingOutline(shape.outline) } : {},
3564
+ children: (shape.text?.paragraphs ?? []).map(
3565
+ (child) => paragraph(child, ctx)
3566
+ ),
3567
+ ...shape.text ? { bodyProperties: bodyProperties(shape.text) } : {}
3568
+ }
3569
+ };
3570
+ }
3571
+ function groupPicture(picture2, ctx) {
3572
+ const id = ctx.nextDrawingId();
3573
+ const media = imageMedia(ctx, picture2.resourceId, {
3574
+ widthEmu: picture2.frame.widthEmu,
3575
+ heightEmu: picture2.frame.heightEmu
3576
+ });
3577
+ return {
3578
+ type: media.type,
3579
+ data: media.data,
3580
+ fileName: media.fileName,
3581
+ ...media.fallback ? {
3582
+ fallback: {
3583
+ ...media.fallback,
3584
+ fileName: media.fallbackFileName
3585
+ }
3586
+ } : {},
3587
+ transformation: childTransformation(picture2.frame),
3588
+ nonVisualProperties: {
3589
+ id,
3590
+ ...picture2.name ? { name: picture2.name } : {},
3591
+ ...picture2.altText ? { description: picture2.altText } : {}
3592
+ },
3593
+ ...picture2.crop ? { sourceRectangle: sourceRectangle(picture2.crop) } : {}
3594
+ };
3595
+ }
3596
+ function childTransformation(frame) {
3597
+ const flip = {
3598
+ ...frame.flipHorizontal ? { horizontal: true } : {},
3599
+ ...frame.flipVertical ? { vertical: true } : {}
3600
+ };
3601
+ return {
3602
+ offset: {
3603
+ emus: { x: frame.xEmu, y: frame.yEmu },
3604
+ pixels: {
3605
+ x: Math.round(emuToPixels(frame.xEmu)),
3606
+ y: Math.round(emuToPixels(frame.yEmu))
3607
+ }
3608
+ },
3609
+ emus: { x: frame.widthEmu, y: frame.heightEmu },
3610
+ pixels: {
3611
+ x: Math.round(emuToPixels(frame.widthEmu)),
3612
+ y: Math.round(emuToPixels(frame.heightEmu))
3613
+ },
3614
+ // Degrees: the backend multiplies by 60000 on the way into `@rot`.
3615
+ ...frame.rotationDegrees !== void 0 ? { rotation: frame.rotationDegrees } : {},
3616
+ ...Object.keys(flip).length > 0 ? { flip } : {}
3617
+ };
3618
+ }
3619
+ function drawingFill(fill) {
3620
+ if (fill.kind === "none") return { type: "none" };
3621
+ return {
3622
+ type: "solid",
3623
+ color: {
3624
+ type: "rgb",
3625
+ value: fill.color.hex,
3626
+ // Transparency becomes an alpha transform on the colour. DrawingML
3627
+ // states *opacity*, so the value is the complement of what the author
3628
+ // wrote; the backend scales the percentage into `a:alpha`'s thousandths
3629
+ // itself, so it must be handed a plain percentage here.
3630
+ ...fill.transparencyPercent !== void 0 ? { transforms: { alpha: 100 - fill.transparencyPercent } } : {}
3631
+ }
3632
+ };
3633
+ }
3634
+ function drawingOutline(outline) {
3635
+ return {
3636
+ ...outline.widthEmu !== void 0 ? { width: outline.widthEmu } : {},
3637
+ ...outline.dash ? { dash: outline.dash } : {},
3638
+ ...outline.color ? { type: "solidFill", color: { value: outline.color.hex } } : {}
3639
+ };
3640
+ }
3641
+ function bodyProperties(text) {
3642
+ const insets = text.insetsEmu;
3643
+ return {
3644
+ ...text.anchor ? { anchor: BODY_ANCHOR[text.anchor] } : {},
3645
+ // `lIns`/`tIns`/`rIns`/`bIns` is the vocabulary `a:bodyPr` actually has;
3646
+ // the backend also accepts a `margins` object and folds it into the same
3647
+ // attributes.
3648
+ ...insets?.left !== void 0 ? { lIns: insets.left } : {},
3649
+ ...insets?.top !== void 0 ? { tIns: insets.top } : {},
3650
+ ...insets?.right !== void 0 ? { rIns: insets.right } : {},
3651
+ ...insets?.bottom !== void 0 ? { bIns: insets.bottom } : {}
3652
+ };
3653
+ }
3654
+ function sourceRectangle(crop) {
3655
+ const thousandths = (fraction) => Math.round(fraction * 1e5);
3656
+ return {
3657
+ ...crop.left !== void 0 ? { left: thousandths(crop.left) } : {},
3658
+ ...crop.top !== void 0 ? { top: thousandths(crop.top) } : {},
3659
+ ...crop.right !== void 0 ? { right: thousandths(crop.right) } : {},
3660
+ ...crop.bottom !== void 0 ? { bottom: thousandths(crop.bottom) } : {}
3661
+ };
3662
+ }
3479
3663
  function shapeOptions(shape, ctx) {
3480
3664
  const id = String(ctx.nextDrawingId());
3481
3665
  return {
@@ -3493,23 +3677,8 @@ function shapeOptions(shape, ctx) {
3493
3677
  color: { type: "rgb", value: shape.fill.hex }
3494
3678
  }
3495
3679
  } : {},
3496
- ...shape.outline ? {
3497
- outline: {
3498
- ...shape.outline.widthEmu !== void 0 ? { width: shape.outline.widthEmu } : {},
3499
- fill: {
3500
- type: "solid",
3501
- color: { type: "rgb", value: shape.outline.color.hex }
3502
- }
3503
- }
3504
- } : {},
3505
- ...shape.insetsEmu ? {
3506
- bodyProperties: {
3507
- ...shape.insetsEmu.top !== void 0 ? { topInset: shape.insetsEmu.top } : {},
3508
- ...shape.insetsEmu.bottom !== void 0 ? { bottomInset: shape.insetsEmu.bottom } : {},
3509
- ...shape.insetsEmu.left !== void 0 ? { leftInset: shape.insetsEmu.left } : {},
3510
- ...shape.insetsEmu.right !== void 0 ? { rightInset: shape.insetsEmu.right } : {}
3511
- }
3512
- } : {},
3680
+ ...shape.outline ? { outline: drawingOutline(shape.outline) } : {},
3681
+ ...shape.insetsEmu ? { bodyProperties: bodyProperties({ insetsEmu: shape.insetsEmu }) } : {},
3513
3682
  ...shape.floating ? { floating: floatingOptions2(shape.floating) } : {}
3514
3683
  };
3515
3684
  }
@@ -3866,7 +4035,7 @@ function numberingConfig2(numbering) {
3866
4035
  }))
3867
4036
  };
3868
4037
  }
3869
- var WRAP_TYPE2, TOC_PAGE_TAB_TWIPS;
4038
+ var WRAP_TYPE2, BODY_ANCHOR, TOC_PAGE_TAB_TWIPS;
3870
4039
  var init_emit2 = __esm({
3871
4040
  "src/renderers/office-open/emit.ts"() {
3872
4041
  "use strict";
@@ -3877,6 +4046,11 @@ var init_emit2 = __esm({
3877
4046
  tight: 2,
3878
4047
  topAndBottom: 3
3879
4048
  };
4049
+ BODY_ANCHOR = {
4050
+ top: "t",
4051
+ middle: "ctr",
4052
+ bottom: "b"
4053
+ };
3880
4054
  TOC_PAGE_TAB_TWIPS = 9025;
3881
4055
  }
3882
4056
  });
@@ -4054,18 +4228,9 @@ async function prepareImages2(ir) {
4054
4228
  index === 0 ? data : distinguishImageBytes(data, type, size)
4055
4229
  );
4056
4230
  });
4057
- resources.set(resource.id, (image, drawingId) => {
4058
- const rasterSize = {
4059
- width: emuToPixels(image.widthEmu),
4060
- height: emuToPixels(image.heightEmu)
4061
- };
4062
- const sizeKey = `${rasterSize.width}x${rasterSize.height}`;
4063
- const transformation = {
4064
- // Raw numbers are EMUs in @office-open/docx. Passing pixels here makes
4065
- // a normal image only a few hundred EMUs wide, effectively a dot.
4066
- width: image.widthEmu,
4067
- height: image.heightEmu
4068
- };
4231
+ resources.set(resource.id, (placement) => {
4232
+ const sizeKey = placementKey(placement);
4233
+ const stem = `${resource.id}-${sizeKey}`;
4069
4234
  return {
4070
4235
  type,
4071
4236
  // @office-open/docx stores a drawing transformation on its deduplicated
@@ -4073,29 +4238,28 @@ async function prepareImages2(ir) {
4073
4238
  // reuse the first size, so each distinct placement size gets equivalent
4074
4239
  // image bytes carrying a harmless format-native marker.
4075
4240
  data: placementData.get(sizeKey) ?? data,
4076
- transformation,
4077
- // The id is stated rather than left to the backend's process-global
4078
- // counter. `name` stays empty, which is what it was before and what
4079
- // the backend falls back to.
4080
- altText: { id: String(drawingId) },
4241
+ // Named after the resource and the size it is drawn at, so two
4242
+ // placements of one image at one size share a single part and a third
4243
+ // at another size gets its own.
4244
+ fileName: `${stem}.${type}`,
4081
4245
  ...type === "svg" ? {
4082
4246
  // Word before 2016 draws the fallback rather than the vector. A
4083
4247
  // raster that could not be produced falls back to the SVG bytes,
4084
4248
  // which is what this pipeline has always shipped.
4085
4249
  fallback: {
4086
4250
  type: "png",
4087
- data: rasters.get(`${image.resourceId}:${sizeKey}`) ?? data
4088
- }
4089
- } : {},
4090
- ...image.floating ? { floating: floatingOptions2(image.floating) } : {}
4091
- // No `description` or `title`: no DOCX this pipeline has produced
4092
- // carries `wp:docPr` alt text, and the compiler warns so the gap is
4093
- // visible rather than silent.
4251
+ data: rasters.get(`${resource.id}:${sizeKey}`) ?? data
4252
+ },
4253
+ fallbackFileName: `${stem}-fallback.png`
4254
+ } : {}
4094
4255
  };
4095
4256
  });
4096
4257
  }
4097
4258
  return resources;
4098
4259
  }
4260
+ function placementKey(placement) {
4261
+ return `${emuToPixels(placement.widthEmu)}x${emuToPixels(placement.heightEmu)}`;
4262
+ }
4099
4263
  function distinguishImageBytes(data, mediaType, placement) {
4100
4264
  const marker = Buffer.from(`json-to-office:${placement}`, "utf8");
4101
4265
  switch (mediaType) {
@@ -4194,12 +4358,27 @@ function crc32(data) {
4194
4358
  }
4195
4359
  function collectImagePlacements2(ir) {
4196
4360
  const placements = /* @__PURE__ */ new Map();
4361
+ const record = (resourceId, placement) => {
4362
+ const sizes = placements.get(resourceId) ?? /* @__PURE__ */ new Set();
4363
+ sizes.add(placementKey(placement));
4364
+ placements.set(resourceId, sizes);
4365
+ };
4197
4366
  const visitInline = (inline) => {
4198
4367
  if (inline.kind === "image") {
4199
- const key = `${emuToPixels(inline.widthEmu)}x${emuToPixels(inline.heightEmu)}`;
4200
- const sizes = placements.get(inline.resourceId) ?? /* @__PURE__ */ new Set();
4201
- sizes.add(key);
4202
- placements.set(inline.resourceId, sizes);
4368
+ record(inline.resourceId, inline);
4369
+ return;
4370
+ }
4371
+ if (inline.kind === "drawingGroup") {
4372
+ for (const child of inline.children) {
4373
+ if (child.kind === "picture") {
4374
+ record(child.resourceId, {
4375
+ widthEmu: child.frame.widthEmu,
4376
+ heightEmu: child.frame.heightEmu
4377
+ });
4378
+ continue;
4379
+ }
4380
+ child.text?.paragraphs.forEach(visitBlock);
4381
+ }
4203
4382
  return;
4204
4383
  }
4205
4384
  if (inline.kind === "hyperlink" || inline.kind === "revision") {
@@ -4372,6 +4551,9 @@ import {
4372
4551
  DEFAULT_VISUAL_DPI,
4373
4552
  MAX_RASTERIZE_BATCH_SLIDES
4374
4553
  } from "@json-to-office/shared";
4554
+ import {
4555
+ isNativeVisualProps
4556
+ } from "@json-to-office/shared-docx";
4375
4557
 
4376
4558
  // src/components/visual.ts
4377
4559
  import { createHash } from "crypto";
@@ -4584,7 +4766,9 @@ function collectVisualProps(root) {
4584
4766
  const obj = node;
4585
4767
  if (typeof obj.name === "string" && obj.enabled === false) return;
4586
4768
  if (obj.name === "visual" && obj.props && typeof obj.props === "object") {
4587
- found.push(obj.props);
4769
+ if (!isNativeVisualProps(obj.props)) {
4770
+ found.push(obj.props);
4771
+ }
4588
4772
  return;
4589
4773
  }
4590
4774
  for (const value of Object.values(obj)) visit(value);
@@ -4864,6 +5048,9 @@ import {
4864
5048
  clampVisualDpi as clampVisualDpi2,
4865
5049
  DEFAULT_VISUAL_DPI as DEFAULT_VISUAL_DPI2
4866
5050
  } from "@json-to-office/shared";
5051
+ import {
5052
+ isNativeVisualProps as isNativeVisualProps2
5053
+ } from "@json-to-office/shared-docx";
4867
5054
 
4868
5055
  // src/components/highcharts.ts
4869
5056
  init_colorUtils();
@@ -5025,6 +5212,7 @@ async function desugarExternals(document, options) {
5025
5212
  return transformComponents(document, async (node) => {
5026
5213
  if (node.enabled === false) return void 0;
5027
5214
  if (node.name === "visual") {
5215
+ if (isNativeVisualProps2(node.props)) return void 0;
5028
5216
  return withNodeIdentity(node, {
5029
5217
  name: "image",
5030
5218
  props: await visualImageProps(
@@ -5077,6 +5265,9 @@ async function visualImageProps(props, prerastered, options) {
5077
5265
 
5078
5266
  // src/core/imageResources.ts
5079
5267
  init_imageUtils();
5268
+ import {
5269
+ isNativeVisualProps as isNativeVisualProps3
5270
+ } from "@json-to-office/shared-docx";
5080
5271
  async function loadImageResources(components) {
5081
5272
  const sources = /* @__PURE__ */ new Set();
5082
5273
  for (const component of components) collectSources(component, sources);
@@ -5115,6 +5306,9 @@ function collectSources(component, out) {
5115
5306
  const children = component.children;
5116
5307
  for (const child of children ?? []) collectSources(child, out);
5117
5308
  const props = component.props ?? {};
5309
+ if (component.name === "visual" && isNativeVisualProps3(props)) {
5310
+ collectNativeVisualSources(props, out);
5311
+ }
5118
5312
  for (const key of ["header", "footer"]) {
5119
5313
  const part = props[key];
5120
5314
  if (Array.isArray(part)) {
@@ -5136,6 +5330,23 @@ function collectSources(component, out) {
5136
5330
  }
5137
5331
  }
5138
5332
  }
5333
+ function collectNativeVisualSources(props, out) {
5334
+ const canvas = props.canvas;
5335
+ const background = canvas?.background?.image;
5336
+ if (background) {
5337
+ const source = resolveImageSource(background);
5338
+ if (source) out.add(source);
5339
+ }
5340
+ const elements = props.elements;
5341
+ if (!Array.isArray(elements)) return;
5342
+ for (const element of elements) {
5343
+ if (!element || element.name !== "image") continue;
5344
+ const source = resolveImageSource(
5345
+ element.props ?? {}
5346
+ );
5347
+ if (source) out.add(source);
5348
+ }
5349
+ }
5139
5350
  function collectCellSource(content, out) {
5140
5351
  if (content && typeof content === "object") {
5141
5352
  collectSources(content, out);
@@ -5148,6 +5359,516 @@ import {
5148
5359
  FeatureRequirementCollector
5149
5360
  } from "@json-to-office/shared/rendering";
5150
5361
  import { synthesizeFamilyName } from "@json-to-office/shared";
5362
+ import {
5363
+ isNativeVisualProps as isNativeVisualProps4
5364
+ } from "@json-to-office/shared-docx";
5365
+
5366
+ // src/ir/nativeVisual.ts
5367
+ init_units();
5368
+ init_imageUtils();
5369
+ var PIXELS_PER_INCH2 = 96;
5370
+ var DEFAULT_WIDTH_FRACTION = 0.75;
5371
+ var ASSUMED_FONT_SIZE_POINTS = 18;
5372
+ var MIN_DERIVED_TEXT_HEIGHT_INCHES = 0.5;
5373
+ var DERIVED_LINE_HEIGHT_RATIO = 1.6;
5374
+ var GEOMETRY_ALIASES = {
5375
+ arrow: "rightArrow",
5376
+ lightning: "lightningBolt"
5377
+ };
5378
+ var DASH_VALUES = /* @__PURE__ */ new Set([
5379
+ "solid",
5380
+ "dash",
5381
+ "dot",
5382
+ "dashDot"
5383
+ ]);
5384
+ var ANCHOR = {
5385
+ top: "top",
5386
+ middle: "middle",
5387
+ bottom: "bottom"
5388
+ };
5389
+ var ALIGNMENT = {
5390
+ left: "left",
5391
+ center: "center",
5392
+ right: "right",
5393
+ justify: "justified"
5394
+ };
5395
+ function compileNativeVisualGroup(props, outer) {
5396
+ const canvas = {
5397
+ widthEmu: inchesToEmu(props.canvas.width),
5398
+ heightEmu: inchesToEmu(props.canvas.height)
5399
+ };
5400
+ const children = [];
5401
+ let refused = false;
5402
+ const deps = {
5403
+ ...outer,
5404
+ reject: (detail) => {
5405
+ refused = true;
5406
+ outer.reject(detail);
5407
+ }
5408
+ };
5409
+ const background = props.canvas.background;
5410
+ if (background?.color) {
5411
+ const color = deps.color(background.color);
5412
+ if (!color) {
5413
+ deps.reject(`canvas.background.color "${background.color}"`);
5414
+ } else {
5415
+ children.push({
5416
+ kind: "shape",
5417
+ frame: fullBleed(canvas),
5418
+ geometry: "rect",
5419
+ fill: { kind: "solid", color },
5420
+ name: "Canvas background"
5421
+ });
5422
+ }
5423
+ }
5424
+ if (background?.image) {
5425
+ const source = resolveImageSource(background.image);
5426
+ const resource = source ? deps.picture(source) : void 0;
5427
+ if (!resource) {
5428
+ deps.reject("canvas.background.image could not be loaded");
5429
+ } else {
5430
+ children.push({
5431
+ kind: "picture",
5432
+ frame: fullBleed(canvas),
5433
+ resourceId: resource.resourceId,
5434
+ name: "Canvas background"
5435
+ });
5436
+ }
5437
+ }
5438
+ for (const [index, element] of (props.elements ?? []).entries()) {
5439
+ if (element.enabled === false) continue;
5440
+ const child = compileElement(element, index, canvas, deps);
5441
+ if (child) children.push(child);
5442
+ }
5443
+ return refused ? void 0 : { canvas, children };
5444
+ }
5445
+ function compileElement(element, index, canvas, deps) {
5446
+ const at = `elements[${index}]`;
5447
+ switch (element.name) {
5448
+ case "text":
5449
+ return compileText(element.props, at, canvas, deps);
5450
+ case "shape":
5451
+ return compileShape(element.props, at, canvas, deps);
5452
+ case "image":
5453
+ return compileImage(element.props, at, canvas, deps);
5454
+ default:
5455
+ deps.reject(`${at} is a "${element.name}"`);
5456
+ return void 0;
5457
+ }
5458
+ }
5459
+ function compileText(props, at, canvas, deps) {
5460
+ const runs = textRuns(props);
5461
+ if (!runs) {
5462
+ deps.warn(
5463
+ `[core-docx] native visual ${at} has neither "text" nor "runs"; nothing was drawn for it.`
5464
+ );
5465
+ return void 0;
5466
+ }
5467
+ const colors = resolveColors(
5468
+ [
5469
+ ["color", props.color],
5470
+ ["fill.color", props.fill?.color],
5471
+ ...runs.map((run, i) => [`runs[${i}].color`, run.color]),
5472
+ ...runs.map(
5473
+ (run, i) => [
5474
+ `runs[${i}].underline.color`,
5475
+ underlineColor(run.underline)
5476
+ ]
5477
+ ),
5478
+ ["underline.color", underlineColor(props.underline)]
5479
+ ],
5480
+ at,
5481
+ deps
5482
+ );
5483
+ if (!colors) return void 0;
5484
+ const frame = resolveFrame(props, canvas, {
5485
+ defaultHeightInches: derivedTextHeightInches(props, runs)
5486
+ });
5487
+ const paragraphs = textParagraphs(runs, props, colors, at);
5488
+ return {
5489
+ kind: "shape",
5490
+ frame,
5491
+ geometry: "rect",
5492
+ isTextBox: true,
5493
+ // A text box with no fill is transparent, which is what the rasterized
5494
+ // path draws; saying so explicitly stops Word applying its own default.
5495
+ fill: props.fill?.color ? solidFill(colors.get("fill.color"), props.fill.transparency) : { kind: "none" },
5496
+ text: {
5497
+ paragraphs,
5498
+ // pptx anchors a text box at the top unless told otherwise, and a
5499
+ // drawing that changes anchor between render modes moves its text.
5500
+ anchor: ANCHOR[props.valign ?? "top"] ?? "top",
5501
+ // A text box's insets default to nothing, again matching pptx. A shape
5502
+ // deliberately leaves them unstated so OOXML's own defaults apply.
5503
+ insetsEmu: marginInsets(props.margin) ?? ZERO_INSETS
5504
+ },
5505
+ name: `Text ${at}`
5506
+ };
5507
+ }
5508
+ var ZERO_INSETS = { top: 0, bottom: 0, left: 0, right: 0 };
5509
+ function textRuns(props) {
5510
+ if (props.runs?.length) return props.runs;
5511
+ if (props.text === void 0) return void 0;
5512
+ return [{ text: props.text }];
5513
+ }
5514
+ function textParagraphs(runs, props, colors, at) {
5515
+ const paragraphs = [];
5516
+ let current = [];
5517
+ const flush = () => {
5518
+ paragraphs.push({
5519
+ kind: "paragraph",
5520
+ id: `${at}.p${paragraphs.length}`,
5521
+ path: `${at}.paragraphs[${paragraphs.length}]`,
5522
+ children: current,
5523
+ formatting: {
5524
+ ...props.align ? { alignment: ALIGNMENT[props.align] } : {},
5525
+ // A drawing's text is set flush against the shape; the document's
5526
+ // paragraph spacing would push it away from the top edge, which is not
5527
+ // what an absolutely-placed box means.
5528
+ spacing: { beforeTwips: 0, afterTwips: 0 }
5529
+ }
5530
+ });
5531
+ current = [];
5532
+ };
5533
+ runs.forEach((run, index) => {
5534
+ const formatting = runFormatting(run, props, colors, index);
5535
+ const lines = run.text.split("\n");
5536
+ lines.forEach((line, lineIndex) => {
5537
+ if (lineIndex > 0) current.push({ kind: "lineBreak" });
5538
+ if (line) {
5539
+ current.push({ kind: "text", text: line, ...formatting });
5540
+ }
5541
+ });
5542
+ if (run.breakLine && index < runs.length - 1) flush();
5543
+ });
5544
+ flush();
5545
+ return paragraphs;
5546
+ }
5547
+ function runFormatting(run, props, colors, index) {
5548
+ const fontFamily = run.fontFace ?? props.fontFace;
5549
+ const sizePoints = run.fontSize ?? props.fontSize;
5550
+ const color = colors.get(`runs[${index}].color`) ?? (run.color === void 0 ? colors.get("color") : void 0);
5551
+ const bold = run.bold ?? props.bold;
5552
+ const italic = run.italic ?? props.italic;
5553
+ const strike = run.strike ?? props.strike;
5554
+ const underline = compileUnderline(
5555
+ run.underline ?? props.underline,
5556
+ colors.get(
5557
+ run.underline !== void 0 ? `runs[${index}].underline.color` : "underline.color"
5558
+ )
5559
+ );
5560
+ const formatting = {
5561
+ ...fontFamily ? { fontFamily } : {},
5562
+ ...sizePoints !== void 0 ? { sizeHalfPoints: pointsToHalfPoints(sizePoints) } : {},
5563
+ ...color ? { color } : {},
5564
+ ...bold !== void 0 ? { bold } : {},
5565
+ ...italic !== void 0 ? { italic } : {},
5566
+ ...strike !== void 0 ? { strike } : {},
5567
+ ...underline ? { underline } : {}
5568
+ };
5569
+ return Object.keys(formatting).length > 0 ? { formatting } : {};
5570
+ }
5571
+ var UNDERLINE_TYPES = {
5572
+ sng: "single",
5573
+ dbl: "double",
5574
+ dash: "dash",
5575
+ dotted: "dotted"
5576
+ };
5577
+ function compileUnderline(value, color) {
5578
+ if (value === void 0 || value === false) return void 0;
5579
+ const style = value === true ? "sng" : value.style ?? "sng";
5580
+ return {
5581
+ type: UNDERLINE_TYPES[style] ?? "single",
5582
+ ...color ? { color } : {}
5583
+ };
5584
+ }
5585
+ function underlineColor(value) {
5586
+ return typeof value === "object" ? value.color : void 0;
5587
+ }
5588
+ function derivedTextHeightInches(props, runs) {
5589
+ const fontSize = props.fontSize ?? ASSUMED_FONT_SIZE_POINTS;
5590
+ const lineCount = runs.reduce(
5591
+ (total, run) => total + (run.breakLine ? 1 : 0) + (run.text.match(/\n/g)?.length ?? 0),
5592
+ 1
5593
+ );
5594
+ return Math.max(
5595
+ MIN_DERIVED_TEXT_HEIGHT_INCHES,
5596
+ fontSize / 72 * DERIVED_LINE_HEIGHT_RATIO * lineCount
5597
+ );
5598
+ }
5599
+ function compileShape(props, at, canvas, deps) {
5600
+ const segments = shapeSegments(props);
5601
+ const colors = resolveColors(
5602
+ [
5603
+ ["fill.color", props.fill?.color],
5604
+ ["line.color", props.line?.color],
5605
+ ["fontColor", props.fontColor],
5606
+ ...segments.map(
5607
+ (segment, i) => [`text[${i}].color`, segment.color]
5608
+ )
5609
+ ],
5610
+ at,
5611
+ deps
5612
+ );
5613
+ if (!colors) return void 0;
5614
+ const frame = resolveFrame(props, canvas, { defaultHeightInches: 0 });
5615
+ if (props.flipH) frame.flipHorizontal = true;
5616
+ if (props.flipV) frame.flipVertical = true;
5617
+ const dash = props.line?.dashType;
5618
+ if (dash !== void 0 && !DASH_VALUES.has(dash)) {
5619
+ deps.reject(`${at}.line.dashType "${dash}"`);
5620
+ return void 0;
5621
+ }
5622
+ const outline = props.line ? {
5623
+ ...colors.get("line.color") ? { color: colors.get("line.color") } : {},
5624
+ ...props.line.width !== void 0 ? { widthEmu: pointsToEmu(props.line.width) } : {},
5625
+ ...dash && dash !== "solid" ? { dash } : {}
5626
+ } : void 0;
5627
+ const text = segments.length ? {
5628
+ paragraphs: shapeParagraphs(segments, props, colors, at),
5629
+ anchor: ANCHOR[props.valign ?? "middle"] ?? "middle",
5630
+ // Unstated insets are left unstated so OOXML's own shape defaults
5631
+ // apply, which is what the raster path draws.
5632
+ ...marginInsets(props.margin) ? { insetsEmu: marginInsets(props.margin) } : {}
5633
+ } : void 0;
5634
+ return {
5635
+ kind: "shape",
5636
+ frame,
5637
+ geometry: GEOMETRY_ALIASES[props.type] ?? props.type,
5638
+ ...props.fill ? {
5639
+ fill: props.fill.color ? solidFill(colors.get("fill.color"), props.fill.transparency) : { kind: "none" }
5640
+ } : {},
5641
+ ...outline && Object.keys(outline).length > 0 ? { outline } : {},
5642
+ ...text ? { text } : {},
5643
+ name: `Shape ${at}`
5644
+ };
5645
+ }
5646
+ function shapeSegments(props) {
5647
+ if (props.text === void 0) return [];
5648
+ if (typeof props.text === "string") {
5649
+ return props.text ? [{ text: props.text }] : [];
5650
+ }
5651
+ return props.text;
5652
+ }
5653
+ function shapeParagraphs(segments, props, colors, at) {
5654
+ const paragraphs = [];
5655
+ let current = [];
5656
+ const flush = () => {
5657
+ paragraphs.push({
5658
+ kind: "paragraph",
5659
+ id: `${at}.p${paragraphs.length}`,
5660
+ path: `${at}.paragraphs[${paragraphs.length}]`,
5661
+ children: current,
5662
+ formatting: {
5663
+ // A shape centres its text unless told otherwise, which is what the
5664
+ // rasterized shape draws.
5665
+ alignment: props.align ? ALIGNMENT[props.align] : "center",
5666
+ spacing: { beforeTwips: 0, afterTwips: 0 }
5667
+ }
5668
+ });
5669
+ current = [];
5670
+ };
5671
+ segments.forEach((segment, index) => {
5672
+ const fontFamily = segment.fontFace ?? props.fontFace;
5673
+ const sizePoints = segment.fontSize ?? props.fontSize;
5674
+ const color = colors.get(`text[${index}].color`) ?? (segment.color === void 0 ? colors.get("fontColor") : void 0);
5675
+ const bold = segment.bold ?? props.bold;
5676
+ const italic = segment.italic ?? props.italic;
5677
+ const formatting = {
5678
+ ...fontFamily ? { fontFamily } : {},
5679
+ ...sizePoints !== void 0 ? { sizeHalfPoints: pointsToHalfPoints(sizePoints) } : {},
5680
+ ...color ? { color } : {},
5681
+ ...bold !== void 0 ? { bold } : {},
5682
+ ...italic !== void 0 ? { italic } : {}
5683
+ };
5684
+ const wrapped = Object.keys(formatting).length > 0 ? { formatting } : void 0;
5685
+ segment.text.split("\n").forEach((line, lineIndex) => {
5686
+ if (lineIndex > 0) current.push({ kind: "lineBreak" });
5687
+ if (line) current.push({ kind: "text", text: line, ...wrapped });
5688
+ });
5689
+ if (segment.breakLine && index < segments.length - 1) flush();
5690
+ });
5691
+ flush();
5692
+ return paragraphs;
5693
+ }
5694
+ function compileImage(props, at, canvas, deps) {
5695
+ const source = resolveImageSource(props);
5696
+ if (!source) {
5697
+ deps.reject(`${at} has none of "path", "base64" or "svg"`);
5698
+ return void 0;
5699
+ }
5700
+ const resource = deps.picture(source);
5701
+ if (!resource) {
5702
+ deps.reject(`${at} image could not be loaded`);
5703
+ return void 0;
5704
+ }
5705
+ const aspect = resource.intrinsic ? resource.intrinsic.width / resource.intrinsic.height : void 0;
5706
+ const intrinsicInches = resource.intrinsic ? {
5707
+ width: resource.intrinsic.width / PIXELS_PER_INCH2,
5708
+ height: resource.intrinsic.height / PIXELS_PER_INCH2
5709
+ } : void 0;
5710
+ const box = resolveImageBox(props, canvas, aspect, intrinsicInches);
5711
+ const sizing = props.sizing;
5712
+ if (!sizing || !aspect) {
5713
+ if (sizing && !aspect) {
5714
+ deps.warn(
5715
+ `[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.`
5716
+ );
5717
+ }
5718
+ return picture(box, props, resource, at);
5719
+ }
5720
+ const boxAspect = box.widthEmu / box.heightEmu;
5721
+ if (sizing.type === "contain") {
5722
+ const fitted = boxAspect > aspect ? {
5723
+ widthEmu: Math.round(box.heightEmu * aspect),
5724
+ heightEmu: box.heightEmu
5725
+ } : {
5726
+ widthEmu: box.widthEmu,
5727
+ heightEmu: Math.round(box.widthEmu / aspect)
5728
+ };
5729
+ return picture(
5730
+ {
5731
+ ...box,
5732
+ xEmu: box.xEmu + Math.round((box.widthEmu - fitted.widthEmu) / 2),
5733
+ yEmu: box.yEmu + Math.round((box.heightEmu - fitted.heightEmu) / 2),
5734
+ ...fitted
5735
+ },
5736
+ props,
5737
+ resource,
5738
+ at
5739
+ );
5740
+ }
5741
+ const visible = boxAspect > aspect ? { widthFraction: 1, heightFraction: aspect / boxAspect } : { widthFraction: boxAspect / aspect, heightFraction: 1 };
5742
+ const crop = sizing.type === "cover" ? {
5743
+ ...visible.widthFraction < 1 ? {
5744
+ left: (1 - visible.widthFraction) / 2,
5745
+ right: (1 - visible.widthFraction) / 2
5746
+ } : {},
5747
+ ...visible.heightFraction < 1 ? {
5748
+ top: (1 - visible.heightFraction) / 2,
5749
+ bottom: (1 - visible.heightFraction) / 2
5750
+ } : {}
5751
+ } : {
5752
+ ...visible.widthFraction < 1 ? { right: 1 - visible.widthFraction } : {},
5753
+ ...visible.heightFraction < 1 ? { bottom: 1 - visible.heightFraction } : {}
5754
+ };
5755
+ return picture(box, props, resource, at, crop);
5756
+ }
5757
+ function picture(frame, props, resource, at, crop) {
5758
+ return {
5759
+ kind: "picture",
5760
+ frame,
5761
+ resourceId: resource.resourceId,
5762
+ ...props.alt ? { altText: props.alt } : {},
5763
+ ...crop && Object.keys(crop).length > 0 ? { crop } : {},
5764
+ name: `Image ${at}`
5765
+ };
5766
+ }
5767
+ function resolveImageBox(props, canvas, aspect, intrinsicInches) {
5768
+ const width = props.sizing?.w ?? props.w;
5769
+ const height = props.sizing?.h ?? props.h;
5770
+ const widthEmu = width !== void 0 ? resolveExtent(width, canvas.widthEmu) : void 0;
5771
+ const heightEmu = height !== void 0 ? resolveExtent(height, canvas.heightEmu) : void 0;
5772
+ const resolved = (() => {
5773
+ if (widthEmu !== void 0 && heightEmu !== void 0) {
5774
+ return { widthEmu, heightEmu };
5775
+ }
5776
+ if (widthEmu !== void 0) {
5777
+ return {
5778
+ widthEmu,
5779
+ heightEmu: aspect ? Math.round(widthEmu / aspect) : Math.round(widthEmu * (3 / 4))
5780
+ };
5781
+ }
5782
+ if (heightEmu !== void 0) {
5783
+ return {
5784
+ widthEmu: aspect ? Math.round(heightEmu * aspect) : Math.round(heightEmu * (4 / 3)),
5785
+ heightEmu
5786
+ };
5787
+ }
5788
+ if (intrinsicInches) {
5789
+ return {
5790
+ widthEmu: inchesToEmu(intrinsicInches.width),
5791
+ heightEmu: inchesToEmu(intrinsicInches.height)
5792
+ };
5793
+ }
5794
+ const fallbackWidth = Math.round(canvas.widthEmu * DEFAULT_WIDTH_FRACTION);
5795
+ return {
5796
+ widthEmu: fallbackWidth,
5797
+ heightEmu: Math.round(fallbackWidth * (3 / 4))
5798
+ };
5799
+ })();
5800
+ return {
5801
+ xEmu: props.x !== void 0 ? resolveOffset(props.x, canvas.widthEmu) : 0,
5802
+ yEmu: props.y !== void 0 ? resolveOffset(props.y, canvas.heightEmu) : 0,
5803
+ ...resolved,
5804
+ ...rotation(props.rotate)
5805
+ };
5806
+ }
5807
+ function fullBleed(canvas) {
5808
+ return {
5809
+ xEmu: 0,
5810
+ yEmu: 0,
5811
+ widthEmu: canvas.widthEmu,
5812
+ heightEmu: canvas.heightEmu
5813
+ };
5814
+ }
5815
+ function resolveFrame(props, canvas, options) {
5816
+ return {
5817
+ xEmu: props.x !== void 0 ? resolveOffset(props.x, canvas.widthEmu) : 0,
5818
+ yEmu: props.y !== void 0 ? resolveOffset(props.y, canvas.heightEmu) : 0,
5819
+ widthEmu: props.w !== void 0 ? resolveExtent(props.w, canvas.widthEmu) : Math.round(canvas.widthEmu * DEFAULT_WIDTH_FRACTION),
5820
+ heightEmu: props.h !== void 0 ? resolveExtent(props.h, canvas.heightEmu) : inchesToEmu(options.defaultHeightInches),
5821
+ ...rotation(props.rotate)
5822
+ };
5823
+ }
5824
+ function rotation(degrees) {
5825
+ if (degrees === void 0 || degrees % 360 === 0) return {};
5826
+ return { rotationDegrees: degrees };
5827
+ }
5828
+ function resolveOffset(value, axisEmu) {
5829
+ return resolveLength(value, axisEmu);
5830
+ }
5831
+ function resolveExtent(value, axisEmu) {
5832
+ return Math.max(0, resolveLength(value, axisEmu));
5833
+ }
5834
+ function resolveLength(value, axisEmu) {
5835
+ if (typeof value === "number") return inchesToEmu(value);
5836
+ const percent = Number.parseFloat(value);
5837
+ if (!Number.isFinite(percent)) return 0;
5838
+ return Math.round(percent / 100 * axisEmu);
5839
+ }
5840
+ function marginInsets(margin) {
5841
+ if (margin === void 0) return void 0;
5842
+ const [top, right, bottom, left] = Array.isArray(margin) ? margin : [margin, margin, margin, margin];
5843
+ return {
5844
+ top: pointsToEmu(top ?? 0),
5845
+ right: pointsToEmu(right ?? 0),
5846
+ bottom: pointsToEmu(bottom ?? 0),
5847
+ left: pointsToEmu(left ?? 0)
5848
+ };
5849
+ }
5850
+ function solidFill(color, transparency) {
5851
+ return {
5852
+ kind: "solid",
5853
+ color,
5854
+ ...transparency !== void 0 && transparency > 0 ? { transparencyPercent: transparency } : {}
5855
+ };
5856
+ }
5857
+ function resolveColors(entries, at, deps) {
5858
+ const resolved = /* @__PURE__ */ new Map();
5859
+ let failed = false;
5860
+ for (const [key, value] of entries) {
5861
+ if (value === void 0) continue;
5862
+ const color = deps.color(value);
5863
+ if (!color) {
5864
+ deps.reject(`${at}.${key} "${value}"`);
5865
+ failed = true;
5866
+ continue;
5867
+ }
5868
+ resolved.set(key, color);
5869
+ }
5870
+ return failed ? void 0 : resolved;
5871
+ }
5151
5872
 
5152
5873
  // src/styles/themeToStyles.ts
5153
5874
  init_themes();
@@ -6990,7 +7711,7 @@ function decoratedStyle(match, base) {
6990
7711
  function pushSegment(out, text, options, override = {}) {
6991
7712
  const bold = override.bold ?? options.base.bold;
6992
7713
  const italic = override.italic ?? options.base.italic;
6993
- const formatting = runFormatting(options, { bold, italic });
7714
+ const formatting = runFormatting2(options, { bold, italic });
6994
7715
  const lines = text.split("\n");
6995
7716
  for (const [lineIndex, line] of lines.entries()) {
6996
7717
  if (!line && lineIndex === 0) continue;
@@ -7068,7 +7789,7 @@ function pushNoProofWords(out, text, formatting, options) {
7068
7789
  out.push({ kind: "text", text: text.slice(lastIndex), formatting });
7069
7790
  }
7070
7791
  }
7071
- function runFormatting(options, state) {
7792
+ function runFormatting2(options, state) {
7072
7793
  const formatting = { ...options.base };
7073
7794
  if (state.bold !== void 0) formatting.bold = state.bold;
7074
7795
  if (state.italic !== void 0) formatting.italic = state.italic;
@@ -7440,7 +8161,9 @@ function compileComponent(component, scope) {
7440
8161
  case "columns":
7441
8162
  return compileColumns(component, scope);
7442
8163
  case "image":
7443
- return compileImage(component, scope);
8164
+ return compileImage2(component, scope);
8165
+ case "visual":
8166
+ return compileVisual(component, scope);
7444
8167
  case "table":
7445
8168
  return compileTable(component, scope);
7446
8169
  default:
@@ -7684,7 +8407,7 @@ function compileTextBox(component, scope) {
7684
8407
  const style = props.style;
7685
8408
  const padding = style?.padding;
7686
8409
  if (props.renderAs === "shape") {
7687
- const shape = compileShape(props, children, scope);
8410
+ const shape = compileShape2(props, children, scope);
7688
8411
  if (shape) return shape;
7689
8412
  }
7690
8413
  ctx.features.require("tables", path4);
@@ -7738,7 +8461,7 @@ function compileTextBox(component, scope) {
7738
8461
  ];
7739
8462
  }
7740
8463
  var EMU_PER_PIXEL = 9525;
7741
- function compileShape(props, children, scope) {
8464
+ function compileShape2(props, children, scope) {
7742
8465
  const { ctx, path: path4 } = scope;
7743
8466
  const width = shapeSize(props.width, "width", ctx);
7744
8467
  const height = shapeSize(props.height, "height", ctx);
@@ -8232,7 +8955,7 @@ function reportUnlowered(component, props, text, scope) {
8232
8955
  }
8233
8956
  function compileRuns(props, text, ctx, path4, mode = "inline") {
8234
8957
  const font = props.font ?? {};
8235
- const base = runFormatting2(font, props, ctx);
8958
+ const base = runFormatting3(font, props, ctx);
8236
8959
  if (base.language) ctx.features.require("proofing-language", path4);
8237
8960
  if (base.noProof) ctx.features.require("proofing-language", path4);
8238
8961
  const words = mergeNoProofWords(noProofWords(ctx.theme), props.noProofWords);
@@ -8502,7 +9225,7 @@ function isoDate(date) {
8502
9225
  function isoDateTime(date) {
8503
9226
  return date.toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "Z");
8504
9227
  }
8505
- function runFormatting2(font, props, ctx) {
9228
+ function runFormatting3(font, props, ctx) {
8506
9229
  const hasWeightRequest = font.fontWeight != null || font.bold === true;
8507
9230
  const effectiveFamily = font.family ?? (hasWeightRequest ? resolveBodyFamily(ctx.theme) : void 0);
8508
9231
  const weighted = applyFontWeightAlias({
@@ -8665,8 +9388,107 @@ function headingLevel(level) {
8665
9388
  const value = typeof level === "number" ? level : 1;
8666
9389
  return value >= 1 && value <= 6 ? value : 1;
8667
9390
  }
9391
+ function compileVisual(component, scope) {
9392
+ const { ctx, path: path4 } = scope;
9393
+ const props = component.props ?? {};
9394
+ if (!isNativeVisualProps4(props)) {
9395
+ ctx.unsupported.push({
9396
+ name: "visual",
9397
+ path: path4,
9398
+ detail: "a raster visual reached the compiler; it should have been rasterized during desugaring"
9399
+ });
9400
+ return [];
9401
+ }
9402
+ const group = compileNativeVisualGroup(
9403
+ props,
9404
+ nativeVisualDeps(ctx, path4)
9405
+ );
9406
+ if (!group) return [];
9407
+ const size = visualPlacementPixels(props, group.canvas, {
9408
+ widthPx: Math.round(twipsToPixels(getAvailableWidthTwips(ctx.theme))),
9409
+ heightPx: Math.round(twipsToPixels(getAvailableHeightTwips(ctx.theme)))
9410
+ });
9411
+ const drawing = {
9412
+ kind: "drawingGroup",
9413
+ widthEmu: pixelsToEmu(size.width),
9414
+ heightEmu: pixelsToEmu(size.height),
9415
+ canvasWidthEmu: group.canvas.widthEmu,
9416
+ canvasHeightEmu: group.canvas.heightEmu,
9417
+ children: group.children,
9418
+ ...typeof props.alt === "string" && props.alt ? { altText: props.alt } : {},
9419
+ ...props.floating ? { floating: compileFloating(props.floating, ctx, path4) } : {}
9420
+ };
9421
+ ctx.features.require("drawing-groups", path4);
9422
+ if (props.floating) ctx.features.require("floating-images", path4);
9423
+ const spacing = {};
9424
+ if (props.spacing?.before !== void 0) {
9425
+ spacing.beforeTwips = pointsToTwips2(props.spacing.before);
9426
+ }
9427
+ if (props.spacing?.after !== void 0) {
9428
+ spacing.afterTwips = pointsToTwips2(props.spacing.after);
9429
+ }
9430
+ const blocks = [
9431
+ {
9432
+ kind: "paragraph",
9433
+ id: scope.id,
9434
+ path: path4,
9435
+ children: [drawing],
9436
+ formatting: {
9437
+ // A floating drawing is anchored, so aligning its paragraph would move
9438
+ // the anchor rather than the graphic — the same rule an image follows.
9439
+ ...props.floating ? {} : { alignment: compileAlignment(props.alignment) ?? "center" },
9440
+ ...Object.keys(spacing).length > 0 ? { spacing } : {},
9441
+ ...props.keepNext !== void 0 ? { keepNext: props.keepNext } : {},
9442
+ ...props.keepLines !== void 0 ? { keepLines: props.keepLines } : {}
9443
+ }
9444
+ }
9445
+ ];
9446
+ const caption = captionBlock(props.caption, scope, "visual");
9447
+ if (caption === void 0) return [];
9448
+ if (caption) blocks.push(caption);
9449
+ return blocks;
9450
+ }
9451
+ function nativeVisualDeps(ctx, path4) {
9452
+ return {
9453
+ color: (value) => {
9454
+ try {
9455
+ return irColor(resolveColor(value, ctx.theme));
9456
+ } catch {
9457
+ return void 0;
9458
+ }
9459
+ },
9460
+ picture: (source) => {
9461
+ const loaded = ctx.images.get(source);
9462
+ if (!loaded) return void 0;
9463
+ const mediaType = detectImageType(source, loaded.contentType);
9464
+ if (mediaType === "svg") ctx.features.require("svg-images", path4);
9465
+ return {
9466
+ resourceId: declareResource(loaded, mediaType, ctx),
9467
+ mediaType,
9468
+ ...loaded.intrinsic ? { intrinsic: loaded.intrinsic } : {}
9469
+ };
9470
+ },
9471
+ reject: (detail) => ctx.unsupported.push({ name: "visual", path: path4, detail }),
9472
+ warn: (message) => warnOnce(ctx, `visual:${message}`, message)
9473
+ };
9474
+ }
9475
+ function visualPlacementPixels(props, canvas, reference) {
9476
+ const canvasWidthPx = Math.round(emuToPixels(canvas.widthEmu));
9477
+ const canvasHeightPx = Math.round(emuToPixels(canvas.heightEmu));
9478
+ const targetWidth = props.width !== void 0 ? parseWidthValue(props.width, reference.widthPx) : void 0;
9479
+ const targetHeight = props.height !== void 0 ? parseDimensionValue(props.height, reference.heightPx) : void 0;
9480
+ if (targetWidth === void 0 && targetHeight === void 0) {
9481
+ return { width: canvasWidthPx, height: canvasHeightPx };
9482
+ }
9483
+ return calculateMissingDimension(
9484
+ canvasWidthPx,
9485
+ canvasHeightPx,
9486
+ targetWidth,
9487
+ targetHeight
9488
+ );
9489
+ }
8668
9490
  var UNLOWERED_IMAGE_PROPS = ["comment"];
8669
- function compileImage(component, scope) {
9491
+ function compileImage2(component, scope) {
8670
9492
  const { ctx, path: path4 } = scope;
8671
9493
  const props = component.props ?? {};
8672
9494
  for (const prop of UNLOWERED_IMAGE_PROPS) {
@@ -8727,32 +9549,37 @@ function compileImage(component, scope) {
8727
9549
  }
8728
9550
  }
8729
9551
  ];
8730
- if (props.caption) {
8731
- const caption = String(props.caption);
8732
- const syntax = containsUnsupportedSyntax(caption);
8733
- if (syntax) {
8734
- ctx.unsupported.push({ name: "image", path: path4, detail: syntax });
8735
- return [];
8736
- }
8737
- blocks.push({
8738
- kind: "paragraph",
8739
- id: `${scope.id}:caption`,
8740
- path: `${path4}.caption`,
8741
- styleId: "Normal",
8742
- // Captions default to left alignment, whatever the figure did.
8743
- formatting: { alignment: "left" },
8744
- // A caption reaches the parser only when it carries a decorator, which
8745
- // is why a link in an otherwise plain caption stays literal.
8746
- children: DECORATED.test(caption) ? parseInline(caption, {
8747
- base: {},
8748
- hyperlinks: true,
8749
- resolvePlaceholder: placeholderResolver(ctx),
8750
- resolveCrossReference: crossReferenceResolver(ctx, path4)
8751
- }) : parseLiteral(caption, { base: {} })
8752
- });
8753
- }
9552
+ const caption = captionBlock(props.caption, scope, "image");
9553
+ if (caption === void 0) return [];
9554
+ if (caption) blocks.push(caption);
8754
9555
  return blocks;
8755
9556
  }
9557
+ function captionBlock(value, scope, componentName) {
9558
+ if (!value) return null;
9559
+ const { ctx, path: path4 } = scope;
9560
+ const caption = String(value);
9561
+ const syntax = containsUnsupportedSyntax(caption);
9562
+ if (syntax) {
9563
+ ctx.unsupported.push({ name: componentName, path: path4, detail: syntax });
9564
+ return void 0;
9565
+ }
9566
+ return {
9567
+ kind: "paragraph",
9568
+ id: `${scope.id}:caption`,
9569
+ path: `${path4}.caption`,
9570
+ styleId: "Normal",
9571
+ // Captions default to left alignment, whatever the figure did.
9572
+ formatting: { alignment: "left" },
9573
+ // A caption reaches the parser only when it carries a decorator, which
9574
+ // is why a link in an otherwise plain caption stays literal.
9575
+ children: DECORATED.test(caption) ? parseInline(caption, {
9576
+ base: {},
9577
+ hyperlinks: true,
9578
+ resolvePlaceholder: placeholderResolver(ctx),
9579
+ resolveCrossReference: crossReferenceResolver(ctx, path4)
9580
+ }) : parseLiteral(caption, { base: {} })
9581
+ };
9582
+ }
8756
9583
  function relativeFrom(value, axis) {
8757
9584
  const allowed = axis === "horizontal" ? ["character", "column", "margin", "page"] : ["margin", "page", "paragraph", "line"];
8758
9585
  return typeof value === "string" && allowed.includes(value) ? value : void 0;
@@ -9176,6 +10003,9 @@ function cellChildren(cell, baseStyle, scope, rowRevision) {
9176
10003
  if (content.name === "image") {
9177
10004
  return wrap(cellImage(content, base, ctx));
9178
10005
  }
10006
+ if (content.name === "visual") {
10007
+ return wrap(cellVisual(content, scope));
10008
+ }
9179
10009
  if (content.name !== "paragraph") {
9180
10010
  return wrap([
9181
10011
  {
@@ -9215,6 +10045,40 @@ function cellChildren(cell, baseStyle, scope, rowRevision) {
9215
10045
  })
9216
10046
  );
9217
10047
  }
10048
+ function cellVisual(component, scope) {
10049
+ const { ctx, path: path4 } = scope;
10050
+ const props = component.props ?? {};
10051
+ if (!isNativeVisualProps4(props)) {
10052
+ ctx.unsupported.push({
10053
+ name: "visual",
10054
+ path: path4,
10055
+ detail: "a raster visual reached the compiler; it should have been rasterized during desugaring"
10056
+ });
10057
+ return [];
10058
+ }
10059
+ const group = compileNativeVisualGroup(
10060
+ props,
10061
+ nativeVisualDeps(ctx, path4)
10062
+ );
10063
+ if (!group) return [];
10064
+ const size = visualPlacementPixels(props, group.canvas, {
10065
+ widthPx: CELL_DRAWING_REFERENCE.width,
10066
+ heightPx: CELL_DRAWING_REFERENCE.height
10067
+ });
10068
+ ctx.features.require("drawing-groups", path4);
10069
+ return [
10070
+ {
10071
+ kind: "drawingGroup",
10072
+ widthEmu: pixelsToEmu(size.width),
10073
+ heightEmu: pixelsToEmu(size.height),
10074
+ canvasWidthEmu: group.canvas.widthEmu,
10075
+ canvasHeightEmu: group.canvas.heightEmu,
10076
+ children: group.children,
10077
+ ...typeof props.alt === "string" && props.alt ? { altText: props.alt } : {}
10078
+ }
10079
+ ];
10080
+ }
10081
+ var CELL_DRAWING_REFERENCE = { width: 300, height: 200 };
9218
10082
  function cellImage(component, base, ctx) {
9219
10083
  const props = component.props ?? {};
9220
10084
  const source = resolveImageSource(props);
@@ -10330,6 +11194,9 @@ import {
10330
11194
  clampVisualDpi as clampVisualDpi3,
10331
11195
  DEFAULT_VISUAL_DPI as DEFAULT_VISUAL_DPI3
10332
11196
  } from "@json-to-office/shared";
11197
+ import {
11198
+ isNativeVisualProps as isNativeVisualProps5
11199
+ } from "@json-to-office/shared-docx";
10333
11200
  var DEFAULT_CONCURRENCY = 4;
10334
11201
  async function flattenVisuals(doc, options) {
10335
11202
  let rasterize = options.rasterize;
@@ -10375,8 +11242,10 @@ async function flattenVisuals(doc, options) {
10375
11242
  doc,
10376
11243
  async (node) => (
10377
11244
  // A disabled visual is left as-is: it is filtered out at render anyway, so
10378
- // rasterizing it would be wasted work.
10379
- node.name === "visual" && node.enabled !== false ? rasterizeVisual(node, ctx) : void 0
11245
+ // rasterizing it would be wasted work. A native one is left as-is for a
11246
+ // stronger reason it is already service-free, and flattening exists to
11247
+ // remove the service dependency, not to trade editable objects for pixels.
11248
+ node.name === "visual" && node.enabled !== false && !isNativeVisualProps5(node.props) ? rasterizeVisual(node, ctx) : void 0
10380
11249
  )
10381
11250
  );
10382
11251
  }