Need to add helpers for validating Graphics rendering
There are two components to this. The first is relatively easy- we need helpers and some examples to view GDI calls generated when rendering to a Graphics object. The second is more complicated- we need to be able to look at GDI+ records.
Recording and playing back GDI records from a Graphics object can be done in a few ways:
- We can create a Graphics object around a Metafile HDC we create and just look at that HDC the way we're already doing it.
- We can create a System.Drawing MetaFile around a stream and create a Graphics object from that to record to. You can then grab the HENHMETAFILE from the MetaFile and enumerate the way we already are.
Enumerating the GDI+ records is more complicated as the record headers and parsing need to be created from scratch by following the EMF+ specification.
I've started fiddling with the lower level APIs with this here: https://github.com/JeremyKuhne/WInterop/blob/main/src/Tests/WInterop.Tests/GdiPlus/Metafiles.cs#L65 This code can be used to find the equivalent System.Drawing entry points.
The EMF+ specification: https://docs.microsoft.com/openspecs/windows_protocols/ms-emfplus/5f92c789-64f2-46b5-9ed4-15a9bb0946c6
https://github.com/dotnet/winforms/issues/4238 can get a regression test when this is feature is moved forward.
I'm interested in seeing how this will work. I'm learning about enhanced metafiles now and was able to save the DropDownHolder in #4238 with the fixed resize grip to a disk-based metafile using CreateEnhMetaFile and can open the metafile in Paint. I can kind of see how the validation will work by enumerating the records.
@willibrandon Enumerating the top-level records in EMF+ is relatively easy as GDI+ gives you a record enumerator (exposed in System.Drawing.Imaging.Metafile). Getting the details out of each record takes a little bit of work to get started as you have to sequentially scan as there aren't offsets to let you skip to the data you care about. I started playing with this in the WInterop project I linked above (the tests in the link above show how the data presents itself when viewed as a standard EMF or through the EMF+ enumerator). It can be used freely as a base/reference to doing a more complete parser. I intend to keep expanding the implementation there, but I don't have an expected timeframe.
@JeremyKuhne Thank you, taking a look at Winterop now and looking at the EMF+ specification. I can see how the record types are easily validated in the tests you linked due to the System.Drawing.Imaging.Metafile record enumerator. Am I correct that the details for each record will need to be parsed out of IntPtr data in the callback?
Am I correct that the details for each record will need to be parsed out of IntPtr data in the callback?
I believe so. I'm not positive where the data pointer starts, was just about to that point in my code. EMF+ records are stored in regular EMF comment records, which I was starting to tear apart in the linked test. Enumerating the same data via the equivalent callback that I have and comparing offsets would validate exactly where data points in the record.
The process is a little slow as it involves multiple jumps around the specification. :)
Making some progress. I can now take the MetafilePlusObject in your code and tear the Pen object out of it and inspect its EmfPlusPen Object properties. I can inspect the Version, Type, PenData, and BrushObject.
For example, if I fiddle with the Pen and specify a GpUnit of UnitInch like using Pen pen = new(Color.Purple, 1, GpUnit.UnitInch), I can then validate the PenUnit like so:
@object->Pen.PenData.PenUnit.Should().Be(UnitType.UnitTypeInch);
Now working on accessing the DrawLine record which is eluding me at the moment.
Making more progress. I can now traverse from the EmfPlusObject record to the EmfPlusDrawLines record.
public static unsafe MetafilePlusRecord* GetNextEmfPlusRecord(MetafilePlusRecord* record)
{
return (MetafilePlusRecord*)((byte*)record + record->Size);
}
...
record = MetafilePlusRecord.GetNextEmfPlusRecord(record);
record->Type.Should().Be(RecordType.EmfPlusDrawLines);
MetafilePlusDrawLines* @drawLines = (MetafilePlusDrawLines*)record;
@drawLines->CompressedData.Should().Be(true);
@drawLines->ExtraLine.Should().Be(false);
@drawLines->RelativeLocation.Should().Be(false);
@drawLines->ObjectId.Should().Be(0); // The index of an EmfPlusPen object in the EMF+ Object Table to draw the lines.
Now working on tearing the DrawLine record apart.
I can now inspect the PointData in the EmfPlusDrawLines record. I understand what you mean about the process being slow.
@drawLines->Count.Should().Be(2); // Number of points
MetafilePlusPoint from = @drawLines->GetPoint(0);
MetafilePlusPoint to = @drawLines->GetPoint(1);
from.X.Should().Be(1);
from.Y.Should().Be(1);
to.X.Should().Be(3);
to.Y.Should().Be(5);