Fix oversized GDI glyph buffers

This commit is contained in:
gamer147
2026-08-16 11:24:07 -04:00
parent eef1225b56
commit 7b0987d9a6
4 changed files with 97 additions and 15 deletions

View File

@@ -1,6 +1,7 @@
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Text;
using Age.Engine.Model;
using Age.Engine.Text;
using Age.Engine.Text.Windows;
@@ -73,6 +74,48 @@ public class WindowsGdiGlyphMaskRasterizerTests
Assert.All(space.Coverage.ToArray(), value => Assert.Equal(0, value));
}
[Fact]
public void Sc0000OpeningNarrationLineRasterizesEveryGlyph()
{
if (!WindowsGdiGlyphMaskRasterizer.TryGetAvailability(out _)) return;
const string text =
"だが怨嗟に満ちた言葉は、呪いのように彼らの心に暗い影を落とした。";
var style = AdvTextStyle.Default with
{
PrimaryFontSize = 24,
Bold = true,
FontFace = " 明朝",
RenderMode = 3,
LineSpacing = 8,
};
using var rasterizer = new WindowsGdiGlyphMaskRasterizer();
foreach (GlyphRasterRequest request in
ImmediateSurfaceTextRenderer.CreateRequests(text, style))
{
Exception? error = Record.Exception(() => rasterizer.Rasterize(request));
Assert.True(
error == null,
$"U+{request.UnicodeScalar:X4} CP932 0x{request.Cp932Code:X4}: {error}");
}
}
[Fact]
public void Gray4StorageBeyondMetricRowsIsTrimmed()
{
if (!WindowsGdiGlyphMaskRasterizer.TryGetAvailability(out _)) return;
GlyphRasterRequest request = NativeRequest(" 明朝", 24, -12, 700, 'p');
DirectGlyph direct = RasterizeUnicodeDirect(" 明朝", 24, -12, 700, 'p');
using var rasterizer = new WindowsGdiGlyphMaskRasterizer();
GlyphMask actual = rasterizer.Rasterize(request);
Assert.True(direct.RawBufferSize > direct.Coverage.Length);
Assert.Equal(direct.Coverage, actual.Coverage.ToArray());
Assert.Equal(actual.Stride * actual.Height, actual.Coverage.Length);
}
[Fact]
public void FontHandlesUseTheSharedBoundedLruAndDisposeCleanly()
{
@@ -135,17 +178,19 @@ public class WindowsGdiGlyphMaskRasterizerTests
int glyphWidth = checked((int)metrics.BlackBoxX);
int glyphHeight = checked((int)metrics.BlackBoxY);
int stride = checked((glyphWidth + 3) & ~3);
Assert.Equal(checked(stride * glyphHeight), (int)size);
byte[] coverage = new byte[size];
int expectedBytes = checked(stride * glyphHeight);
Assert.True(size >= expectedBytes);
byte[] rawCoverage = new byte[size];
if (size > 0)
{
identity = Mat2.Identity;
uint written = Native.GetGlyphOutlineW(
dc, (uint)scalar, GgoGray4Bitmap,
out GlyphMetrics second, size, coverage, ref identity);
out GlyphMetrics second, size, rawCoverage, ref identity);
Assert.Equal(size, written);
Assert.Equal(metrics, second);
}
byte[] coverage = rawCoverage.AsSpan(0, expectedBytes).ToArray();
string text = char.ConvertFromUtf32(scalar);
if (!Native.GetTextExtentPoint32W(dc, text, text.Length, out NativeSize cell))
@@ -154,7 +199,7 @@ public class WindowsGdiGlyphMaskRasterizerTests
glyphWidth, glyphHeight, stride,
metrics.GlyphOrigin.X, metrics.GlyphOrigin.Y,
metrics.CellIncrementX, metrics.CellIncrementY,
cell.Width, cell.Height, coverage);
cell.Width, cell.Height, coverage, checked((int)size));
}
finally
{
@@ -178,7 +223,8 @@ public class WindowsGdiGlyphMaskRasterizerTests
int CellAdvanceY,
int CellWidth,
int CellHeight,
byte[] Coverage);
byte[] Coverage,
int RawBufferSize);
[StructLayout(LayoutKind.Sequential)]
private struct NativePoint

View File

@@ -126,22 +126,31 @@ public sealed class WindowsGdiGlyphMaskRasterizer
int expectedBytes = checked(stride * height);
// CP932 0x8140 (U+3000 IDEOGRAPHIC SPACE) is a zero-ink spacing glyph. GDI reports
// its placement as a nominal 1x1 black box but returns a zero-byte required buffer.
// Preserve those metrics and materialize the implied transparent mask; a nonzero
// short/oversized payload still means that the bitmap contract is inconsistent.
if (size != 0 && size != expectedBytes)
// Preserve those metrics and materialize the implied transparent mask. GDI can also
// return storage beyond the metric-defined rows (for example, bold 24px MS Mincho 'p'
// reports 12x15 but requests 192 bytes rather than 180). Treat the queried size as the
// allocation contract and the GLYPHMETRICS box as the raster-consumption contract: read
// exactly the requested buffer, then retain only the rows described by the metrics. Bytes
// outside that box are not drawable coverage and are not required to be initialized to zero.
if (size != 0 && size < expectedBytes)
throw new InvalidOperationException(
$"GDI gray-4 buffer size {size} disagrees with {width}x{height}, stride {stride}.");
$"GDI gray-4 buffer size {size} for CP932 0x{code:x4} " +
$"('{request.FontFace}', {request.PixelHeight}px, width {request.RequestedWidth}, " +
$"weight {request.Weight}) is smaller than its {width}x{height} metric box, " +
$"stride {stride} ({expectedBytes} bytes).");
byte[] coverage = new byte[expectedBytes];
if (size > 0)
{
byte[] gdiBuffer = new byte[checked((int)size)];
identity = Mat2.Identity;
uint written = NativeMethods.GetGlyphOutlineA(
_displayIc.DangerousGetHandle(), code, GgoGray4Bitmap,
out GlyphMetrics secondMetrics, size, coverage, ref identity);
out GlyphMetrics secondMetrics, size, gdiBuffer, ref identity);
if (written == GdiError) ThrowWin32($"GetGlyphOutlineA read failed for CP932 0x{code:x4}");
if (written != size || !metrics.Equals(secondMetrics))
throw new InvalidOperationException("GDI glyph metrics changed between query and read.");
gdiBuffer.AsSpan(0, expectedBytes).CopyTo(coverage);
}
byte[] encoded = EncodeCp932Code(code);