Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -841,6 +841,29 @@ File overloads reject an oversized source before buffering. Applications that
previously attached larger XML must reduce or split the payload before using
these methods.

## ChartForgeX 2.0 adapter integration

Rebuild callers of `OfficeIMO.ChartForgeX` against ChartForgeX 2.0. The bridge keeps
its core-only ChartForgeX dependency. Add `ChartForgeX.Visuals` to the application
when rendering factual tables, canvases, or watermarks; optional story and motion
producers belong to `ChartForgeX.Stories`. Pass their common static artifacts to
the existing `ToOfficeVisual` and document-placement APIs.

Replace `VisualArtifactRenderOptions.Watermarks.Add(watermark)` with
`artifact.WithWatermarks(watermark)` before conversion. Native editable Visio
does not project those static layers. Inspect `WatermarkNotProjected`, or reject
it through `RejectedDiagnostics`/`RequireLossless`; the diagnostic also survives
interchange envelope and UTF-8 JSON handoff.

Use `prepared.ToArtifact(id, kind)` when placing a prepared chart or diagram.
It retains the resolved viewport, accessible text, and producer-owned semantics.
Complete topology projection consumes resolved connector routes and label bounds;
unprepared envelopes retain the authored waypoint and named-port contract.
Flow and sequence keep native editable semantics with `LayoutRecomputed` and
reject `LayoutMode = Preserve`. Their native page does not promise the prepared
scene's placement. The Markdown companion uses `ChartForgeX.Markup.Mermaid` 2.0
and keeps static fence materialization and document placement in their owners.

## Native ChartForgeX topology placement

`OfficeVisioVisualOptions.LayoutMode` defaults to `Auto`. A topology envelope with complete viewport, node, and included-group bounds now keeps those bounds instead of being laid out again. `PixelsPerInch` controls their physical size. Set `LayoutMode = OfficeVisioVisualLayoutMode.Reflow` to retain the previous native-layout behavior. Flow, sequence, and incomplete topology envelopes continue to use native layout in `Auto` mode. Native graph styling now uses source theme colors with portable Arial text; set `NativeTheme = VisioStyleTheme.Technical()` to retain the previous native palette and typography.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@
<ProjectReference Include="$(ChartForgeXMarkupMermaidProjectPath)" />
</ItemGroup>
<ItemGroup Condition="'$(ChartForgeXMarkupMermaidProjectPath)' == ''">
<PackageReference Include="ChartForgeX.Markup.Mermaid" Version="[1.8.2,2.0.0)" />
<PackageReference Include="ChartForgeX.Markup.Mermaid" Version="2.0.0" />
</ItemGroup>
</Project>
2 changes: 2 additions & 0 deletions OfficeIMO.ChartForgeX.Markdown/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

Render Mermaid fences as static diagrams in OfficeIMO Markdown workflows. This optional adapter uses [ChartForgeX.Markup.Mermaid](https://github.com/EvotecIT/ChartForgeX) for parsing, layout, SVG and PNG rendering. Word, PDF and PowerPoint engines keep their own document and image-placement behavior.

The adapter uses the ChartForgeX 2.0 Mermaid package. It renders through the common static artifact contract and does not add Visuals or Stories dependencies to ordinary OfficeIMO converters.

## Word and PDF

Materialize a parsed Markdown document before exporting it:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@

<ItemGroup>
<ProjectReference Include="..\OfficeIMO.ChartForgeX\OfficeIMO.ChartForgeX.csproj" />
<PackageReference Include="ChartForgeX.Visuals" Version="2.0.0" />
</ItemGroup>

<ItemGroup Condition="'$(ChartForgeXProjectPath)' != '' And Exists('$(ChartForgeXProjectPath)')">
<ProjectReference Include="$(ChartForgeXProjectPath)" />
</ItemGroup>

<ItemGroup Condition="'$(ChartForgeXProjectPath)' == '' Or !Exists('$(ChartForgeXProjectPath)')">
<PackageReference Include="ChartForgeX" Version="[1.6.0,2.0.0)" />
<PackageReference Include="ChartForgeX" Version="2.0.0" />
</ItemGroup>
</Project>
34 changes: 34 additions & 0 deletions OfficeIMO.ChartForgeX.Tests/OfficeVisioVisualDecorationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System;
using System.Linq;
using ChartForgeX.Topology;
using ChartForgeX.VisualArtifacts;
using Xunit;

namespace OfficeIMO.ChartForgeX.Tests
{
public sealed partial class OfficeVisioVisualIntegrationTests {
[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(2)]
public void VisioProjectionReportsWatermarksAcrossTypedEnvelopeAndJsonIngress(int ingress) {
var artifact = TopologyChart.Create().AddAutoNode("service", "Service")
.ToVisualArtifact().WithWatermarks(VisualWatermark.FromText("CONFIDENTIAL"));
Func<OfficeVisioVisualOptions, OfficeVisioVisualConversionResult> project = ingress switch {
0 => options => artifact.ToOfficeVisio(options),
1 => options => artifact.ToInterchangeEnvelope().ToOfficeVisio(options),
_ => options => artifact.ToInterchangeUtf8Json().ToOfficeVisio(options)
};

var result = project(new OfficeVisioVisualOptions());
var warning = Assert.Single(result.Report.Diagnostics, item =>
item.Code == OfficeVisioVisualDiagnosticCode.WatermarkNotProjected);
Assert.Equal(artifact.Id, warning.EntityId);
Assert.Contains("SVG or PNG", warning.Message);
var strict = new OfficeVisioVisualOptions();
strict.RejectedDiagnostics.Add(OfficeVisioVisualDiagnosticCode.WatermarkNotProjected);
var error = Assert.Throws<OfficeVisioVisualFidelityException>(() => project(strict));
Assert.Contains(error.Report.Diagnostics, item => item.Code == OfficeVisioVisualDiagnosticCode.WatermarkNotProjected);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,9 @@ public void ThemeLossesIdentifyTheArtifactInsteadOfANode() {
[Fact]
public void RejectedTypedProjectionIncludesAllRequestedFeatureLosses() {
var chart = TopologyChart.Create().WithTheme(TopologyTheme.Light()).AddAutoNode("service", "Service");
var render = new VisualArtifactRenderOptions();
render.Watermarks.Add(VisualWatermark.FromText("CONFIDENTIAL"));
var error = Assert.Throws<OfficeVisioVisualFidelityException>(() => chart.ToVisualArtifact().ToOfficeVisio(
new OfficeVisioVisualOptions { RequireLossless = true }, render));
var artifact = chart.ToVisualArtifact().WithWatermarks(VisualWatermark.FromText("CONFIDENTIAL"));
var error = Assert.Throws<OfficeVisioVisualFidelityException>(() => artifact.ToOfficeVisio(
new OfficeVisioVisualOptions { RequireLossless = true }));
Assert.Contains(error.Report.Diagnostics, item => item.Code == OfficeVisioVisualDiagnosticCode.WatermarkNotProjected);
Assert.Contains(error.Report.Diagnostics, item => item.Code != OfficeVisioVisualDiagnosticCode.WatermarkNotProjected);
}
Expand Down
12 changes: 0 additions & 12 deletions OfficeIMO.ChartForgeX.Tests/OfficeVisioVisualIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -746,18 +746,6 @@ public void DisabledShapeDataAndHyperlinksAreReportedAsSemanticLoss() {
diagnostic.EntityId == "api" && diagnostic.Severity == OfficeVisioVisualDiagnosticSeverity.Warning);
}

[Fact]
public void TypedVisioProjectionReportsRenderWatermarksThatRemainInStaticFallbacks() {
TopologyChart topology = TopologyChart.Create();
topology.Nodes.Add(new TopologyNode { Id = "service", Label = "Service" });
var renderOptions = new VisualArtifactRenderOptions();
renderOptions.Watermarks.Add(VisualWatermark.FromText("CONFIDENTIAL"));

OfficeVisioVisualConversionResult result = topology.ToVisualArtifact().ToOfficeVisio(renderOptions: renderOptions);

Assert.Contains(result.Report.Warnings, warning => warning.Contains("watermarks") && warning.Contains("SVG or PNG"));
}

[Fact]
public void VisioOptionsRejectInvalidPageAndPixelDensity() {
Assert.Throws<ArgumentException>(() => new OfficeVisioVisualOptions { PageName = " " });
Expand Down
110 changes: 110 additions & 0 deletions OfficeIMO.ChartForgeX.Tests/OfficeVisioVisualPreparedTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using System;
using System.IO;
using System.Linq;
using ChartForgeX.Primitives;
using ChartForgeX.Rendering;
using ChartForgeX.Topology;
using ChartForgeX.VisualArtifacts;
using OfficeIMO.Visio;
using Xunit;

namespace OfficeIMO.ChartForgeX.Tests;

public sealed partial class OfficeVisioVisualIntegrationTests {
[Theory]
[InlineData(false)]
[InlineData(true)]
public void PreparedTopologyRetainsResolvedRoutesLabelsAndViewportThroughSave(bool jsonIngress) {
var chart = TopologyChart.Create().WithViewport(640, 400);
chart.LayoutMode = TopologyLayoutMode.Manual;
chart.Nodes.Add(new TopologyNode { Id = "api", Label = "API", X = 40, Y = 100, Width = 120, Height = 64 });
chart.Nodes.Add(new TopologyNode { Id = "worker", Label = "Worker", X = 380, Y = 240, Width = 120, Height = 64 });
chart.Edges.Add(new TopologyEdge { Id = "request", SourceNodeId = "api", TargetNodeId = "worker", Label = "Request", Routing = TopologyEdgeRouting.ObstacleAvoidingOrthogonal });
var prepared = chart.Prepare(new VisualRenderContext(
layout: new VisualLayoutOptions(new VisualSize(640, 400)), frame: new VisualFrame(showLegend: false)));
var artifact = prepared.ToArtifact("prepared-topology", VisualArtifactKind.Topology);
var envelope = artifact.ToInterchangeEnvelope();
var edge = Assert.Single(envelope.Edges);
Assert.Empty(edge.Topology!.Waypoints);
Assert.True(edge.ResolvedRoute.Count >= 2);
Assert.NotNull(edge.ResolvedLabelBounds);
var options = new OfficeVisioVisualOptions { PixelsPerInch = 144, LayoutMode = OfficeVisioVisualLayoutMode.Preserve };
var result = jsonIngress ? artifact.ToInterchangeUtf8Json().ToOfficeVisio(options) : artifact.ToOfficeVisio(options);
Assert.DoesNotContain(result.Report.Diagnostics, item => item.Code == OfficeVisioVisualDiagnosticCode.LayoutRecomputed ||
item.Feature == "computedRoute" || item.Feature == "selfRoute");

void Check(VisioPage page) {
double ppi = options.PixelsPerInch;
Assert.Equal(envelope.Width!.Value / ppi, page.Width, 6);
Assert.Equal(envelope.Height!.Value / ppi, page.Height, 6);
foreach (var source in envelope.Nodes) {
var shape = page.Shapes.Single(item => item.Id == source.Id);
Assert.Equal((source.X!.Value + source.Width!.Value / 2) / ppi, shape.PinX, 6);
Assert.Equal((envelope.Height.Value - source.Y!.Value - source.Height!.Value / 2) / ppi, shape.PinY, 6);
Assert.Equal(source.Width.Value / ppi, shape.Width, 6);
Assert.Equal(source.Height.Value / ppi, shape.Height, 6);
}
var connector = Assert.Single(page.Connectors);
Assert.Equal(VisioConnectorRerouteBehavior.Never, connector.RerouteBehavior);
Assert.Equal(edge.ResolvedRoute.Count - 2, connector.Waypoints.Count);
var first = edge.ResolvedRoute[0];
Assert.Equal(first.X / ppi - connector.From.PinX + connector.From.Width / 2, connector.FromConnectionPoint!.X, 6);
Assert.Equal((envelope.Height.Value - first.Y) / ppi - connector.From.PinY + connector.From.Height / 2, connector.FromConnectionPoint.Y, 6);
var last = edge.ResolvedRoute[edge.ResolvedRoute.Count - 1];
Assert.Equal(last.X / ppi - connector.To.PinX + connector.To.Width / 2, connector.ToConnectionPoint!.X, 6);
Assert.Equal((envelope.Height.Value - last.Y) / ppi - connector.To.PinY + connector.To.Height / 2, connector.ToConnectionPoint.Y, 6);
for (int index = 0; index < connector.Waypoints.Count; index++) {
Assert.Equal(edge.ResolvedRoute[index + 1].X / ppi, connector.Waypoints[index].X, 6);
Assert.Equal((envelope.Height.Value - edge.ResolvedRoute[index + 1].Y) / ppi, connector.Waypoints[index].Y, 6);
}
var label = edge.ResolvedLabelBounds!.Value;
var bounds = connector.GetLabelBounds();
Assert.Equal(label.X / ppi, bounds.Left, 6);
Assert.Equal((envelope.Height.Value - label.Y - label.Height) / ppi, bounds.Bottom, 6);
Assert.Equal((label.X + label.Width) / ppi, bounds.Right, 6);
Assert.Equal((envelope.Height.Value - label.Y) / ppi, bounds.Top, 6);
}

Check(result.Page);
string path = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".vsdx");
try {
result.Document.Save(path);
Assert.Empty(VisioValidator.Validate(path));
Check(VisioDocument.Load(path).Pages[0]);
} finally { if (File.Exists(path)) File.Delete(path); }
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public void PreparedFlowAndSequenceRemainEditableAndReportNativeReflowThroughSave(bool sequence) {
PreparedVisual prepared;
if (sequence) {
var source = SequenceArtifact.Create("sequence").WithSize(640, 400)
.AddParticipant("api", "API").AddParticipant("worker", "Worker").AddMessage("api", "worker", "Request");
prepared = source.Prepare(new VisualRenderContext(
layout: new VisualLayoutOptions(new VisualSize(640, 400)), frame: new VisualFrame(showLegend: false)));
} else {
var source = FlowArtifact.Create("flow").AddLane("team", "Team")
.AddStep("api", "API", FlowArtifactStepKind.Start, "team")
.AddStep("worker", "Worker", FlowArtifactStepKind.Decision, "team").AddConnector("api", "worker", "Request");
prepared = source.Prepare(new VisualRenderContext(
layout: new VisualLayoutOptions(new VisualSize(640, 400)), frame: new VisualFrame(showLegend: false)));
}
var artifact = prepared.ToArtifact(sequence ? "prepared-sequence" : "prepared-flow",
sequence ? VisualArtifactKind.Sequence : VisualArtifactKind.Flow);
var result = artifact.ToInterchangeUtf8Json().ToOfficeVisio(new OfficeVisioVisualOptions { UseNaturalPageSize = true });
Assert.True(result.Report.AllProjectedObjectsEditable);
Assert.Contains(result.Report.Diagnostics, item => item.Code == OfficeVisioVisualDiagnosticCode.LayoutRecomputed);
Assert.Throws<NotSupportedException>(() => artifact.ToOfficeVisio(new OfficeVisioVisualOptions { LayoutMode = OfficeVisioVisualLayoutMode.Preserve }));
string path = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".vsdx");
try {
result.Document.Save(path);
Assert.Empty(VisioValidator.Validate(path));
var page = VisioDocument.Load(path).Pages[0];
Assert.Contains(page.Shapes, shape => (sequence ? shape.GetShapeDataValue("CFX.Id") : shape.Id) == "api");
Assert.Contains(page.Shapes, shape => (sequence ? shape.GetShapeDataValue("CFX.Id") : shape.Id) == "worker");
Assert.Contains(page.Connectors, connector => connector.Label == "Request");
} finally { if (File.Exists(path)) File.Delete(path); }
}
}
34 changes: 20 additions & 14 deletions OfficeIMO.ChartForgeX.Tests/OfficeVisualIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
using System.IO;
using System.Linq;
using System.Text;
using global::ChartForgeX.Core;
using global::ChartForgeX.Primitives;
using global::ChartForgeX.Raster;
using global::ChartForgeX.SvgRaster;
using global::ChartForgeX.VisualArtifacts;
using OfficeIMO.ChartForgeX;
Expand All @@ -15,7 +17,7 @@

namespace OfficeIMO.ChartForgeX.Tests;

public sealed class OfficeVisualIntegrationTests {
public sealed partial class OfficeVisualIntegrationTests {
[Fact]
public void ConversionPreservesVectorPayloadDimensionsAccessibilityAndRegions() {
VisualArtifact artifact = CreateArtifact();
Expand Down Expand Up @@ -108,14 +110,12 @@ public void PortableSvgSourceSupportsCrossProcessAndPowerShellExchange() {
public void VectorImportWithoutNaturalSizeUsesSvgViewportDimensions() {
VisualArtifact artifact = CreateArtifact();
artifact.NaturalSize = null;
var renderOptions = new VisualArtifactRenderOptions();
renderOptions.Watermarks.Add(VisualWatermark.FromImage(
artifact.WithWatermarks(VisualWatermark.FromImage(
CreateArtifact().ToPng(),
"image/png"));

OfficeVisualConversionResult result = artifact.ToOfficeVisual(new OfficeVisualConversionOptions {
SvgPolicy = OfficeVisualSvgPolicy.RasterizeWhenNeeded,
RenderOptions = renderOptions
SvgPolicy = OfficeVisualSvgPolicy.RasterizeWhenNeeded
});

Assert.True(result.Report.IsVector);
Expand All @@ -127,8 +127,7 @@ public void VectorImportWithoutNaturalSizeUsesSvgViewportDimensions() {

OfficeVisualConversionResult widthOnly = artifact.ToOfficeVisual(new OfficeVisualConversionOptions {
WidthPoints = 300D,
SvgPolicy = OfficeVisualSvgPolicy.RasterizeWhenNeeded,
RenderOptions = renderOptions
SvgPolicy = OfficeVisualSvgPolicy.RasterizeWhenNeeded
});
Assert.Equal(300D, widthOnly.WidthPoints, 6);
Assert.Equal(300D * result.HeightPoints / result.WidthPoints, widthOnly.HeightPoints, 6);
Expand Down Expand Up @@ -184,10 +183,20 @@ public void PortableSvgSourceRasterizesSafeDegenerateGeometryThatCannotBeVectorP
const string degeneratePolygon = "<svg xmlns='http://www.w3.org/2000/svg' width='16' height='8'><polygon points='1,1 2,1 3,1'/></svg>";

OfficeVisualConversionResult result = new OfficeVisualSource(degeneratePolygon).ToOfficeVisual(
new OfficeVisualConversionOptions { SvgPolicy = OfficeVisualSvgPolicy.RasterizeWhenNeeded });
new OfficeVisualConversionOptions {
SvgPolicy = OfficeVisualSvgPolicy.RasterizeWhenNeeded,
RenderOptions = new VisualArtifactRenderOptions { Raster = new RasterImageOptions { Dpi = 144 } }
});

Assert.True(result.Report.UsedRasterFallback);
Assert.Equal(OfficeVisualMediaFormat.Png, result.PlacementFormat);
var bytes = result.GetPlacementBytes();
var pixels = RasterImageDecoder.Decode(bytes);
Assert.Equal(16, pixels.Width);
Assert.Equal(8, pixels.Height);
Assert.Equal(12D, result.WidthPoints, 6);
Assert.Equal(6D, result.HeightPoints, 6);
Assert.Equal(5669U, PngPixelsPerMeter(bytes));
}

[Fact]
Expand Down Expand Up @@ -236,19 +245,16 @@ public void PlacementCreatesReadableWordExcelPowerPointAndPdfPackages() {
string folder = Path.Combine(Path.GetTempPath(), "OfficeIMO-ChartForgeX-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(folder);
try {
var renderOptions = new VisualArtifactRenderOptions();
renderOptions.Watermarks.Add(VisualWatermark.FromText("INTERNAL"));
VisualWatermark imageWatermark = VisualWatermark.FromImage(
CreateArtifact().ToPng(),
"image/png");
imageWatermark.Width = 18D;
imageWatermark.Height = 18D;
imageWatermark.Padding = 8D;
renderOptions.Watermarks.Add(imageWatermark);
OfficeVisualConversionResult visual = CreateArtifact().ToOfficeVisual(
OfficeVisualConversionResult visual = CreateArtifact()
.WithWatermarks(VisualWatermark.FromText("INTERNAL"), imageWatermark).ToOfficeVisual(
new OfficeVisualConversionOptions {
WidthPoints = 300D,
RenderOptions = renderOptions
WidthPoints = 300D
});
string wordPath = Path.Combine(folder, "visual.docx");
string excelPath = Path.Combine(folder, "visual.xlsx");
Expand Down
Loading
Loading