Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DWGWriter thumbnail support #558

Open
ChrisClems opened this issue Feb 12, 2025 · 6 comments · May be fixed by #563
Open

DWGWriter thumbnail support #558

ChrisClems opened this issue Feb 12, 2025 · 6 comments · May be fixed by #563
Labels
feature New feature added

Comments

@ChrisClems
Copy link

ChrisClems commented Feb 12, 2025

I want to say before getting to the point, I know this is nobody's top priority and this is likely a huge amount of work. I'd like to take on the heavy lifting and eventually submit a PR for this if it would be welcome and I can figure it out. I'm starting an issue to 1) see if this functionality could be packaged in a way that would be accepted as a PR in ACadSharp and if so 2) consult the experts to see if they have any information that might save me some time and research.

I process massive amounts of individual AutoCAD files. My library is tens of thousands of drawings and I'm working on up to 1,000 at a time in a single project. It is incredibly useful to me to spot-check thumbnails in folders to see if there are any glaring issues I need to address. I usually export DXF files when I'm working with ACadSharp because I have software on my machine which auto-generates thumbnails for them. It would be beneficial to me to be able to output DWG files with thumbnails.
 
The way I understand it, DWG thumbnails are generated by AutoCAD and saved in the file header in various formats depending on the DWG version. Currently, the only way to generate a DWG with a thumbnail which was not originally saved with one is to open it in AutoCAD and save it. It looks like there's some information on storing BMP and WMF thumbnails on page 93 of the ODS DWG spec document. There are format changes in R13C3. I think with this document and a bit of work reading how the DwgWriter works I might be able to implement this as an option to pass when writing a DWG file.

I understand the desire to keep this a lightweight library so any sort of rendering dependencies are obviously out. My alternate idea is optionally passing a user-generated byte array representing a BMP to the DwgWriter that would be inserted at the correct spot. There is already some precedent for this as the DwgReader is retrieving what appears to be preview image data as byte arrays when importing BlockRecords.

Or rather than passing a user-generated byte array, I don't think it would be that insane to implement a simple BMP rasterization class. The BMP spec is dead simple and is not exactly going to be undergoing any major breaking changes that would create a maintenance issue for this class. I wrote up a super simple proof of concept that renders vector line and arc data to a BMP and it's working in under 200 lines of code. Obviously expanding that to support more entities would increase the size but I think a BMP raster class could be quite compact. It would only need to support the most basic entities such as lines, arcs, circles, ellipses, polylines, and splines. Polylines can be exploded for the purposes of rasterization to keep things simple. Text, hatching, and other more complex entities are already being ignored in AutoCAD thumbnails so we can as well. NURBS splines are the only thing I'm having difficulty with in quick testing.

If I can create a working version of DwgWriter that generates valid DWG files with thumbnails and make a reasonably small and basic rasterization class that will create a BMP byte array from primitive entities in model space to pass to it, would you be open to a PR for a new feature?

@DomCR
Copy link
Owner

DomCR commented Feb 14, 2025

Hi @ChrisClems,

We can take a look at this feature, the reading part is easy, check how the thumbnail is stored and read it as a byte array or even a bitmap.

To write it that's another story, the idea of the user giving the byte array is a good one, if they have the tools to generate the image or want a custom want is just a matter of writing those bytes.

The only rule that I would impose is that if the feature needs some external or third party libraries maybe it would be best to create a separated project like I did with ACadSharp.Pdf if more formats are involved maybe I event change the name to ACadSharp.Media and add them there.

I'll take a look when I can and see what we can do.

Thanks for your support.

@DomCR DomCR added the feature New feature added label Feb 14, 2025
@ChrisClems
Copy link
Author

@DomCR Thanks! So I went a little further into this last week than I'd planned before hearing back from you. I left all my work on this at the office so I won't be able to dig back in until next week but here's a brain dump while it's still fresh in my memory.

I believe I've found a bug in the DwgWriter. This was my initial testing process:

  1. I created a BMP byte array and tried to insert it in the preview section based on the linked documentation. I could not get this to work.
  2. Digging into some DWGs straight out of AutoCAD, I determined that at least AutoCAD 2018 did not have any way to write anything but PNG thumbnails. I figured maybe since that documentation was published they dropped BMP support so I started focusing on PNGs.
  3. I reverse engineered what I believe is the preview section for a 2018 DWG file which is different than the BMP version in the linked doc. There are a few sections where I don't know what they do, but the values are static enough that I don't believe we need to know to make it work. The essential entries are the overall size of the image section and the size of the PNG itself, both represented by a 32 bit UINT.
  4. Adding the logic to the DwgPreviewWriter to create the correct code does not work. It creates a seemingly valid file for some versions, but Windows will not pick up the thumbnail.
  5. Some versions will write files that when opened will trigger the recovery dialog in AutoCAD. I didn't have a lot of time to dig into this before I left, but I believe AC2018 and AC2032 were working, while others tested were outputting broken files. Both of these specifically use DwgFileHeaderWriterAC18() to write the header. Not sure if that's relevant.
  6. In the interest of being thorough, I took a working DWG and I hand-wrote a new preview section in a hex editor using the same PNG data that I was trying to inject via the DwgPreviewWriter and Windows picked it up as soon as I saved the file. I cross-referenced my hand-written preview section with what ACadSharp was putting in the file and they matched. This leads me to believe that the preview section may not be where AutoCAD expects it to be in the file. In a working DWG saved from AutoCAD, the preview section is very close to the beginning of the file. In ACadSharp there's much more data and a bunch of zero padding between the beginning of the file and the preview section.

For what it's worth, I also managed to cobble together a PNG encoder that takes lines, arcs, ellipses, elliptical arcs, fit splines, and NURBS splines, and rasterizes them with zero external dependencies. It's under 1,000 lines of code. I needed some help from Claude to get the NURBS spline approximation working but the experimental code had a really clean interface. The PNGs it outputs are passing pngcheck with no issues and the compression is also handled with standard libraries.

I'll do more cross referencing next week with the section layout in my DWG files and see if I can find where the problem is. If I can't figure it out myself by week's end, I'll dump a bunch of examples here so someone else can check my work.

@DomCR
Copy link
Owner

DomCR commented Feb 16, 2025

  1. Some versions will write files that when opened will trigger the recovery dialog in AutoCAD. I didn't have a lot of time to dig into this before I left, but I believe AC2018 and AC2032 were working, while others tested were outputting broken files. Both of these specifically use DwgFileHeaderWriterAC18() to write the header. Not sure if that's relevant.

The version AC14 and AC15 are quite old, the writer is implemented but is not super stable and the files usually need to be restored, it also needs to be refined to be able to write an obsolete object, VP_ENT_HDR_CTRL_OBJ which it stores and controls all the viewports.

The version AC21 is a particular one, Autodesk changed some parts of the encoding, but only for this one, I haven't implemented yet, there is a branch created but the amount is quite high and there are some other priorities before implementing a writer for an older version.

AC18, AC27 (recently fixed) and AC32 are the ones that work better and the best to test with, the writer is the most stable.

  1. In a working DWG saved from AutoCAD, the preview section is very close to the beginning of the file.

This should not matter, in the FileHeader there is the Preview offset which I believe that Autocad will get to render the preview in the browser.

For AC15:

fileheader.PreviewAddress = sreader.ReadInt();

For AC18 and after:

fileheader.PreviewAddress = sreader.ReadRawLong();

In the writer for AC18 and after:

writer.Write((uint)((int)this._descriptors[DwgSectionDefinition.Preview].LocalSections[0].Seeker + 0x20));

In ACadSharp there's much more data and a bunch of zero padding between the beginning of the file and the preview section.

Thanks a lot for this input ❤! the 0s where related to the DxfClasses section not being compressed, now the files are much lighter too.
I've also dig about the AC27 version not working and manage to fix it.

@DomCR
Copy link
Owner

DomCR commented Feb 16, 2025

I've been trying to generate a dxf with a preview image but Autocad doesn't have the feature enabled for some reason, I wanted to try to generate the image from a dxf file which is way more transparent than a dwg.

Where do you create the image preview for your dxf?

@ChrisClems
Copy link
Author

I've been trying to generate a dxf with a preview image but Autocad doesn't have the feature enabled for some reason, I wanted to try to generate the image from a dxf file which is way more transparent than a dwg.

Where do you create the image preview for your dxf?

I use F3D to generate DXF thumbnails system wide. I hadn't tried working out the ASCII DXF thumbnail section because it's so poorly documented. I might loop back and see if I can craft a working DXF thumbnail section but right now my priority is DWG.

AC18, AC27 (recently fixed) and AC32 are the ones that work better and the best to test with, the writer is the most stable.

Understood. I will focus my attention on the latest version for the moment.

You're obviously lightyears ahead of me on the DWG spec so I'll post what I know so far. You might see something obvious that I'm missing, saving some duplicated work.

The attached zip has a few files. An input DXF, an output 1032/2018 DWG from ACadSharp, then the same DWG, opened then saved in AutoCAD 2024. The PNG I am reading and manually injecting into the file in the DwgPreviewWriter with this code for testing:

		public DwgPreviewWriter(ACadVersion version, Stream stream) : base(version)
		{
			this._swriter = DwgStreamWriterBase.GetStreamWriter(version, stream, TextEncoding.Windows1252());
		}

		public void Write()
		{
			var pngData = File.ReadAllBytes(@"E:\DWGTest\output.png");
			// Write start sentinel
			this._swriter.WriteBytes(this._startSentinel);

			// Calculate overall size: 
			// Current overhead before PNG file is 99 bytes. Add better computed value later.
			int overallSize = 99 + pngData.Length;
			this._swriter.WriteRawLong(overallSize);
			
			// Unknown what these are. Current grouping is just my best guess.
			this._swriter.WriteByte(2); // In BMP spec this was the image count.
			this._swriter.WriteByte(1); // In BMP spec this was the image format identifier
			this._swriter.WriteRawLong(487);
			this._swriter.WriteRawLong(80);
			this._swriter.WriteByte(6);
			this._swriter.WriteRawLong(567);
			
			// PNG data size
			this._swriter.WriteRawLong(pngData.Length);
			
			// Zero padding? Always 80 bytes in my drawings
			this._swriter.WriteBytes(new byte[80]);
			
			// Write the image data
			this._swriter.WriteBytes(pngData);

			// Write end sentinel
			this._swriter.WriteBytes(this._endSentinel);
		}

I checked the preview offset you referenced in both files and they appear to be set correctly. This is what I've decoded for the PNG preview section layout. The overall size and the PNG size are the only values that seem to need to be changed.

=== Start Sentinel===
1F 25 6D 07 D4 36 28 28 9D 57 CA 3F 9D 44 10 2B
=== Content ===
A1 04 00 00 // Overall size of thumbnail definition as long. Current tests all show image size + 99
02 01 // Unknown. BMP format used these bytes to dictate image count and format, respectively. Always 02 and 01 in my drawings
E7 01 00 00 // Unknown. Seems to be a long. Changes based on thumbnail settings. Default settings always output E7 01 00 00
50 00 00 00 // Unknown. Does not change in my testing
06 // Unknown. Does not change in my testing
37 02 00 00 // Unknown. Seems to be a long. Changes based on thumbnail settings. Default settings always output 37 02 00 00
3E 04 00 00 // Image size as long
// 80 bytes of zero padding
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
// PNG data
89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52 00 00 00 BD 00 00 00 6D 08 03 00 00 00 C3 37 90 2D 00 00 03 00 50 4C 54 45 00 00 00 80 00 00 00 80 00 80 80 00 00 00 80 80 00 80 00 80 80 C0 C0 C0 C0 DC C0 A6 CA F0 40 20 00 60 20 00 80 20 00 A0 20 00 C0 20 00 E0 20 00 00 40 00 20 40 00 40 40 00 60 40 00 80 40 00 A0 40 00 C0 40 00 E0 40 00 00 60 00 20 60 00 40 60 00 60 60 00 80 60 00 A0 60 00 C0 60 00 E0 60 00 00 80 00 20 80 00 40 80 00 60 80 00 80 80 00 A0 80 00 C0 80 00 E0 80 00 00 A0 00 20 A0 00 40 A0 00 60 A0 00 80 A0 00 A0 A0 00 C0 A0 00 E0 A0 00 00 C0 00 20 C0 00 40 C0 00 60 C0 00 80 C0 00 A0 C0 00 C0 C0 00 E0 C0 00 00 E0 00 20 E0 00 40 E0 00 60 E0 00 80 E0 00 A0 E0 00 C0 E0 00 E0 E0 00 00 00 40 20 00 40 40 00 40 60 00 40 80 00 40 A0 00 40 C0 00 40 E0 00 40 00 20 40 20 20 40 40 20 40 60 20 40 80 20 40 A0 20 40 C0 20 40 E0 20 40 00 40 40 20 40 40 40 40 40 60 40 40 80 40 40 A0 40 40 C0 40 40 E0 40 40 00 60 40 20 60 40 40 60 40 60 60 40 80 60 40 A0 60 40 C0 60 40 E0 60 40 00 80 40 20 80 40 40 80 40 60 80 40 80 80 40 A0 80 40 C0 80 40 E0 80 40 00 A0 40 20 A0 40 40 A0 40 60 A0 40 80 A0 40 A0 A0 40 C0 A0 40 E0 A0 40 00 C0 40 20 C0 40 40 C0 40 60 C0 40 80 C0 40 A0 C0 40 C0 C0 40 E0 C0 40 00 E0 40 20 E0 40 40 E0 40 60 E0 40 80 E0 40 A0 E0 40 C0 E0 40 E0 E0 40 00 00 80 20 00 80 40 00 80 60 00 80 80 00 80 A0 00 80 C0 00 80 E0 00 80 00 20 80 20 20 80 40 20 80 60 20 80 80 20 80 A0 20 80 C0 20 80 E0 20 80 00 40 80 20 40 80 40 40 80 60 40 80 80 40 80 A0 40 80 C0 40 80 E0 40 80 00 60 80 20 60 80 40 60 80 60 60 80 80 60 80 A0 60 80 C0 60 80 E0 60 80 00 80 80 20 80 80 40 80 80 60 80 80 80 80 80 A0 80 80 C0 80 80 E0 80 80 00 A0 80 20 A0 80 40 A0 80 60 A0 80 80 A0 80 A0 A0 80 C0 A0 80 E0 A0 80 00 C0 80 20 C0 80 40 C0 80 60 C0 80 80 C0 80 A0 C0 80 C0 C0 80 E0 C0 80 00 E0 80 20 E0 80 40 E0 80 60 E0 80 80 E0 80 A0 E0 80 C0 E0 80 E0 E0 80 00 00 C0 20 00 C0 40 00 C0 60 00 C0 80 00 C0 A0 00 C0 C0 00 C0 E0 00 C0 00 20 C0 20 20 C0 40 20 C0 60 20 C0 80 20 C0 A0 20 C0 C0 20 C0 E0 20 C0 00 40 C0 20 40 C0 40 40 C0 60 40 C0 80 40 C0 A0 40 C0 C0 40 C0 E0 40 C0 00 60 C0 20 60 C0 40 60 C0 60 60 C0 80 60 C0 A0 60 C0 C0 60 C0 E0 60 C0 00 80 C0 20 80 C0 40 80 C0 60 80 C0 80 80 C0 A0 80 C0 C0 80 C0 E0 80 C0 00 A0 C0 20 A0 C0 40 A0 C0 60 A0 C0 80 A0 C0 A0 A0 C0 C0 A0 C0 E0 A0 C0 00 C0 C0 20 C0 C0 40 C0 C0 60 C0 C0 80 C0 C0 A0 C0 C0 FF FB F0 A0 A0 A4 80 80 80 FF 00 00 00 FF 00 FF FF 00 00 00 FF FF 00 FF 00 FF FF FF FF FF 58 D2 34 44 00 00 00 F9 49 44 41 54 78 DA ED DB 51 0A C4 20 0C 45 D1 6C 2F FB 5F 50 06 86 F9 A8 8E 52 E8 8F 5E 7B 5D C1 49 29 34 26 AF 91 E4 13 EA D5 AB 57 AF 5E BD 7A F5 EA D5 AB 57 AF 5E BD 7A F5 4F F5 B5 F9 B9 D1 6F FE 88 A7 15 20 F4 BF 0A C0 FA 6F 01 64 FD 3F 94 A5 EF A5 30 7D 47 A5 E9 5B 2B 4E DF 60 79 FA AB 56 FD 3A 7E 24 99 1F 99 E0 77 27 92 CC 8F 79 13 41 D2 23 BF 59 91 48 FE 40 CF EB D6 22 91 FC A1 3E 31 F7 DA C2 CC 14 D4 6F A6 2F B2 3E D5 AB 57 AF 5E BD 7A F5 EA D5 AB 57 7F 4C 7F 7F E0 CD 90 32 53 38 6F 9E 83 9E A5 91 E7 98 45 9E 21 23 B7 9E 47 EC 4E 60 F8 56 8F DE 19 D2 F0 64 7D 75 DB 66 37 FD 4B 7A 36 F3 39 EB BA 65 58 AA 8E 9C 4B 23 27 1A 0B 9C C7 1C B6 C0 04 7D DD E4 90 A1 11 70 FF 1E 50 AF 5E BD 7A F5 EA D5 AB 57 AF 5E BD 7A F5 EA D5 BF 50 FF 01 00 E9 10 49 A2 0A 60 65 00 00 00 00 49 45 4E 44 AE 42 60 82
=== End Sentinel===
E0 DA 92 F8 2B C9 D7 D7 62 A8 35 C0 62 BB EF D4

The preview section itself seems to be correct when cross-referencing files with working thumbnails out of AutoCAD. I made sure my PNG data did not have any unknown issues by hand-crafting a preview section with my generated PNG in a hex editor. Saving the file in the hex editor immediately updated the thumbnail in file explorer. The ACadSharp-generated DWG has the preview section at 0x7920 while the one from AutoCAD is at 0x01C0.

My only real idea at the moment is there's some overall length not being computed correctly when I add data to the preview section, and it is not reading far enough to get the thumbnail.

DWGTest.zip

@DomCR
Copy link
Owner

DomCR commented Feb 18, 2025

I managed to make it work using the branch linked to the issue, here is the code to test it:

			CadDocument doc;
			DwgPreview preview;
			using (DwgReader reader = new DwgReader(_file))
			{
				doc = reader.Read();
				preview = reader.ReadPreview();
			}

			string output = Path.Combine(Path.GetDirectoryName(_file),
				$"{Path.GetFileNameWithoutExtension(_file)}.out.dwg");
			using (DwgWriter writer = new DwgWriter(output, new CadDocument()))
			{
				writer.Preview = preview;
				writer.Write();
			}

I'll try to refine the code and fix the offset positioning.

@DomCR DomCR linked a pull request Feb 18, 2025 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New feature added
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants