diff --git a/src/ACadSharp.Tests/IO/Tangent/TchDataMapTests.cs b/src/ACadSharp.Tests/IO/Tangent/TchDataMapTests.cs
new file mode 100644
index 000000000..93841ff2f
--- /dev/null
+++ b/src/ACadSharp.Tests/IO/Tangent/TchDataMapTests.cs
@@ -0,0 +1,111 @@
+using ACadSharp.IO.DWG;
+using ACadSharp.IO.DWG.Tangent;
+using System;
+using System.Linq;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace ACadSharp.Tests.IO.Tangent
+{
+ ///
+ /// Tests for against real Tianzheng payloads.
+ ///
+ ///
+ /// The fixtures are retained custom-object payloads taken from real proxy entities of a Tianzheng
+ /// plumbing drawing (class id 575 == TCH_PIPE, 576 == TCH_PIPEFITTING). They cover both cases:
+ /// objects that carry properties and objects whose property map is empty.
+ ///
+ public class TchDataMapTests
+ {
+ private readonly ITestOutputHelper _output;
+
+ public TchDataMapTests(ITestOutputHelper output)
+ {
+ this._output = output;
+ }
+
+ [Theory]
+ [InlineData(PayloadPipeA, "PIPE_SYSTEM_NAME", "\u6d88\u9632")]
+ [InlineData(PayloadPipeA, "LINKED_DIM_TEXT", "DN25")]
+ [InlineData(PayloadPipeA, "PIPE_OUTER_DN", null)]
+ [InlineData(PayloadPipeA, "PIPE_THICK", null)]
+ public void ReadsNamedPropertiesFromRealPayload(string base64, string name, string expected)
+ {
+ TchDataMap map = read(base64);
+
+ this._output.WriteLine(string.Join(" | ", map.Entries));
+ Assert.True(map.Properties.ContainsKey(name), "missing property " + name);
+
+ if (expected != null)
+ {
+ Assert.Equal(expected, map.Properties[name]);
+ }
+ }
+
+ [Fact]
+ public void EmptyPropertyMapIsReportedAsEmpty()
+ {
+ // class id 576 == TCH_PIPEFITTING carries no properties at all
+ TchDataMap map = read(PayloadFitting);
+
+ Assert.True(map.IsEmpty);
+ Assert.Empty(map.Properties);
+ Assert.True(map.IsComplete);
+ Assert.Equal(2, map.Entries.Count);
+ Assert.Equal(
+ new[] { TchDataMap.BlockBeginMarker, TchDataMap.BlockEndMarker },
+ map.Entries);
+ }
+
+ [Fact]
+ public void DecodesEveryFixtureAndReachesTheClosingMarker()
+ {
+ foreach (string payload in new[] { PayloadPipeA, PayloadPipeB, PayloadPipeC, PayloadFitting })
+ {
+ TchDataMap map = read(payload);
+ Assert.NotNull(map);
+ Assert.True(map.IsComplete, "closing marker not reached");
+ }
+ }
+
+ [Fact]
+ public void TruncatedPayloadStopsWithoutThrowing()
+ {
+ byte[] full = Convert.FromBase64String(PayloadPipeA);
+ byte[] truncated = full.Take(full.Length / 2).ToArray();
+
+ TchDataMap map = TchDataMap.Read(truncated, ACadVersion.AC1027);
+
+ Assert.NotNull(map);
+ Assert.False(map.IsComplete);
+ }
+
+ private static TchDataMap read(string base64)
+ {
+ return TchDataMap.Read(Convert.FromBase64String(base64), ACadVersion.AC1027);
+ }
+
+ private const string PayloadPipeA =
+ "Q9MAEkATgBLAEUARABfAEQASQBNAF8AVABFAFgAVABBEQATgAyADUAQ1QAEkAUABFAF8ATwBVAFQARQBSAF8ARABOAEQUABJAFAA" +
+ "RQBfAFMAWQBTAFQARQBNAF8ATgBBAE0ARQBAohtMpZClAASQBQAEUAXwBUAEgASQBDAEsARhUAEQAQgBYAEQAQQBUAEEATQBBAFA" +
+ "AXwBCAEwATwBDAEsAXwBCAEUARwBJAE4AXwBFlQARABCAFgARABBAFQAQQBNAEEAUABfAEIATABPAEMASwBfAEUATgBEAF8AkCRA" +
+ "BOAI";
+
+ private const string PayloadPipeB =
+ "Q9MAEkATgBLAEUARABfAEQASQBNAF8AVABFAFgAVABBEQATgAyADUAQ1QAEkAUABFAF8ATwBVAFQARQBSAF8ARABOAEQUABJAFAA" +
+ "RQBfAFMAWQBTAFQARQBNAF8ATgBBAE0ARQBAohtMpZClAASQBQAEUAXwBUAEgASQBDAEsARhUAEQAQgBYAEQAQQBUAEEATQBBAFA" +
+ "AXwBCAEwATwBDAEsAXwBCAEUARwBJAE4AXwBFlQARABCAFgARABBAFQAQQBNAEEAUABfAEIATABPAEMASwBfAEUATgBEAF8AkCRA" +
+ "BOAI";
+
+ private const string PayloadPipeC =
+ "Q9MAEkATgBLAEUARABfAEQASQBNAF8AVABFAFgAVABBEQATgAyADUAQ1QAEkAUABFAF8ATwBVAFQARQBSAF8ARABOAEQUABJAFAA" +
+ "RQBfAFMAWQBTAFQARQBNAF8ATgBBAE0ARQBAohtMpZClAASQBQAEUAXwBUAEgASQBDAEsARhUAEQAQgBYAEQAQQBUAEEATQBBAFA" +
+ "AXwBCAEwATwBDAEsAXwBCAEUARwBJAE4AXwBFlQARABCAFgARABBAFQAQQBNAEEAUABfAEIATABPAEMASwBfAEUATgBEAF8AkCRA" +
+ "BOAI";
+
+ private const string PayloadFitting =
+ "RhUAEQAQgBYAEQAQQBUAEEATQBBAFAAXwBCAEwATwBDAEsAXwBCAEUARwBJAE4AXwBFlQARABCAFgARABBAFQAQQBNAEEAUABfAE" +
+ "IATABPAEMASwBfAEUATgBEAF8AqM";
+
+ }
+}
diff --git a/src/ACadSharp.Tests/IO/Tangent/TchObjectTests.cs b/src/ACadSharp.Tests/IO/Tangent/TchObjectTests.cs
new file mode 100644
index 000000000..3107af653
--- /dev/null
+++ b/src/ACadSharp.Tests/IO/Tangent/TchObjectTests.cs
@@ -0,0 +1,785 @@
+using ACadSharp;
+using ACadSharp.IO.DWG.Tangent;
+using System;
+using System.Collections.Generic;
+using Xunit;
+
+namespace ACadSharp.Tests.IO.Tangent
+{
+ ///
+ /// Tests against payloads captured from a drawing that contains native Tianzheng objects
+ /// (TZ.dwg, contributed in issue #1131). Each fixture is the class specific data of one
+ /// object, taken byte for byte from UnknownEntity.RawData.
+ ///
+ public class TchObjectTests
+ {
+ //TCH_TEXT, handle 828, expected position verified against Tianzheng's own flattened export
+ private const string TextPayload =
+"ChGQVQWQOQOBBEwC9uR0z13ECPgOLDkLIeNASZ7AEYAOABGADAARgAxADIARgAtADkAMwA4ADkALQA0ADkARgBDAC0AOAAzA" +
+"DUARAAtADgAQwA4ADYAQwA2AEUARQBFAEUARABCAH0ASzhAOgA4QDmAOEA5wDhAOUA4QDlAOAA6QDhAOUA4QDlAOEA5QDhAO" +
+"UA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUASjhAOkA4QDlAOAA+ADgAOkA4QDlAOE" +
+"A5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAEG4QDmAOEA5QDhAOQA" +
+"RhUAEQAQgBYAEQAQQBUAEEATQBBAFAAXwBCAEwATwBDAEsAXwBCAEUARwBJAE4AXwBBVQAMgAwAGQAMQBBVQAMgAwAGQAMgB" +
+"BVQAMgAwAGQAMwBBVQAMgAwAGQANABFlQARABCAFgARABBAFQAQQBNAEEAUABfAEIATABPAEMASwBfAEUATgBEAF8ASZ7AEI" +
+"AMAA3ADEAMwBFADYAQgAtAEEARgBGADYALQA0ADgANwAxAC0AQQAyADcAMwAtADcAOAA5ADYAOAAzADQARAA2ADYANwA2AH0" +
+"AQbhAOUA4QDlAOEA5gBFFcAYQBsAGwAIAAmACAAVwBpAG4AZABvAHcAIAAmACAARABvAG8AcgBKOEA6ADhAOQA4QDmAOAA6Q" +
+"DhAOoA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUASjhAOYA4QD" +
+"lAOEA5QDgAOkA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QD" +
+"lAEG4QDpAOEA6ADhAOYAQbhAOMA4QDlAOEA4wCKFqkBRSiIoCB/f";
+
+ //TCH_WALL, handle 833
+ private const string WallPayload =
+"ChGQOQOQUAAAAAAAAWUBBQAAAAAAABZQEIQhCEDkIQMAAEAAEVQMAAEAAAwRCSC4AP/XtxAjkZsiqwNPhQAkguAD/17cQISL" +
+"nXOR7X50BJnsARgA4AEYAMABGADEAMgBGAC0AOQAzADgAOQAtADQAOQBGAEMALQA4ADMANQBEAC0AOABDADgANgBDADYARQB" +
+"FAEUARQBEAEIAfQBLOEA5gDhAOYA4QDoAOEA5QDhAOUA4ADpAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDl" +
+"AOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QBKOEA6gDhAOUA4QDnAOAA6QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhA" +
+"OUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUAQbgAPcA4QDkAOEA4wBBuEA6ADhAOoA4QDqAEG4QDjAOEA" +
+"5ADgAPgARhUAEQAQgBYAEQAQQBUAEEATQBBAFAAXwBCAEwATwBDAEsAXwBCAEUARwBJAE4AXwBBVQAMgAwAGIAMgBBVQAMgA" +
+"wAGIAMwBBlQAMgAwAGQAMQAyAEFVAAyADAAZAA3AEFVAAyADAAcwAxAECQQCnfkGVAAyADAAcwAxADQAQEWeEFVAAyADAAcw" +
+"A0AEDbmYakBZ4QVUADIAMABzADgAQVUADIAMABzADkAQPdTyluQlxBlQAMgAwAHUAMQAxAEGVAAyADAAdQAxADYAQVUADIAM" +
+"AB1ADYARZUAEQAQgBYAEQAQQBUAEEATQBBAFAAXwBCAEwATwBDAEsAXwBFAE4ARABfAEmewBCADAANwAxADMARQA2AEIALQB" +
+"BAEYARgA2AC0ANAA4ADcAMQAtAEEAMgA3ADMALQA3ADgAOQA2ADgAMwA0AEQANgA2ADcANgB9AEG4QDqAOEA5QDhAOQASjhA" +
+"OUA4QDlAOEA6ADgAOkA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhA" +
+"OUA4QDlAEG4AD4AOEA5QDhAOQASzhAOYA4QDmAOAA+ADhAOUA4QDlAOAA6QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOU" +
+"A4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUASzhAOkA4QDmAOEA5ADhAOUA4QDlAOAA6QDhAOUA4QDlAOEA" +
+"5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUAS7hAOgA4QDkAOEA6gDhAOUA4" +
+"QDlAOEA5QDgAOkA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4" +
+"QDlAEG4QDqAOEA5QDhAOcAQbhAOgA4QDjAOEA6QBCOEA6ADhAOkA4AD4AOEA5QBBuEA4wDhAOUA4QDpAEq4QDmAOAA9wDhAO" +
+"YA4QDlAOAA6QDhAOUA4AD3AOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5gDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5gDgAP" +
+"gASjhAOYA4QDlAOAA9wDgAOkA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOE" +
+"A5QDhAOUA4QDlAEo4QDmAOEA5QDgAPgA4ADpAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA" +
+"4QDlAOEA5QDhAOUA4QDlAOEA5QBKOEA5QDhAOUA4QDoAOAA6QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5" +
+"QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUASjhAOoA4QDlAOEA6QDgAOkA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4Q" +
+"DlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAEG4QDmAOEA5QDhAOgAQbhAOgA4QDlAOEA5ACSjgAPcA4Q" +
+"DlAOEA5gDgAOkA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4Q" +
+"DlAEG4QDlAOEA5QDhAOcAyjGpATOpAVkhAZ2gIQHWIQHWICAhAa6hAcuoKCkBRykBNqAgICB0A==";
+
+ //TCH_COLUMN, handle 827
+ private const string ColumnPayload =
+"ChGQOQhCEDAABAABBQAAAAAAABZQEICBEJILgA/9e3ECORmyKpAruFAAUE5GbIqsCI4UAkguAD/xPcQLkZsiqwNPhQCSC4AP" +
+"/E9xAuRmyKrA0+FAJILgA/+p3EC5GbIqsCI4UAkguAD/6ncQJJnsAMgBFADAAMABDADIARgA4AC0ARgA2ADUANAAtADQAOQA" +
+"yADgALQBBAEYAMwBEAC0AMABFAEUANgA0ADUANQA1ADgANAAyADMAfQBLOAA+ADhAOYA4QDlAOEA5QDhAOUA4ADpAOEA5QDh" +
+"AOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QBKOEA6QDhAOUA4AD3A" +
+"OAA6QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUAQbhAO" +
+"gA4QDkAOEA5ABBuEA5wDhAOoA4QDkAEG4QDjAOEA5ADhAOgARhUAEQAQgBYAEQAQQBUAEEATQBBAFAAXwBCAEwATwBDAEsAX" +
+"wBCAEUARwBJAE4AXwBBVQAMgAwAGIAMABBVQAMgAwAHMAMQBBaKUS3v3bd1RH1dBVQAMgAwAHMAMgBCVAAVQBCAF8ASABBAF" +
+"QAQwBIAEFVAAyADAAcwAzAEFVAAyADAAcwA0AEFVAAyADAAcwA1AEDopRLezx4RZUAEQAQgBYAEQAQQBUAEEATQBBAFAAXwB" +
+"CAEwATwBDAEsAXwBFAE4ARABfAEmewBCADAANwAxADMARQA2AEIALQBBAEYARgA2AC0ANAA4ADcAMQAtAEEAMgA3ADMALQA3" +
+"ADgAOQA2ADgAMwA0AEQANgA2ADcANgB9AEG4QDoAOEA5QDhAOQAS7hAOYA4QDkAOAA9wDhAOUA4QDlAOEA5QDgAOkA4QDlAO" +
+"EA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAEG4QDoAOEA5gDhAOQ" +
+"AQbgAPcA4QDjAOEA4wBCOEA4wDhAOcA4AD4AOEA5QBBuEA5wDhAOUA4QDnAEG4AD4AOEA5QDhAOMASrhAOcA4AD3AOEA5QDh" +
+"AOUA4ADpAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QBK" +
+"OEA5QDhAOUA4QDlAOAA6QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlA" +
+"OEA5QDhAOUApiHKkBKqEBXiEB1ikBRykBNvm";
+
+ //TCH_CUT, handle 1225, an outline with one arc edge
+ private const string CutPayload =
+"ChGAhEBQQk2ZXF13XuQBin/6kEAdpAYC989e6oe9AGKf/qQQB2kCgL3z17qh70BQ0hKqpMDaQIk2ZXF13XuQFDSEqqkwNpAk" +
+"mewAyAEUAMAAwAEMAMgBGADgALQBGADYANQA0AC0ANAA5ADIAOAAtAEEARgAzAEQALQAwAEUARQA2ADQANQA1ADUAOAA0ADI" +
+"AMwB9AEs4QDqAOEA5gDhAOgA4QDlAOEA5QDgAOkA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA" +
+"5QDhAOUA4QDlAOEA5QDhAOUA4QDlAEo4QDmAOEA5QDhAOgA4ADpAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4" +
+"QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QBBuEA5QDhAOMA4QDlAEG4QDkAOEA5ADhAOYARhUAEQAQgBYAEQAQQBU" +
+"AEEATQBBAFAAXwBCAEwATwBDAEsAXwBCAEUARwBJAE4AXwBFlQARABCAFgARABBAFQAQQBNAEEAUABfAEIATABPAEMASwBfA" +
+"EUATgBEAF8ASZ7AEIAMAA3ADEAMwBFADYAQgAtAEEARgBGADYALQA0ADgANwAxAC0AQQAyADcAMwAtADcAOAA5ADYAOAAzAD" +
+"QARAA2ADYANwA2AH0AQbhAOMA4QDlAOEA6gBBuEA6gDhAOYA4AD3AEG4AD4AOEA5QDhAOMAQbgAPgA4QDlAOEA6gBBuEA5gD" +
+"hAOUA4QDnAEG4QDjAOEA5QDhAOMAbBCpAURR";
+
+ //TCH_WALL, handle 700
+ private const string WallFixture =
+"ChGQOQOQUAAAAAAAAWUBBQAAAAAAABZQEIQhCEDkIQMAAEAAEVQMAAEAAAwRCSC4AP/XtxAjkZsiqwNPhQAkguAD/17cQISL" +
+"nXOR7X50BJnsARgA4AEYAMABGADEAMgBGAC0AOQAzADgAOQAtADQAOQBGAEMALQA4ADMANQBEAC0AOABDADgANgBDADYARQB" +
+"FAEUARQBEAEIAfQBLOEA5gDhAOYA4QDoAOEA5QDhAOUA4ADpAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDl" +
+"AOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QBKOEA6gDhAOUA4QDnAOAA6QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhA" +
+"OUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUAQbgAPcA4QDkAOEA4wBBuEA6ADhAOoA4QDqAEG4QDjAOEA" +
+"5ADgAPgARhUAEQAQgBYAEQAQQBUAEEATQBBAFAAXwBCAEwATwBDAEsAXwBCAEUARwBJAE4AXwBBVQAMgAwAGIAMgBBVQAMgA" +
+"wAGIAMwBBlQAMgAwAGQAMQAyAEFVAAyADAAZAA3AEFVAAyADAAcwAxAECQQCnfkGVAAyADAAcwAxADQAQEWeEFVAAyADAAcw" +
+"A0AEDbmYakBZ4QVUADIAMABzADgAQVUADIAMABzADkAQPdTyluQlxBlQAMgAwAHUAMQAxAEGVAAyADAAdQAxADYAQVUADIAM" +
+"AB1ADYARZUAEQAQgBYAEQAQQBUAEEATQBBAFAAXwBCAEwATwBDAEsAXwBFAE4ARABfAEmewBCADAANwAxADMARQA2AEIALQB" +
+"BAEYARgA2AC0ANAA4ADcAMQAtAEEAMgA3ADMALQA3ADgAOQA2ADgAMwA0AEQANgA2ADcANgB9AEG4QDqAOEA5QDhAOQASjhA" +
+"OUA4QDlAOEA6ADgAOkA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhA" +
+"OUA4QDlAEG4AD4AOEA5QDhAOQASzhAOYA4QDmAOAA+ADhAOUA4QDlAOAA6QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOU" +
+"A4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUASzhAOkA4QDmAOEA5ADhAOUA4QDlAOAA6QDhAOUA4QDlAOEA" +
+"5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUAS7hAOgA4QDkAOEA6gDhAOUA4" +
+"QDlAOEA5QDgAOkA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4" +
+"QDlAEG4QDqAOEA5QDhAOcAQbhAOgA4QDjAOEA6QBCOEA6ADhAOkA4AD4AOEA5QBBuEA4wDhAOUA4QDpAEq4QDmAOAA9wDhAO" +
+"YA4QDlAOAA6QDhAOUA4AD3AOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5gDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5gDgAP" +
+"gASjhAOYA4QDlAOAA9wDgAOkA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOE" +
+"A5QDhAOUA4QDlAEo4QDmAOEA5QDgAPgA4ADpAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA" +
+"4QDlAOEA5QDhAOUA4QDlAOEA5QBKOEA5QDhAOUA4QDoAOAA6QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5" +
+"QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUASjhAOoA4QDlAOEA6QDgAOkA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4Q" +
+"DlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAEG4QDmAOEA5QDhAOgAQbhAOgA4QDlAOEA5ACSjgAPcA4Q" +
+"DlAOEA5gDgAOkA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4Q" +
+"DlAEG4QDlAOEA5QDhAOcAyjGpATOpAVkhAZ2gIQHWIQHWICAhAa6hAcuoKCkBRykBNqAgICB0A==";
+
+ //TCH_OPENING, handle 861
+ private const string OpeningFixture =
+"ChFAVlBhEJILgA/9e3ECCnUKe7V2uVACSC4AM/AdxAgp1Cnu1drlQEmewBEAEYANwAwADgAQgAwADEALQBFAEYAOQAwAC0AN" +
+"AA5ADkAMgAtAEIAOABDADUALQAyAEIAOAA2AEEANwBGADkARAA1ADEANgB9AEs4QDkAOEA5gDgAPcA4QDlAOEA5QDgAOkA4Q" +
+"DlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAEo4QDpAOEA5QD" +
+"hAOUA4ADpAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QB" +
+"BuEA6ADhAOQA4QDlAEG4QDnAOEA6gDhAOUAQbhAOUA4QDkAOEA5wBBOiVl3oodRSQQNuZhqQ6JVGFQARABCAFgARABBAFQAQ" +
+"QBNAEEAUABfAEIATABPAEMASwBfAEIARQBHAEkATgBfAEWVABEAEIAWABEAEEAVABBAE0AQQBQAF8AQgBMAE8AQwBLAF8ARQ" +
+"BOAEQAXwBJnsAQgAwADcAMQAzAEUANgBCAC0AQQBGAEYANgAtADQAOAA3ADEALQBBADIANwAzAC0ANwA4ADkANgA4ADMANAB" +
+"EADYANgA3ADYAfQBBuEA5QDhAOUA4AD3AEs4QDnAOAA9wDhAOcA4QDlAOEA5QDgAOkA4QDlAOEA5QDhAOUA4QDlAOEA5QDhA" +
+"OUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAEs4QDmAOEA5gDhAOMA4QDlAOEA5QDgAOkA4QDlAO" +
+"EA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAEu4AD3AOEA4wDhAOQ" +
+"A4QDmAOEA5QDhAOUA4ADpAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOU" +
+"A4QDlAOEA5QBKOEA4wDhAOUA4QDqAOAA6QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA" +
+"5QDhAOUA4QDlAOEA5QDhAOUAQbgAPcA4QDmAOAA+ABBuEA6gDhAOUA4QDmAEo4QDnAOEA5QDhAOcA4ADpAOEA5QDhAOUA4QD" +
+"lAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QCQbgAPcA4QDlAOAA+ABKOEA5Q" +
+"DhAOQA4QDoAOAA6QDhAOoA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5Q" +
+"DhAOUASzhAOoA4QDmAOEA5gDhAOUA4QDlAOAA6QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QD" +
+"lAOEA5QDhAOUA4QDlAOEA5QDhAOUASrhAOkA4QDpAOEA6gDhAOUA4ADpAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDh" +
+"AOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QBKuEA4wDhAOkA4QDqAOEA5QDgAOkA4QDlAOEA5QDhAOUA4QDlA" +
+"OEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAEG4AD3AOEA5QDhAOoAQbhAOcA4QDlAOA" +
+"A+ABBuEA6ADhAOgA4AD4AEG4QDmAOEA5QDhAOUAko4QDnAOEA5QDhAOUA4ADpAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOE" +
+"A5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QBKOEA4wDhAOUA4QDkAOAA6QDhAOUA4QDlAOEA5QDhAOUA" +
+"4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUASjhAOgA4QDlAOEA5QDgAOkA4QDlAOEA5" +
+"QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAEo4QDlAOEA5QDgAPgA4A" +
+"DpAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QBLOAA+AD" +
+"hAOYA4QDoAOEA5QDhAOUA4ADpAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QD" +
+"hAOUA4QDlAOEA5QBeN6kBKykBn6kBbqEBXikBYykBSCkBZCkBY6kBM/r";
+
+ //TCH_ELEVATION, handle 1232
+ private const string ElevationFixture =
+"ChFAUGQEEEQUR0DuJAUyZtkCsxm7jPlfkQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASZ7ADgAMAA2ADAARAAy" +
+"ADgAMgAtADMARABGADIALQA0ADkARQBEAC0AOABCADAAMQAtAEQAQwA0ADEAOAA0AEUAMQBEADMAMQA3AH0ASzhAOQA4QDmA" +
+"OEA6QDhAOUA4QDlAOAA6QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlA" +
+"OEA5QDhAOUASjgAPcA4QDlAOEA5gDgAOkA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAO" +
+"UA4QDlAOEA5QDhAOUA4QDlAEG4QDqAOEA5QDgAPgAQcHaNia6GzKkShX81OnT0YVABEAEIAWABEAEEAVABBAE0AQQBQAF8AQ" +
+"gBMAE8AQwBLAF8AQgBFAEcASQBOAF8ARZUAEQAQgBYAEQAQQBUAEEATQBBAFAAXwBCAEwATwBDAEsAXwBFAE4ARABfAEmewB" +
+"CADAANwAxADMARQA2AEIALQBBAEYARgA2AC0ANAA4ADcAMQAtAEEAMgA3ADMALQA3ADgAOQA2ADgAMwA0AEQANgA2ADcANgB" +
+"9AEG4QDjAOEA5QDhAOYASjhAOQA4QDkAOEA6ADgAOkA4QDqAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlA" +
+"OEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAEo4QDkAOEA5QDhAOgA4ADpAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAO" +
+"UA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QBBuEA5gDhAOgA4QDnAEI4QDqAOEA5gDhAOUA4QDjAEIMgAyAC4AN" +
+"gA2ADYANgA5AEq4QDmAOEA5gDhAOYA4QDjAOAA6QDhAOkA4AD3AOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4Q" +
+"DlAOEA5QDhAOUA4QDlAOEA5QDhAOkASjgAPcA4QDoAOEA4wDgAOkA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QD" +
+"hAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAEo4QDmAOEA6ADhAOkA4ADpAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDl" +
+"AOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QBMOAA+ADgAOwA4QDjAOEA5gDhAOUA4QDlAOEA5QDgA" +
+"OkA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAEK4QDnAO" +
+"EA5gDgAPcA4QDnAOEA6ACQbhAOMA4QDlAOEA5gDaIKkBQikBWCkBQnA=";
+
+ //TCH_INDEXPOINTER, handle 936
+ private const string IndexPointerFixture =
+"ChFAkCAHQBBEFESLExEaLY9xA3AqZ9idM5kAUWKMixvTZQATxktS8sOZAAJJnsAMgBFADAAMABDADIARgA4AC0ARgA2ADUAN" +
+"AAtADQAOQAyADgALQBBAEYAMwBEAC0AMABFAEUANgA0ADUANQA1ADgANAAyADMAfQBLOEA6ADhAOYA4QDnAOEA5QDhAOUA4A" +
+"DpAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QBKOEA6QD" +
+"hAOUA4AD3AOAA6QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QD" +
+"hAOUAQbhAOcA4QDlAOEA6QBBQdo6GxNT25/uXBCAdo6GyHZVdbN2gPX0kARABGFQARABCAFgARABBAFQAQQBNAEEAUABfAEI" +
+"ATABPAEMASwBfAEIARQBHAEkATgBfAEWVABEAEIAWABEAEEAVABBAE0AQQBQAF8AQgBMAE8AQwBLAF8ARQBOAEQAXwBJnsAQ" +
+"gAwADcAMQAzAEUANgBCAC0AQQBGAEYANgAtADQAOAA3ADEALQBBADIANwAzAC0ANwA4ADkANgA4ADMANABEADYANgA3ADYAf" +
+"QBBuEA5gDhAOUA4QDoAEo4AD4AOEA5ADhAOYA4ADpAOEA6gDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QD" +
+"hAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QBKOEA5wDhAOUA4QDnAOAA6QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDl" +
+"AOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUAQbgAPcA4QDoAOAA9wBBuAA9wDhAOYA4QDkAEo4QDpAOEA6gDgAPg" +
+"A4ADpAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QBKOEA" +
+"6ADhAOgA4QDlAOAA6QDgAPgA4AD3AOEA5gDhAOYA4QDlAOEA5gDgAPgA4QDnAOEA5wDhAOcA4QDoAOEA5wDhAOcA4QDqAOEA" +
+"5gDhAOkASjhAOkA4QDqAOEA6QDgAOkA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4" +
+"QDlAOEA5QDhAOUA4QDlAEBMQBAS0ApBuEA4wDhAOUA4QDjAEG4QDlAOEA5QDgAPgATDhAOMA4ADsAOEA5wDhAOYA4QDlAOEA" +
+"5QDhAOUA4ADpAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA" +
+"5QBKOEA5QDhAOUA4AD4AOAA6QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4" +
+"QDlAOEA5QDhAOUAQbgAPgA4QDkAOAA9wBKOEA5gDhAOUA4QDqAOAA6QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QD" +
+"lAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUAvAlqQFDKQFYKQHSKQFDKH0=";
+
+ //TCH_SYMB_SECTION, handle 937
+ private const string SectionFixture =
+"ChGQOQOBBEDEeJcinLiM91A/MeFGsyK60Cg+G/jcubZQPzHhRrMiutAexSuR+F6ND17FK5H4XokvXsUrkfhejQ9exSuR+F6J" +
+"L0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEmewBEAEYANwAwADgAQgAwADEALQBFAEYAOQAwAC0ANAA5ADkAMg" +
+"AtAEIAOABDADUALQAyAEIAOAA2AEEANwBGADkARAA1ADEANgB9AEs4AD4AOEA5gDhAOUA4QDlAOEA5QDgAOkA4QDlAOEA5QD" +
+"hAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAEo4QDoAOEA5QDgAPgA4ADp" +
+"AOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QBBuEA5gDhA" +
+"OUA4QDmAEYVABEAEIAWABEAEEAVABBAE0AQQBQAF8AQgBMAE8AQwBLAF8AQgBFAEcASQBOAF8AQZUADIAMABkADMANQBBlQA" +
+"MgAwAGQAMwA3AEWVABEAEIAWABEAEEAVABBAE0AQQBQAF8AQgBMAE8AQwBLAF8ARQBOAEQAXwBJnsAQgAwADcAMQAzAEUANg" +
+"BCAC0AQQBGAEYANgAtADQAOAA3ADEALQBBADIANwAzAC0ANwA4ADkANgA4ADMANABEADYANgA3ADYAfQBBuEA6QDhAOUA4AD" +
+"4AEo4QDpAOEA6gDgAPgA4ADpAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDh" +
+"AOUA4QDlAOEA5QBKOEA5gDhAOUA4QDjAOAA6QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlA" +
+"OEA5QDhAOUA4QDlAOEA5QDhAOUAQbgAPcA4QDoAOEA5wBBuEA5gDhAOYA4QDqAEo4QDmAOEA5ADhAOMA4ADpAOEA5gDhAOkA" +
+"4QDmAOEA6gDgAPgA4QDjAOEA5wDhAOoA4QDkAOEA6gDgAPcA4AD4AOEA6ADgAPgA4QDkAOEA5gBBuEA6gDhAOMA4AD3AEBMQ" +
+"BA0EAMAAxAEo4QDlAOEA5ADgAPgA4ADpAOEA6gDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDl" +
+"AOEA5QDhAOUA4QDlAOEA5QBBuAA9wDhAOUA4QDpAEw4AD4AOAA7ADhAOcA4QDmAOEA5QDhAOUA4QDlAOAA6QDhAOUA4QDlAO" +
+"EA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUASjhAOMA4QDlAOAA9wDgAOk" +
+"A4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAEG4QDlAOEA" +
+"5QDhAOoAQrhAOoA4QDmAOEA4wDhAOUA4AD3AEo4QDlAOEA5ADhAOYA4ADpAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5Q" +
+"DhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QBKuEA6QDhAOYA4QDqAOEA5QDgAOkA4QDlAOEA5QDhAOUA4QD" +
+"lAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAEG4AD3AOEA4wDhAOoASjgAPgA4QDlA" +
+"OEA5ADgAOkA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlA" +
+"LgpqQFEKQHSKQFEKQFYf+g==";
+
+ //TCH_RECTSTAIR, handle 956
+ private const string StairFixture =
+"ChFAUHAkAAMMDEQcRObK2JiQ59xAjAYfPrhXnwQEmewA4ADAANgAwAEQAMgA4ADIALQAzAEQARgAyAC0ANAA5AEUARAAtADg" +
+"AQgAwADEALQBEAEMANAAxADgANABFADEARAAzADEANwB9AEs4QDpAOEA5gDhAOoA4QDlAOEA5QDgAOkA4QDlAOEA5QDhAOUA" +
+"4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAEo4QDoAOEA5QDhAOYA4ADpAOEA5" +
+"QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QBBuEA6QDhAOYA4A" +
+"D3AEG4QDmAOEA6gDgAPgAQZrWEVRH3dpX5yYcoJGFQARABCAFgARABBAFQAQQBNAEEAUABfAEIATABPAEMASwBfAEIARQBHA" +
+"EkATgBfAEWVABEAEIAWABEAEEAVABBAE0AQQBQAF8AQgBMAE8AQwBLAF8ARQBOAEQAXwBJnsAQgAwADcAMQAzAEUANgBCAC0" +
+"AQQBGAEYANgAtADQAOAA3ADEALQBBADIANwAzAC0ANwA4ADkANgA4ADMANABEADYANgA3ADYAfQBBuEA6ADhAOUA4QDpAEo4" +
+"QDpAOEA5QDhAOUA4ADpAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4" +
+"QDlAOEA5QBLOEA6ADhAOYA4QDpAOEA6gDhAOUA4ADpAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5Q" +
+"DhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QBLOEA5wDhAOMA4QDmAOEA6ADhAOUA4ADpAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QD" +
+"lAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QBLuAA9wDhAOYA4QDnAOEA4wDhAOQA4QDlAOAA6QDh" +
+"AOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUAS7hAOYA4QDmA" +
+"OEA5wDhAOMA4QDlAOEA5QDgAOkA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlA" +
+"OEA5QDhAOUA4QDlAEI4QDpAOEA5gDhAOUA4QDlAEI4QDqAOEA5gDhAOQA4QDlAEu4QDjAOEA4wDhAOcA4QDqAOEA5wDhAOUA" +
+"4ADpAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QBLOEA6" +
+"ADgAPgA4QDmAOEA5QDhAOUA4ADpAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5" +
+"QDhAOUA4QDlAOEA5QBBuEA6QDhAOYA4QDoAEG4QDmAOEA5QDhAOUAQbhAOkA4QDjAOEA5ABKuEA5wDhAOcA4QDqAOEA5QDgA" +
+"OkA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAEo4QDjAO" +
+"EA5QDhAOcA4ADpAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAO" +
+"EA5QBEOEA6gDhAOQA4QDpAOAA+ADhAOkA4QDoAOEA6gDhAOgASzhAOMA4QDjAOEA5gDhAOUA4QDlAOAA6QDhAOUA4QDlAOEA" +
+"5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUASzhAOUA4QDmAOEA5QDhAOMA4" +
+"QDlAOAA6QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUAS" +
+"zhAOcA4QDmAOEA6gDhAOMA4QDlAOAA6QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5Q" +
+"DhAOUA4QDlAOEA5QDhAOUASzhAOYA4QDpAOEA6gDhAOgA4QDoAOAA6QDhAOgA4QDkAOAA9wDhAOcA4QDqAOAA9wDhAOUA4QD" +
+"jAOEA5wDgAPcA4QDoAOEA6ADgAPcA4QDjAOAA9wDgAPgASzhAOcA4AD3AOEA6gDhAOgA4QDjAOAA6QDhAOMA4QDnAOEA5gDh" +
+"AOQA4QDpAOEA5gDgAPgA4QDoAOEA5ADhAOYA4QDjAOEA4wDhAOYA4QDoAOEA5gDhAOYAQbhAOUA4QDlAOEA5wBKuEA4wDhAO" +
+"cA4QDpAOEA5QDgAOkA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAO" +
+"UA4QDlAEq4QDoAOEA5wDhAOkA4QDlAOAA6QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOE" +
+"A5QDhAOUA4QDlAOEA5QDhAOUASzhAOkA4QDnAOAA9wDhAOgA4QDqAOAA6QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA" +
+"4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUASjhAOYA4QDlAOAA+ADgAOkA4QDqAOEA4wDhAOQA4QDqAOAA+" +
+"ADgAPcA4QDoAOEA6ADhAOoA4QDqAOAA+ADgAPcA4QDjAOAA+ADgAPcA4AD3AEo4AD4AOEA5ADhAOMA4ADpAOEA6gDhAOUA4Q" +
+"DlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QBLuEA6ADhAOYA4QDnAOEA4wD" +
+"hAOUA4QDlAOAA6QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QD" +
+"hAOUAS7gAPgA4QDmAOAA9wDhAOMA4QDlAOEA5QDgAOkA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDl" +
+"AOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAEu4QDmAOEA5gDgAPcA4QDjAOEA5QDhAOUA4ADpAOEA5QDhAOUA4QDlAOEA5QDhA" +
+"OUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QBLuEA6QDhAOYA4QDjAOEA4wDhAOUA4QDlAO" +
+"AA6QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUAQbhAOk" +
+"A4AD4AOEA5ACaUqkBOCApATqoKCkBOikBRSkBWCkBRH6";
+
+ //TCH_DIMENSION2, handle 921
+ private const string DimensionFixture =
+"ChGQUQDIiGcFBiMACEQkguAD/0XcQIqI/r/2H34kBJnsAMgBFADAAMABDADIARgA4AC0ARgA2ADUANAAtADQAOQAyADgALQB" +
+"BAEYAMwBEAC0AMABFAEUANgA0ADUANQA1ADgANAAyADMAfQBLOEA6QDhAOYA4QDmAOEA5QDhAOUA4ADpAOEA5QDhAOUA4QDl" +
+"AOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QBKOEA5wDhAOUA4QDmAOAA6QDhA" +
+"OUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUAQbhAOYA4QDlAO" +
+"AA9wBGFQARABCAFgARABBAFQAQQBNAEEAUABfAEIATABPAEMASwBfAEIARQBHAEkATgBfAEXVAAyADAAVgA5AF8AZABSAGUA" +
+"YQBsAEQAaQBzAHQAMgBEAGkAbQBMAGkAbgBlAEWVABEAEIAWABEAEEAVABBAE0AQQBQAF8AQgBMAE8AQwBLAF8ARQBOAEQAX" +
+"wBJnsAQgAwADcAMQAzAEUANgBCAC0AQQBGAEYANgAtADQAOAA3ADEALQBBADIANwAzAC0ANwA4ADkANgA4ADMANABEADYANg" +
+"A3ADYAfQBBuEA6gDhAOUA4QDjAEo4AD3AOEA5wDhAOoA4ADpAOEA4wDgAPcA4QDkAOEA5gDgAPcA4QDqAOEA5ADhAOUA4QDo" +
+"AOEA5gDhAOgA4AD4AOEA6gDgAPcA4QDnAOEA4wBKuEA4wDgAOwA4QDpAOEA6ADgAOkA4QDoAOEA5wDhAOMA4QDmAOAA+ADhA" +
+"OMA4AD3AOEA5wDhAOMA4QDmAOAA+ADhAOgA4QDpAOAA9wDhAOUA4QDkAEG4QDjAOEA6gDhAOkAS7hAOMA4QDmAOEA5ADhAOY" +
+"A4QDnAOEA5ADgAOkA4QDjAOEA6gDhAOYA4QDoAOEA5QDhAOgA4QDmAOEA5ADgAPcA4QDkAOEA5gDgAPgA4QDlAOEA5ADhAOk" +
+"A4QDmAEu4QDlAOEA5gDhAOMA4QDjAOEA5QDhAOUA4ADpAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA" +
+"5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QBLuEA6QDhAOQA4QDlAOEA5gDhAOQA4QDnAOAA6QDhAOMA4QDqAOEA5wDhAOQA4" +
+"QDoAOEA6QDhAOUA4QDpAOEA6QDhAOoA4QDoAOEA5wDhAOcA4QDqAOAA+ADgAPcASzhAOcA4AD3AOEA6QDhAOYA4QDnAOAA6Q" +
+"DhAOYA4QDmAOEA6QDhAOgA4QDpAOEA5wDhAOUA4QDoAOEA6QDgAPcA4AD4AOEA6ADgAPgA4AD4AOEA5ADgAPcAS7gAPgA4QD" +
+"kAOEA6gDhAOcA4QDnAOEA5wDgAOkA4QDjAOEA6gDgAPgA4QDjAOEA6QDhAOQA4QDkAOEA5wDhAOYA4QDmAOEA5gDhAOQA4QD" +
+"pAOEA6ADhAOcA4QDnAEG4QDmAOEA5QDhAOYA5COpAUGpAcxy";
+
+ //TCH_TEXT, handle 829
+ private const string SquareColumnFixture =
+"ChGQVQWQOQOBBEbBi4iLTH3ECBSlmpfmkOFASZ7AEYAOABGADAARgAxADIARgAtADkAMwA4ADkALQA0ADkARgBDAC0AOAAzA" +
+"DUARAAtADgAQwA4ADYAQwA2AEUARQBFAEUARABCAH0ASzhAOUA4QDmAOEA5ADhAOUA4QDlAOAA6QDhAOUA4QDlAOEA5QDhAO" +
+"UA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUASjhAOkA4QDlAOAA+ADgAOkA4QDlAOE" +
+"A5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAEG4AD4AOEA5QDgAPcA" +
+"RhUAEQAQgBYAEQAQQBUAEEATQBBAFAAXwBCAEwATwBDAEsAXwBCAEUARwBJAE4AXwBBVQAMgAwAGQAMQBBVQAMgAwAGQAMgB" +
+"BVQAMgAwAGQAMwBBVQAMgAwAGQANABFlQARABCAFgARABBAFQAQQBNAEEAUABfAEIATABPAEMASwBfAEUATgBEAF8ASZ7AEI" +
+"AMAA3ADEAMwBFADYAQgAtAEEARgBGADYALQA0ADgANwAxAC0AQQAyADcAMwAtADcAOAA5ADYAOAAzADQARAA2ADYANwA2AH0" +
+"AQbgAPcA4QDlAOEA6gBDVMAcQB1AGEAcgBlACAAQwBvAGwAdQBtAG4ASjhAOkA4QDkAOAA9wDgAOkA4QDqAOEA5QDhAOUA4Q" +
+"DlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAEo4AD3AOEA5QDhAOoA4ADpAOEA5QD" +
+"hAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QBBuEA4wDhAOgA4QDk" +
+"AEG4QDlAOEA5QDhAOcAGhapAUUoiKAgf7w==";
+
+ //TCH_MTEXT, handle 949
+ private const string MTextFixture =
+"ChGQVQWQOQOBBEhF+0xNYe1kCEJmZ7s+5OVAAhFJnsARABGADcAMAA4AEIAMAAxAC0ARQBGADkAMAAtADQAOQA5ADIALQBCA" +
+"DgAQwA1AC0AMgBCADgANgBBADcARgA5AEQANQAxADYAfQBLOEA5gDhAOYA4QDqAOEA5QDhAOUA4ADpAOEA5QDhAOUA4QDlAO" +
+"EA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QBKOEA4wDhAOUA4QDqAOAA6QDhAOU" +
+"A4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUAQbgAPgA4QDlAOEA" +
+"4wBGFQARABCAFgARABBAFQAQQBNAEEAUABfAEIATABPAEMASwBfAEIARQBHAEkATgBfAEFVAAyADAAZAAxAEFVAAyADAAZAA" +
+"yAEFVAAyADAAZAAzAEFVAAyADAAZAA0AEWVABEAEIAWABEAEEAVABBAE0AQQBQAF8AQgBMAE8AQwBLAF8ARQBOAEQAXwBJns" +
+"AQgAwADcAMQAzAEUANgBCAC0AQQBGAEYANgAtADQAOAA3ADEALQBBADIANwAzAC0ANwA4ADkANgA4ADMANABEADYANgA3ADY" +
+"AfQBBuEA5ADhAOUA4QDpAErTQB1AGwAdABpAGwAaQBuAGUAIABUAGUAeAB0AA0ACgBMAGkAbgBlACAAMgANAAoATABpAG4AZ" +
+"QAgADMADQAKAC4ALgAuAA0ACgBMAGkAbgBlACAATgBKOEA4wDhAOQA4QDqAOAA6QDhAOoA4QDlAOEA5QDhAOUA4QDlAOEA5Q" +
+"DhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUASjgAPgA4QDlAOEA6ADgAOkA4QDlAOEA5QDhAOUA4QD" +
+"lAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAEI4QDpAOEA5gDhAOQA4QDlAEG4QDpA" +
+"OEA5QDhAOgASzhAOQA4QDjAOEA6gDhAOUA4QDlAOAA6QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAO" +
+"UA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUASjhAOoA4QDmAOEA5wDgAOkA4QDpAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOE" +
+"A5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDmAG4dqQFFKIigIHF";
+
+ //TCH_DRAWINGNAME, handle 939
+ private const string DrawingNameFixture =
+"ChGQQRqD65+LEYs81AgCkSsDO6PkQEmewBGADgARgAwAEYAMQAyAEYALQA5ADMAOAA5AC0ANAA5AEYAQwAtADgAMwA1AEQAL" +
+"QA4AEMAOAA2AEMANgBFAEUARQBFAEQAQgB9AEs4AD4AOEA5gDhAOcA4QDlAOEA5QDgAOkA4QDlAOEA5QDhAOUA4QDlAOEA5Q" +
+"DhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAEo4QDqAOEA5QDhAOMA4ADpAOEA5QDhAOUA4QD" +
+"lAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QBBuAA9wDhAOUA4QDqAEYVABEA" +
+"EIAWABEAEEAVABBAE0AQQBQAF8AQgBMAE8AQwBLAF8AQgBFAEcASQBOAF8ARZUAEQAQgBYAEQAQQBUAEEATQBBAFAAXwBCAE" +
+"wATwBDAEsAXwBFAE4ARABfAEmewBCADAANwAxADMARQA2AEIALQBBAEYARgA2AC0ANAA4ADcAMQAtAEEAMgA3ADMALQA3ADg" +
+"AOQA2ADgAMwA0AEQANgA2ADcANgB9AEG4AD4AOEA5QDhAOQAQ1EAHIAYQB3AGkAbgBnACAAVABpAHQAbABlAEFMQA6ADEAMA" +
+"AwAEo4QDoAOEA5QDhAOgA4ADpAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QD" +
+"hAOUA4QDlAOEA5QBKOEA5wDhAOgA4QDoAOAA6QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDl" +
+"AOEA5QDhAOUA4QDlAOEA5QDhAOUASjhAOcA4QDqAOEA4wDgAOkA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhA" +
+"OUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAEo4QDlAOEA5QDgAPgA4ADpAOEA6ADhAOUA4QDlAOEA5QDhAOUA4QDlAO" +
+"EA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QBKOEA5gDhAOUA4QDqAOAA6QDhAOcA4QDlAOEA5QDhAOU" +
+"A4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUAQbhAOcA4QDlAOEA4wBBuAA+ADhAOYA4" +
+"QDmAEG4QDqAOEA6ADhAOcAFB2pAUQoiKiI/L";
+
+ //TCH_DRAWINGINDEX, handle 1269
+ private const string DrawingIndexFixture =
+"ChFAXIAAAAAAAABzAQORVA5CEFAAAAAAAACRAQUAAAAAAAAIkBBQAAAAAAAAcwEDQFA5A4EEQMRFPT7DLFl2kAAudgrRQ70Q" +
+"EmewBGADgARgAwAEYAMQAyAEYALQA5ADMAOAA5AC0ANAA5AEYAQwAtADgAMwA1AEQALQA4AEMAOAA2AEMANgBFAEUARQBFAE" +
+"QAQgB9AEs4QDlAOEA5gDhAOQA4QDlAOEA5QDgAOkA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOE" +
+"A5QDhAOUA4QDlAOEA5QDhAOUA4QDlAEo4QDoAOEA5QDgAPgA4ADpAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA" +
+"4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QBBuEA5wDhAOUA4AD4AEFWQC5ZRFU3Y27eUYVABEAEIAWABEAEEAVAB" +
+"BAE0AQQBQAF8AQgBMAE8AQwBLAF8AQgBFAEcASQBOAF8AQVUADIAMABkADEAQVUADIAMABkADIAQZUADIAMABkADIANwBBVQ" +
+"AMgAwAGQAMwBBP5WDVSTj2VRQVUADIAMABkADQAQVUADIAMABkADUAQVUADIAMABkADYAQVUADIAMABkADcAQVUADIAMABkA" +
+"DgAQVUADIAMABkADkARZUAEQAQgBYAEQAQQBUAEEATQBBAFAAXwBCAEwATwBDAEsAXwBFAE4ARABfAEmewBCADAANwAxADMA" +
+"RQA2AEIALQBBAEYARgA2AC0ANAA4ADcAMQAtAEEAMgA3ADMALQA3ADgAOQA2ADgAMwA0AEQANgA2ADcANgB9AEG4QDnAOEA5" +
+"QDhAOcASjhAOgA4QDkAOAA+ADgAOkA4QDqAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4Q" +
+"DlAOEA5QDhAOUA4QDlAEo4QDkAOEA5QDhAOUA4ADpAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QD" +
+"hAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QBBuEA6gDhAOgA4QDpAEo4AD4AOEA6ADhAOgA4ADpAOEA5QDhAOUA4QDlAOEA5QDhA" +
+"OUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QBKOEA5ADhAOUA4AD4AOAA6QDhAOoA4QDlAO" +
+"EA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUAQExAJBDEAOgAyADAASjhAO" +
+"UA4QDkAOAA9wDgAOkA4QDqAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAO" +
+"UA4QDlAEo4AD3AOAA+ADhAOQA4ADpAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOE" +
+"A5QDhAOUA4QDlAOEA5QBBuEA5gDhAOYA4QDoAEG4QDoAOEA4wDhAOkASjgAPcA4QDlAOAA+ADgAOkA4QDlAOEA5QDhAOUA4Q" +
+"DlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAEo4QDnAOEA5wDgAPgA4ADpAOEA5QD" +
+"hAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QDhAOUA4QDlAOEA5QAcKKkBQykBWCkB0ik" +
+"BQykBWHJ";
+
+ private static byte[] decode(string value) => Convert.FromBase64String(value);
+
+ [Fact]
+ public void TchTextPositionMatchesTheFlattenedExport()
+ {
+ TchText text = TchText.Parse("TCH_TEXT", decode(TextPayload), ACadVersion.AC1021);
+
+ Assert.True(text.HasPosition);
+ Assert.Equal(39176.3507, text.InsertionPoint.X, 4);
+ Assert.Equal(29653.1919, text.InsertionPoint.Y, 4);
+ }
+
+ [Fact]
+ public void TchTextReadsTheContent()
+ {
+ TchText text = TchText.Parse("TCH_TEXT", decode(TextPayload), ACadVersion.AC1021);
+
+ //The export splits this into per-word text entities, but the object stores it whole.
+ Assert.Equal("Wall and Window and Door".Replace("and", "&"), text.Value);
+ }
+
+ [Fact]
+ public void TchTextReadsContentThatLooksLikeNothingElse()
+ {
+ TchText text = TchText.Parse("TCH_TEXT", decode(SquareColumnFixture), ACadVersion.AC1021);
+
+ Assert.Equal("Square Column", text.Value);
+ }
+
+ [Fact]
+ public void TchTextReadsTheContentOfAMultilineText()
+ {
+ TchText text = TchText.Parse("TCH_MTEXT", decode(MTextFixture), ACadVersion.AC1021);
+
+ //The export splits this into one entity per word per line; the object stores it whole.
+ //The line breaks are the reason this read as nothing for a while: a filter that allowed
+ //only printable characters rejected every multiline text while every single line one
+ //still came through.
+ Assert.Equal("Multiline Text\r\nLine 2\r\nLine 3\r\n...\r\nLine N", text.Value);
+ }
+
+ [Fact]
+ public void TchTextReadsTheNameOfATitleBlock()
+ {
+ TchText text = TchText.Parse("TCH_DRAWINGNAME", decode(DrawingNameFixture), ACadVersion.AC1021);
+
+ Assert.Equal("Drawing Title", text.Value);
+ }
+
+ [Fact]
+ public void TchTextTakesNoContentFromAClassThatHasNone()
+ {
+ //A column's payload holds property names such as PUB_HATCH, which must not come back as
+ //text.
+ TchText text = TchText.Parse("TCH_COLUMN", decode(ColumnPayload), ACadVersion.AC1021);
+
+ Assert.Null(text.Value);
+ }
+
+ [Fact]
+ public void TchTextRejectsAShortPayload()
+ {
+ TchText text = TchText.Parse("TCH_TEXT", new byte[4], ACadVersion.AC1021);
+
+ Assert.False(text.HasPosition);
+ Assert.Equal(CSMath.XYZ.Zero, text.InsertionPoint);
+ }
+
+ [Fact]
+ public void TchTextThrowsOnANullPayload()
+ {
+ Assert.Throws(() => TchText.Parse("TCH_TEXT", null, ACadVersion.AC1021));
+ }
+
+ [Fact]
+ public void TchPolygonRecoversTheColumnSection()
+ {
+ TchPolygon polygon = TchPolygon.FindInPayload(decode(ColumnPayload), ACadVersion.AC1021, out long offset);
+
+ Assert.NotNull(polygon);
+ Assert.True(polygon.IsComplete);
+ Assert.Equal(332, offset);
+ Assert.Equal(4, polygon.Vertices.Count);
+
+ //The four corners of the column section, in order.
+ Assert.Equal(35910.0208, polygon.Vertices[0].Position.X, 4);
+ Assert.Equal(28751.9846, polygon.Vertices[0].Position.Y, 4);
+ Assert.Equal(36510.0208, polygon.Vertices[1].Position.X, 4);
+ Assert.Equal(28751.9846, polygon.Vertices[1].Position.Y, 4);
+ Assert.Equal(36510.0208, polygon.Vertices[2].Position.X, 4);
+ Assert.Equal(29351.9846, polygon.Vertices[2].Position.Y, 4);
+ Assert.Equal(35910.0208, polygon.Vertices[3].Position.X, 4);
+ Assert.Equal(29351.9846, polygon.Vertices[3].Position.Y, 4);
+
+ //A straight edged outline, so every bulge is zero.
+ foreach (TchVertex vertex in polygon.Vertices)
+ {
+ Assert.Equal(0.0, vertex.Bulge);
+ }
+ }
+
+ [Fact]
+ public void TchPolygonRecoversTheSectionSize()
+ {
+ TchPolygon polygon = TchPolygon.FindInPayload(decode(ColumnPayload), ACadVersion.AC1021, out _);
+
+ double width = polygon.Vertices[1].Position.X - polygon.Vertices[0].Position.X;
+ double height = polygon.Vertices[2].Position.Y - polygon.Vertices[1].Position.Y;
+
+ //The column is square, and its edge lands on the same grid as the walls in the flattened
+ //export of the same drawing.
+ Assert.Equal(600.0, width, 4);
+ Assert.Equal(600.0, height, 4);
+ }
+
+ [Fact]
+ public void TchPolygonConvertsToAPolylineWithoutLosingTheShape()
+ {
+ TchPolygon polygon = TchPolygon.FindInPayload(decode(ColumnPayload), ACadVersion.AC1021, out _);
+
+ ACadSharp.Entities.LwPolyline polyline = polygon.ToLwPolyline();
+
+ Assert.True(polyline.IsClosed);
+ Assert.Equal(polygon.Vertices.Count, polyline.Vertices.Count);
+
+ for (int i = 0; i < polygon.Vertices.Count; i++)
+ {
+ Assert.Equal(polygon.Vertices[i].Position.X, polyline.Vertices[i].Location.X);
+ Assert.Equal(polygon.Vertices[i].Position.Y, polyline.Vertices[i].Location.Y);
+ Assert.Equal(polygon.Vertices[i].Bulge, polyline.Vertices[i].Bulge);
+ }
+ }
+
+ [Fact]
+ public void TchPolygonCarriesTheColourAndLayerOfTheSourceObject()
+ {
+ TchPolygon polygon = TchPolygon.FindInPayload(decode(ColumnPayload), ACadVersion.AC1021, out _);
+
+ //A third of the objects in the corpus carry an explicit colour rather than ByLayer, so
+ //the conversion has to carry it across.
+ ACadSharp.Entities.Line source = new ACadSharp.Entities.Line();
+ source.Layer = new ACadSharp.Tables.Layer("COLUMN");
+ source.Color = new ACadSharp.Color(30);
+ source.LineWeight = ACadSharp.LineWeightType.W50;
+
+ ACadSharp.Entities.LwPolyline polyline = polygon.ToLwPolyline(source);
+
+ Assert.Equal(source.Color, polyline.Color);
+ Assert.Equal(source.Layer.Name, polyline.Layer.Name);
+ Assert.Equal(source.LineWeight, polyline.LineWeight);
+ }
+
+ [Fact]
+ public void TchPolygonLeavesPropertiesAtTheirDefaultsWithoutASource()
+ {
+ TchPolygon polygon = TchPolygon.FindInPayload(decode(ColumnPayload), ACadVersion.AC1021, out _);
+
+ ACadSharp.Entities.LwPolyline polyline = polygon.ToLwPolyline();
+
+ Assert.True(polyline.Color.IsByLayer);
+ }
+
+ [Fact]
+ public void TchPolygonConvertsAnArcEdgeAsABulge()
+ {
+ //TCH_CUT carries an outline with a bulge of 1 on its first vertex, a semicircular edge.
+ //The conversion has to keep it rather than flatten it.
+ TchPolygon polygon = TchPolygon.FindInPayload(decode(CutPayload), ACadVersion.AC1021, out _);
+
+ ACadSharp.Entities.LwPolyline polyline = polygon.ToLwPolyline();
+
+ Assert.Equal(1.0, polyline.Vertices[0].Bulge);
+ Assert.Equal(0.0, polyline.Vertices[1].Bulge);
+ }
+
+ [Fact]
+ public void TchGeometryFindsTheStartOfAWallCurve()
+ {
+ Assert.True(TchGeometry.TryFindPoint(decode(WallFixture), ACadVersion.AC1021, out CSMath.XYZ point, out long offset));
+
+ //The wall in the flattened export runs from 36510.021 along y = 29051.985, and the
+ //start of its centre line is what the object stores.
+ Assert.Equal(36510.0208, point.X, 4);
+ Assert.Equal(29051.9846, point.Y, 4);
+ Assert.Equal(0.0, point.Z);
+ Assert.Equal(428, offset);
+ }
+
+ [Fact]
+ public void TchGeometryFindsAnOpeningPosition()
+ {
+ Assert.True(TchGeometry.TryFindPoint(decode(OpeningFixture), ACadVersion.AC1021, out CSMath.XYZ point, out _));
+
+ //The opening sits in a gap of the wall, and the export draws that gap between 44358.685
+ //and 45158.685, so the opening is centred on the midpoint.
+ Assert.Equal(44758.6853, point.X, 4);
+ }
+
+ [Fact]
+ public void TchGeometryFindsTheCentreLineOfAWall()
+ {
+ IReadOnlyList points = TchGeometry.FindPositions(decode(WallFixture), ACadVersion.AC1021);
+
+ Assert.Equal(2, points.Count);
+
+ //The export runs the wall from 36510.021 to 48824.945 along y = 29051.985, and those are
+ //the two positions.
+ Assert.Equal(36510.0208, points[0].X, 3);
+ Assert.Equal(29051.9846, points[0].Y, 3);
+ Assert.Equal(48824.9446, points[1].X, 3);
+ Assert.Equal(29051.9846, points[1].Y, 3);
+ }
+
+ [Fact]
+ public void TchGeometryFindsThePositionOfAStair()
+ {
+ IReadOnlyList points = TchGeometry.FindPositions(decode(StairFixture), ACadVersion.AC1021);
+
+ Assert.NotEmpty(points);
+
+ //The export draws the flight starting from x = 67480.37.
+ Assert.Equal(67480.3701, points[0].X, 3);
+ }
+
+ [Fact]
+ public void TchGeometryFindsThePositionOfADimension()
+ {
+ IReadOnlyList points = TchGeometry.FindPositions(decode(DimensionFixture), ACadVersion.AC1021);
+
+ Assert.NotEmpty(points);
+
+ //The export draws this one from (38843.062, 28951.985).
+ Assert.Equal(38843.0625, points[0].X, 3);
+ Assert.Equal(28951.9846, points[0].Y, 3);
+ }
+
+ [Fact]
+ public void TchGeometryFindsThePositionOfATextObject()
+ {
+ IReadOnlyList points = TchGeometry.FindPositions(decode(TextPayload), ACadVersion.AC1021);
+
+ Assert.Single(points);
+ Assert.Equal(39176.3507, points[0].X, 3);
+ Assert.Equal(29653.1919, points[0].Y, 3);
+ }
+
+ [Fact]
+ public void TchGeometryFindsThePositionOfADrawingIndex()
+ {
+ IReadOnlyList points = TchGeometry.FindPositions(decode(DrawingIndexFixture), ACadVersion.AC1021);
+
+ //The export puts a polyline at exactly this point, and the drawing number and scale next
+ //to it. This is the object that needed the tighter alignment tried as well: its position
+ //sits where the wider one says there is no room left before the class fields end.
+ Assert.Single(points);
+ Assert.Equal(82148.3232, points[0].X, 3);
+ Assert.Equal(27030.7664, points[0].Y, 3);
+ }
+
+ [Fact]
+ public void TchGeometryThrowsOnANullPayloadWhenFindingPositions()
+ {
+ Assert.Throws(() => TchGeometry.FindPositions(null, ACadVersion.AC1021));
+ }
+
+ [Fact]
+ public void TchGeometryFindsThePointsOfAnIndexPointer()
+ {
+ IReadOnlyList points = TchGeometry.FindPoints(decode(IndexPointerFixture), ACadVersion.AC1021, out long offset);
+
+ Assert.Equal(2, points.Count);
+ Assert.Equal(86, offset);
+
+ //The export draws the pointer as a line from (45665.25, 29070.18) to
+ //(46469.90, 26579.10), and those are the two points.
+ Assert.Equal(45665.2489, points[0].X, 3);
+ Assert.Equal(29070.1762, points[0].Y, 3);
+ Assert.Equal(46469.9009, points[1].X, 3);
+ Assert.Equal(26579.0959, points[1].Y, 3);
+ }
+
+ [Fact]
+ public void TchGeometryFindsThePointsOfASectionMark()
+ {
+ IReadOnlyList points = TchGeometry.FindPoints(decode(SectionFixture), ACadVersion.AC1021, out _);
+
+ Assert.Equal(2, points.Count);
+
+ //The export puts a polyline at (55806.38, 29903.54), so the Y agrees exactly.
+ Assert.Equal(29903.5382, points[0].Y, 3);
+ }
+
+ [Fact]
+ public void TchGeometryReturnsNoPointsForATextObject()
+ {
+ Assert.Empty(TchGeometry.FindPoints(decode(TextPayload), ACadVersion.AC1021, out _));
+ }
+
+ [Fact]
+ public void TchGeometryThrowsOnANullPayloadWhenFindingPoints()
+ {
+ Assert.Throws(() => TchGeometry.FindPoints(null, ACadVersion.AC1021, out _));
+ }
+
+ [Fact]
+ public void TgDataEncryptUsesTheRecoveredAlphabet()
+ {
+ Assert.Equal(12, TgDataEncrypt.Alphabet.Count);
+
+ //The digits are deliberately out of order: 6 sits below 0.
+ Assert.Equal('0', TgDataEncrypt.Alphabet[58]);
+ Assert.Equal('6', TgDataEncrypt.Alphabet[50]);
+ Assert.Equal('9', TgDataEncrypt.Alphabet[51]);
+ Assert.Equal('8', TgDataEncrypt.Alphabet[56]);
+ Assert.Equal('7', TgDataEncrypt.Alphabet[57]);
+ Assert.Equal('.', TgDataEncrypt.Alphabet[36]);
+ }
+
+ [Fact]
+ public void TgDataEncryptDecodesTheColumnHeight()
+ {
+ IReadOnlyList values = TgDataEncrypt.ReadAllFrom(decode(ColumnPayload), ACadVersion.AC1021);
+
+ Assert.NotEmpty(values);
+
+ //The column section is 600 by 600, which its outline gives, and its height is 2600.
+ //Those come from different parts of the payload, so they confirm each other.
+ Assert.Contains(2600.0, values);
+ }
+
+ [Fact]
+ public void TgDataEncryptDecodesTheWallDimensions()
+ {
+ IReadOnlyList values = TgDataEncrypt.ReadAllFrom(decode(WallFixture), ACadVersion.AC1021);
+
+ Assert.NotEmpty(values);
+
+ //All plausible drawing dimensions: nothing negative, nothing astronomical.
+ foreach (double value in values)
+ {
+ Assert.InRange(value, 0.0, 1.0e6);
+ }
+
+ Assert.Contains(41700.0, values);
+ Assert.Contains(11900.0, values);
+ }
+
+ [Fact]
+ public void TgDataEncryptRejectsAStringOutsideTheAlphabet()
+ {
+ //A class id is not ciphertext and must not be read as a number.
+ Assert.False(TgDataEncrypt.TryDecode("{F8F0F12F-9389-49FC-835D-8C86C6EEEEDB}", out _));
+ }
+
+ [Fact]
+ public void TgDataEncryptThrowsOnANullString()
+ {
+ Assert.Throws(() => TgDataEncrypt.Decode(null));
+ }
+
+ [Fact]
+ public void TchGeometryReadsAnElevationPosition()
+ {
+ Assert.True(TchGeometry.TryReadPosition("TCH_ELEVATION", decode(ElevationFixture), ACadVersion.AC1021, out CSMath.XY position));
+
+ //The export draws the symbol on the DIM_ELEV layer starting at exactly this point:
+ //Line (41657.97, 5785.30) -> (41357.97, 6085.30)
+ Assert.Equal(41657.97, position.X, 2);
+ Assert.Equal(5785.30, position.Y, 2);
+ }
+
+ [Fact]
+ public void TchGeometryHasNoPositionForAnUnrecordedClass()
+ {
+ Assert.False(TchGeometry.TryReadPosition("TCH_NOT_A_CLASS", decode(ElevationFixture), ACadVersion.AC1021, out _));
+ }
+
+ [Fact]
+ public void TchGeometryRejectsAShortPayload()
+ {
+ Assert.False(TchGeometry.TryFindPoint(new byte[8], ACadVersion.AC1021, out _, out _));
+ }
+
+ [Fact]
+ public void TchGeometryThrowsOnANullPayload()
+ {
+ Assert.Throws(() => TchGeometry.TryFindPoint(null, ACadVersion.AC1021, out _, out _));
+ }
+
+ [Fact]
+ public void TchPolygonReturnsNullWhenThereIsNoOutline()
+ {
+ //A text object carries no outline.
+ TchPolygon polygon = TchPolygon.FindInPayload(decode(TextPayload), ACadVersion.AC1021, out long offset);
+
+ Assert.Null(polygon);
+ Assert.Equal(-1, offset);
+ }
+
+ [Fact]
+ public void TchPolygonThrowsOnANullPayload()
+ {
+ Assert.Throws(() => TchPolygon.FindInPayload(null, ACadVersion.AC1021, out _));
+ }
+
+ [Fact]
+ public void TchDataMapFindsTheBlockInsideANativePayload()
+ {
+ TchDataMap map = TchDataMap.ReadFromPayload(decode(WallPayload), ACadVersion.AC1021);
+
+ Assert.True(map.IsComplete);
+ Assert.Contains(TchDataMap.BlockEndMarker, map.Entries);
+ Assert.Contains("T20s1", map.Entries);
+ }
+
+ [Fact]
+ public void TchDataMapPairsDescriptorsWithTheirValues()
+ {
+ TchDataMap map = TchDataMap.ReadFromPayload(decode(WallPayload), ACadVersion.AC1021);
+
+ Assert.False(map.IsEmpty);
+ Assert.Equal("A级", map.Properties["T20s1"]);
+ Assert.Equal("普通砖", map.Properties["T20s4"]);
+ Assert.Equal("保温层", map.Properties["T20s9"]);
+ }
+
+ [Fact]
+ public void TchDataMapTreatsANameLookingValueAsAValue()
+ {
+ //PUB_HATCH is the value of T20s2, it only looks like a property name
+ TchDataMap map = TchDataMap.ReadFromPayload(decode(ColumnPayload), ACadVersion.AC1021);
+
+ Assert.Equal("PUB_HATCH", map.Properties["T20s2"]);
+ Assert.Equal("钢筋混凝土", map.Properties["T20s1"]);
+ Assert.Equal("钢筋砼", map.Properties["T20s5"]);
+ }
+
+ [Fact]
+ public void TchDataMapReturnsAnEmptyMapWhenThereIsNoBlock()
+ {
+ TchDataMap map = TchDataMap.ReadFromPayload(new byte[64], ACadVersion.AC1021);
+
+ Assert.False(map.IsComplete);
+ Assert.True(map.IsEmpty);
+ }
+ }
+}
diff --git a/src/ACadSharp/Entities/UnknownEntity.cs b/src/ACadSharp/Entities/UnknownEntity.cs
index d030367b2..d91a81c31 100644
--- a/src/ACadSharp/Entities/UnknownEntity.cs
+++ b/src/ACadSharp/Entities/UnknownEntity.cs
@@ -14,6 +14,20 @@ public class UnknownEntity : Entity
///
public override ObjectType ObjectType => ObjectType.UNDEFINED;
+ ///
+ /// Gets the part of the object data that lays beyond the common entity data, as it is
+ /// stored in the file.
+ ///
+ ///
+ /// ACadSharp has no typed representation for this object, so the bytes written by the
+ /// class's own dwgOutFields are kept here verbatim. They are the only source of the
+ /// class specific fields, and they are what an application that knows the class reads back.
+ ///
+ /// Only populated when the reader is configured to keep unknown entities.
+ ///
+ ///
+ public byte[] RawData { get; internal set; }
+
///
public override string ObjectName
{
diff --git a/src/ACadSharp/IO/DWG/DwgStreamReaders/DwgObjectReader.cs b/src/ACadSharp/IO/DWG/DwgStreamReaders/DwgObjectReader.cs
index 5c13baa9e..3d0c26dc9 100644
--- a/src/ACadSharp/IO/DWG/DwgStreamReaders/DwgObjectReader.cs
+++ b/src/ACadSharp/IO/DWG/DwgStreamReaders/DwgObjectReader.cs
@@ -7337,9 +7337,34 @@ private CadTemplate readUnknownEntity(DxfClass dxfClass)
this.readCommonEntityData(template);
+ //The class specific fields have no reader, keep the bytes so that nothing is lost
+ entity.RawData = this.readRemainingObjectData();
+
return template;
}
+ ///
+ /// Reads the object data that has not been consumed yet, up to the end of the object.
+ ///
+ ///
+ /// The section has no length of its own, so it is delimited by the end of the object. That
+ /// end includes the handle section, which is left in place: consumers of these bytes look
+ /// for what they know, and the padding is harmless.
+ ///
+ private byte[] readRemainingObjectData()
+ {
+ long start = this._objectReader.PositionInBits();
+ long end = this._objectInitialPos + ((long)this._size * 8L);
+
+ long bits = end - start;
+ if (bits <= 0)
+ {
+ return null;
+ }
+
+ return this._objectReader.ReadBytes((int)((bits + 7L) / 8L));
+ }
+
private CadTemplate readUnknownNonGraphicalObject(DxfClass dxfClass)
{
UnknownNonGraphicalObject obj = new UnknownNonGraphicalObject(dxfClass);
diff --git a/src/ACadSharp/IO/DWG/Tangent/TchDataMap.cs b/src/ACadSharp/IO/DWG/Tangent/TchDataMap.cs
new file mode 100644
index 000000000..c60e98324
--- /dev/null
+++ b/src/ACadSharp/IO/DWG/Tangent/TchDataMap.cs
@@ -0,0 +1,400 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace ACadSharp.IO.DWG.Tangent
+{
+ ///
+ /// Decoded Tianzheng (TArch / Tangent) property map stored inside a custom object payload.
+ ///
+ ///
+ ///
+ /// Tianzheng custom objects (TCH_* / TDb*) carry a self-describing property block.
+ /// It is written through AcDbDwgFiler, so it uses the ordinary DWG string encoding:
+ /// a BitShort character count followed by UTF-16LE characters.
+ ///
+ ///
+ /// The block is sparse: only properties that differ from their default are present, and
+ /// different classes carry different properties. Property names are upper snake case ASCII
+ /// (PIPE_SYSTEM_NAME, PIPE_THICK, ...); values may be numeric text or localized text.
+ ///
+ ///
+ /// The sequence ends with an (usually empty) TDBXDATAMAP_BLOCK_BEGIN_ /
+ /// TDBXDATAMAP_BLOCK_END_ marker pair. These markers terminate the map; they do not wrap it.
+ ///
+ ///
+ public sealed class TchDataMap
+ {
+ /// Marker written after the property sequence.
+ public const string BlockBeginMarker = "TDBXDATAMAP_BLOCK_BEGIN_";
+
+ /// Closing marker of the terminating marker pair.
+ public const string BlockEndMarker = "TDBXDATAMAP_BLOCK_END_";
+
+ private const int DefaultMaxEntries = 4096;
+
+ private const int MaxStringLength = 200;
+
+ private readonly List _entries = new List();
+
+ private readonly Dictionary _properties = new Dictionary(StringComparer.Ordinal);
+
+ private TchDataMap() { }
+
+ /// Gets the decoded strings in stream order.
+ public IReadOnlyList Entries => _entries;
+
+ /// Gets the property name to value map built from .
+ ///
+ /// Tianzheng writes a property name immediately before its value. Names without a following
+ /// value (for example when a flag is set) are still listed in but are
+ /// not present here.
+ ///
+ public IReadOnlyDictionary Properties => _properties;
+
+ /// Gets a value indicating whether the closing marker was reached.
+ public bool IsComplete { get; private set; }
+
+ /// Gets a value indicating whether the object carried no properties.
+ ///
+ /// The terminating marker pair is not considered a property, so an object whose payload only
+ /// contains the markers is reported as empty.
+ ///
+ public bool IsEmpty => _properties.Count == 0;
+
+ ///
+ /// Reads a Tianzheng property map from the current position of .
+ ///
+ /// Reader positioned at the start of the map.
+ /// Safety limit for the number of decoded strings.
+ /// The decoded map; never null.
+ ///
+ /// The reader position is restored if a string cannot be decoded, so the caller can continue
+ /// reading the enclosing object.
+ ///
+ internal static TchDataMap Read(IDwgStreamReader reader, int maxEntries = DefaultMaxEntries)
+ {
+ if (reader == null)
+ {
+ throw new ArgumentNullException(nameof(reader));
+ }
+
+ TchDataMap map = new TchDataMap();
+
+ for (int i = 0; i < maxEntries; i++)
+ {
+ if (reader.IsEmpty)
+ {
+ break;
+ }
+
+ long start = reader.PositionInBits();
+ string value;
+ try
+ {
+ value = reader.ReadVariableText();
+ }
+ catch (Exception)
+ {
+ reader.SetPositionInBits(start);
+ break;
+ }
+
+ if (string.IsNullOrEmpty(value) || value.Length > MaxStringLength || !isPrintable(value))
+ {
+ reader.SetPositionInBits(start);
+ break;
+ }
+
+ map._entries.Add(value);
+
+ if (value == BlockEndMarker)
+ {
+ map.IsComplete = true;
+ break;
+ }
+ }
+
+ map.buildProperties();
+ return map;
+ }
+
+ ///
+ /// Reads a Tianzheng property map from a detached payload, such as the retained data of a
+ /// proxy entity.
+ ///
+ /// Raw payload bytes.
+ /// Drawing version used for the string encoding.
+ public static TchDataMap Read(byte[] payload, ACadVersion version)
+ {
+ if (payload == null)
+ {
+ throw new ArgumentNullException(nameof(payload));
+ }
+
+ using (System.IO.MemoryStream stream = new System.IO.MemoryStream(payload, writable: false))
+ {
+ IDwgStreamReader reader = DwgStreamReaderBase.GetStreamHandler(version, stream, resetPositon: true);
+ return Read(reader);
+ }
+ }
+
+ ///
+ /// Reads a Tianzheng property map from the whole class specific payload of an object, such
+ /// as .
+ ///
+ ///
+ /// Unlike the map is not at the start of the payload
+ /// here: it follows the class's own fields and is bit aligned to whatever they consumed. The
+ /// block is located by trying every bit offset and keeping the longest readable string
+ /// sequence that contains the closing marker.
+ ///
+ /// Raw payload bytes.
+ /// Drawing version used for the string encoding.
+ /// The decoded map; never null. An empty map when no block is found.
+ public static TchDataMap ReadFromPayload(byte[] payload, ACadVersion version)
+ {
+ if (payload == null)
+ {
+ throw new ArgumentNullException(nameof(payload));
+ }
+
+ if (payload.Length == 0)
+ {
+ return new TchDataMap();
+ }
+
+ using (System.IO.MemoryStream stream = new System.IO.MemoryStream(payload, writable: false))
+ {
+ IDwgStreamReader reader = DwgStreamReaderBase.GetStreamHandler(version, stream, resetPositon: true);
+
+ return ReadFromPayload(reader, (long)payload.Length * 8L);
+ }
+ }
+
+ private static TchDataMap ReadFromPayload(IDwgStreamReader reader, long limit)
+ {
+ TchDataMap best = null;
+
+ for (long offset = 0; offset < limit; offset++)
+ {
+ TchDataMap candidate = tryReadSequence(reader, offset);
+ if (candidate == null)
+ {
+ continue;
+ }
+
+ //The longest run wins: the properties precede the markers, so the sequence that
+ //starts earliest and still reaches the closing marker is the whole block.
+ if (best == null || candidate._entries.Count > best._entries.Count)
+ {
+ best = candidate;
+ }
+ }
+
+ return best ?? new TchDataMap();
+ }
+
+ private static TchDataMap tryReadSequence(IDwgStreamReader reader, long offset)
+ {
+ TchDataMap map = new TchDataMap();
+ reader.SetPositionInBits(offset);
+
+ for (int i = 0; i < DefaultMaxEntries; i++)
+ {
+ if (reader.IsEmpty)
+ {
+ break;
+ }
+
+ long start = reader.PositionInBits();
+ string value;
+ try
+ {
+ value = reader.ReadVariableText();
+ }
+ catch (Exception)
+ {
+ reader.SetPositionInBits(start);
+ break;
+ }
+
+ if (string.IsNullOrEmpty(value) || value.Length > MaxStringLength || !isPrintable(value))
+ {
+ break;
+ }
+
+ map._entries.Add(value);
+
+ if (value == BlockEndMarker)
+ {
+ map.IsComplete = true;
+ break;
+ }
+ }
+
+ //Only a sequence that reached the closing marker describes a real block
+ if (!map.IsComplete || map._entries.Count < 2)
+ {
+ return null;
+ }
+
+ map.buildProperties();
+ return map;
+ }
+
+ private static bool isPrintable(string value)
+ {
+ foreach (char c in value)
+ {
+ if (char.IsControl(c))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private void buildProperties()
+ {
+ string pending = null;
+ foreach (string entry in _entries)
+ {
+ if (entry == BlockBeginMarker || entry == BlockEndMarker)
+ {
+ continue;
+ }
+
+ if (isPropertyName(entry))
+ {
+ //A descriptor followed by a plain name looking entry is a pair: the second one is
+ //a value that happens to look like a name, such as the hatch pattern PUB_HATCH.
+ if (pending != null && isTypedFieldDescriptor(pending) && !isTypedFieldDescriptor(entry))
+ {
+ _properties[pending] = entry;
+ pending = null;
+ continue;
+ }
+
+ // A name with no value of its own: it is either a flag or its value is written
+ // through the native (non string) path.
+ if (pending != null)
+ {
+ _properties[pending] = null;
+ }
+
+ pending = entry;
+ if (!_properties.ContainsKey(entry))
+ {
+ _properties[entry] = null;
+ }
+ }
+ else if (pending != null)
+ {
+ _properties[pending] = entry;
+ pending = null;
+ }
+ }
+ }
+
+ ///
+ /// Decides whether an entry looks like a Tianzheng property name.
+ ///
+ ///
+ /// Names are upper case ASCII. Numeric looking entries such as DN25 are values, not
+ /// names, so an entry containing digits is only accepted as a name when it also contains an
+ /// underscore.
+ ///
+ ///
+ /// Decides whether an entry is a typed field descriptor rather than a value.
+ ///
+ ///
+ /// Native objects describe their fields as T20 followed by an optional version prefix
+ /// V<n>_, a type letter (b boolean, d double, s string,
+ /// u unsigned) and either an index or a readable name:
+ /// T20s1, T20b2, T20d12, T20V9_dRealDist2DimLine.
+ ///
+ private static bool isTypedFieldDescriptor(string value)
+ {
+ if (value.Length < 5 || !value.StartsWith("T20", StringComparison.Ordinal))
+ {
+ return false;
+ }
+
+ int i = 3;
+
+ if (i < value.Length && value[i] == 'V')
+ {
+ i++;
+ while (i < value.Length && value[i] >= '0' && value[i] <= '9')
+ {
+ i++;
+ }
+
+ if (i >= value.Length || value[i] != '_')
+ {
+ return false;
+ }
+
+ i++;
+ }
+
+ if (i >= value.Length)
+ {
+ return false;
+ }
+
+ char type = value[i];
+ if (type != 'b' && type != 'd' && type != 's' && type != 'u')
+ {
+ return false;
+ }
+
+ return i + 1 < value.Length;
+ }
+
+ private static bool isPropertyName(string value)
+ {
+ if (isTypedFieldDescriptor(value))
+ {
+ return true;
+ }
+
+ if (value.Length < 3)
+ {
+ return false;
+ }
+
+ bool hasLetter = false;
+ bool hasDigit = false;
+ bool hasUnderscore = false;
+
+ foreach (char c in value)
+ {
+ if (c >= 'a' && c <= 'z')
+ {
+ return false;
+ }
+ else if (c >= 'A' && c <= 'Z')
+ {
+ hasLetter = true;
+ }
+ else if (c >= '0' && c <= '9')
+ {
+ hasDigit = true;
+ }
+ else if (c == '_')
+ {
+ hasUnderscore = true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ return hasLetter && (!hasDigit || hasUnderscore);
+ }
+ }
+}
diff --git a/src/ACadSharp/IO/DWG/Tangent/TchEntityLayout.cs b/src/ACadSharp/IO/DWG/Tangent/TchEntityLayout.cs
new file mode 100644
index 000000000..050a76379
--- /dev/null
+++ b/src/ACadSharp/IO/DWG/Tangent/TchEntityLayout.cs
@@ -0,0 +1,60 @@
+using System.Collections.Generic;
+
+namespace ACadSharp.IO.DWG.Tangent
+{
+ ///
+ /// Positions at which individual classes store geometry that is not part of an outline.
+ ///
+ ///
+ ///
+ /// Most classes can be handled without a table: their outline is a polygon and
+ /// finds it, or their curve is a 3-D point and
+ /// finds that. A few classes store a bare 2-D point and
+ /// nothing else, and for those neither search has anything to key on, so the offset is recorded
+ /// here instead.
+ ///
+ ///
+ /// **These offsets are valid for the version the sample drawings use.** They sit after the base
+ /// class fields, whose length is version dependent, so a drawing written by a different version
+ /// will need them re-measured. The same caveat applies to , which relies on
+ /// the same kind of offset.
+ ///
+ ///
+ /// Each entry was found by comparing instances of one class: fields that hold geometry differ
+ /// between objects, while defaults, property block contents and padding repeat. Taking the
+ /// offsets where the values vary, and only those, leaves the geometry. Each is then confirmed
+ /// against the coordinates in Tianzheng's own flattened export of the same drawing.
+ ///
+ ///
+ internal static class TchEntityLayout
+ {
+ private static readonly Dictionary _positions = new()
+ {
+ //TCH_ELEVATION, three instances, confirmed against the DIM_ELEV layer of the export:
+ //the export draws the symbol from exactly these points. Note that X sits at the higher
+ //offset; the same is true of TchText, so the pair is written Y first.
+ { "TCH_ELEVATION", (142, 78) },
+ };
+
+ ///
+ /// Gets the bit offsets of a 2-D position stored by the given class.
+ ///
+ /// DXF name of the class, for example TCH_ELEVATION.
+ /// Receives the bit offset of the X coordinate.
+ /// Receives the bit offset of the Y coordinate.
+ /// True when the class has a recorded position.
+ public static bool TryGetPositionOffset(string dxfName, out int xOffset, out int yOffset)
+ {
+ if (dxfName != null && _positions.TryGetValue(dxfName, out (int X, int Y) offsets))
+ {
+ xOffset = offsets.X;
+ yOffset = offsets.Y;
+ return true;
+ }
+
+ xOffset = -1;
+ yOffset = -1;
+ return false;
+ }
+ }
+}
diff --git a/src/ACadSharp/IO/DWG/Tangent/TchGeometry.cs b/src/ACadSharp/IO/DWG/Tangent/TchGeometry.cs
new file mode 100644
index 000000000..ea962deaf
--- /dev/null
+++ b/src/ACadSharp/IO/DWG/Tangent/TchGeometry.cs
@@ -0,0 +1,481 @@
+using CSMath;
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace ACadSharp.IO.DWG.Tangent
+{
+ ///
+ /// Reads the geometric primitives Tianzheng stores on an object but that are not an outline.
+ ///
+ ///
+ ///
+ /// A class that has no filled section, a wall for instance, still stores the curve it is built
+ /// on. That curve is a pair of 3-D points, and a point is written as three BitDouble
+ /// values rather than as three raw doubles, so it is variable length: the 2-bit selector in
+ /// front of each coordinate is 0 when eight bytes follow, 1 for 1.0 and 2 for 0.0. A point of a
+ /// horizontal wall therefore occupies 134 bits, not the 192 a fixed layout would take.
+ ///
+ ///
+ /// That variable length is why a search for a 24-byte point finds nothing, and it is the reason
+ /// this helper exists rather than a fixed offset table.
+ ///
+ ///
+ public static class TchGeometry
+ {
+ /// Bit size of a point whose X and Y are both stored in full and whose Z is zero.
+ public const int PointBits = 134;
+
+ ///
+ /// Smallest magnitude accepted for a horizontal coordinate.
+ ///
+ ///
+ /// The filter has to reject the values a misaligned read produces, and those cluster near
+ /// zero: reading a tag or a short as a double yields values of a few units. Architectural
+ /// drawings are dimensioned in millimetres, so requiring three digits keeps the real
+ /// coordinates and drops that noise. A drawing in metres would need this lowered.
+ ///
+ public const double MinimumCoordinate = 100.0;
+
+ ///
+ /// Reads the 2-D position that a class stores at a recorded offset.
+ ///
+ ///
+ /// For the classes in the position is a bare pair of
+ /// coordinates with nothing around it to recognise, so it is read at its offset rather than
+ /// searched for. Use for the classes that store a curve.
+ ///
+ /// DXF name of the class, for example TCH_ELEVATION.
+ /// The class specific data of the object.
+ /// Drawing version used by the stream reader.
+ /// Receives the position that was read.
+ /// True when the class has a recorded position and the payload is long enough.
+ public static bool TryReadPosition(string dxfName, byte[] payload, ACadVersion version, out XY position)
+ {
+ if (payload == null)
+ {
+ throw new ArgumentNullException(nameof(payload));
+ }
+
+ position = XY.Zero;
+
+ if (!TchEntityLayout.TryGetPositionOffset(dxfName, out int xOffset, out int yOffset))
+ {
+ return false;
+ }
+
+ if ((long)payload.Length * 8L < yOffset + 64)
+ {
+ return false;
+ }
+
+ using (MemoryStream stream = new MemoryStream(payload, writable: false))
+ {
+ IDwgStreamReader reader = DwgStreamReaderBase.GetStreamHandler(version, stream, resetPositon: true);
+
+ reader.SetPositionInBits(xOffset);
+ double x = reader.ReadDouble();
+
+ reader.SetPositionInBits(yOffset);
+ double y = reader.ReadDouble();
+
+ position = new XY(x, y);
+ return true;
+ }
+ }
+
+ ///
+ /// Searches a class specific payload for the first well formed 3-D point.
+ ///
+ ///
+ /// The point does not sit at a fixed offset, so every bit offset is tried and a candidate is
+ /// accepted only when all three coordinates decode to plausible values. On a wall this
+ /// returns the start of the curve the wall is built on.
+ ///
+ /// The class specific data of the object.
+ /// Drawing version used by the stream reader.
+ /// Receives the point that was found.
+ /// Receives the bit offset the point was found at.
+ /// True when a point was found.
+ public static bool TryFindPoint(byte[] payload, ACadVersion version, out XYZ point, out long bitOffset)
+ {
+ if (payload == null)
+ {
+ throw new ArgumentNullException(nameof(payload));
+ }
+
+ point = XYZ.Zero;
+ bitOffset = -1;
+
+ if (payload.Length < 32)
+ {
+ return false;
+ }
+
+ using (MemoryStream stream = new MemoryStream(payload, writable: false))
+ {
+ IDwgStreamReader reader = DwgStreamReaderBase.GetStreamHandler(version, stream, resetPositon: true);
+ long limit = (long)payload.Length * 8L;
+
+ for (long offset = 0; offset < limit - PointBits; offset++)
+ {
+ if (!tryReadPoint(reader, offset, limit, out XYZ candidate))
+ {
+ continue;
+ }
+
+ point = candidate;
+ bitOffset = offset;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Searches a class specific payload for the positions an object carries.
+ ///
+ ///
+ ///
+ /// This is the general form of the shape and
+ /// were special cases of, and it is present in every class in
+ /// the corpus. A position is three raw doubles: Y, then X sixty-eight bits later, then Z
+ /// twenty-four bits after that. The two gaps differ because the fields in front of them do,
+ /// and they hold for every class measured.
+ ///
+ ///
+ /// A wall carries two of these, its centre line; a text object one; a window one; a stair
+ /// one. Nothing marks them, so the search takes every offset that produces a plausible
+ /// position and returns them in order. Z is required to be small, which is what keeps a
+ /// misaligned read from passing: an X or Y read at the wrong bit is almost always
+ /// astronomical, but a Z read at the wrong bit can look ordinary unless it is bounded.
+ ///
+ ///
+ /// Each result is confirmed against the flattened export elsewhere in this library's tests;
+ /// for the wall the two positions are the ends of the centre line the export draws, and for
+ /// a stair the X lands on the first line of the flight.
+ ///
+ ///
+ /// The class specific data of the object.
+ /// Drawing version used by the stream reader.
+ /// The positions in the order they appear; empty when there are none.
+ public static IReadOnlyList FindPositions(byte[] payload, ACadVersion version)
+ {
+ if (payload == null)
+ {
+ throw new ArgumentNullException(nameof(payload));
+ }
+
+ List positions = new List();
+
+ if (payload.Length < 32)
+ {
+ return positions;
+ }
+
+ using (MemoryStream stream = new MemoryStream(payload, writable: false))
+ {
+ IDwgStreamReader reader = DwgStreamReaderBase.GetStreamHandler(version, stream, resetPositon: true);
+ long limit = (long)payload.Length * 8L;
+
+ long boundary = findFieldBoundary(reader, limit);
+
+ //The tighter alignment needs less room, so the loop has to allow for it: a drawing
+ //index puts its position exactly where the wider check would say there is no room.
+ for (long offset = 0; offset + XOffsetTight + CoordinateBits <= boundary; offset++)
+ {
+ if (tryReadPosition(reader, offset, XOffset, out XYZ position)
+ || tryReadPosition(reader, offset, XOffsetTight, out position))
+ {
+ positions.Add(position);
+ }
+ }
+ }
+
+ return positions;
+ }
+
+ ///
+ /// Bit gap between Y and X within a stored position.
+ ///
+ ///
+ /// Most classes put 68 bits between the two. Two in the corpus use 64, which is the same
+ /// shape written from a class whose fields in front end two bits earlier. The search tries
+ /// both rather than assuming one.
+ ///
+ public const int XOffset = 68;
+
+ /// The same gap for the classes that align it two bits earlier.
+ public const int XOffsetTight = 64;
+
+ /// Bit gap between X and Z within a stored position.
+ public const int ZOffset = 24;
+
+ ///
+ /// Locates the point in a payload past which the class fields end.
+ ///
+ ///
+ ///
+ /// The fields the base class writes, which is where the position lives, all come before the
+ /// class id and the property block. Past that boundary the payload holds text, and text read
+ /// at a misaligned bit yields values that pass for coordinates — the same pair turns up in
+ /// every object because the same block sits in every object.
+ ///
+ ///
+ /// So the search stops at the first class id or block marker rather than filtering its
+ /// results afterwards. The boundary is generous: a position measured at bit 84 of a text
+ /// object and one at 450 of a drawing index both sit far below their boundary.
+ ///
+ ///
+ private static long findFieldBoundary(IDwgStreamReader reader, long limit)
+ {
+ long mark = reader.PositionInBits();
+
+ for (long offset = 0; offset < limit; offset++)
+ {
+ string value;
+
+ try
+ {
+ reader.SetPositionInBits(offset);
+ value = reader.ReadVariableText();
+ }
+ catch (Exception)
+ {
+ continue;
+ }
+
+ if (value != null && isBoundary(value))
+ {
+ reader.SetPositionInBits(mark);
+ return offset;
+ }
+ }
+
+ reader.SetPositionInBits(mark);
+ return limit;
+ }
+
+ private static bool isBoundary(string value)
+ {
+ if (value.Length == 38 && value[0] == '{' && value[value.Length - 1] == '}')
+ {
+ return true;
+ }
+
+ return value.StartsWith("TDBXDATAMAP", System.StringComparison.Ordinal);
+ }
+
+ private static bool tryReadPosition(IDwgStreamReader reader, long offset, int xOffset, out XYZ position)
+ {
+ position = XYZ.Zero;
+
+ try
+ {
+ reader.SetPositionInBits(offset);
+ double y = reader.ReadDouble();
+
+ reader.SetPositionInBits(offset + xOffset);
+ double x = reader.ReadDouble();
+
+ reader.SetPositionInBits(offset + xOffset + ZOffset);
+ double z = reader.ReadDouble();
+
+ if (!isPlausible(x) || !isPlausible(y) || !isFinite(z) || Math.Abs(z) > 1.0e5)
+ {
+ return false;
+ }
+
+ position = new XYZ(x, y, z);
+ return true;
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Searches a class specific payload for a run of 2-D points.
+ ///
+ ///
+ ///
+ /// Several classes store the points they are drawn from as a bare run of coordinates with
+ /// nothing in front of them: an index pointer is a line between two points, a section mark
+ /// carries its three, and so on. Each point is written Y first then X, 64 bits apart, and
+ /// the next point begins immediately after, so the whole run is a sequence of plausible
+ /// coordinates at a 64 bit stride.
+ ///
+ ///
+ /// The longest such run wins. A run of one coordinate is not accepted, because a single
+ /// plausible double carries too little information to call it a point.
+ ///
+ ///
+ /// Verified on an index pointer in the sample drawing, whose two points come out as
+ /// (45665.2489, 29070.1762) and (46469.9009, 26579.0959), while the flattened export
+ /// draws the line from (45665.25, 29070.18) to (46469.90, 26579.10).
+ ///
+ ///
+ /// The class specific data of the object.
+ /// Drawing version used by the stream reader.
+ /// Receives the bit offset of the first Y coordinate.
+ /// The points in order; empty when the payload holds no such run.
+ public static IReadOnlyList FindPoints(byte[] payload, ACadVersion version, out long bitOffset)
+ {
+ if (payload == null)
+ {
+ throw new ArgumentNullException(nameof(payload));
+ }
+
+ bitOffset = -1;
+
+ List best = new List();
+
+ if (payload.Length < 32)
+ {
+ return best;
+ }
+
+ using (MemoryStream stream = new MemoryStream(payload, writable: false))
+ {
+ IDwgStreamReader reader = DwgStreamReaderBase.GetStreamHandler(version, stream, resetPositon: true);
+ long limit = (long)payload.Length * 8L;
+
+ for (long offset = 0; offset < limit - 2L * CoordinateBits; offset++)
+ {
+ List run = tryReadRun(reader, offset, limit);
+ if (run != null && run.Count > best.Count)
+ {
+ best = run;
+ bitOffset = offset;
+ }
+ }
+ }
+
+ return best;
+ }
+
+ private static List tryReadRun(IDwgStreamReader reader, long offset, long limit)
+ {
+ List points = new List();
+
+ try
+ {
+ reader.SetPositionInBits(offset);
+
+ while (reader.PositionInBits() + 2L * CoordinateBits <= limit)
+ {
+ double y = reader.ReadDouble();
+ double x = reader.ReadDouble();
+
+ if (!isPlausible(x) || !isPlausible(y))
+ {
+ break;
+ }
+
+ points.Add(new XY(x, y));
+ }
+ }
+ catch (Exception)
+ {
+ return null;
+ }
+
+ //Two coordinates are one point and tell us nothing; a run is worth having from two
+ //points, that is four coordinates, upwards.
+ return points.Count >= 2 ? points : null;
+ }
+
+ /// Bit size of one coordinate of a stored point.
+ public const int CoordinateBits = 64;
+
+ ///
+ /// Searches a class specific payload for a point whose X and Y match the given values.
+ ///
+ /// The class specific data of the object.
+ /// Drawing version used by the stream reader.
+ /// Expected X.
+ /// Expected Y.
+ /// Accepted difference, in drawing units.
+ /// Receives the bit offset the point was found at.
+ public static bool ContainsPoint(byte[] payload, ACadVersion version, double x, double y, double tolerance, out long bitOffset)
+ {
+ if (payload == null)
+ {
+ throw new ArgumentNullException(nameof(payload));
+ }
+
+ return findMatching(payload, version, x, y, tolerance, out bitOffset);
+ }
+
+ private static bool findMatching(byte[] payload, ACadVersion version, double x, double y, double tolerance, out long bitOffset)
+ {
+ bitOffset = -1;
+
+ using (MemoryStream stream = new MemoryStream(payload, writable: false))
+ {
+ IDwgStreamReader reader = DwgStreamReaderBase.GetStreamHandler(version, stream, resetPositon: true);
+ long limit = (long)payload.Length * 8L;
+
+ for (long offset = 0; offset < limit - PointBits; offset++)
+ {
+ if (!tryReadPoint(reader, offset, limit, out XYZ candidate))
+ {
+ continue;
+ }
+
+ if (Math.Abs(candidate.X - x) <= tolerance && Math.Abs(candidate.Y - y) <= tolerance)
+ {
+ bitOffset = offset;
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private static bool tryReadPoint(IDwgStreamReader reader, long offset, long limit, out XYZ point)
+ {
+ point = XYZ.Zero;
+
+ try
+ {
+ reader.SetPositionInBits(offset);
+
+ if (reader.PositionInBits() + PointBits > limit)
+ {
+ return false;
+ }
+
+ double x = reader.ReadBitDouble();
+ double y = reader.ReadBitDouble();
+ double z = reader.ReadBitDouble();
+
+ if (!isPlausible(x) || !isPlausible(y) || !isFinite(z) || Math.Abs(z) > 1.0e7)
+ {
+ return false;
+ }
+
+ point = new XYZ(x, y, z);
+ return true;
+ }
+ catch (Exception)
+ {
+ //A misaligned offset produces values the reader rejects.
+ return false;
+ }
+ }
+
+ private static bool isPlausible(double value)
+ {
+ return isFinite(value) && Math.Abs(value) > MinimumCoordinate && Math.Abs(value) < 1.0e7;
+ }
+
+ private static bool isFinite(double value)
+ {
+ return !double.IsNaN(value) && !double.IsInfinity(value);
+ }
+ }
+}
diff --git a/src/ACadSharp/IO/DWG/Tangent/TchPolygon.cs b/src/ACadSharp/IO/DWG/Tangent/TchPolygon.cs
new file mode 100644
index 000000000..7298b326f
--- /dev/null
+++ b/src/ACadSharp/IO/DWG/Tangent/TchPolygon.cs
@@ -0,0 +1,281 @@
+using ACadSharp.Entities;
+using CSMath;
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace ACadSharp.IO.DWG.Tangent
+{
+ ///
+ /// A vertex of a .
+ ///
+ public readonly struct TchVertex
+ {
+ /// Gets the position of the vertex.
+ public XY Position { get; }
+
+ ///
+ /// Gets the bulge of the edge that starts at this vertex, which is the tangent of a quarter
+ /// of the arc's included angle. Zero means a straight segment.
+ ///
+ public double Bulge { get; }
+
+ internal TchVertex(double x, double y, double bulge)
+ {
+ this.Position = new XY(x, y);
+ this.Bulge = bulge;
+ }
+
+ ///
+ public override string ToString() => $"({this.Position.X}, {this.Position.Y}) bulge {this.Bulge}";
+ }
+
+ ///
+ /// A polygon as Tianzheng stores the outline of an object.
+ ///
+ ///
+ ///
+ /// The layout matches TchExport2::ReadPoly2D (0x1C4C6880): a flags byte, a vertex
+ /// count, then that many vertices, and finally an object id. Every value is read through a plain
+ /// filer call, so the outline is stored in the clear and no part of the obfuscation layer
+ /// applies to it.
+ ///
+ ///
+ /// flags byte
+ /// count byte
+ /// repeat count:
+ /// X 64-bit little endian double
+ /// Y 64-bit little endian double
+ /// bulge BitDouble: 2-bit selector, 0 reads a further double, 1 is 1.0, 2 is 0.0
+ ///
+ ///
+ /// The vertex stride is therefore 130 bits for a straight edge and 194 bits for an arc, which is
+ /// what makes the layout self-delimiting.
+ ///
+ ///
+ /// Verified on a TCH_COLUMN in the sample drawing: the recovered four vertices form the
+ /// 600 by 600 section of the column, and their coordinates land on the same grid as the walls in
+ /// Tianzheng's own flattened export of the same drawing.
+ ///
+ ///
+ public sealed class TchPolygon
+ {
+ /// Bit size of a vertex whose edge is straight.
+ public const int StraightVertexBits = 130;
+
+ /// Bit size of a vertex whose edge is an arc.
+ public const int ArcVertexBits = 194;
+
+ private readonly List _vertices = new List();
+
+ private TchPolygon(byte flags)
+ {
+ this.Flags = flags;
+ }
+
+ /// Gets the flags byte that precedes the vertex list.
+ public byte Flags { get; }
+
+ /// Gets the vertices in the order they are stored.
+ public IReadOnlyList Vertices => _vertices;
+
+ /// Gets a value indicating whether the outline was read in full.
+ public bool IsComplete { get; private set; }
+
+ ///
+ /// Reads a polygon at the current position of .
+ ///
+ internal static TchPolygon Read(IDwgStreamReader reader)
+ {
+ byte flags = reader.ReadByte();
+ byte count = reader.ReadByte();
+
+ TchPolygon polygon = new TchPolygon(flags);
+
+ for (int i = 0; i < count; i++)
+ {
+ double x = reader.ReadDouble();
+ double y = reader.ReadDouble();
+ double bulge = reader.ReadBitDouble();
+
+ polygon._vertices.Add(new TchVertex(x, y, bulge));
+ }
+
+ polygon.IsComplete = true;
+ return polygon;
+ }
+
+ ///
+ /// Searches a class specific payload for the outline Tianzheng stored for an object.
+ ///
+ ///
+ ///
+ /// The outline does not sit at a fixed offset: it follows the fields the class wrote before
+ /// it, and for some classes it is only present when a flag is set. The search therefore tries
+ /// every bit offset and keeps a candidate only when every vertex decodes to a coordinate of a
+ /// plausible magnitude, which is a strong enough filter that the correct offset is normally
+ /// the only one that survives.
+ ///
+ ///
+ /// Returns null when the object carries no outline, which is the normal case for a class with
+ /// no filled section, such as a text object.
+ ///
+ ///
+ /// The class specific data of the object.
+ /// Drawing version used by the stream reader.
+ /// Receives the bit offset the outline was found at.
+ public static TchPolygon FindInPayload(byte[] payload, ACadVersion version, out long bitOffset)
+ {
+ if (payload == null)
+ {
+ throw new ArgumentNullException(nameof(payload));
+ }
+
+ bitOffset = -1;
+
+ if (payload.Length < 32)
+ {
+ return null;
+ }
+
+ using (MemoryStream stream = new MemoryStream(payload, writable: false))
+ {
+ IDwgStreamReader reader = DwgStreamReaderBase.GetStreamHandler(version, stream, resetPositon: true);
+ long limit = (long)payload.Length * 8L;
+
+ TchPolygon best = null;
+ long bestOffset = -1;
+
+ for (long offset = 0; offset < limit - StraightVertexBits; offset++)
+ {
+ TchPolygon candidate = tryReadAt(reader, offset, limit);
+ if (candidate == null)
+ {
+ continue;
+ }
+
+ //Keep the longest: a match that starts inside a real outline yields a shorter
+ //run of that outline's vertices, so the longest candidate is the whole one.
+ if (best == null || candidate._vertices.Count > best._vertices.Count)
+ {
+ best = candidate;
+ bestOffset = offset;
+ }
+ }
+
+ bitOffset = bestOffset;
+ return best;
+ }
+ }
+
+ private static TchPolygon tryReadAt(IDwgStreamReader reader, long offset, long limit)
+ {
+ try
+ {
+ return tryReadAtCore(reader, offset, limit);
+ }
+ catch (Exception)
+ {
+ //A misaligned offset produces values the reader rejects, which is the expected
+ //outcome for all but one of the offsets tried.
+ return null;
+ }
+ }
+
+ private static TchPolygon tryReadAtCore(IDwgStreamReader reader, long offset, long limit)
+ {
+ reader.SetPositionInBits(offset);
+
+ if (reader.PositionInBits() + 16 > limit)
+ {
+ return null;
+ }
+
+ byte flags = reader.ReadByte();
+ byte count = reader.ReadByte();
+
+ if (count < 2 || count > 64)
+ {
+ return null;
+ }
+
+ TchPolygon polygon = new TchPolygon(flags);
+
+ for (int i = 0; i < count; i++)
+ {
+ if (reader.PositionInBits() + StraightVertexBits > limit)
+ {
+ return null;
+ }
+
+ double x = reader.ReadDouble();
+ double y = reader.ReadDouble();
+ double bulge = reader.ReadBitDouble();
+
+ if (!isPlausible(x) || !isPlausible(y))
+ {
+ return null;
+ }
+
+ polygon._vertices.Add(new TchVertex(x, y, bulge));
+ }
+
+ polygon.IsComplete = true;
+ return polygon;
+ }
+
+ ///
+ /// Converts the outline into a lightweight polyline, which is how a consumer draws it.
+ ///
+ ///
+ ///
+ /// The two representations line up exactly: a vertex carries a position and a bulge in both,
+ /// and the bulge has the same meaning, so the shape survives the conversion without
+ /// approximation. Nothing is lost except the flags byte.
+ ///
+ ///
+ /// Colour, layer, linetype and the rest of the common properties live on the source object
+ /// rather than in its payload, and they are carried across through
+ /// . That matters in practice: of the objects in
+ /// the test corpus, 71 are ByLayer and 40 carry an explicit indexed colour, so a conversion
+ /// that left the colour at its default would draw a third of them wrong.
+ ///
+ ///
+ /// Object to take the common properties from, or null to leave them at
+ /// their defaults.
+ /// Whether to mark the polyline closed, which is the usual case for an
+ /// object outline but not for every class.
+ /// A polyline with one vertex per outline vertex.
+ public LwPolyline ToLwPolyline(IEntity source = null, bool closed = true)
+ {
+ LwPolyline polyline = new LwPolyline();
+ polyline.IsClosed = closed;
+
+ foreach (TchVertex vertex in _vertices)
+ {
+ polyline.Vertices.Add(new LwPolyline.Vertex(vertex.Position)
+ {
+ Bulge = vertex.Bulge
+ });
+ }
+
+ if (source != null)
+ {
+ polyline.MatchProperties(source);
+ }
+
+ return polyline;
+ }
+
+ private static bool isPlausible(double value)
+ {
+ //A drawing coordinate. The bound is loose on purpose: it only has to reject the bit
+ //patterns that a misaligned read produces, which are almost always astronomically large
+ //or denormal.
+ return !double.IsNaN(value)
+ && !double.IsInfinity(value)
+ && Math.Abs(value) > 1.0
+ && Math.Abs(value) < 1.0e7;
+ }
+ }
+}
diff --git a/src/ACadSharp/IO/DWG/Tangent/TchText.cs b/src/ACadSharp/IO/DWG/Tangent/TchText.cs
new file mode 100644
index 000000000..6daa49738
--- /dev/null
+++ b/src/ACadSharp/IO/DWG/Tangent/TchText.cs
@@ -0,0 +1,218 @@
+using ACadSharp.IO.DWG;
+using CSMath;
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace ACadSharp.IO.DWG.Tangent
+{
+ ///
+ /// Position of a Tianzheng text object (TCH_TEXT / TCH_MTEXT).
+ ///
+ ///
+ ///
+ /// The offsets are relative to the start of the class specific data, which is what
+ /// holds. They were verified on a drawing with
+ /// eleven TCH_TEXT objects: the extracted points match the insertion points of the
+ /// corresponding text entities in Tianzheng's own flattened export of the same drawing exactly,
+ /// with no offset at all. That export is produced by a different tool, so the check is
+ /// independent rather than a re-reading of the same assumption.
+ ///
+ ///
+ /// The layout is fixed for these classes because everything in front of it is constant:
+ ///
+ ///
+ /// bit 0 .. 7 base class version, 10 in the sample drawings
+ /// bit 8 .. 15 tag, 17 in the sample drawings
+ /// bit 16 .. 17 layout name, empty
+ /// bit 18 .. 83 base class fields covered by the obfuscation layer
+ /// bit 84 .. 147 Y coordinate, IEEE double, little endian
+ /// bit 148 .. 151 gap
+ /// bit 152 .. 215 X coordinate, IEEE double, little endian
+ /// bit 216 .. class id, property block and the remaining fields
+ ///
+ ///
+ /// Only the position is exposed. The text content is stored further along, interleaved with
+ /// values that use the same string encoding, and telling them apart needs the field walk to know
+ /// where the class's own fields end. A heuristic scan of the entries cannot do it reliably, so
+ /// no attempt is made here.
+ ///
+ ///
+ public sealed class TchText
+ {
+ /// Bit offset of the Y coordinate.
+ public const int YOffset = 84;
+
+ /// Bit offset of the X coordinate.
+ public const int XOffset = 152;
+
+ private const int RequiredBits = XOffset + 64;
+
+ private TchText()
+ {
+ }
+
+ /// Gets the insertion point of the text, in drawing coordinates.
+ public XYZ InsertionPoint { get; private set; } = XYZ.Zero;
+
+ /// Gets a value indicating whether the payload carried a readable position.
+ public bool HasPosition { get; private set; }
+
+ ///
+ /// Gets the text content, or null when the payload carried none that could be told apart from
+ /// the object's own fields.
+ ///
+ ///
+ ///
+ /// The entries in a payload interleave three kinds of string and only the last is content.
+ /// The class id and the block markers are recognisable by shape, and the scalars are
+ /// recognisable because they decode through the alphabet of §4.2.2 — that is what makes this
+ /// readable now, where an earlier attempt failed. What is left is text.
+ ///
+ ///
+ /// Misaligned reads of the other kinds survive that filter as short strings of unusual
+ /// characters, so the longest candidate is taken. On the sample drawing that gives the right
+ /// content for every text object: Wall & Window & Door, Square Column,
+ /// Elevation, Breakline, U Stair, Line Stair,
+ /// Multi-flight Staircase, Callout Title and axial grid with Dimension.
+ ///
+ ///
+ public string Value { get; private set; }
+
+ ///
+ /// Reads the position of a TCH_TEXT or TCH_MTEXT object.
+ ///
+ /// DXF name of the class. Content is only taken from the classes this
+ /// reader is for; the payload of any other class holds property names that would otherwise
+ /// be mistaken for text.
+ /// The class specific data of the object.
+ /// Drawing version used by the string encoding.
+ /// The parsed position; never null.
+ public static TchText Parse(string dxfName, byte[] payload, ACadVersion version)
+ {
+ if (payload == null)
+ {
+ throw new ArgumentNullException(nameof(payload));
+ }
+
+ TchText text = new TchText();
+
+ if ((long)payload.Length * 8L < RequiredBits)
+ {
+ return text;
+ }
+
+ using (MemoryStream stream = new MemoryStream(payload, writable: false))
+ {
+ IDwgStreamReader reader = DwgStreamReaderBase.GetStreamHandler(version, stream, resetPositon: true);
+
+ reader.SetPositionInBits(YOffset);
+ double y = reader.ReadDouble();
+
+ reader.SetPositionInBits(XOffset);
+ double x = reader.ReadDouble();
+
+ text.InsertionPoint = new XYZ(x, y, 0.0);
+ text.HasPosition = true;
+ }
+
+ //Only the classes that write text take content from this; for anything else the longest
+ //readable string is a property name, which is not content. TCH_DRAWINGNAME carries the
+ //title block's name and its scale as two strings.
+ if (dxfName == "TCH_TEXT" || dxfName == "TCH_MTEXT" || dxfName == "TCH_DRAWINGNAME")
+ {
+ readContent(text, payload, version);
+ }
+
+ return text;
+ }
+ private static void readContent(TchText text, byte[] payload, ACadVersion version)
+ {
+ using (MemoryStream stream = new MemoryStream(payload, writable: false))
+ {
+ IDwgStreamReader reader = DwgStreamReaderBase.GetStreamHandler(version, stream, resetPositon: true);
+ long limit = (long)payload.Length * 8L;
+
+ while (reader.PositionInBits() < limit)
+ {
+ long start = reader.PositionInBits();
+ string value;
+
+ try
+ {
+ value = reader.ReadVariableText();
+ }
+ catch (Exception)
+ {
+ reader.SetPositionInBits(start + 1);
+ continue;
+ }
+
+ if (isContent(value) && (text.Value == null || value.Length > text.Value.Length))
+ {
+ text.Value = value;
+ }
+
+ reader.SetPositionInBits(start + 1);
+ }
+ }
+ }
+
+ private static bool isContent(string value)
+ {
+ if (value == null || value.Length < 2 || value.Length > 400)
+ {
+ return false;
+ }
+
+ //Markers, field descriptors and the class id are all recognisable by shape.
+ if (value.StartsWith("TDBXDATAMAP", StringComparison.Ordinal) || value.StartsWith("T20", StringComparison.Ordinal))
+ {
+ return false;
+ }
+
+ if (value[0] == '{' || value[value.Length - 1] == '}')
+ {
+ return false;
+ }
+
+ //A scalar decodes through the alphabet, so it is not content.
+ if (TgDataEncrypt.TryDecode(value, out _))
+ {
+ return false;
+ }
+
+ int core = 0;
+ foreach (char c in value)
+ {
+ int o = c;
+
+ //The blocks a drawing's text actually uses: ASCII, CJK ideographs, CJK punctuation,
+ //fullwidth forms, and a few typographic marks.
+ //A multiline text carries its line breaks, so the control characters a drawing
+ //legitimately contains are allowed. Leaving them out rejected every multiline text
+ //while every single line one still read, which is why the gap went unnoticed.
+ bool allowed = (o >= 0x20 && o <= 0x7E)
+ || o == '\t' || o == '\n' || o == '\r' || o == '\f' || o == '\v'
+ || (o >= 0x4E00 && o <= 0x9FFF)
+ || (o >= 0x3000 && o <= 0x303F)
+ || (o >= 0xFF00 && o <= 0xFFEF)
+ || o == 0x2011 || o == 0x2013 || o == 0x2014
+ || o == 0x2018 || o == 0x2019 || o == 0x201C || o == 0x201D
+ || o == 0x2026 || o == 0x00B0 || o == 0x00D7 || o == 0x00B1;
+
+ if (!allowed)
+ {
+ return false;
+ }
+
+ if (char.IsLetterOrDigit(c))
+ {
+ core++;
+ }
+ }
+
+ return core >= 2;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ACadSharp/IO/DWG/Tangent/TgDataEncrypt.cs b/src/ACadSharp/IO/DWG/Tangent/TgDataEncrypt.cs
new file mode 100644
index 000000000..2822e1d23
--- /dev/null
+++ b/src/ACadSharp/IO/DWG/Tangent/TgDataEncrypt.cs
@@ -0,0 +1,209 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Text;
+
+namespace ACadSharp.IO.DWG.Tangent
+{
+ ///
+ /// Decoder for the layer Tianzheng puts between its numeric fields and the file.
+ ///
+ ///
+ ///
+ /// A scalar field is not stored as a number. It is stored as a string whose characters are
+ /// drawn from a small alphabet, and each pair of characters carries one character of the
+ /// decimal text the value would have been printed as. TgDataEncrypt::TDReadDouble reads
+ /// the string, decodes it and hands the result to _wtof.
+ ///
+ ///
+ /// One pair decodes to (c1 - K) * 26 + (c2 - K), where K comes from a table
+ /// indexed by the tag and the context the object carries. For the drawings available here the
+ /// layout name is empty, so the context is 0, the tag is 17 and K is 0xDF. That
+ /// the pair values land in 0..25 for every string in the corpus is what confirms the
+ /// key rather than merely fitting it.
+ ///
+ ///
+ /// The pair value then maps to a character through , which is a table
+ /// rather than a shift: the digits are not in value order. The table was recovered by decoding
+ /// every payload in the corpus and reading the results as the dimensions they are — the
+ /// column in the sample decodes to a height of 2600 next to the 600 by 600 section its outline
+ /// gives, and those two come from different parts of the format, so they confirm each other.
+ ///
+ ///
+ public static class TgDataEncrypt
+ {
+ ///
+ /// The pair value that stands for each character of the decoded text.
+ ///
+ ///
+ /// Twelve entries, being the ten digits, the decimal point and the sign. The digits are
+ /// deliberately out of order: 6, 9 and 8 and 7 sit below 0.
+ ///
+ public static readonly IReadOnlyDictionary Alphabet = new Dictionary
+ {
+ { 39, '-' },
+ { 36, '.' },
+ { 50, '6' },
+ { 51, '9' },
+ { 56, '8' },
+ { 57, '7' },
+ { 58, '0' },
+ { 59, '1' },
+ { 60, '2' },
+ { 61, '3' },
+ { 62, '4' },
+ { 63, '5' },
+ };
+
+ ///
+ /// The key that applies to the objects in the sample drawings.
+ ///
+ public const int DefaultKey = 0xDF;
+
+ ///
+ /// Reads the number stored in a string written through the obfuscation layer.
+ ///
+ /// Ciphertext as it was read from the file.
+ /// Key for the object, for the samples.
+ /// The decoded text.
+ /// A pair decodes to a value outside the alphabet.
+ public static string Decode(string cipher, int key = DefaultKey)
+ {
+ if (cipher == null)
+ {
+ throw new ArgumentNullException(nameof(cipher));
+ }
+
+ StringBuilder text = new StringBuilder(cipher.Length / 2);
+
+ for (int i = 0; i + 1 < cipher.Length; i += 2)
+ {
+ int value = ((cipher[i] & 0xFF) - key) * 26 + ((cipher[i + 1] & 0xFF) - key);
+
+ if (!Alphabet.TryGetValue(value, out char character))
+ {
+ throw new FormatException($"Pair at {i} decodes to {value}, which is not in the alphabet");
+ }
+
+ text.Append(character);
+ }
+
+ return text.ToString();
+ }
+
+ ///
+ /// Reads every obfuscated number in a class specific payload.
+ ///
+ ///
+ /// The scalar fields of a class sit in its payload with nothing to delimit them, so they
+ /// are found the way the other readers find their data: by looking for what they are. A
+ /// ciphertext string is recognisable because every character of it has a low byte of
+ /// 0xC0 or more, which no ordinary text and no class id does, and because it decodes
+ /// to a number. Strings that fail either test are skipped.
+ ///
+ /// The class specific data of the object.
+ /// Drawing version used by the stream reader.
+ /// Key for the object, for the samples.
+ /// The values in the order they appear; never null.
+ public static IReadOnlyList ReadAllFrom(byte[] payload, ACadVersion version, int key = DefaultKey)
+ {
+ if (payload == null)
+ {
+ throw new ArgumentNullException(nameof(payload));
+ }
+
+ List values = new List();
+
+ if (payload.Length < 16)
+ {
+ return values;
+ }
+
+ using (MemoryStream stream = new MemoryStream(payload, writable: false))
+ {
+ IDwgStreamReader reader = DwgStreamReaderBase.GetStreamHandler(version, stream, resetPositon: true);
+ long limit = (long)payload.Length * 8L;
+
+ while (reader.PositionInBits() < limit)
+ {
+ long start = reader.PositionInBits();
+ string cipher;
+
+ try
+ {
+ cipher = reader.ReadVariableText();
+ }
+ catch (Exception)
+ {
+ reader.SetPositionInBits(start + 1);
+ continue;
+ }
+
+ if (!isCiphertext(cipher))
+ {
+ //Step one bit and try again: the string may start at a different alignment.
+ reader.SetPositionInBits(start + 1);
+ continue;
+ }
+
+ if (TryDecode(cipher, out double value, key))
+ {
+ values.Add(value);
+ }
+ }
+ }
+
+ return values;
+ }
+
+ private static bool isCiphertext(string value)
+ {
+ if (value == null || value.Length < 2 || value.Length > 120 || value.Length % 2 != 0)
+ {
+ return false;
+ }
+
+ foreach (char c in value)
+ {
+ //Every character of a ciphertext string sits in the high half of Latin-1, which is
+ //what makes it distinguishable from a class id, a property name and a marker.
+ if ((c & 0xFF) < 0xC0)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ /// Tries to read the number stored in a string written through the obfuscation layer.
+ ///
+ /// Ciphertext as it was read from the file.
+ /// Receives the decoded number.
+ /// Key for the object, for the samples.
+ /// True when the string decoded to a number.
+ public static bool TryDecode(string cipher, out double value, int key = DefaultKey)
+ {
+ value = 0.0;
+
+ if (cipher == null || cipher.Length < 2 || cipher.Length % 2 != 0)
+ {
+ return false;
+ }
+
+ string text;
+ try
+ {
+ text = Decode(cipher, key);
+ }
+ catch (FormatException)
+ {
+ return false;
+ }
+
+ return double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out value);
+ }
+ }
+}