Files
ExportDXF/ExportDXF/Utilities/ContentHasher.cs
AJ Isaacs 5d2948d563 feat: replace text-based DXF hash with geometric content hash
SolidWorks re-exports produce files with identical geometry but different
entity ordering, handle assignments, style names, and floating-point
epsilon values. This caused hash mismatches and unnecessary API updates.

Uses ACadSharp to parse DXF entities and build canonical, sorted
signatures (LINE, ARC, CIRCLE, MTEXT) with coordinates rounded to 4
decimal places. Falls back to raw file hash if parsing fails.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 07:40:51 -05:00

138 lines
4.4 KiB
C#

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
{
/// <summary>
/// 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.
/// </summary>
public static string ComputeDxfContentHash(string filePath)
{
try
{
return ComputeGeometricHash(filePath);
}
catch
{
return ComputeFileHash(filePath);
}
}
/// <summary>
/// Computes a SHA256 hash of the entire file contents (for PDFs and other binary files).
/// </summary>
public static string ComputeFileHash(string filePath)
{
using (var sha = SHA256.Create())
using (var stream = File.OpenRead(filePath))
{
var bytes = sha.ComputeHash(stream);
return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant();
}
}
private static string ComputeGeometricHash(string filePath)
{
using (var reader = new DxfReader(filePath))
{
var doc = reader.Read();
var signatures = new List<string>();
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();
}
}
}
private static string GetEntitySignature(Entity entity)
{
var layer = entity.Layer?.Name ?? "";
switch (entity)
{
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}";
}
}
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);
// 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 $"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)}";
}
}
}