Skip to content

Commit 03313a0

Browse files
committed
完成转换逻辑对接
1 parent 087095e commit 03313a0

3 files changed

Lines changed: 342 additions & 8 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System.Text;
2+
using Oxage.Wmf;
3+
4+
namespace SkiaWmfRenderer.Rendering;
5+
6+
static class Converters
7+
{
8+
public static Encoding CharacterSetToEncoding(this CharacterSet characterSet)
9+
{
10+
var codePageId = characterSet switch
11+
{
12+
CharacterSet.ANSI_CHARSET
13+
// DEFAULT_CHARSET: Specifies a character set based on the current system locale; for example, when the system locale is United States English, the default character set is ANSI_CHARSET.
14+
or CharacterSet.DEFAULT_CHARSET => 1252,
15+
CharacterSet.OEM_CHARSET => 437,
16+
CharacterSet.SHIFTJIS_CHARSET => 932,
17+
CharacterSet.HANGUL_CHARSET => 949,
18+
CharacterSet.JOHAB_CHARSET => 1361,
19+
CharacterSet.GB2312_CHARSET => 936,
20+
CharacterSet.CHINESEBIG5_CHARSET => 950,
21+
CharacterSet.HEBREW_CHARSET => 1255,
22+
CharacterSet.ARABIC_CHARSET => 1256,
23+
CharacterSet.GREEK_CHARSET => 1253,
24+
CharacterSet.TURKISH_CHARSET => 1254,
25+
CharacterSet.BALTIC_CHARSET => 1257,
26+
CharacterSet.EASTEUROPE_CHARSET => 1250,
27+
CharacterSet.RUSSIAN_CHARSET => 1251,
28+
CharacterSet.THAI_CHARSET => 874,
29+
CharacterSet.VIETNAMESE_CHARSET => 1258,
30+
CharacterSet.SYMBOL_CHARSET => 42, // Symbol font is not a code page, but 42 is often used for Symbol font
31+
_ => 1252,
32+
};
33+
34+
return Encoding.GetEncoding(codePageId);
35+
}
36+
}

Workbench/Wmf/SkiaWmfRenderer/src/SkiaWmfRenderer/Rendering/WmfRenderer.cs

Lines changed: 302 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,86 @@
1-
using System.Diagnostics.CodeAnalysis;
2-
using System.Text;
3-
using Oxage.Wmf;
1+
using Oxage.Wmf;
42
using SkiaSharp;
3+
using System.Diagnostics.CodeAnalysis;
4+
using System.Drawing;
5+
using System.Text;
6+
using System.Xml;
7+
using Oxage.Wmf.Records;
8+
using static System.Net.Mime.MediaTypeNames;
9+
10+
namespace SkiaWmfRenderer.Rendering;
11+
12+
class WmfRenderStatus : IDisposable
13+
{
14+
public required float CurrentX { get; set; }
15+
public required float CurrentY { get; set; }
16+
17+
public required int Width { get; set; }
18+
public required int Height { get; set; }
19+
20+
public int CurrentRecordsIndex { get; set; }
21+
22+
public SKColor CurrentPenColor { get; set; } = SKColors.Empty;
23+
public float CurrentPenThickness { get; set; } = 0;
24+
25+
public float CurrentFontSize { get; set; }
26+
27+
public string? CurrentFontName { get; set; } = null;
28+
29+
public bool IsItalic { get; set; } = false;
30+
public int FontWeight { get; set; } = 400;
31+
32+
public CharacterSet CurrentCharacterSet
33+
{
34+
get => _currentCharacterSet;
35+
set
36+
{
37+
_currentCharacterSet = value;
38+
CurrentEncoding = _currentCharacterSet.CharacterSetToEncoding();
39+
}
40+
}
41+
42+
private CharacterSet _currentCharacterSet = CharacterSet.DEFAULT_CHARSET;
43+
44+
public Encoding CurrentEncoding { get; private set; } = CharacterSet.DEFAULT_CHARSET.CharacterSetToEncoding();
545

6-
namespace SkiaWmfRenderer;
46+
public float LastDxOffset { get; set; } = 0;
47+
48+
public SKColor CurrentTextColor { get; set; } =
49+
SKColors.Black;
50+
51+
public SKPaint Paint { get; } = new SKPaint()
52+
{
53+
IsAntialias = true
54+
};
55+
56+
public SKFont SKFont { get; } = new SKFont();
57+
58+
59+
public void UpdateSkiaTextStatus()
60+
{
61+
var skFont = SKFont;
62+
skFont.Size = CurrentFontSize;
63+
skFont.Typeface = SKTypeface.FromFamilyName(CurrentFontName, (SKFontStyleWeight) FontWeight,
64+
SKFontStyleWidth.Normal, IsItalic ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright);
65+
66+
Paint.Style = SKPaintStyle.Fill;
67+
Paint.Color = CurrentTextColor;
68+
}
69+
70+
public void UpdateSkiaStrokeStatus()
71+
{
72+
Paint.IsStroke = true;
73+
Paint.Color = CurrentPenColor;
74+
Paint.StrokeWidth = CurrentPenThickness;
75+
Paint.Style = SKPaintStyle.Stroke;
76+
}
77+
78+
public void Dispose()
79+
{
80+
Paint.Dispose();
81+
SKFont.Dispose();
82+
}
83+
}
784

885
class WmfRenderer
986
{
@@ -15,8 +92,6 @@ public bool TryRender([NotNullWhen(true)] out SKBitmap? skBitmap)
1592
{
1693
skBitmap = null;
1794

18-
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
19-
2095
var format = WmfDocument.Format;
2196

2297
var x = Math.Min(format.Left, format.Right);
@@ -25,13 +100,21 @@ public bool TryRender([NotNullWhen(true)] out SKBitmap? skBitmap)
25100
var width = Math.Abs(format.Right - format.Left);
26101
var height = Math.Abs(format.Bottom - format.Top);
27102

103+
using WmfRenderStatus renderStatus = new()
104+
{
105+
CurrentX = x,
106+
CurrentY = y,
107+
Width = width,
108+
Height = height
109+
};
110+
28111
skBitmap = new SKBitmap(width, height, SKColorType.Bgra8888, SKAlphaType.Premul);
29112

30113
using SKCanvas canvas = new SKCanvas(skBitmap);
31114

32115
try
33116
{
34-
var result = TryRenderInner(canvas);
117+
var result = TryRenderInner(canvas, renderStatus);
35118

36119
if (result)
37120
{
@@ -50,8 +133,219 @@ public bool TryRender([NotNullWhen(true)] out SKBitmap? skBitmap)
50133
}
51134
}
52135

53-
private bool TryRenderInner(SKCanvas canvas)
136+
private bool TryRenderInner(SKCanvas canvas, WmfRenderStatus renderStatus)
54137
{
138+
for (var i = 0; i < WmfDocument.Records.Count; i++)
139+
{
140+
var wmfDocumentRecord = WmfDocument.Records[i];
141+
renderStatus.CurrentRecordsIndex = i;
142+
143+
switch (wmfDocumentRecord)
144+
{
145+
// The META_SETBKMODE Record defines the background raster operation mix mode in the playback device context. The background mix mode is the mode for combining pens, text, hatched brushes, and interiors of filled objects with background colors on the output surface.
146+
case WmfSetBkModeRecord setBkModeRecord:
147+
{
148+
// RecordFunction (2 bytes): A 16-bit unsigned integer that defines this WMF record type. The lower byte MUST match the lower byte of the RecordType Enumeration (section 2.1.1.1) table value META_SETBKMODE.
149+
// BkMode (2 bytes): A 16-bit unsigned integer that defines background mix mode. This MUST be one of the values in the MixMode Enumeration (section 2.1.1.20).
150+
151+
break;
152+
}
153+
case WmfSetTextAlignRecord setTextAlignRecord:
154+
{
155+
// RecordFunction (2 bytes): A 16-bit unsigned integer that defines this WMF record type. The lower byte MUST match the lower byte of the RecordType Enumeration (section 2.1.1.1) table value META_SETTEXTALIGN.
156+
// TextAlignmentMode (2 bytes): A 16-bit unsigned integer that defines text alignment. This value MUST be a combination of one or more TextAlignmentMode Flags (section 2.1.2.3) for text with a horizontal baseline, and VerticalTextAlignmentMode Flags (section 2.1.2.4) for text with a vertical baseline.
157+
158+
break;
159+
}
160+
case WmfCreatePenIndirectRecord createPenIndirectRecord:
161+
{
162+
Color color = createPenIndirectRecord.Color;
163+
renderStatus.CurrentPenColor = new SKColor(color.R, color.G, color.B, color.A);
164+
renderStatus. CurrentPenThickness = Math.Max(createPenIndirectRecord.Width.X, createPenIndirectRecord.Width.Y);
165+
break;
166+
}
167+
// - [11] {== WmfMoveToRecord ==
168+
// RecordSize: 5 words = 10 bytes
169+
// RecordType: 0x0214 (RecordType.META_MOVETO)
170+
// X: 1372Y: 865} Oxage.Wmf.IBinaryRecord {Oxage.Wmf.Records.WmfMoveToRecord}
171+
//
172+
case WmfMoveToRecord moveToRecord:
173+
{
174+
renderStatus.CurrentX = moveToRecord.X;
175+
renderStatus.CurrentY = moveToRecord.Y;
176+
renderStatus.LastDxOffset = 0;
177+
break;
178+
}
179+
// - [12] {== WmfLineToRecord ==
180+
// RecordSize: 5 words = 10 bytes
181+
// RecordType: 0x0213 (RecordType.META_LINETO)
182+
// X: 1840Y: 865} Oxage.Wmf.IBinaryRecord {Oxage.Wmf.Records.WmfLineToRecord}
183+
//
184+
case WmfLineToRecord lineToRecord:
185+
{
186+
renderStatus.UpdateSkiaStrokeStatus();
187+
//paint.IsStroke = true;
188+
//paint.Color = currentPenColor;
189+
//paint.StrokeWidth = currentPenThickness;
190+
191+
canvas.DrawLine(renderStatus.CurrentX, renderStatus.CurrentY, lineToRecord.X, lineToRecord.Y, renderStatus.Paint);
192+
193+
break;
194+
}
195+
// - [13] {== WmfSetTextColorRecord ==
196+
// RecordSize: 5 words = 10 bytes
197+
// RecordType: 0x0209 (RecordType.META_SETTEXTCOLOR)
198+
// ColorRef: Color [A=255, R=0, G=0, B=0]
199+
// } Oxage.Wmf.IBinaryRecord {Oxage.Wmf.Records.WmfSetTextColorRecord}
200+
//
201+
case WmfSetTextColorRecord setTextColorRecord:
202+
{
203+
renderStatus.CurrentTextColor = ToSKColor(setTextColorRecord.Color);
204+
205+
break;
206+
}
207+
// - [15] {== WmfCreateFontIndirectRecord ==
208+
// RecordSize: 28 words = 56 bytes
209+
// RecordType: 0x02fb (RecordType.META_CREATEFONTINDIRECT)
210+
// Height: -636
211+
// Width: 0
212+
// Escapement: 0
213+
// Orientation: 0
214+
// Weight: 400
215+
// Italic: False
216+
// Underline: False
217+
// StrikeOut: False
218+
// CharSet: 0x0000 (CharacterSet.ANSI_CHARSET)
219+
// OutPrecision: 0x00 (OutPrecision.OUT_DEFAULT_PRECIS)
220+
// ClipPrecision: 0x02 (ClipPrecision.CLIP_STROKE_PRECIS)
221+
// Quality: 0x00 (FontQuality.DEFAULT_QUALITY)
222+
// Pitch: 0x00 (PitchFont.DEFAULT_PITCH)
223+
// Family: 0x00 (FamilyFont.FF_DONTCARE)
224+
// Facename: Times New Roman Oxage.Wmf.IBinaryRecord {Oxage.Wmf.Records.WmfCreateFontIndirectRecord}
225+
//
226+
case WmfCreateFontIndirectRecord createFontIndirectRecord:
227+
{
228+
// "Times New Roman\0è±ñwñ±ñw @ów³\u0011fÁ"
229+
renderStatus.CurrentFontName = createFontIndirectRecord.FaceName.ToString();
230+
renderStatus.CurrentCharacterSet = createFontIndirectRecord.CharSet;
231+
232+
// Width (2 bytes): A 16-bit signed integer that defines the average width, in logical units, of characters in the font. If Width is 0x0000, the aspect ratio of the device SHOULD be matched against the digitization aspect ratio of the available fonts to find the closest match, determined by the absolute value of the difference.
233+
var fontWidth = createFontIndirectRecord.Width;
234+
235+
// Height (2 bytes): A 16-bit signed integer that specifies the height, in logical units, of the font's character cell. The character height is computed as the character cell height minus the internal leading. The font mapper SHOULD interpret the height as follows.
236+
// value < 0x0000 The font mapper SHOULD transform this value into device units and match its absolute value against the character height of available fonts.
237+
// 0x0000 A default height value MUST be used when creating a physical font.
238+
// 0x0000 < value The font mapper SHOULD transform this value into device units and match it against the cell height of available fonts.
239+
// For all height comparisons, the font mapper SHOULD find the largest physical font that does not exceed the requested size.<40>
240+
// <40> Section 2.2.1.2: All Windows versions: mapping the logical font size to the available physical fonts occurs the first time the logical font needs to be used in a drawing operation.
241+
// For the MM_TEXT mapping mode, the following formula can be used to compute the height of a font with a specified point size.
242+
// Height = -MulDiv(PointSize, GetDeviceCaps(hDC, LOGPIXELSY), 72);
243+
//
244+
var fontHeight = createFontIndirectRecord.Height;
245+
246+
fontWidth = Math.Abs(fontWidth);
247+
fontHeight = Math.Abs(fontHeight);
248+
249+
renderStatus.CurrentFontSize = Math.Max(fontWidth, fontHeight);
250+
251+
renderStatus.IsItalic = createFontIndirectRecord.Italic;
252+
renderStatus.FontWeight = createFontIndirectRecord.Weight;
253+
254+
break;
255+
}
256+
case WmfUnknownRecord unknownRecord:
257+
{
258+
switch (unknownRecord.RecordType)
259+
{
260+
case RecordType.META_EXTTEXTOUT:
261+
{
262+
// 关于字间距的规则:
263+
// 1. 如果两个 META_EXTTEXTOUT 相邻,中间没有 MoveTo 之类
264+
// 则第二个 META_EXTTEXTOUT 将需要使用前一个 META_EXTTEXTOUT 的 dx 末项
265+
// 2. 可选的 dx 是存放在字符串末尾的可选项,从文档 2.3.3.5 上可见 dx 是顶格写的,这就意味着这个值是一定对齐整数倍的。由于 dx 是放在数据末尾,可通过减法算出 dx 长度,即数据总长度减去所有已知字段的长度加上字符串长度,剩余的就是 dx 长度。如果计算返回的 dx 长度是奇数,则首个 byte 是需要跳过的,如此就能确保在 16bit 下的 wmf 格式里面,读取的 dx 是从整数倍开始读取
266+
// 参考 https://learn.microsoft.com/zh-cn/windows/win32/api/wingdi/nf-wingdi-exttextoutw
267+
268+
// 测试 17 项
269+
var memoryStream = new MemoryStream(unknownRecord.Data);
270+
var binaryReader = new BinaryReader(memoryStream);
271+
var ty = binaryReader.ReadUInt16();
272+
var tx = binaryReader.ReadUInt16();
273+
var stringLength = binaryReader.ReadUInt16();
274+
var fwOpts = (ExtTextOutOptions)binaryReader.ReadUInt16();
275+
// Rectangle (8 bytes): An optional 8-byte Rect Object (section 2.2.2.18).) When either ETO_CLIPPED, ETO_OPAQUE, or both are specified, the rectangle defines the dimensions, in logical coordinates, used for clipping, opaquing, or both. When neither ETO_CLIPPED nor ETO_OPAQUE is specified, the coordinates in Rectangle are ignored.
276+
var st = 8; /*2byte ofr ty tx stringLength fwOpts*/
277+
if (fwOpts is ExtTextOutOptions.ETO_CLIPPED or ExtTextOutOptions.ETO_OPAQUE)
278+
{
279+
// 此时才有 Rectangle 的值
280+
binaryReader.ReadBytes(8);
281+
st += 8;
282+
}
283+
284+
st += stringLength;
285+
286+
// String (variable): A variable-length string that specifies the text to be drawn. The string does not need to be null-terminated, because StringLength specifies the length of the string. If the length is odd, an extra byte is placed after it so that the following member (optional Dx) is aligned on a 16-bit boundary. The string will be decoded based on the font object currently selected into the playback device context. If a font matching the font object’s specification is not found, the decoding is undefined. If a matching font is found that matches the charset specified in the font object, the string should be decoded with the codepages in the following table.
287+
var stringBuffer = binaryReader.ReadBytes(stringLength);
288+
var text = renderStatus.CurrentEncoding.GetString(stringBuffer);
289+
290+
// Dx (variable): An optional array of 16-bit signed integers that indicate the distance between origins of adjacent character cells. For example, Dx[i] logical units separate the origins of character cell i and character cell i + 1. If this field is present, there MUST be the same number of values as there are characters in the string.
291+
//Debug.Assert(st == unknownRecord.RecordSize);
292+
var dxLength = unknownRecord.Data.Length - st;
293+
294+
renderStatus.UpdateSkiaTextStatus();
295+
296+
var currentXOffset = renderStatus.CurrentX + tx + renderStatus.LastDxOffset;
297+
298+
if (dxLength == 0)
299+
{
300+
canvas.DrawText(text, currentXOffset, renderStatus.CurrentY + ty, renderStatus.SKFont, renderStatus.Paint);
301+
}
302+
else
303+
{
304+
// 如果这里计算出来不是偶数,则首个需要跳过。这是经过测试验证的。~~但没有相关说明内容。且跳过的 byte 是有内容的~~ String (variable): If the length is odd, an extra byte is placed after it so that the following member (optional Dx) is aligned on a 16-bit boundary.
305+
// 如果字符串的长度是奇数,则在字符串后面放置一个额外的字节,以便下一个成员(可选的 Dx)对齐到 16 位边界。
306+
if (dxLength > ((dxLength / sizeof(UInt16)) * sizeof(UInt16)))
307+
{
308+
// 读取掉这个额外的字节,以便 Dx 对齐到 16 位边界
309+
var r = binaryReader.ReadByte();
310+
_ = r;
311+
}
312+
313+
UInt16[] dxArray = new UInt16[dxLength / sizeof(UInt16)];
314+
for (var t = 0; t < dxArray.Length; t++)
315+
{
316+
dxArray[t] = binaryReader.ReadUInt16();
317+
}
318+
319+
if (dxArray.Length != text.Length)
320+
{
321+
continue;
322+
}
323+
324+
for (var textIndex = 0; textIndex < text.Length; textIndex++)
325+
{
326+
canvas.DrawText(text[textIndex].ToString(), currentXOffset, renderStatus.CurrentY + ty, renderStatus.SKFont,
327+
renderStatus.Paint);
328+
329+
currentXOffset += dxArray[textIndex];
330+
}
331+
332+
renderStatus.LastDxOffset = dxArray[^1];
333+
}
334+
335+
break;
336+
}
337+
}
338+
339+
break;
340+
}
341+
}
342+
}
343+
55344
return true;
56345
}
346+
347+
private static SKColor ToSKColor(Color color)
348+
{
349+
return new SKColor(color.R, color.G, color.B, color.A);
350+
}
57351
}

0 commit comments

Comments
 (0)