diff --git a/ExportDXF/ExportDXF.csproj b/ExportDXF/ExportDXF.csproj
index fd1c36f..e68e775 100644
--- a/ExportDXF/ExportDXF.csproj
+++ b/ExportDXF/ExportDXF.csproj
@@ -14,6 +14,9 @@
+
+
+
diff --git a/ExportDXF/Services/DxfExportService.cs b/ExportDXF/Services/DxfExportService.cs
index 5f8a67b..f6528be 100644
--- a/ExportDXF/Services/DxfExportService.cs
+++ b/ExportDXF/Services/DxfExportService.cs
@@ -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);
diff --git a/ExportDXF/Utilities/ContentHasher.cs b/ExportDXF/Utilities/ContentHasher.cs
index 4b08925..318e359 100644
--- a/ExportDXF/Utilities/ContentHasher.cs
+++ b/ExportDXF/Utilities/ContentHasher.cs
@@ -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
}
}
+ ///
+ /// 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.
+ ///
+ 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(pngStream))
+ {
+ var algorithm = new DifferenceHash();
+ var hash = algorithm.Hash(image);
+ return hash.ToString("x16");
+ }
+ }
+ }
+ catch
+ {
+ return ComputeFileHash(filePath);
+ }
+ }
+
///
/// Computes a SHA256 hash of the entire file contents (for PDFs and other binary files).
///