From 05c9b1a436a70965d101e71321a08ce96745d382 Mon Sep 17 00:00:00 2001 From: ilCosmico Date: Wed, 30 Sep 2026 09:34:35 +0200 Subject: [PATCH 1/9] Write every CadValue type in DXF and read the date back writeCadValue now writes the unit type, the format and the formatted value, skips the data of empty and unknown values and writes dates as the 16 byte block AutoCAD uses. The reader decodes that block back into the date. --- .../DxfStreamReader/DxfSectionReaderBase.cs | 20 +++++ .../DxfStreamWriter/DxfSectionWriterBase.cs | 90 ++++++++++++------- 2 files changed, 76 insertions(+), 34 deletions(-) diff --git a/src/ACadSharp/IO/DXF/DxfStreamReader/DxfSectionReaderBase.cs b/src/ACadSharp/IO/DXF/DxfStreamReader/DxfSectionReaderBase.cs index 5f4a2c09e..77b9da0a6 100644 --- a/src/ACadSharp/IO/DXF/DxfStreamReader/DxfSectionReaderBase.cs +++ b/src/ACadSharp/IO/DXF/DxfStreamReader/DxfSectionReaderBase.cs @@ -5,6 +5,7 @@ using ACadSharp.XData; using CSMath; using CSMath.Extensions; +using CSUtilities.Converters; using CSUtilities.Extensions; using System; using System.Collections.Generic; @@ -634,12 +635,31 @@ protected CadValueTemplate readCadValue(CadValue value) case 91: value.SetValue(this._reader.ValueAsInt); break; + case 92: + //Size of the date data in 310 + break; case 93: value.Flags = this._reader.ValueAsInt; break; case 140: value.SetValue(this._reader.ValueAsDouble); break; + case 310 when value.ValueType == CadValueType.Date: + { + byte[] array = this._reader.ValueAsBinaryChunk; + if (array.Length == 16) + { + int year = LittleEndianConverter.Instance.ToInt16(array, 0); + int month = LittleEndianConverter.Instance.ToInt16(array, 2); + int day = LittleEndianConverter.Instance.ToInt16(array, 6); + int hour = LittleEndianConverter.Instance.ToInt16(array, 8); + int minute = LittleEndianConverter.Instance.ToInt16(array, 10); + int second = LittleEndianConverter.Instance.ToInt16(array, 12); + int millisecond = LittleEndianConverter.Instance.ToInt16(array, 14); + value.SetValue(new DateTime(year, month, day, hour, minute, second, millisecond)); + } + } + break; case 330: template.ValueHandle = this._reader.ValueAsHandle; break; diff --git a/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.cs b/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.cs index e88701cfd..4b2c0755d 100644 --- a/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.cs +++ b/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.cs @@ -3,7 +3,9 @@ using ACadSharp.Tables; using ACadSharp.XData; using CSMath; +using CSUtilities.Converters; using System; +using System.IO; using System.Linq; namespace ACadSharp.IO.DXF; @@ -50,43 +52,63 @@ protected void writeCadValue(CadValue value) this._writer.Write(93, value.Flags); this._writer.Write(90, (int)value.ValueType); - switch (value.ValueType) + if (!value.IsEmpty) { - case CadValueType.Unknown: - this._writer.Write(91, 0); - break; - case CadValueType.Double: - this._writer.Write(140, Convert.ToDouble(value.Value)); - break; - case CadValueType.Date: - //TODO: Implement date for dxf - this._writer.Write(91, 0); - break; - case CadValueType.General: - case CadValueType.String: - this.writeLongTextValue(1, 2, (string)value.Value); - break; - case CadValueType.Point2D: - case CadValueType.Point3D: - this._writer.Write(11, (XYZ)value.Value); - break; - case CadValueType.Long: - this._writer.Write(91, Convert.ToInt32(value.Value)); - break; - case CadValueType.Handle: - var handleObj = value.Value as IHandledCadObject; - if (handleObj != null) - { - this._writer.WriteHandle(330, handleObj); - } - break; - case CadValueType.Buffer: - case CadValueType.ResultBuffer: - //TODO: Implement date for dxf - this._writer.Write(91, 0); - break; + switch (value.ValueType) + { + case CadValueType.Unknown: + break; + case CadValueType.Double: + this._writer.Write(140, Convert.ToDouble(value.Value)); + break; + case CadValueType.Date: + if (value.Value is DateTime date) + { + byte[] array = new byte[16]; + var stream = new MemoryStream(array); + + stream.Write(LittleEndianConverter.Instance.GetBytes((short)date.Year), 0, 2); + stream.Write(LittleEndianConverter.Instance.GetBytes((short)date.Month), 0, 2); + stream.Write(LittleEndianConverter.Instance.GetBytes((short)date.DayOfWeek), 0, 2); + stream.Write(LittleEndianConverter.Instance.GetBytes((short)date.Day), 0, 2); + stream.Write(LittleEndianConverter.Instance.GetBytes((short)date.Hour), 0, 2); + stream.Write(LittleEndianConverter.Instance.GetBytes((short)date.Minute), 0, 2); + stream.Write(LittleEndianConverter.Instance.GetBytes((short)date.Second), 0, 2); + stream.Write(LittleEndianConverter.Instance.GetBytes((short)date.Millisecond), 0, 2); + + this._writer.Write(92, array.Length); + this._writer.Write(310, array); + } + break; + case CadValueType.General: + case CadValueType.String: + this.writeLongTextValue(1, 2, (string)value.Value); + break; + case CadValueType.Point2D: + case CadValueType.Point3D: + this._writer.Write(11, (XYZ)value.Value); + break; + case CadValueType.Long: + this._writer.Write(91, Convert.ToInt32(value.Value)); + break; + case CadValueType.Handle: + var handleObj = value.Value as IHandledCadObject; + if (handleObj != null) + { + this._writer.WriteHandle(330, handleObj); + } + break; + case CadValueType.Buffer: + case CadValueType.ResultBuffer: + //TODO: Implement date for dxf + this._writer.Write(91, 0); + break; + } } + this._writer.Write(94, (int)value.Units); + this._writer.Write(300, value.Format ?? string.Empty); + this._writer.Write(302, value.FormattedValue ?? string.Empty); this._writer.Write(304, DxfFileToken.ValueEnd); } From d07a97d2b1398f267261482c48d4403ce2be277e Mon Sep 17 00:00:00 2001 From: ilCosmico Date: Wed, 30 Sep 2026 09:47:34 +0200 Subject: [PATCH 2/9] Keep the data of unknown CadValues that are not empty in DXF An unknown value without the empty flag, as read from a pre R2007 DWG, carries its data in 91 like the DWG writer does, otherwise it is lost when the DXF is read back. --- src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.cs b/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.cs index 4b2c0755d..3976bf434 100644 --- a/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.cs +++ b/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.cs @@ -57,6 +57,7 @@ protected void writeCadValue(CadValue value) switch (value.ValueType) { case CadValueType.Unknown: + this._writer.Write(91, 0); break; case CadValueType.Double: this._writer.Write(140, Convert.ToDouble(value.Value)); From 2d4fb28861090060a7d3e02ca0e425e843d7cd3c Mon Sep 17 00:00:00 2001 From: ilCosmico Date: Wed, 30 Sep 2026 11:23:21 +0200 Subject: [PATCH 3/9] Test the DXF round trip of the CadValue types through a field --- .../IO/DXF/DxfWriterSingleObjectTests.cs | 29 +++++++++ .../IO/WriterSingleObjectTests.cs | 63 +++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/src/ACadSharp.Tests/IO/DXF/DxfWriterSingleObjectTests.cs b/src/ACadSharp.Tests/IO/DXF/DxfWriterSingleObjectTests.cs index 0cb984d96..1d3279967 100644 --- a/src/ACadSharp.Tests/IO/DXF/DxfWriterSingleObjectTests.cs +++ b/src/ACadSharp.Tests/IO/DXF/DxfWriterSingleObjectTests.cs @@ -1,6 +1,8 @@ using ACadSharp.Entities; using ACadSharp.IO; +using ACadSharp.Objects; using CSMath; +using System; using System.IO; using System.Linq; using Xunit; @@ -54,6 +56,33 @@ public void WriteCasesAC1032(SingleCaseGenerator data) this.writeDxfFile(data, ACadVersion.AC1032); } + [Fact] + public void WriteFieldDateValue() + { + DateTime date = new DateTime(2026, 9, 30, 10, 15, 30, 250); + + SingleCaseGenerator data = new SingleCaseGenerator(); + data.FieldCadValues(); + + MText text = Assert.Single(data.Document.Entities.OfType()); + Field field = text.XDictionary.GetEntry("ACAD_FIELD").GetEntry("TEXT"); + CadValue dateValue = new CadValue(); + dateValue.SetValue(date, CadValueType.Date); + field.Values.Add("date", dateValue); + + using MemoryStream output = new MemoryStream(); + DxfWriter.Write(output, data.Document); + + using MemoryStream input = new MemoryStream(output.ToArray()); + CadDocument result = DxfReader.Read(input); + Field writtenField = result.GetCadObject(field.Handle); + + Assert.NotNull(writtenField); + Assert.True(writtenField.Values.TryGetValue("date", out CadValue value)); + Assert.Equal(CadValueType.Date, value.ValueType); + Assert.Equal(date, value.Value); + } + [Theory] [InlineData(-90, 270, -90, 270)] [InlineData(180, 540, -180, 180)] diff --git a/src/ACadSharp.Tests/IO/WriterSingleObjectTests.cs b/src/ACadSharp.Tests/IO/WriterSingleObjectTests.cs index bd3e8fcb8..27a948dad 100644 --- a/src/ACadSharp.Tests/IO/WriterSingleObjectTests.cs +++ b/src/ACadSharp.Tests/IO/WriterSingleObjectTests.cs @@ -101,6 +101,7 @@ static WriterSingleObjectTests() Data.Add(new(nameof(SingleCaseGenerator.SingleMeshWithTextureCoordinates))); Data.Add(new(nameof(SingleCaseGenerator.SingleMaterial))); Data.Add(new(nameof(SingleCaseGenerator.LeaderWithArrowHead))); + Data.Add(new(nameof(SingleCaseGenerator.FieldCadValues))); } public WriterSingleObjectTests(ITestOutputHelper output) : base(output) @@ -1699,6 +1700,68 @@ public void EntityTransparency() }; } + public void FieldCadValues() + { + MText text = new MText(); + text.Value = "cad values"; + this.Document.Entities.Add(text); + + Field field = new Field(); + field.EvaluatorId = "_text"; + field.FieldCode = "cad values"; + field.FormatString = string.Empty; + field.Value.SetValue("cad values", CadValueType.String); + + CadValue unknownEmpty = new CadValue(); + unknownEmpty.IsEmpty = true; + field.Values.Add("unknown_empty", unknownEmpty); + + //Unknown value that is not empty, as read from a pre R2007 dwg + CadValue unknown = new CadValue(); + unknown.SetValue(0); + field.Values.Add("unknown", unknown); + + CadValue longValue = new CadValue(); + longValue.SetValue(42, CadValueType.Long); + field.Values.Add("long", longValue); + + CadValue doubleValue = new CadValue(); + doubleValue.SetValue(0.5, CadValueType.Double); + field.Values.Add("double", doubleValue); + + CadValue stringValue = new CadValue(); + stringValue.SetValue("text value", CadValueType.String); + field.Values.Add("string", stringValue); + + CadValue pointValue = new CadValue(); + pointValue.SetValue(new XYZ(1, 2, 3), CadValueType.Point3D); + field.Values.Add("point", pointValue); + + CadDictionary dict = text.CreateExtendedDictionary(); + CadDictionary fields = new CadDictionary("ACAD_FIELD"); + dict.Add(fields); + fields.Add("TEXT", field); + + this.AssertRoundtrip = (doc) => + { + if (this.Format == CadFileFormat.DWG) + { + return; + } + + Field result = doc.GetCadObject(field.Handle); + + Assert.NotNull(result); + foreach (var item in field.Values) + { + Assert.True(result.Values.TryGetValue(item.Key, out CadValue value), $"Value {item.Key} not found"); + Assert.Equal(item.Value.ValueType, value.ValueType); + Assert.Equal(item.Value.IsEmpty, value.IsEmpty); + Assert.Equal(item.Value.Value, value.Value); + } + }; + } + public void GenerateExampleDxf() { this.Document.Header.UnitMode = (short)ACadSharp.Types.Units.UnitsType.Millimeters; From 07dfddb3b49250e9b00fa9154b6480dc0a2a364f Mon Sep 17 00:00:00 2001 From: ilCosmico Date: Wed, 30 Sep 2026 11:50:54 +0200 Subject: [PATCH 4/9] Share the SYSTEMTIME date conversion of CadValue between DWG and DXF --- src/ACadSharp/CadUtils.cs | 44 ++++++++++++++++++- .../DwgObjectReader.Entities.cs | 12 +---- .../DwgObjectWriter.Objects.cs | 12 +---- .../DxfStreamReader/DxfSectionReaderBase.cs | 10 +---- .../DxfStreamWriter/DxfSectionWriterBase.cs | 14 +----- 5 files changed, 47 insertions(+), 45 deletions(-) diff --git a/src/ACadSharp/CadUtils.cs b/src/ACadSharp/CadUtils.cs index 7dc0c773e..02dc319e2 100644 --- a/src/ACadSharp/CadUtils.cs +++ b/src/ACadSharp/CadUtils.cs @@ -1,6 +1,8 @@ -using CSUtilities.Text; +using CSUtilities.Converters; +using CSUtilities.Text; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; @@ -364,4 +366,44 @@ public static void DateToJulian(DateTime date, out int jdate, out int milisecond jdate = date.Day + (int)System.Math.Floor((153.0 * (double)(month + 12 * day - 3) + 2.0) / 5.0) + 365 * year + (int)System.Math.Floor((double)year / 4.0) - (int)System.Math.Floor((double)year / 100.0) + (int)System.Math.Floor((double)year / 400.0) - 32045; miliseconds = date.Millisecond + date.Second * 1000 + date.Minute * 60000 + date.Hour * 3600000; } + + /// + /// Converts a date into the 16 bytes of a SYSTEMTIME structure: year, month, day of the week, day, hour, minute, second and millisecond, each one as a little endian short. + /// + /// The date to convert. + /// The 16 bytes that represent the date. + public static byte[] ToSystemTime(DateTime date) + { + byte[] array = new byte[16]; + var stream = new MemoryStream(array); + + stream.Write(LittleEndianConverter.Instance.GetBytes((short)date.Year), 0, 2); + stream.Write(LittleEndianConverter.Instance.GetBytes((short)date.Month), 0, 2); + stream.Write(LittleEndianConverter.Instance.GetBytes((short)date.DayOfWeek), 0, 2); + stream.Write(LittleEndianConverter.Instance.GetBytes((short)date.Day), 0, 2); + stream.Write(LittleEndianConverter.Instance.GetBytes((short)date.Hour), 0, 2); + stream.Write(LittleEndianConverter.Instance.GetBytes((short)date.Minute), 0, 2); + stream.Write(LittleEndianConverter.Instance.GetBytes((short)date.Second), 0, 2); + stream.Write(LittleEndianConverter.Instance.GetBytes((short)date.Millisecond), 0, 2); + + return array; + } + + /// + /// Converts the 16 bytes of a SYSTEMTIME structure into a date. The day of the week is ignored. + /// + /// The 16 bytes that represent the date. + /// The date represented by the bytes. + public static DateTime FromSystemTime(byte[] array) + { + int year = LittleEndianConverter.Instance.ToInt16(array, 0); + int month = LittleEndianConverter.Instance.ToInt16(array, 2); + int day = LittleEndianConverter.Instance.ToInt16(array, 6); + int hour = LittleEndianConverter.Instance.ToInt16(array, 8); + int minute = LittleEndianConverter.Instance.ToInt16(array, 10); + int second = LittleEndianConverter.Instance.ToInt16(array, 12); + int millisecond = LittleEndianConverter.Instance.ToInt16(array, 14); + + return new DateTime(year, month, day, hour, minute, second, millisecond); + } } \ No newline at end of file diff --git a/src/ACadSharp/IO/DWG/DwgStreamReaders/DwgObjectReader.Entities.cs b/src/ACadSharp/IO/DWG/DwgStreamReaders/DwgObjectReader.Entities.cs index f5a2e1ef9..560718435 100644 --- a/src/ACadSharp/IO/DWG/DwgStreamReaders/DwgObjectReader.Entities.cs +++ b/src/ACadSharp/IO/DWG/DwgStreamReaders/DwgObjectReader.Entities.cs @@ -192,17 +192,7 @@ private void readCustomTableData(CustomDataEntry entry) switch (size) { case 16: - { - int year = LittleEndianConverter.Instance.ToInt16(array, 0); - int month = LittleEndianConverter.Instance.ToInt16(array, 2); - int day = LittleEndianConverter.Instance.ToInt16(array, 6); - int hour = LittleEndianConverter.Instance.ToInt16(array, 8); - int minute = LittleEndianConverter.Instance.ToInt16(array, 10); - int second = LittleEndianConverter.Instance.ToInt16(array, 12); - int millisecond = LittleEndianConverter.Instance.ToInt16(array, 14); - - return new System.DateTime(year, month, day, hour, minute, second, millisecond); - } + return CadUtils.FromSystemTime(array); case 14: { int year = LittleEndianConverter.Instance.ToInt16(array, 0); diff --git a/src/ACadSharp/IO/DWG/DwgStreamWriters/DwgObjectWriter.Objects.cs b/src/ACadSharp/IO/DWG/DwgStreamWriters/DwgObjectWriter.Objects.cs index 9cd5b527c..b4e5f0325 100644 --- a/src/ACadSharp/IO/DWG/DwgStreamWriters/DwgObjectWriter.Objects.cs +++ b/src/ACadSharp/IO/DWG/DwgStreamWriters/DwgObjectWriter.Objects.cs @@ -829,17 +829,7 @@ private void writeDateCadValue(System.DateTime? date) byte[] array; if (this.R2007Plus) { - array = new byte[16]; - var stream = new MemoryStream(array); - - stream.Write(LittleEndianConverter.Instance.GetBytes((short)date.Value.Year), 0, 2); - stream.Write(LittleEndianConverter.Instance.GetBytes((short)date.Value.Month), 0, 2); - stream.Write(LittleEndianConverter.Instance.GetBytes((short)date.Value.DayOfWeek), 0, 2); - stream.Write(LittleEndianConverter.Instance.GetBytes((short)date.Value.Day), 0, 2); - stream.Write(LittleEndianConverter.Instance.GetBytes((short)date.Value.Hour), 0, 2); - stream.Write(LittleEndianConverter.Instance.GetBytes((short)date.Value.Minute), 0, 2); - stream.Write(LittleEndianConverter.Instance.GetBytes((short)date.Value.Second), 0, 2); - stream.Write(LittleEndianConverter.Instance.GetBytes((short)date.Value.Millisecond), 0, 2); + array = CadUtils.ToSystemTime(date.Value); } else { diff --git a/src/ACadSharp/IO/DXF/DxfStreamReader/DxfSectionReaderBase.cs b/src/ACadSharp/IO/DXF/DxfStreamReader/DxfSectionReaderBase.cs index 77b9da0a6..a3337c988 100644 --- a/src/ACadSharp/IO/DXF/DxfStreamReader/DxfSectionReaderBase.cs +++ b/src/ACadSharp/IO/DXF/DxfStreamReader/DxfSectionReaderBase.cs @@ -5,7 +5,6 @@ using ACadSharp.XData; using CSMath; using CSMath.Extensions; -using CSUtilities.Converters; using CSUtilities.Extensions; using System; using System.Collections.Generic; @@ -649,14 +648,7 @@ protected CadValueTemplate readCadValue(CadValue value) byte[] array = this._reader.ValueAsBinaryChunk; if (array.Length == 16) { - int year = LittleEndianConverter.Instance.ToInt16(array, 0); - int month = LittleEndianConverter.Instance.ToInt16(array, 2); - int day = LittleEndianConverter.Instance.ToInt16(array, 6); - int hour = LittleEndianConverter.Instance.ToInt16(array, 8); - int minute = LittleEndianConverter.Instance.ToInt16(array, 10); - int second = LittleEndianConverter.Instance.ToInt16(array, 12); - int millisecond = LittleEndianConverter.Instance.ToInt16(array, 14); - value.SetValue(new DateTime(year, month, day, hour, minute, second, millisecond)); + value.SetValue(CadUtils.FromSystemTime(array)); } } break; diff --git a/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.cs b/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.cs index 3976bf434..ec2879fd1 100644 --- a/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.cs +++ b/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.cs @@ -3,9 +3,7 @@ using ACadSharp.Tables; using ACadSharp.XData; using CSMath; -using CSUtilities.Converters; using System; -using System.IO; using System.Linq; namespace ACadSharp.IO.DXF; @@ -65,17 +63,7 @@ protected void writeCadValue(CadValue value) case CadValueType.Date: if (value.Value is DateTime date) { - byte[] array = new byte[16]; - var stream = new MemoryStream(array); - - stream.Write(LittleEndianConverter.Instance.GetBytes((short)date.Year), 0, 2); - stream.Write(LittleEndianConverter.Instance.GetBytes((short)date.Month), 0, 2); - stream.Write(LittleEndianConverter.Instance.GetBytes((short)date.DayOfWeek), 0, 2); - stream.Write(LittleEndianConverter.Instance.GetBytes((short)date.Day), 0, 2); - stream.Write(LittleEndianConverter.Instance.GetBytes((short)date.Hour), 0, 2); - stream.Write(LittleEndianConverter.Instance.GetBytes((short)date.Minute), 0, 2); - stream.Write(LittleEndianConverter.Instance.GetBytes((short)date.Second), 0, 2); - stream.Write(LittleEndianConverter.Instance.GetBytes((short)date.Millisecond), 0, 2); + byte[] array = CadUtils.ToSystemTime(date); this._writer.Write(92, array.Length); this._writer.Write(310, array); From 342752d16b1e643887bf2a8046ef8908329bd889 Mon Sep 17 00:00:00 2001 From: ilCosmico Date: Wed, 20 May 2026 13:04:07 +0200 Subject: [PATCH 5/9] Add minimal TableEntity write support in DxfWriter DxfWriter previously skipped TableEntity with a "not implemented" notification, so saving a CadDocument containing an ACAD_TABLE produced a DXF with an empty modelspace. Emit the AcDbBlockReference (Insert) subclass first so the viewer can instantiate the anonymous BlockRecord that holds the table's rendered geometry, then the AcDbTable subclass with the table-level data (version, horizontal direction, value flag, row/column counts and sizes, style and block references) and a minimal per-cell payload (type, edge/merge flags, autofit, border counts, optional text content and rotation). Advanced cell features (block-cell payloads, multi-run formatted content, custom data, borders, format runs, merged ranges) are not yet round-tripped and will emit a Warning notification so consumers know some data is lost. --- .../DxfSectionWriterBase.Entities.cs | 105 +++++++++++++++++- 1 file changed, 104 insertions(+), 1 deletion(-) diff --git a/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.Entities.cs b/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.Entities.cs index eecf48192..3d1bfb6c3 100644 --- a/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.Entities.cs +++ b/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.Entities.cs @@ -45,6 +45,9 @@ protected void writeEntity(T entity) case Hatch hatch: this.writeHatch(hatch); break; + case TableEntity tableEntity: + this.writeTableEntity(tableEntity); + break; case Insert insert: this.writeInsert(insert); break; @@ -155,7 +158,6 @@ private bool isEntitySupported(Entity entity) case Shape: return this.Configuration.WriteShapes; case ProxyEntity: - case TableEntity: case Solid3D: case CadBody: case Region: @@ -644,6 +646,107 @@ private void writeHatchPattern(Hatch hatch, HatchPattern pattern) } } + private void writeTableEntity(TableEntity table) + { + // Emit the AcDbBlockReference (Insert) layer first. AutoCAD reads the block + // reference to instantiate the anonymous BlockRecord that holds the rendered + // table geometry, so even before the AcDbTable cells are populated the viewer + // has something to draw. + this._writer.Write(DxfCode.Subclass, DxfSubclassMarker.Insert); + if (table.Block != null) + { + this._writer.Write(2, table.Block.Name); + } + this._writer.Write(10, table.InsertPoint); + this._writer.Write(41, table.XScale); + this._writer.Write(42, table.YScale); + this._writer.Write(43, table.ZScale); + this._writer.Write(50, table.Rotation); + this._writer.Write(210, table.Normal); + + // AcDbTable subclass with the table-level data. This is the minimal set of + // codes the DXF reader expects; advanced features (per-cell content beyond a + // single text run, custom data, borders, format runs, merged ranges) are not + // emitted yet and will be lost on round-trip. + this._writer.Write(DxfCode.Subclass, DxfSubclassMarker.TableEntity); + this._writer.Write(280, table.Version); + + this._writer.Write(11, table.HorizontalDirection); + + int flag = table.ValueFlag; + if (table.OverrideFlag) + { + flag |= 1; + } + this._writer.Write(90, flag); + + this._writer.Write(91, table.Rows.Count); + this._writer.Write(92, table.Columns.Count); + + if (table.OverrideBorderColor) + { + this._writer.Write(94, 1); + } + if (table.OverrideBorderLineWeight) + { + this._writer.Write(95, 1); + } + if (table.OverrideBorderVisibility) + { + this._writer.Write(96, 1); + } + + if (table.Style != null) + { + this._writer.Write(342, table.Style.Handle); + } + if (table.Block != null) + { + this._writer.Write(343, table.Block.Handle); + } + + foreach (var col in table.Columns) + { + this._writer.Write(142, col.Width); + } + + foreach (var row in table.Rows) + { + this._writer.Write(141, row.Height); + + foreach (var cell in row.Cells) + { + this._writer.Write(171, (short)cell.Type); + this._writer.Write(172, (short)cell.EdgeFlags); + this._writer.Write(173, (short)cell.MergedValue); + this._writer.Write(174, cell.AutoFit); + this._writer.Write(175, cell.BorderWidth); + this._writer.Write(176, cell.BorderHeight); + + // First text content if available. Block-cell payloads and multi-run + // content are intentionally skipped in this initial pass. + string textValue = null; + if (cell.Content != null && cell.Content.CadValue.Value is string s) + { + textValue = s; + } + else if (cell.Contents.Count > 0 && cell.Contents[0].CadValue.Value is string s2) + { + textValue = s2; + } + + if (!string.IsNullOrEmpty(textValue)) + { + this._writer.Write(1, textValue); + } + + this._writer.Write(145, cell.Rotation); + } + } + + this.notify("TableEntity DXF writer emits a minimal subset of cell data; advanced cell content, borders, and merged ranges are not yet round-tripped.", NotificationType.Warning); + } + private void writeInsert(Insert insert) { DxfClassMap map = DxfClassMap.Create(); From c33d9c83ff598905777ded04800bff38efb870eb Mon Sep 17 00:00:00 2001 From: ilCosmico Date: Fri, 22 May 2026 14:35:09 +0200 Subject: [PATCH 6/9] Suppress AcDbTable cell data to avoid double rendering in AutoCAD When the writer emits both a populated AcDbBlockReference (pointing at the anonymous BlockRecord that holds the exploded table geometry) and a fully populated AcDbTable subclass with rows, columns and per-cell content, AutoCAD renders the table twice: once from the BlockReference and once from the AcDbTable cells, with the two renderings offset because AutoCAD's AcDbTable renderer does not pick up the Insert's transform. Emit only the AcDbTable header (version, horizontal direction, value flag, style and block handles) with row and column counts set to 0. AutoCAD then has no cell data to render from and falls back to drawing the BlockReference, which gives a single correctly positioned table. Cell metadata is not round-tripped through DXF in this minimal pass; a follow-up can add it together with the proxy graphic preview that AutoCAD-produced files emit (DXF group codes 160/310) so the AcDbTable renderer is shadowed by the proxy graphic instead of being suppressed. --- .../DxfSectionWriterBase.Entities.cs | 58 ++++--------------- 1 file changed, 12 insertions(+), 46 deletions(-) diff --git a/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.Entities.cs b/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.Entities.cs index 3d1bfb6c3..2ad31bdba 100644 --- a/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.Entities.cs +++ b/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.Entities.cs @@ -664,10 +664,15 @@ private void writeTableEntity(TableEntity table) this._writer.Write(50, table.Rotation); this._writer.Write(210, table.Normal); - // AcDbTable subclass with the table-level data. This is the minimal set of - // codes the DXF reader expects; advanced features (per-cell content beyond a - // single text run, custom data, borders, format runs, merged ranges) are not - // emitted yet and will be lost on round-trip. + // AcDbTable subclass with the table-level header. Rows/columns/cells are + // intentionally not emitted in this minimal pass: when both AcDbBlockReference + // and a fully populated AcDbTable describe the same entity, AutoCAD ends up + // rendering the table twice (once from the anonymous BlockRecord pointed to + // by code 2, once from the AcDbTable cells), and the two renderings are + // usually offset because the AcDbTable renderer does not pick up the Insert's + // transform. Emitting only the header lets AutoCAD treat the entity as an + // Insert visually while keeping it tagged as ACAD_TABLE for tooling that + // inspects the class. this._writer.Write(DxfCode.Subclass, DxfSubclassMarker.TableEntity); this._writer.Write(280, table.Version); @@ -680,8 +685,8 @@ private void writeTableEntity(TableEntity table) } this._writer.Write(90, flag); - this._writer.Write(91, table.Rows.Count); - this._writer.Write(92, table.Columns.Count); + this._writer.Write(91, 0); // row count placeholder + this._writer.Write(92, 0); // column count placeholder if (table.OverrideBorderColor) { @@ -705,46 +710,7 @@ private void writeTableEntity(TableEntity table) this._writer.Write(343, table.Block.Handle); } - foreach (var col in table.Columns) - { - this._writer.Write(142, col.Width); - } - - foreach (var row in table.Rows) - { - this._writer.Write(141, row.Height); - - foreach (var cell in row.Cells) - { - this._writer.Write(171, (short)cell.Type); - this._writer.Write(172, (short)cell.EdgeFlags); - this._writer.Write(173, (short)cell.MergedValue); - this._writer.Write(174, cell.AutoFit); - this._writer.Write(175, cell.BorderWidth); - this._writer.Write(176, cell.BorderHeight); - - // First text content if available. Block-cell payloads and multi-run - // content are intentionally skipped in this initial pass. - string textValue = null; - if (cell.Content != null && cell.Content.CadValue.Value is string s) - { - textValue = s; - } - else if (cell.Contents.Count > 0 && cell.Contents[0].CadValue.Value is string s2) - { - textValue = s2; - } - - if (!string.IsNullOrEmpty(textValue)) - { - this._writer.Write(1, textValue); - } - - this._writer.Write(145, cell.Rotation); - } - } - - this.notify("TableEntity DXF writer emits a minimal subset of cell data; advanced cell content, borders, and merged ranges are not yet round-tripped.", NotificationType.Warning); + this.notify("TableEntity DXF writer emits the entity as a block reference; cell data is not round-tripped.", NotificationType.Warning); } private void writeInsert(Insert insert) From dcab5b267be156c69e63eaab2de6e8f6b183e29d Mon Sep 17 00:00:00 2001 From: ilCosmico Date: Fri, 22 May 2026 16:56:42 +0200 Subject: [PATCH 7/9] Round-trip TableEntity cell alignment and merge ranges in DXF The DXF writer for TableEntity was emitting cell rows and cell contents but dropping the per-cell alignment (group code 170) and the cell flag bits that mark which overrides are active (group code 91). A strict reader fell back to the table style defaults, so the original alignment set on each cell was lost on round-trip. Emit the cell flag with the baseline state value AutoCAD-produced files use (262192) and OR in 0x01 plus a 170 record when the cell carries an explicit alignment override. On the reader side, capture group 170 onto the cell StyleOverride so callers see the alignment through the regular API. The DXF cell stream also encodes merged cells per-cell only: the top-left anchor carries the column and row span in BorderWidth/BorderHeight (175/176) and the remaining cells carry MergedValue=1 (173). The table-level MergedCellRanges collection that consumers actually iterate was never populated. Rebuild it in CadTableEntityTemplate.build() by scanning the grid after all cells are parsed, so DXF and DWG paths expose the same shape. --- .../DxfStreamReader/DxfSectionReaderBase.cs | 25 +- .../DxfSectionWriterBase.Entities.cs | 219 ++++++++++++++++-- ...ableEntityTemplate.CadTableCellTemplate.cs | 22 ++ .../IO/Templates/CadTableEntityTemplate.cs | 48 ++++ 4 files changed, 288 insertions(+), 26 deletions(-) diff --git a/src/ACadSharp/IO/DXF/DxfStreamReader/DxfSectionReaderBase.cs b/src/ACadSharp/IO/DXF/DxfStreamReader/DxfSectionReaderBase.cs index a3337c988..aba018afd 100644 --- a/src/ACadSharp/IO/DXF/DxfStreamReader/DxfSectionReaderBase.cs +++ b/src/ACadSharp/IO/DXF/DxfStreamReader/DxfSectionReaderBase.cs @@ -546,7 +546,18 @@ private bool readTableEntity(CadEntityTemplate template, DxfMap map, string subc tmp.CurrentCell.Rotation = this._reader.ValueAsDouble; return true; case 170: - //Has data flag + // Per-cell alignment (set when the cell flag at code 91 has the + // CellAlignment override bit). Capture it on the cell-level + // StyleOverride so callers reading the table back via DXF can + // resolve alignment exactly like they do through DWG. + if (tmp.CurrentCell != null) + { + tmp.CurrentCell.StyleOverride.HasData = true; + tmp.CurrentCell.StyleOverride.CellAlignment = + (ACadSharp.Objects.TableStyle.CellAlignmentType)this._reader.ValueAsShort; + tmp.CurrentCell.StyleOverride.PropertyOverrideFlags |= + ACadSharp.Objects.TableStyle.CellStylePropertyFlags.Alignment; + } return true; case 171: tmp.CreateCell((TableEntity.CellType)this._reader.ValueAsInt); @@ -572,6 +583,18 @@ private bool readTableEntity(CadEntityTemplate template, DxfMap map, string subc case 179: //Unknown value return true; + case 70: + // Table-level flow direction (0 = top to bottom, 1 = bottom to top). + // Stored on the table-level CellStyleOverride so callers can read it + // through the same API used for round-tripping the rest of the + // override flags. + if (this._reader.ValueAsShort == 1) + { + table.CellStyleOverride.HasData = true; + table.CellStyleOverride.TableCellStylePropertyFlags |= + ACadSharp.Objects.TableStyle.CellStylePropertyFlags.FlowDirectionBottomToTop; + } + return true; case 301: content = new TableEntity.CellContent(); var contentTemplate = new CadTableCellContentTemplate(content); diff --git a/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.Entities.cs b/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.Entities.cs index 2ad31bdba..52c0ead9f 100644 --- a/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.Entities.cs +++ b/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.Entities.cs @@ -650,8 +650,7 @@ private void writeTableEntity(TableEntity table) { // Emit the AcDbBlockReference (Insert) layer first. AutoCAD reads the block // reference to instantiate the anonymous BlockRecord that holds the rendered - // table geometry, so even before the AcDbTable cells are populated the viewer - // has something to draw. + // table geometry, so it always has something to draw. this._writer.Write(DxfCode.Subclass, DxfSubclassMarker.Insert); if (table.Block != null) { @@ -664,17 +663,27 @@ private void writeTableEntity(TableEntity table) this._writer.Write(50, table.Rotation); this._writer.Write(210, table.Normal); - // AcDbTable subclass with the table-level header. Rows/columns/cells are - // intentionally not emitted in this minimal pass: when both AcDbBlockReference - // and a fully populated AcDbTable describe the same entity, AutoCAD ends up - // rendering the table twice (once from the anonymous BlockRecord pointed to - // by code 2, once from the AcDbTable cells), and the two renderings are - // usually offset because the AcDbTable renderer does not pick up the Insert's - // transform. Emitting only the header lets AutoCAD treat the entity as an - // Insert visually while keeping it tagged as ACAD_TABLE for tooling that - // inspects the class. + // AcDbTable subclass with the full cell data so the table round-trips through + // a DxfReader without losing rows, columns or text content. AutoCAD prefers a + // proxy graphic preview (codes 160/310 in AcDbEntity) when one is present and + // ignores the cell data for layout; without a preview, AutoCAD also renders + // the cells on top of the BlockReference, producing a second offset copy of + // the table. That visual artifact is the cost of keeping the data + // round-trippable: callers that need a single rendering should provide a + // proxy graphic via Entity.PreviewGraphic. + // + // Field order mirrors what AutoCAD emits so the strict ODA-based reader does + // not bail with "Invalid index" partway through the entity. this._writer.Write(DxfCode.Subclass, DxfSubclassMarker.TableEntity); - this._writer.Write(280, table.Version); + + if (table.Style != null) + { + this._writer.Write(342, table.Style.Handle); + } + if (table.Block != null) + { + this._writer.Write(343, table.Block.Handle); + } this._writer.Write(11, table.HorizontalDirection); @@ -685,32 +694,192 @@ private void writeTableEntity(TableEntity table) } this._writer.Write(90, flag); - this._writer.Write(91, 0); // row count placeholder - this._writer.Write(92, 0); // column count placeholder + this._writer.Write(91, table.Rows.Count); + this._writer.Write(92, table.Columns.Count); + + // Override count placeholder. The exact semantics of code 93 at the table + // level are not documented in OpenDesign but AutoCAD-produced files always + // emit it. Emit 0 here; the reader does not error out on it. + this._writer.Write(93, 0); - if (table.OverrideBorderColor) + this._writer.Write(94, table.OverrideBorderColor ? 1 : 0); + this._writer.Write(95, table.OverrideBorderLineWeight ? 1 : 0); + this._writer.Write(96, table.OverrideBorderVisibility ? 1 : 0); + + // Flow direction is emitted only when set to bottom-to-top. Down-direction + // tables in AutoCAD-produced files omit the code entirely. + bool isFlowUp = table.CellStyleOverride != null + && table.CellStyleOverride.TableCellStylePropertyFlags.HasFlag(ACadSharp.Objects.TableStyle.CellStylePropertyFlags.FlowDirectionBottomToTop); + if (isFlowUp) { - this._writer.Write(94, 1); + this._writer.Write(70, (short)1); } - if (table.OverrideBorderLineWeight) + + // Table-level cell margins. AutoCAD reads these here even when the per-cell + // style override carries the same values; omitting them causes the strict + // ODA-based reader to abort with "Invalid index". + double horMargin = 0.06; + double verMargin = 0.06; + if (table.CellStyleOverride != null + && table.CellStyleOverride.MarginOverrideFlags.HasFlag(ACadSharp.Objects.TableStyle.MarginFlags.Override)) { - this._writer.Write(95, 1); + horMargin = table.CellStyleOverride.HorizontalMargin; + verMargin = table.CellStyleOverride.VerticalMargin; } - if (table.OverrideBorderVisibility) + else if (table.Style != null) { - this._writer.Write(96, 1); + horMargin = table.Style.HorizontalCellMargin; + verMargin = table.Style.VerticalCellMargin; } + this._writer.Write(40, horMargin); + this._writer.Write(41, verMargin); - if (table.Style != null) + this.notify("TableEntity DXF writer emits cell data without a proxy graphic preview; AutoCAD may draw the table twice (once from the block reference, once from the cells with no transform applied).", NotificationType.Warning); + + foreach (var row in table.Rows) { - this._writer.Write(342, table.Style.Handle); + this._writer.Write(141, row.Height); } - if (table.Block != null) + + foreach (var col in table.Columns) { - this._writer.Write(343, table.Block.Handle); + this._writer.Write(142, col.Width); + } + + // Translate table-level MergedCellRanges into the per-cell flags AutoCAD reads: + // the top-left cell of each range carries the column/row span in BorderWidth and + // BorderHeight while the remaining cells in the range have MergedValue = 1. + int rowCount = table.Rows.Count; + int colCount = table.Columns.Count; + short[,] mergeBorderWidth = new short[rowCount, colCount]; + short[,] mergeBorderHeight = new short[rowCount, colCount]; + short[,] mergeFlag = new short[rowCount, colCount]; + for (int i = 0; i < rowCount; i++) + { + for (int j = 0; j < colCount; j++) + { + mergeBorderWidth[i, j] = 1; + mergeBorderHeight[i, j] = 1; + } + } + foreach (var range in table.MergedCellRanges) + { + int top = System.Math.Max(0, range.TopRowIndex); + int bottom = System.Math.Min(rowCount - 1, range.BottomRowIndex); + int left = System.Math.Max(0, range.LeftColumnIndex); + int right = System.Math.Min(colCount - 1, range.RightColumnIndex); + int rowSpan = bottom - top + 1; + int colSpan = right - left + 1; + if (rowSpan < 1 || colSpan < 1) + { + continue; + } + mergeBorderWidth[top, left] = (short)colSpan; + mergeBorderHeight[top, left] = (short)rowSpan; + for (int i = top; i <= bottom; i++) + { + for (int j = left; j <= right; j++) + { + if (i == top && j == left) + { + continue; + } + mergeFlag[i, j] = 1; + } + } } - this.notify("TableEntity DXF writer emits the entity as a block reference; cell data is not round-tripped.", NotificationType.Warning); + for (int i = 0; i < rowCount; i++) + { + var row = table.Rows[i]; + for (int j = 0; j < row.Cells.Count; j++) + { + var cell = row.Cells[j]; + short borderWidth = j < colCount ? mergeBorderWidth[i, j] : (short)1; + short borderHeight = j < colCount ? mergeBorderHeight[i, j] : (short)1; + short mergedValue = j < colCount ? mergeFlag[i, j] : (short)cell.MergedValue; + this._writer.Write(171, (short)cell.Type); + this._writer.Write(172, (short)cell.EdgeFlags); + this._writer.Write(173, mergedValue); + this._writer.Write(174, cell.AutoFit); + this._writer.Write(175, (int)borderWidth); + this._writer.Write(176, (int)borderHeight); + + // Cell flag (state + override bits). AutoCAD-produced files always emit + // 262192 baseline and set bit 0x01 when the cell-level CellAlignment + // override is present; code 170 with the alignment value follows when + // that bit is set. Without bit 0x01 the strict ODA-based reader falls + // back to the TableStyle default and per-cell alignments leak away. + const int baseCellFlag = 262192; + bool hasAlignmentOverride = cell.StyleOverride != null + && cell.StyleOverride.HasData + && cell.StyleOverride.CellAlignment != default(ACadSharp.Objects.TableStyle.CellAlignmentType); + int cellFlag = baseCellFlag; + if (hasAlignmentOverride) + { + cellFlag |= 0x01; + } + this._writer.Write(91, cellFlag); + this._writer.Write(178, (short)cell.VirtualEdgeFlag); + + this._writer.Write(145, cell.Rotation); + + if (hasAlignmentOverride) + { + this._writer.Write(170, (short)cell.StyleOverride.CellAlignment); + } + + ACadSharp.Tables.TextStyle textStyle = cell.StyleOverride?.TextStyle + ?? (cell.Contents.Count > 0 ? cell.Contents[0].Format?.TextStyle : null); + if (textStyle != null) + { + this._writer.Write(7, textStyle.Name); + } + + double textHeight = 0.0; + if (cell.StyleOverride != null && cell.StyleOverride.TextHeight > 0) + { + textHeight = cell.StyleOverride.TextHeight; + } + else if (cell.Contents.Count > 0 && cell.Contents[0].Format != null && cell.Contents[0].Format.TextHeight > 0) + { + textHeight = cell.Contents[0].Format.TextHeight; + } + if (textHeight > 0) + { + this._writer.Write(140, textHeight); + } + + // Cell value state flag. AutoCAD-produced files always emit this before the + // CELL_VALUE block; omitting it causes the strict ODA-based reader to bail + // with "Invalid index". + this._writer.Write(92, 0); + + // Cell text content wrapped in the CELL_VALUE block. The reader keys on + // code 301 to enter readCadValue and exits at 304. + string textValue = null; + if (cell.Content != null && cell.Content.CadValue.Value is string s) + { + textValue = s; + } + else if (cell.Contents.Count > 0 && cell.Contents[0].CadValue.Value is string s2) + { + textValue = s2; + } + + if (!string.IsNullOrEmpty(textValue)) + { + this._writer.Write(301, "CELL_VALUE"); + this._writer.Write(93, 6); + this._writer.Write(90, (int)ACadSharp.CadValueType.String); + this._writer.Write(1, textValue); + this._writer.Write(94, 0); + this._writer.Write(300, string.Empty); + this._writer.Write(302, textValue); + this._writer.Write(304, "ACVALUE_END"); + } + } + } } private void writeInsert(Insert insert) diff --git a/src/ACadSharp/IO/Templates/CadTableEntityTemplate.CadTableCellTemplate.cs b/src/ACadSharp/IO/Templates/CadTableEntityTemplate.CadTableCellTemplate.cs index 8c837c674..cb55e34f7 100644 --- a/src/ACadSharp/IO/Templates/CadTableEntityTemplate.CadTableCellTemplate.cs +++ b/src/ACadSharp/IO/Templates/CadTableEntityTemplate.CadTableCellTemplate.cs @@ -42,6 +42,28 @@ public void Build(CadDocumentBuilder builder) { contentTemplate.Build(builder); } + + // Apply the cell-level text height read from group code 140 (or 144) so + // consumers can reach it through the regular cell content/format API + // rather than poking at the template. Without this the value parsed by + // readTableEntity is silently dropped. + if (this.FormatTextHeight.HasValue && this.FormatTextHeight.Value > 0) + { + double height = this.FormatTextHeight.Value; + if (this.Cell.StyleOverride != null && this.Cell.StyleOverride.TextHeight <= 0) + { + this.Cell.StyleOverride.TextHeight = height; + this.Cell.StyleOverride.HasData = true; + } + foreach (var content in this.Cell.Contents) + { + if (content.Format != null && content.Format.TextHeight <= 0) + { + content.Format.TextHeight = height; + content.Format.HasData = true; + } + } + } } } } \ No newline at end of file diff --git a/src/ACadSharp/IO/Templates/CadTableEntityTemplate.cs b/src/ACadSharp/IO/Templates/CadTableEntityTemplate.cs index d73294baf..8984326f9 100644 --- a/src/ACadSharp/IO/Templates/CadTableEntityTemplate.cs +++ b/src/ACadSharp/IO/Templates/CadTableEntityTemplate.cs @@ -75,6 +75,54 @@ protected override void build(CadDocumentBuilder builder) cellTemplate.Build(builder); } + // DXF stores merged cells per-cell (anchor carries the span via BorderWidth / + // BorderHeight, the rest carry MergedValue=1) but never emits the + // table-level MergedCellRanges collection that callers actually consume. Walk + // the grid here and rebuild that collection so readers see the same shape + // regardless of whether the table arrived through DXF or DWG. + int rowCount = this.TableEntity.Rows.Count; + int colCount = this.TableEntity.Columns.Count; + for (int i = 0; i < rowCount; i++) + { + var row = this.TableEntity.Rows[i]; + int cellLimit = System.Math.Min(colCount, row.Cells.Count); + for (int j = 0; j < cellLimit; j++) + { + var cell = row.Cells[j]; + int colSpan = cell.BorderWidth > 1 ? cell.BorderWidth : 1; + int rowSpan = cell.BorderHeight > 1 ? cell.BorderHeight : 1; + if (colSpan == 1 && rowSpan == 1) + { + continue; + } + + int bottomRow = System.Math.Min(rowCount - 1, i + rowSpan - 1); + int rightCol = System.Math.Min(colCount - 1, j + colSpan - 1); + + bool exists = false; + foreach (var range in this.TableEntity.MergedCellRanges) + { + if (range.TopRowIndex == i && range.LeftColumnIndex == j) + { + exists = true; + break; + } + } + if (exists) + { + continue; + } + + this.TableEntity.MergedCellRanges.Add(new TableEntity.CellRange + { + TopRowIndex = i, + LeftColumnIndex = j, + BottomRowIndex = bottomRow, + RightColumnIndex = rightCol, + }); + } + } + foreach (var component in this.CadTableComponentTemplates) { component.Build(builder, this.TableEntity.Style); From b0bd52954ec5150fb4686de9ba034ac683e79937 Mon Sep 17 00:00:00 2001 From: ilCosmico Date: Wed, 23 Sep 2026 09:48:09 +0200 Subject: [PATCH 8/9] Reword TableEntity writer comments about stricter readers --- .../DxfSectionWriterBase.Entities.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.Entities.cs b/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.Entities.cs index 52c0ead9f..8b237a34d 100644 --- a/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.Entities.cs +++ b/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.Entities.cs @@ -672,8 +672,8 @@ private void writeTableEntity(TableEntity table) // round-trippable: callers that need a single rendering should provide a // proxy graphic via Entity.PreviewGraphic. // - // Field order mirrors what AutoCAD emits so the strict ODA-based reader does - // not bail with "Invalid index" partway through the entity. + // Field order mirrors what AutoCAD emits so stricter readers do not bail with + // "Invalid index" partway through the entity. this._writer.Write(DxfCode.Subclass, DxfSubclassMarker.TableEntity); if (table.Style != null) @@ -698,7 +698,7 @@ private void writeTableEntity(TableEntity table) this._writer.Write(92, table.Columns.Count); // Override count placeholder. The exact semantics of code 93 at the table - // level are not documented in OpenDesign but AutoCAD-produced files always + // level are not documented but AutoCAD-produced files always // emit it. Emit 0 here; the reader does not error out on it. this._writer.Write(93, 0); @@ -716,8 +716,8 @@ private void writeTableEntity(TableEntity table) } // Table-level cell margins. AutoCAD reads these here even when the per-cell - // style override carries the same values; omitting them causes the strict - // ODA-based reader to abort with "Invalid index". + // style override carries the same values; omitting them causes stricter + // readers to abort with "Invalid index". double horMargin = 0.06; double verMargin = 0.06; if (table.CellStyleOverride != null @@ -808,8 +808,8 @@ private void writeTableEntity(TableEntity table) // Cell flag (state + override bits). AutoCAD-produced files always emit // 262192 baseline and set bit 0x01 when the cell-level CellAlignment // override is present; code 170 with the alignment value follows when - // that bit is set. Without bit 0x01 the strict ODA-based reader falls - // back to the TableStyle default and per-cell alignments leak away. + // that bit is set. Without bit 0x01 stricter readers fall back to the + // TableStyle default and per-cell alignments leak away. const int baseCellFlag = 262192; bool hasAlignmentOverride = cell.StyleOverride != null && cell.StyleOverride.HasData @@ -851,8 +851,8 @@ private void writeTableEntity(TableEntity table) } // Cell value state flag. AutoCAD-produced files always emit this before the - // CELL_VALUE block; omitting it causes the strict ODA-based reader to bail - // with "Invalid index". + // CELL_VALUE block; omitting it causes stricter readers to bail with + // "Invalid index". this._writer.Write(92, 0); // Cell text content wrapped in the CELL_VALUE block. The reader keys on From 5d8ee8cdadc787aefce60b19aacc06e02d9ea3d7 Mon Sep 17 00:00:00 2001 From: ilCosmico Date: Wed, 23 Sep 2026 09:48:20 +0200 Subject: [PATCH 9/9] Write every TableEntity cell value type found in AutoCAD DXF files Cells now write their CELL_VALUE through the shared writeCadValue instead of a text only block, for the value types found in AutoCAD files: unknown, long, double, string, date and point. --- .../DxfSectionWriterBase.Entities.cs | 35 ++++++++----------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.Entities.cs b/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.Entities.cs index 8b237a34d..5b29a5fe6 100644 --- a/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.Entities.cs +++ b/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.Entities.cs @@ -855,28 +855,21 @@ private void writeTableEntity(TableEntity table) // "Invalid index". this._writer.Write(92, 0); - // Cell text content wrapped in the CELL_VALUE block. The reader keys on - // code 301 to enter readCadValue and exits at 304. - string textValue = null; - if (cell.Content != null && cell.Content.CadValue.Value is string s) + // Cell value wrapped in the CELL_VALUE block. The reader keys on code 301 to + // enter readCadValue and exits at 304. Only the value types found in AutoCAD + // files are written, the encoding of the others is not known. + CadValue value = cell.Contents.Count > 0 ? cell.Contents[0].CadValue : null; + switch (value?.ValueType) { - textValue = s; - } - else if (cell.Contents.Count > 0 && cell.Contents[0].CadValue.Value is string s2) - { - textValue = s2; - } - - if (!string.IsNullOrEmpty(textValue)) - { - this._writer.Write(301, "CELL_VALUE"); - this._writer.Write(93, 6); - this._writer.Write(90, (int)ACadSharp.CadValueType.String); - this._writer.Write(1, textValue); - this._writer.Write(94, 0); - this._writer.Write(300, string.Empty); - this._writer.Write(302, textValue); - this._writer.Write(304, "ACVALUE_END"); + case CadValueType.Unknown: + case CadValueType.Long: + case CadValueType.Double: + case CadValueType.String: + case CadValueType.Date: + case CadValueType.Point3D: + this._writer.Write(301, "CELL_VALUE"); + this.writeCadValue(value); + break; } } }