File: SynPdf.pas (Synopse framework 1.18, Copyright (C) 2023 Arnaud Bouchez)
Affected code: EMR_POLYDRAW and EMR_POLYDRAW16 in the metafile enumeration callback (around lines 9509 and 9559 in our copy)
Symptom
When a metafile containing PolyDraw records with Bézier segments is rendered into a PDF via TPdfDocumentGDI, the curves come out distorted: outlines drift progressively, glyph shapes overlap, and stray line segments appear that are not in the source. The same metafile drawn to a screen or printer DC with PlayEnhMetaFile renders correctly — only the PDF output is wrong, which makes the problem easy to miss.
Root cause
Per the PolyDraw contract, a PT_BEZIERTO segment consists of three consecutive points (aptl[i], aptl[i+1], aptl[i+2]), all flagged PT_BEZIERTO; the curve's start point is the current position. The current code reads the points one index too far and then advances the loop counter one step too many:
PT_BEZIERTO: begin
E.Canvas.CurveToCI(PEMRPolyDraw16(R)^.apts[i+1].X,PEMRPolyDraw16(R)^.apts[i+1].Y,
PEMRPolyDraw16(R)^.apts[i+2].X,PEMRPolyDraw16(R)^.apts[i+2].Y,
PEMRPolyDraw16(R)^.apts[i+3].X,PEMRPolyDraw16(R)^.apts[i+3].Y);
inc(i,3);
Because the enclosing while loop also performs inc(i) at the end of each iteration, every Bézier segment consumes four points instead of three. The offset accumulates along the path, so the longer the outline, the worse the distortion.
Both the 32-bit (EMR_POLYDRAW, aptl) and 16-bit (EMR_POLYDRAW16, apts) branches are affected. In practice the 16-bit branch is the one that matters, since GDI stores coordinates within ±32767 as 16-bit records.
Consistency check inside SynPDF itself
The neighbouring EMR_POLYBEZIERTO16 branch already implements the correct indexing:
for i := 0 to (PEMRPolyBezierTo16(R)^.cpts div 3)-1 do
E.Canvas.CurveToCI(PEMRPolyBezierTo16(R)^.apts[i3].X, PEMRPolyBezierTo16(R)^.apts[i3].Y,
PEMRPolyBezierTo16(R)^.apts[i3+1].X, PEMRPolyBezierTo16(R)^.apts[i3+1].Y,
PEMRPolyBezierTo16(R)^.apts[i3+2].X, PEMRPolyBezierTo16(R)^.apts[i3+2].Y);
Three points per curve, starting at the segment's own first point — exactly what the PolyDraw branch should do.
Out-of-bounds read
There is a second consequence. If a path ends with a Bézier segment (points n-3, n-2, n-1), the current code reads apts[n-2], apts[n-1] and apts[n] — one element past the end of the array. The polytypes^[i] test after inc(i,3) reads past the type array as well. With the corrected indexing both accesses stay in bounds.
Reproduction
Create an EMF containing curves recorded via PolyDraw. A simple way: select a TrueType font, capture glyph outlines with BeginPath / TextOut / EndPath / GetPath, then replay the returned points into a metafile DC with PolyDraw inside BeginPath / EndPath / FillPath.
Render that EMF into a PDF through TPdfDocumentGDI (PlayEnhMetaFile onto VCLCanvas, then SaveToFile).
Compare against the same artwork recorded with MoveToEx / PolyBezierTo / LineTo instead of PolyDraw. Before the fix the two differ visibly.
Suggested patch
EMR_POLYDRAW:
PT_BEZIERTO: begin
E.Canvas.CurveToCI(PEMRPolyDraw(R)^.aptl[i].X,PEMRPolyDraw(R)^.aptl[i].Y,
PEMRPolyDraw(R)^.aptl[i+1].X,PEMRPolyDraw(R)^.aptl[i+1].Y,
PEMRPolyDraw(R)^.aptl[i+2].X,PEMRPolyDraw(R)^.aptl[i+2].Y);
inc(i,2);
EMR_POLYDRAW16:
PT_BEZIERTO: begin
E.Canvas.CurveToCI(PEMRPolyDraw16(R)^.apts[i].X,PEMRPolyDraw16(R)^.apts[i].Y,
PEMRPolyDraw16(R)^.apts[i+1].X,PEMRPolyDraw16(R)^.apts[i+1].Y,
PEMRPolyDraw16(R)^.apts[i+2].X,PEMRPolyDraw16(R)^.apts[i+2].Y);
inc(i,2);
The rest of each branch — the PT_CLOSEFIGURE handling and the Position update — stays unchanged and is then correct, because after inc(i,2) the index points at the segment's last point, which is where the PT_CLOSEFIGURE flag is carried.
Verification
We produced one PDF containing the same artwork twice: page 1 from an EMF using PolyDraw records, page 2 from an EMF using PolyBezierTo records, both replayed through TPdfDocumentGDI. After applying the patch the two rendered pages are pixel-identical (ImageMagick compare -metric AE returns 0). Before the patch, page 1 was clearly distorted.
We verified this against our copy of SynPdf.pas (version 1.18, dated 2023). Please check whether the same code is present in current releases.
File: SynPdf.pas (Synopse framework 1.18, Copyright (C) 2023 Arnaud Bouchez)
Affected code: EMR_POLYDRAW and EMR_POLYDRAW16 in the metafile enumeration callback (around lines 9509 and 9559 in our copy)
Symptom
When a metafile containing PolyDraw records with Bézier segments is rendered into a PDF via TPdfDocumentGDI, the curves come out distorted: outlines drift progressively, glyph shapes overlap, and stray line segments appear that are not in the source. The same metafile drawn to a screen or printer DC with PlayEnhMetaFile renders correctly — only the PDF output is wrong, which makes the problem easy to miss.
Root cause
Per the PolyDraw contract, a PT_BEZIERTO segment consists of three consecutive points (aptl[i], aptl[i+1], aptl[i+2]), all flagged PT_BEZIERTO; the curve's start point is the current position. The current code reads the points one index too far and then advances the loop counter one step too many:
PT_BEZIERTO: begin
E.Canvas.CurveToCI(PEMRPolyDraw16(R)^.apts[i+1].X,PEMRPolyDraw16(R)^.apts[i+1].Y,
PEMRPolyDraw16(R)^.apts[i+2].X,PEMRPolyDraw16(R)^.apts[i+2].Y,
PEMRPolyDraw16(R)^.apts[i+3].X,PEMRPolyDraw16(R)^.apts[i+3].Y);
inc(i,3);
Because the enclosing while loop also performs inc(i) at the end of each iteration, every Bézier segment consumes four points instead of three. The offset accumulates along the path, so the longer the outline, the worse the distortion.
Both the 32-bit (EMR_POLYDRAW, aptl) and 16-bit (EMR_POLYDRAW16, apts) branches are affected. In practice the 16-bit branch is the one that matters, since GDI stores coordinates within ±32767 as 16-bit records.
Consistency check inside SynPDF itself
The neighbouring EMR_POLYBEZIERTO16 branch already implements the correct indexing:
for i := 0 to (PEMRPolyBezierTo16(R)^.cpts div 3)-1 do
E.Canvas.CurveToCI(PEMRPolyBezierTo16(R)^.apts[i3].X, PEMRPolyBezierTo16(R)^.apts[i3].Y,
PEMRPolyBezierTo16(R)^.apts[i3+1].X, PEMRPolyBezierTo16(R)^.apts[i3+1].Y,
PEMRPolyBezierTo16(R)^.apts[i3+2].X, PEMRPolyBezierTo16(R)^.apts[i3+2].Y);
Three points per curve, starting at the segment's own first point — exactly what the PolyDraw branch should do.
Out-of-bounds read
There is a second consequence. If a path ends with a Bézier segment (points n-3, n-2, n-1), the current code reads apts[n-2], apts[n-1] and apts[n] — one element past the end of the array. The polytypes^[i] test after inc(i,3) reads past the type array as well. With the corrected indexing both accesses stay in bounds.
Reproduction
Create an EMF containing curves recorded via PolyDraw. A simple way: select a TrueType font, capture glyph outlines with BeginPath / TextOut / EndPath / GetPath, then replay the returned points into a metafile DC with PolyDraw inside BeginPath / EndPath / FillPath.
Render that EMF into a PDF through TPdfDocumentGDI (PlayEnhMetaFile onto VCLCanvas, then SaveToFile).
Compare against the same artwork recorded with MoveToEx / PolyBezierTo / LineTo instead of PolyDraw. Before the fix the two differ visibly.
Suggested patch
EMR_POLYDRAW:
EMR_POLYDRAW16:
The rest of each branch — the PT_CLOSEFIGURE handling and the Position update — stays unchanged and is then correct, because after inc(i,2) the index points at the segment's last point, which is where the PT_CLOSEFIGURE flag is carried.
Verification
We produced one PDF containing the same artwork twice: page 1 from an EMF using PolyDraw records, page 2 from an EMF using PolyBezierTo records, both replayed through TPdfDocumentGDI. After applying the patch the two rendered pages are pixel-identical (ImageMagick compare -metric AE returns 0). Before the patch, page 1 was clearly distorted.
We verified this against our copy of SynPdf.pas (version 1.18, dated 2023). Please check whether the same code is present in current releases.