diff --git a/ExportDXF/Utilities/ContentHasher.cs b/ExportDXF/Utilities/ContentHasher.cs
index 0be2612..4b08925 100644
--- a/ExportDXF/Utilities/ContentHasher.cs
+++ b/ExportDXF/Utilities/ContentHasher.cs
@@ -1,26 +1,32 @@
using System;
+using System.Collections.Generic;
+using System.Globalization;
using System.IO;
+using System.Linq;
using System.Security.Cryptography;
using System.Text;
+using ACadSharp.Entities;
+using ACadSharp.IO;
namespace ExportDXF.Utilities
{
public static class ContentHasher
{
///
- /// Computes a SHA256 hash of DXF file content, skipping the HEADER section
- /// which contains timestamps that change on every save.
+ /// Computes a SHA256 hash of DXF geometry, ignoring entity ordering,
+ /// handle assignments, style names, and floating-point epsilon differences
+ /// that SolidWorks changes between re-exports of identical geometry.
+ /// Falls back to a raw file hash if ACadSharp parsing fails.
///
public static string ComputeDxfContentHash(string filePath)
{
- var text = File.ReadAllText(filePath);
- var contentStart = FindEndOfHeader(text);
- var content = contentStart >= 0 ? text.Substring(contentStart) : text;
-
- using (var sha = SHA256.Create())
+ try
{
- var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(content));
- return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant();
+ return ComputeGeometricHash(filePath);
+ }
+ catch
+ {
+ return ComputeFileHash(filePath);
}
}
@@ -37,82 +43,95 @@ namespace ExportDXF.Utilities
}
}
- ///
- /// Finds the position immediately after the HEADER section's ENDSEC marker.
- /// DXF HEADER format:
- /// 0\nSECTION\n2\nHEADER\n...variables...\n0\nENDSEC\n
- /// Returns -1 if no HEADER section is found.
- ///
- private static int FindEndOfHeader(string text)
+ private static string ComputeGeometricHash(string filePath)
{
- // Find the HEADER section start
- var headerIndex = FindGroupCode(text, 0, "2", "HEADER");
- if (headerIndex < 0)
- return -1;
-
- // Advance past the HEADER value line so pair scanning stays aligned
- var headerLineEnd = text.IndexOf('\n', headerIndex);
- if (headerLineEnd < 0)
- return -1;
-
- // Find the ENDSEC that closes the HEADER section
- var pos = headerLineEnd + 1;
- while (pos < text.Length)
+ using (var reader = new DxfReader(filePath))
{
- var endsecIndex = FindGroupCode(text, pos, "0", "ENDSEC");
- if (endsecIndex < 0)
- return -1;
+ var doc = reader.Read();
+ var signatures = new List();
- // Move past the ENDSEC line
- var lineEnd = text.IndexOf('\n', endsecIndex);
- return lineEnd >= 0 ? lineEnd + 1 : text.Length;
+ foreach (var entity in doc.Entities)
+ {
+ signatures.Add(GetEntitySignature(entity));
+ }
+
+ signatures.Sort(StringComparer.Ordinal);
+ var combined = string.Join("\n", signatures);
+
+ using (var sha = SHA256.Create())
+ {
+ var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(combined));
+ return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant();
+ }
}
-
- return -1;
}
- ///
- /// Finds a DXF group code pair (code line followed by value line) starting from the given position.
- /// Returns the position of the value line, or -1 if not found.
- ///
- private static int FindGroupCode(string text, int startIndex, string groupCode, string value)
+ private static string GetEntitySignature(Entity entity)
{
- var pos = startIndex;
- while (pos < text.Length)
+ var layer = entity.Layer?.Name ?? "";
+
+ switch (entity)
{
- // Skip whitespace/newlines to find the group code
- while (pos < text.Length && (text[pos] == '\r' || text[pos] == '\n' || text[pos] == ' '))
- pos++;
+ case Line line:
+ return GetLineSignature(line, layer);
+ case Arc arc:
+ return GetArcSignature(arc, layer);
+ case Circle circle:
+ return GetCircleSignature(circle, layer);
+ case MText mtext:
+ return GetMTextSignature(mtext, layer);
+ default:
+ return $"{entity.GetType().Name}|{layer}";
+ }
+ }
- if (pos >= text.Length)
- break;
+ private static string GetLineSignature(Line line, string layer)
+ {
+ var p1 = FormatPoint(line.StartPoint.X, line.StartPoint.Y);
+ var p2 = FormatPoint(line.EndPoint.X, line.EndPoint.Y);
- // Read the group code line
- var codeLineEnd = text.IndexOf('\n', pos);
- if (codeLineEnd < 0)
- break;
-
- var codeLine = text.Substring(pos, codeLineEnd - pos).Trim();
-
- // Move to the value line
- var valueStart = codeLineEnd + 1;
- if (valueStart >= text.Length)
- break;
-
- var valueLineEnd = text.IndexOf('\n', valueStart);
- if (valueLineEnd < 0)
- valueLineEnd = text.Length;
-
- var valueLine = text.Substring(valueStart, valueLineEnd - valueStart).Trim();
-
- if (codeLine == groupCode && string.Equals(valueLine, value, StringComparison.OrdinalIgnoreCase))
- return valueStart;
-
- // Move to the next pair
- pos = valueLineEnd + 1;
+ // Normalize endpoint order so direction doesn't affect the hash
+ if (string.Compare(p1, p2, StringComparison.Ordinal) > 0)
+ {
+ var tmp = p1;
+ p1 = p2;
+ p2 = tmp;
}
- return -1;
+ return $"LINE|{layer}|{p1}|{p2}";
+ }
+
+ private static string GetArcSignature(Arc arc, string layer)
+ {
+ var center = FormatPoint(arc.Center.X, arc.Center.Y);
+ var r = R(arc.Radius);
+ var sa = R(arc.StartAngle);
+ var ea = R(arc.EndAngle);
+ return $"ARC|{layer}|{center}|{r}|{sa}|{ea}";
+ }
+
+ private static string GetCircleSignature(Circle circle, string layer)
+ {
+ var center = FormatPoint(circle.Center.X, circle.Center.Y);
+ var r = R(circle.Radius);
+ return $"CIRCLE|{layer}|{center}|{r}";
+ }
+
+ private static string GetMTextSignature(MText mtext, string layer)
+ {
+ var point = FormatPoint(mtext.InsertPoint.X, mtext.InsertPoint.Y);
+ var text = mtext.Value ?? "";
+ return $"MTEXT|{layer}|{point}|{text}";
+ }
+
+ private static string R(double value)
+ {
+ return Math.Round(value, 4).ToString(CultureInfo.InvariantCulture);
+ }
+
+ private static string FormatPoint(double x, double y)
+ {
+ return $"{R(x)},{R(y)}";
}
}
}