feat: use perceptual hash for PDF change detection
Render PDF page 1 to an image and compute a DifferenceHash instead of SHA256 on raw file bytes. This ignores metadata/timestamp changes that SolidWorks varies between exports, preventing false revision bumps on Drawing entities. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CoenM.ImageSharp.ImageHash" Version="1.1.5" />
|
||||
<PackageReference Include="PDFtoImage" Version="4.1.1" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -207,7 +207,6 @@ namespace ExportDXF.Services
|
||||
private async Task ExportDrawingAsync(ExportContext context, string drawingNumber, string tempDir)
|
||||
{
|
||||
LogProgress(context, "Active document is a Drawing");
|
||||
LogProgress(context, "Finding BOM tables...");
|
||||
|
||||
var drawing = context.ActiveDocument.NativeDocument as DrawingDoc;
|
||||
if (drawing == null)
|
||||
@@ -216,16 +215,6 @@ namespace ExportDXF.Services
|
||||
return;
|
||||
}
|
||||
|
||||
var items = _bomExtractor.ExtractFromDrawing(drawing, context.ProgressCallback);
|
||||
|
||||
if (items == null || items.Count == 0)
|
||||
{
|
||||
LogProgress(context, "Error: Bill of materials not found.", LogLevel.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
LogProgress(context, $"Found {items.Count} component(s)");
|
||||
|
||||
// Export drawing to PDF in temp dir
|
||||
_drawingExporter.ExportToPdf(drawing, tempDir, context);
|
||||
|
||||
@@ -239,7 +228,7 @@ namespace ExportDXF.Services
|
||||
if (pdfs.Length > 0)
|
||||
{
|
||||
var pdfTempPath = pdfs[0];
|
||||
var pdfHash = ContentHasher.ComputeFileHash(pdfTempPath);
|
||||
var pdfHash = ContentHasher.ComputePdfContentHash(pdfTempPath);
|
||||
|
||||
var uploadResult = await _apiClient.UploadPdfAsync(
|
||||
pdfTempPath,
|
||||
@@ -264,8 +253,74 @@ namespace ExportDXF.Services
|
||||
LogProgress(context, $"PDF upload error: {ex.Message}", LogLevel.Error);
|
||||
}
|
||||
|
||||
// Export parts to DXF and save BOM items
|
||||
await ExportItemsAsync(items, tempDir, context, exportRecord?.Id);
|
||||
// Extract BOM items from drawing tables
|
||||
LogProgress(context, "Finding BOM tables...");
|
||||
var items = _bomExtractor.ExtractFromDrawing(drawing, context.ProgressCallback);
|
||||
|
||||
if (items != null && items.Count > 0)
|
||||
{
|
||||
LogProgress(context, $"Found {items.Count} component(s)");
|
||||
await ExportItemsAsync(items, tempDir, context, exportRecord?.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No BOM table — fall back to exporting the part referenced by the drawing views
|
||||
LogProgress(context, "No BOM table found. Checking drawing views for referenced part...");
|
||||
|
||||
var (part, configuration) = GetReferencedPartFromViews(drawing);
|
||||
if (part == null)
|
||||
{
|
||||
LogProgress(context, "No referenced part found in drawing views.", LogLevel.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
LogProgress(context, $"Found referenced part, exporting as single part...");
|
||||
|
||||
var item = _partExporter.ExportSinglePart(part, tempDir, context);
|
||||
if (item != null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(configuration))
|
||||
item.Configuration = configuration;
|
||||
|
||||
var existingItemNo = await FindExistingItemNoAsync(exportRecord?.Id, item.PartName, item.Configuration);
|
||||
item.ItemNo = existingItemNo ?? await GetNextItemNumberAsync(drawingNumber);
|
||||
|
||||
var bomItem = new BomItem
|
||||
{
|
||||
ExportRecordId = exportRecord?.Id ?? 0,
|
||||
ItemNo = item.ItemNo,
|
||||
PartNo = item.FileName ?? item.PartName ?? "",
|
||||
SortOrder = 0,
|
||||
Qty = item.Quantity,
|
||||
TotalQty = item.Quantity,
|
||||
Description = item.Description ?? "",
|
||||
PartName = item.PartName ?? "",
|
||||
ConfigurationName = item.Configuration ?? "",
|
||||
Material = item.Material ?? ""
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(item.LocalTempPath))
|
||||
{
|
||||
var uploadResult = await UploadDxfAsync(item, context);
|
||||
if (uploadResult != null)
|
||||
{
|
||||
bomItem.CutTemplate = new CutTemplate
|
||||
{
|
||||
DxfFilePath = uploadResult.StoredFilePath,
|
||||
ContentHash = item.ContentHash,
|
||||
Thickness = item.Thickness > 0 ? item.Thickness : null,
|
||||
KFactor = item.KFactor > 0 ? item.KFactor : null,
|
||||
DefaultBendRadius = item.BendRadius > 0 ? item.BendRadius : null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
context.BomItemCallback?.Invoke(bomItem);
|
||||
|
||||
if (exportRecord != null)
|
||||
await SaveBomItemAsync(exportRecord.Id, bomItem, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -600,6 +655,24 @@ namespace ExportDXF.Services
|
||||
throw new ArgumentException("ProgressCallback cannot be null.", nameof(context));
|
||||
}
|
||||
|
||||
private (PartDoc part, string configuration) GetReferencedPartFromViews(DrawingDoc drawing)
|
||||
{
|
||||
var view = (IView)drawing.GetFirstView();
|
||||
// First view is the sheet itself — skip it
|
||||
view = (IView)view.GetNextView();
|
||||
|
||||
while (view != null)
|
||||
{
|
||||
var doc = view.ReferencedDocument;
|
||||
if (doc is PartDoc part)
|
||||
return (part, view.ReferencedConfiguration);
|
||||
|
||||
view = (IView)view.GetNextView();
|
||||
}
|
||||
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
private void LogProgress(ExportContext context, string message, LogLevel level = LogLevel.Info, string file = null)
|
||||
{
|
||||
context.ProgressCallback?.Invoke(message, level, file);
|
||||
|
||||
@@ -7,6 +7,11 @@ using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using ACadSharp.Entities;
|
||||
using ACadSharp.IO;
|
||||
using CoenM.ImageHash;
|
||||
using CoenM.ImageHash.HashAlgorithms;
|
||||
using PDFtoImage;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace ExportDXF.Utilities
|
||||
{
|
||||
@@ -30,6 +35,36 @@ namespace ExportDXF.Utilities
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a perceptual hash of a PDF by rendering page 1 to an image,
|
||||
/// so only visual changes affect the hash (metadata/timestamp changes are ignored).
|
||||
/// Falls back to a raw file hash if rendering fails.
|
||||
/// </summary>
|
||||
public static string ComputePdfContentHash(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var pdfStream = File.OpenRead(filePath))
|
||||
using (var pngStream = new MemoryStream())
|
||||
{
|
||||
Conversion.SavePng(pngStream, pdfStream, page: 0,
|
||||
options: new RenderOptions(Dpi: 72));
|
||||
pngStream.Position = 0;
|
||||
|
||||
using (var image = Image.Load<Rgba32>(pngStream))
|
||||
{
|
||||
var algorithm = new DifferenceHash();
|
||||
var hash = algorithm.Hash(image);
|
||||
return hash.ToString("x16");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return ComputeFileHash(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a SHA256 hash of the entire file contents (for PDFs and other binary files).
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user