dotnet / winforms

Windows Forms is a .NET UI framework for building Windows desktop applications.
MIT License
4.41k stars 981 forks source link

Need to add helpers for validating Graphics rendering #4341

Open JeremyKuhne opened 3 years ago

JeremyKuhne commented 3 years ago

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:

  1. 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.
  2. 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

JeremyKuhne commented 3 years ago

https://github.com/dotnet/winforms/issues/4238 can get a regression test when this is feature is moved forward.

willibrandon commented 3 years ago

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.

DropDownHolder

JeremyKuhne commented 3 years ago

@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.

willibrandon commented 3 years ago

@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?

JeremyKuhne commented 3 years ago

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. :)

willibrandon commented 3 years ago

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.

willibrandon commented 3 years ago

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.

willibrandon commented 3 years ago

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);