ReactOS 图形系统分析(51):元文件子系统 — metafile.c

ReactOS 图形系统分析(51):元文件子系统 --- metafile.c

1. 概述

1.1 什么是元文件

元文件(Metafile,WMF,Windows Metafile)是一种"矢量化的绘图指令序列"。它不像位图那样保存每个像素的颜色值,而是把一次会话中对 GDI 的绘图调用(画线、画矩形、输出文本、设置映射模式......)逐条记录下来,形成一串 METARECORD(元文件记录)。之后可以在任意 DC 上"重放"(PlayMetaFile)这串记录,从而在别的设备(屏幕、打印机、另一个位图 DC)上复现同样的画面。

元文件的典型应用场景:

场景 说明
剪贴板数据交换 把绘图结果以"矢量"形式放进剪贴板(CF_METAFILEPICT),目标程序可无损缩放重绘
打印机假脱机 把打印作业记录为元文件,稍后交给打印驱动重放
图元库/控件 需要"记忆"一段绘制过程以便反复重放
EMF↔WMF 转换 增强型元文件(EMF)可降级转换为传统 WMF 格式

1.2 内核侧文件的作用

metafile.c(file:///d:/reactos/win32ss/gdi/ntgdi/metafile.c) 位于 win32ss/gdi/ntgdi/,是 win32k.sys 中与元文件相关的系统服务入口 集合。它只实现了 4 个 NtGdi* 服务函数,其中 3 个已实现、1 个是未实现的桩。它的定位非常"轻":真正的元文件录制与播放完全在用户态 gdi32.dll 完成,内核侧只负责:

  1. 提供一个"元文件 DC"类型的内核 DC 句柄(NtGdiCreateMetafileDC,实际是一个 ALT DC);
  2. 维护"服务器元文件对象"------一个内核 GDI 对象,用于在跨进程场景(主要是剪贴板)中保存/传递元文件的原始字节(NtGdiCreateServerMetaFile / NtGdiGetServerMetaFileBits);
  3. 预留一个坐标换算入口(NtGdiConvertMetafileRect,目前是 UNIMPLEMENTED 桩)。

1.3 设计动机:为什么播放/录制要放在用户态

Windows 的元文件机制从 16 位时代延续而来。传统 WMF 的记录格式(META_* 函数码、16 位坐标、LOGPEN16/LOGFONT16 等 16 位对象描述)带有浓重的 16 位遗产色彩。把录制和播放放在用户态 gdi32 中的好处:

  • 无需每次 GDI 调用都陷入内核 :录制时 gdi32 的"元文件物理驱动"(mfdrv)在用户态把 GDI 调用组装成 METARECORD,直接写内存缓冲或磁盘文件,几乎不产生系统调用;
  • 格式兼容由 gdi32 负责 :16 位坐标转换、ANSI 字符串、LOGBRUSH16 等兼容性细节不需要内核关心;
  • 内核只保存"数据容器" :服务器元文件对象本质上是一个"带标签的字节缓冲"(iType + 可变长数据),内核用统一的对象机制(GDIOBJ_AllocObjWithHandle)管理其生命周期、进程归属与引用计数,具体字节含义由用户态解析。

1.4 总体架构图

复制代码
                      应用(记录)                    应用(播放/转换)
                        │                                │
   CreateMetaFileW ─────┤                          PlayMetaFile / EnumMetaFile
   CloseMetaFile ───────┤                                │
                        ▼                                ▼
        ┌─────────────────────────────┐     ┌──────────────────────────┐
        │   gdi32.dll(用户态)        │     │   gdi32.dll(用户态)      │
        │                             │     │                          │
        │  MFDRV_* 物理驱动           │     │  get_metafile_bits()     │
        │  (wine/mfdrv/init.c 等)     │     │  → 循环 METARECORD        │
        │   · MFDRV_WriteRecord       │     │  → PlayMetaFileRecord()   │
        │   · MFDRV_MetaParam0/1/2/4  │     │  → 普通 GDI API           │
        │     /6/8(组装记录)         │     │                          │
        │   · 内存:METARECORD 追加   │     └──────────┬───────────────┘
        │   · 磁盘:WriteFile         │                │ GDI 调用
        └─────────────┬───────────────┘                ▼
                      │ (仅剪贴板/EMF转换场景)   ┌─────────────────────┐
                      │ NtGdi* 系统服务          │  win32k.sys(内核)  │
                      ▼                          │                     │
        ┌─────────────────────────────┐          │ · DCTYPE_INFO ALT DC│
        │  内核元文件对象 METATYPEOBJ │          │ · META_TYPE 对象     │
        │  (metafile.c + gdiobj.c)   │          └─────────────────────┘
        └─────────────────────────────┘

1.5 内核/用户态分工总表

职责 所在层 实现
录制:GDI 调用 → METARECORD 用户态 gdi32 wine/mfdrv/*.c(MFDRV_* 系列)
录制:记录缓冲/磁盘文件管理 用户态 gdi32 MFDRV_WriteRecordCreateMetaFileWCloseMetaFile
播放:METARECORD → GDI 调用 用户态 gdi32 PlayMetaFilePlayMetaFileRecord
元文件 DC 的内核句柄 内核 win32k NtGdiCreateMetafileDCGreCreateCompatibleDC(hdc, TRUE)
服务器元文件对象(剪贴板用) 内核 win32k NtGdiCreateServerMetaFile / NtGdiGetServerMetaFileBits
元文件对象句柄/生命周期管理 内核 win32k gdiobj.cGDIObjType_META_TYPE = 0x15
坐标换算桩 内核 win32k NtGdiConvertMetafileRect(UNIMPLEMENTED)

2. 核心数据结构

2.1 METAHEADER(wingdi.h)

METAHEADER 定义在 sdk/include/psdk/wingdi.h(file:///d:/reactos/sdk/include/psdk/wingdi.h)(约 2754~2763 行),用 pshpack2(2 字节对齐)打包,共 18 字节 = 9 个 WORD

c 复制代码
#include "pshpack2.h"
typedef struct tagMETAHEADER {
    WORD mtType;         /* 元文件类型:METAFILE_MEMORY(1) 或 METAFILE_DISK(2) */
    WORD mtHeaderSize;   /* 头大小(以 WORD 为单位),恒为 sizeof(METAHEADER)/2 = 9 */
    WORD mtVersion;      /* 版本号,MFVERSION = 0x0300 */
    DWORD mtSize;        /* 整个元文件(头+所有记录)的字节数 / 2(WORD 数) */
    WORD mtNoObjects;    /* 元文件中创建的 GDI 对象数(画笔/画刷/字体/调色板等) */
    DWORD mtMaxRecord;   /* 最大的单条记录大小(WORD 数) */
    WORD mtNoParameters; /* 未用,恒为 0 */
} METAHEADER,*PMETAHEADER,*LPMETAHEADER;
#include "poppack.h"

字段含义要点:

  • mtSize 是"总长度 ÷ 2",因为元文件记录的大小均以 WORD 为单位表达;
  • mtHeaderSize 恒等于 9,播放器从 mtHeaderSize * 2 = 18 字节处开始遍历记录;
  • 内存型元文件 = METAHEADER + 一串连续 METARECORD(文件型元文件的磁盘镜像也把 mtType 写成 METAFILE_MEMORY,见 gdi32/wine/metafile.c 顶部注释);
  • 磁盘型元文件在 METAHEADER 之后还跟一个 METAHEADERDISK 扩展结构(见 2.5)。

2.2 METARECORD(wingdi.h)

c 复制代码
typedef struct tagMETARECORD {
    DWORD rdSize;      /* 记录总大小(WORD 数),含 rdSize 自身 */
    WORD rdFunction;   /* 记录类型(META_* 函数码) */
    WORD rdParm[1];    /* 参数区(可变长,rdSize-3 个 WORD) */
} METARECORD,*PMETARECORD,*LPMETARECORD;

记录机制约定(录制与播放必须一致):

  • 每条记录最小 3 个 WORD(rdSize = 3):rdSize + rdFunction,无参数;
  • 参数逆序存放rdParm[0] 保存调用参数中的最后一个rdParm[1] 保存倒数第二个,依此类推。这是因为 16 位元文件的设计把所有参数当 WORD 压栈/存表,播放时按逆序弹出。例如 MFDRV_MoveTo(dev, x, y) 生成 rdParm[0]=y, rdParm[1]=x,播放端 MoveToEx(hdc, rdParm[1], rdParm[0])
  • 32 位坐标(LONG)会被拆成两个 WORD(低位在前、高位在后)存放;
  • 记录以 META_EOFMETA_EOF 定义见 wingdi.h)结束。

META_* 函数码从 0x01 到 0xFFF,按"所需参数个数"编码(高位半字节是参数 WORD 数)。常见的有:

记录 参数数 说明
META_SAVEDC 0x001E 0 保存 DC 状态
META_RESTOREDC 0x0127 1 恢复 DC 状态
META_SETMAPMODE 0x0103 1 设置映射模式
META_SETBKCOLOR 0x0201 2 背景色(两个 WORD 拼 COLORREF)
META_MOVETO 0x0214 2 移动当前位置
META_LINETO 0x0213 2 画线
META_RECTANGLE 0x0418 4 画矩形(left/top/right/bottom)
META_ELLIPSE 0x0418→0x418 4 画椭圆(注意 META_ELLIPSE 实为 0x418)
META_POLYGON 0x0324 变长 多边形(count + POINTS 数组)
META_POLYLINE 0x0325 变长 折线
META_POLYPOLYGON 0x0538 变长 复合多边形
META_ARC 0x0817 8 圆弧
META_PIE 0x081A 8 饼图
META_CHORD 0x0830 8
META_EXTTEXTOUT 0x0A32 变长 扩展文本输出
META_CREATEPENINDIRECT 0x02FA 变长 创建画笔(LOGPEN16)
META_CREATEFONTINDIRECT 0x02FB 变长 创建字体(LOGFONT16)
META_CREATEBRUSHINDIRECT 0x02FC 变长 创建画刷(LOGBRUSH16)
META_CREATEREGION 0x06FF 变长 创建区域
META_DELETEOBJECT 0x01F0 1 删除对象(按句柄表索引)
META_SELECTOBJECT 0x012D 1 选入对象(按句柄表索引)
META_BITBLT 0x0922 变长 位块传输
META_DIBSTRETCHBLT 0x0B41 变长 DIB 拉伸传输
META_EOF 0x0000 0 元文件结束

2.3 内核服务器元文件对象:METATYPEOBJ(metafile.c 内部)

metafile.c(file:///d:/reactos/win32ss/gdi/ntgdi/metafile.c) 第 17~27 行定义了仅供本文件使用的结构:

c 复制代码
// Internal Use Only
typedef struct _METATYPEOBJ
{
    BASEOBJECT  BaseObject;   /* GDI 对象头(句柄/引用计数/所有者等) */
    DWORD iType;              /* 数据标签:GDITAG_TYPE_EMF 或 GDITAG_TYPE_MFP */
    DWORD mm;                 /* 映射模式(METAFILEPICT.mm 的透传保存) */
    DWORD xExt;               /* 推荐显示宽度(METAFILEPICT.xExt) */
    DWORD yExt;               /* 推荐显示高度(METAFILEPICT.yExt) */
    ULONG cjData;             /* 元文件原始数据字节数 */
    PBYTE pjData[4];          /* 占位符:实际数据紧跟结构体尾部 */
} METATYPEOBJ, *PMETATYPEOBJ;

要点:

  • 该对象不是独立分配的"数据块 + 指针",而是把原始数据整体追加在结构体后面 :分配时请求 sizeof(METATYPEOBJ) + cjData 字节(见 NtGdiCreateServerMetaFile),pjData 数组只是充当"尾部数据起始地址"的锚点,RtlCopyMemory(pmto->pjData, pjData, cjData) 直接拷贝到对象内存末尾;
  • iType 是区分数据格式的标签,取自 ntgdityp.h(file:///d:/reactos/win32ss/include/ntgdityp.h):
    • #define GDITAG_TYPE_EMF 'XEFM'("MFEX" 反读 = MetaFile EXtended,即 EMF 数据)
    • #define GDITAG_TYPE_MFP '_PFM'("MFP" = MetaFile Picture,即 METAFILEPICT 承载的传统 WMF)
  • mm/xExt/yExt 三个字段是为 METAFILEPICT(剪贴板 CF_METAFILEPICT 格式的描述头)透传保存的"推荐显示方式",录制/读取时原样保存、原样取回;
  • 内核 GDI 对象类型为 GDIObjType_META_TYPE = 0x15,低位句柄类型 GDILoObjType_LO_META_TYPE = 0x150000(ntgdityp.h 第 141、166 行)。

2.4 DC 类型与元文件 DC(dc.h)

ntgdi/dc.h(file:///d:/reactos/win32ss/gdi/ntgdi/dc.h) 第 39~44 行定义了内核 DC 的三种类型:

c 复制代码
typedef enum _DCTYPE
{
    DCTYPE_DIRECT = 0,  /* Normal device context  普通设备上下文 */
    DCTYPE_MEMORY = 1,  /* Memory device context  内存设备上下文(兼容 DC) */
    DCTYPE_INFO   = 2,  /* Information context    信息上下文(查询设备能力用) */
} DCTYPE;

NtGdiCreateMetafileDC 创建的是 DCTYPE_INFO 类型 的"替代 DC"(ALT DC,句柄类型 GDILoObjType_LO_ALTDC_TYPE = 0x210000)。这类 DC 不绑定真实表面(pSurface 为空),主要用于承载用户态 EMFDC 的设备能力查询与记录上下文。DC 结构(dc.h 第 95~137 行)中的 dctype 字段记录该类型;DCLEVEL(第 49~89 行)保存 DC 的完整状态快照(画笔/画刷/字体/剪裁区/映射矩阵等),元文件播放时通过 SaveDC/RestoreDC 记录保护现场。

与元文件相关的主要句柄类型(ntgdityp.h 第 155~174 行):

类型常量 说明
GDILoObjType_LO_META_TYPE 0x150000 内核服务器元文件对象
GDILoObjType_LO_ALTDC_TYPE 0x210000 替代 DC(元文件/EMF DC 的内核底座)
GDILoObjType_LO_METADC16_TYPE 0x660000 16 位元文件 DC(用户态 mfdrv 的 DC 魔数)
GDILoObjType_LO_METAFILE_TYPE 0x460000 用户态 HMETAFILE
GDILoObjType_LO_METAFILE16_TYPE 0x260000 16 位元文件句柄

2.5 用户态相关结构

METAFILEDRV_PDEVICEgdi32/wine/mfdrv/metafiledrv.h(file:///d:/reactos/win32ss/gdi/gdi32/wine/mfdrv/metafiledrv.h) 第 33~40 行):

c 复制代码
typedef struct
{
    struct gdi_physdev dev;   /* 物理设备基类(含 hdc、函数表 funcs 等) */
    METAHEADER  *mh;          /* 元文件头指针(内存型时也是记录缓冲的起始) */
    UINT       handles_size, cur_handles;  /* 句柄表容量/当前占用 */
    HGDIOBJ   *handles;       /* GDI 对象句柄表(录制期创建的对象) */
    HANDLE     hFile;         /* 磁盘型元文件的文件句柄 */
} METAFILEDRV_PDEVICE;

这是 gdi32 侧"元文件物理驱动"的设备对象,嵌入在 Wine DC(DC,魔数 OBJ_METADC)的物理设备链上。

HANDLETABLE(wingdi.h 第 2800~2802 行):播放时用于把记录中的"对象索引"映射为真实 GDI 句柄:

c 复制代码
typedef struct tagHANDLETABLE {
    HGDIOBJ objectHandle[1];   /* 实际长度由 mtNoObjects 决定 */
} HANDLETABLE,*PHANDLETABLE, *LPHANDLETABLE;

METAHEADERDISKgdi32/wine/metafile.c(file:///d:/reactos/win32ss/gdi/gdi32/wine/metafile.c) 第 70~76 行,pshpack1 打包):磁盘型元文件紧随 METAHEADER 之后的扩展块,保存原始磁盘文件路径:

c 复制代码
typedef struct
{
    DWORD dw1, dw2, dw3;
    WORD w4;
    CHAR filename[0x100];   /* 磁盘元文件的路径(ANSI) */
} METAHEADERDISK;

常量:METAFILE_MEMORY = 1METAFILE_DISK = 2MFVERSION = 0x300gdi32/wine/gdi_private.h(file:///d:/reactos/win32ss/gdi/gdi32/wine/gdi_private.h) 第 74~77 行)。

2.6 METAFILEPICT:剪贴板格式描述头

METAFILEPICT 是剪贴板 CF_METAFILEPICT 格式的数据头,定义于 wingdi.h,紧随其后的才是元文件本身。它是连接"内核服务器元文件对象"与剪贴板的关键------NtGdiCreateServerMetaFilemm/xExt/yExt 三个参数正是从它透传:

c 复制代码
typedef struct tagMETAFILEPICT
{
    LONG mm;      /* 推荐映射模式(MM_*,如 MM_ANISOTROPIC) */
    LONG xExt;    /* 推荐显示宽度(MM_ISOTROPIC/ANISOTROPIC 时的比例基准) */
    LONG yExt;    /* 推荐显示高度 */
    HMETAFILE hMF;/* 元文件句柄(本结构的元文件部分) */
} METAFILEPICT;

使用它的两个 ReactOS 函数:

  • GdiCreateLocalMetaFilePict(hmo):内核服务器元文件对象 → HGLOBAL(METAFILEPICT + WMF 数据),供放进剪贴板;
  • GdiConvertMetaFilePict(hMetaFilePict):剪贴板里的 METAFILEPICT → 内核服务器元文件对象(GDITAG_TYPE_MFP)。

它们分别对应"内核 → 用户态本地对象"与"用户态本地对象 → 内核"两个方向,实现剪贴板数据的会话内中转。

2.7 OBJ_* 用户态对象魔数与内核类型的对应

用户态魔数(gdi32) 对应内核句柄类型 说明
OBJ_METADC GDILoObjType_LO_METADC16_TYPE (0x660000) 16 位元文件录制 DC(mfdrv)
OBJ_METAFILE GDILoObjType_LO_METAFILE_TYPE (0x460000) HMETAFILE
OBJ_ENHMETADC GDILoObjType_LO_ALTDC_TYPE (0x210000) EMF 录制 DC 的内核底座(NtGdiCreateMetafileDC)
OBJ_ENHMETAFILE (用户态 EMF 句柄) HENHMETAFILE,录制在用户态 EMR 缓冲
(无 OBJ) GDIObjType_META_TYPE (0x15) 内核服务器元文件对象(剪贴板桥)

3. 内核系统服务函数逐一展开

本节逐个分析 metafile.c(file:///d:/reactos/win32ss/gdi/ntgdi/metafile.c) 中的 4 个 NtGdi* 服务函数及其依赖的内核辅助机制。

3.1 NtGdiCreateMetafileDC ------ 创建元文件 DC(ALT DC)

签名:

c 复制代码
HDC
APIENTRY
NtGdiCreateMetafileDC(IN HDC hdc)

参数:

参数 含义
hdc 参考 DC 句柄。源码中该参数实际上未参与分支判断(原本计划"非空则参考、空则取桌面 DC",该逻辑被注释掉)

返回值: 新 DC 句柄(ALT DC),失败返回 NULL。

实现流程(metafile.c 第 35~44 行):

c 复制代码
HDC
APIENTRY
NtGdiCreateMetafileDC(IN HDC hdc)
{
    //if (hdc)
        /* Call the internal function to create an alternative info DC */
        return GreCreateCompatibleDC(hdc, TRUE);
    // No HDC call NtUser.
    //return UserGetDesktopDC(DCTYPE_INFO, TRUE, FALSE);
}
  1. 无论 hdc 是否为空,都调用内部函数 GreCreateCompatibleDC(hdc, TRUE)
  2. 被注释掉的 UserGetDesktopDC(DCTYPE_INFO, TRUE, FALSE) 提示了原设计意图:hdc 为空时应从桌面 DC 派生一个 DCTYPE_INFO 的信息上下文。

核心调用 GreCreateCompatibleDC(hdc, bAltDc=TRUE)(ntgdi/dclife.c(file:///d:/reactos/win32ss/gdi/ntgdi/dclife.c) 第 817~880 行):

  1. hdc 非空:DC_LockDc(hdc) 锁定源 DC,取其 ppdev(物理设备对象)并 PDEVOBJ_vReference 引用,然后解锁;
  2. hdc 为空:EngpGetPDEV(NULL) 获取默认显示设备(display PDEV);
  3. 依据 bAltDc 选择句柄类型:bAltDc ? GDILoObjType_LO_ALTDC_TYPE : GDILoObjType_LO_DC_TYPE,调用 DC_AllocDcWithHandle 分配新 DC;
  4. DC_vInitDc(pdcNew, bAltDc ? DCTYPE_INFO : DCTYPE_MEMORY, ppdev) 初始化:元文件 DC 的 dctype = DCTYPE_INFO(信息上下文),普通兼容 DC 的 dctype = DCTYPE_MEMORY
  5. DC_InitHack(pdcNew)(代码注释 FIXME: HACK!)与 DC_bAllocDcAttr 补齐属性块;
  6. 解锁并返回 pdcNew->BaseObject.hHmgr

使用方式: gdi32 在创建 EMF DC(增强元文件 DC)时调用它。见 gdi32/wine/rosglue.c(file:///d:/reactos/win32ss/gdi/gdi32/wine/rosglue.c) 第 121~136 行 alloc_dc_ptr(OBJ_ENHMETADC):先 NtGdiCreateMetafileDC(NULL) 拿到内核 ALT DC 作为 Wine DC 的 hdc 底座,再 GdiSetLDC 把用户态 WINEDC 挂到该句柄上,iType = LDC_EMFLDCgdi32/wine/emfdc.c(file:///d:/reactos/win32ss/gdi/gdi32/wine/emfdc.c) 第 2401 行也保留了对 NtGdiCreateMetafileDC(hdc) 的(注释掉的)引用。

注意事项:

  • 该函数创建的内核 DC 只是"记录上下文容器",本身不含任何元文件字节;真正的 EMF 记录由用户态 emfdc.cEMR* 记录)负责;
  • 与 16 位 CreateMetaFile(gdi32 mfdrv,返回 OBJ_METADC)不同,这里不建立元文件缓冲;
  • 被注释的 UserGetDesktopDC 分支意味着 ReactOS 当前对空 hdc 走 EngpGetPDEV(NULL),语义与 Windows 略有差异(不影响基本功能)。

3.2 NtGdiCreateServerMetaFile ------ 创建服务器元文件对象

签名:

c 复制代码
HANDLE
APIENTRY
NtGdiCreateServerMetaFile(
    IN DWORD iType,     /* 数据标签:GDITAG_TYPE_EMF / GDITAG_TYPE_MFP */
    IN ULONG cjData,    /* 数据字节数 */
    IN PBYTE pjData,    /* 数据指针(用户态地址) */
    IN DWORD mm,        /* METAFILEPICT.mm 透传 */
    IN DWORD xExt,      /* METAFILEPICT.xExt 透传 */
    IN DWORD yExt       /* METAFILEPICT.yExt 透传 */
)

返回值: 成功返回内核 GDI 句柄(hmo),失败返回 NULL。

实现流程(metafile.c 第 49~100 行):

  1. 入参校验 :只有 iType == GDITAG_TYPE_EMF || iType == GDITAG_TYPE_MFPcjData && pjData 才继续,否则直接返回 NULL(既不允许空数据,也不允许未知标签);
  2. 分配对象GDIOBJ_AllocObjWithHandle(GDILoObjType_LO_META_TYPE, sizeof(METATYPEOBJ) + cjData) ------ 对象总大小为结构体加上原始数据,一次分配;
  3. 填充字段pmto->iType/mm/xExt/yExt/cjData 直接赋值;
  4. 安全拷贝_SEH2_TRY 内先 ProbeForRead(pjData, cjData, 1) 探测用户缓冲区可读,再 RtlCopyMemory(pmto->pjData, pjData, cjData) 把数据拷进对象尾部;任何异常进入 _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)Pass 置 FALSE(防止蓝屏);
  5. 设置所有者并解锁 :成功则 GDIOBJ_vSetObjectOwner(&pmto->BaseObject, GDI_OBJ_HMGR_PUBLIC) ------ 把对象标记为公共句柄GDI_OBJ_HMGR_PUBLIC 允许其他进程通过句柄访问),随后 GDIOBJ_vUnlockObject 释放创建期的排他锁,返回 pmto->BaseObject.hHmgr
  6. 失败清理 :拷贝失败则 GDIOBJ_vDeleteObject(&pmto->BaseObject) 撤销对象。

依赖的内核机制(gdiobj.c):

  • GDIOBJ_AllocObjWithHandle(ObjectType, cjSize)(gdiobj.c 第 1535 行):从 ObjectType 高 16 位提取对象类型 objt,调用 GDIOBJ_AllocateObject 分配对象体,再 GDIOBJ_hInsertObject 插入句柄表,返回对象指针(内含句柄 hHmgr);
  • GDIOBJ_vSetObjectOwner(pobj, GDI_OBJ_HMGR_PUBLIC):把句柄表项的所有者设为公共,使句柄可被其他进程使用(剪贴板跨进程传递的关键);
  • 清理:gdiobj.capfnCleanup[GDIObjType_META_TYPE] = GDIOBJ_vCleanup(第 181 行),进程退出时 GDI_CleanupForProcess 会按所有者回收所有 META 对象。

使用方式(两个调用方都在 gdi32):

  • gdi32/objects/enhmfile.c(file:///d:/reactos/win32ss/gdi/gdi32/objects/enhmfile.c) 第 60 行 GdiConvertEnhMetaFileNtGdiCreateServerMetaFile(GDITAG_TYPE_EMF, nSize, Buffer, 0, 0, 0) ------ 把 EMF 位数据存进内核;
  • gdi32/objects/metafile.c(file:///d:/reactos/win32ss/gdi/gdi32/objects/metafile.c) 第 232 行 GdiConvertMetaFilePictNtGdiCreateServerMetaFile(GDITAG_TYPE_MFP, nSize, Buffer, pInfo->mm, pInfo->xExt, pInfo->yExt) ------ 把 METAFILEPICT 里的传统 WMF 数据连同推荐尺寸存进内核。

注意事项:

  • GDI_OBJ_HMGR_PUBLIC 的语义是"公开句柄":句柄值本身可以被传递,但 NtGdiGetServerMetaFileBits 读取时仍要求调用进程与对象在同一会话(会话隔离由 win32k 句柄表保证);
  • 数据是一次性拷贝进内核的,调用方之后可以释放自己的缓冲;
  • 只接受 EMF/MFP 两种标签,其它格式一律拒绝。

3.3 NtGdiGetServerMetaFileBits ------ 取服务器元文件位数据

签名:

c 复制代码
ULONG
APIENTRY
NtGdiGetServerMetaFileBits(
    IN HANDLE hmo,               /* 服务器元文件句柄 */
    IN ULONG cjData,             /* 请求的字节数(0 表示仅查询大小) */
    OUT OPTIONAL PBYTE pjData,   /* 输出缓冲 */
    OUT PDWORD piType,           /* 输出 iType */
    OUT PDWORD pmm,              /* 输出 mm */
    OUT PDWORD pxExt,            /* 输出 xExt */
    OUT PDWORD pyExt             /* 输出 yExt */
)

返回值: 成功返回可用的数据字节数(cjData == 0 时为总大小;cjData != 0 时为 pmto->cjData),失败或参数不匹配返回 0。

实现流程(metafile.c 第 105~165 行):

  1. GDIOBJ_LockObject(hmo, GDIObjType_META_TYPE) 锁定对象(gdiobj.c 第 833 行:校验句柄高 16 位类型 == 0x15、引用句柄表项、获取排他锁);锁定失败返回 0;
  2. 校验 iType 必须是 GDITAG_TYPE_EMFGDITAG_TYPE_MFP,否则返回 0;
  3. cjRet = pmto->cjData(先返回"需要的大小");
  4. cjData != 0要求恰好等于 pmto->cjData (即一次性取完,不允许部分读取;大小不符则 cjRet = 0);
  5. _SEH2_TRY 中依次 ProbeForWrite 探测 piType/pmm/pxExt/pyExt/pjData 五个用户缓冲,然后写入 iType/mm/xExt/yExtRtlCopyMemory(pjData, pmto->pjData, cjData) 拷出数据;任何异常使 cjRet = 0
  6. GDIOBJ_vUnlockObject 解锁,返回 cjRet

使用方式(两段式调用约定): 调用方先以 cjData=0, pjData=NULL 查询总大小,再分配缓冲、以精确大小第二次调用取数据。典型调用见:

  • gdi32/objects/enhmfile.c(file:///d:/reactos/win32ss/gdi/gdi32/objects/enhmfile.c) 第 81/91 行(GdiCreateLocalEnhMetaFile:先查大小再取数据并解出 iType/mm/xExt/yExt);
  • gdi32/objects/metafile.c(file:///d:/reactos/win32ss/gdi/gdi32/objects/metafile.c) 第 115/123 行(GdiCreateLocalMetaFilePict 同样两段式)。

注意事项:

  • 输出参数全部可选(piType/pmm/pxExt/pyExt 可为 NULL,pjData 可为 NULL);
  • ProbeForWrite 只探测不捕获后续访问;真正写入仍在 _SEH2_TRY 内完成,故非法地址不会导致内核崩溃;
  • "必须整取"的限制意味着该接口不适合流式/增量读取,这与服务器元文件对象"整块快照"的定位一致。

3.4 NtGdiConvertMetafileRect ------ 矩形坐标转换(未实现)

签名:

c 复制代码
LONG
APIENTRY
NtGdiConvertMetafileRect(IN HDC hDC, IN OUT PRECTL pRect)

实现(metafile.c 第 170~177 行):

c 复制代码
    UNIMPLEMENTED;
    return 0;
  • 标注为 @unimplemented,调用 UNIMPLEMENTED 宏会在调试串口输出 WARNING: ... is UNIMPLEMENTED! 并返回 0;
  • 设计意图(由函数名与参数推断):把 pRect 从"元文件记录坐标"换算到"显示/设备坐标"(结合 DC 的映射模式与窗口/视口范围做缩放),供在元文件 DC 上做坐标预换算使用;
  • 由于 gdi32 的录制/播放全部自行处理坐标(录制时按当前 DC 的逻辑坐标直接记录,播放时在目标 DC 上按逻辑坐标重放),该桩对现有功能无影响。

注意事项:

  • 切勿修改此函数的返回语义(调用方 gdi32 目前无任何调用点,属于预留接口);
  • 若未来实现,必须正确处理 MM_ISOTROPIC/MM_ANISOTROPIC 的窗口/视口比例缩放,并保留 SetLastError 约定。

3.5 内核对象机制小结

机制 函数 作用
分配 GDIOBJ_AllocObjWithHandle 分配对象体并插入句柄表(metafile.c 用它分配 METATYPEOBJ)
加锁 GDIOBJ_LockObject 校验类型 → 引用句柄项 → 排他锁(META_TYPE 属排他对象类型,见 gdiobj.c 第 115~119 行 ASSERT_EXCLUSIVE_OBJECT_TYPE)
解锁 GDIOBJ_vUnlockObject 释放锁与引用
所有权 GDIOBJ_vSetObjectOwner 设为公共句柄以便跨进程使用
清理 apfnCleanup[0x15] = GDIOBJ_vCleanup 对象销毁/进程退出时的内存回收

4. 元文件记录机制(gdi32 mfdrv:GDI 调用 → METARECORD)

4.1 物理驱动架构:gdi_dc_funcs 函数表

gdi32 采用"物理设备链"架构:每个 DC 上挂一串物理驱动,每个驱动实现一组 gdi_dc_funcs 回调。元文件物理驱动(mfdrv)的回调表 MFDRV_Funcs 定义在 mfdrv/init.c(file:///d:/reactos/win32ss/gdi/gdi32/wine/mfdrv/init.c) 第 100~231 行,优先级 GDI_PRIORITY_GRAPHICS_DRV

关键点:

  • 录制对象、图形、DC 状态、文本、位块传输的入口都被指派为 MFDRV_* 函数;查询类(GetDeviceCaps 等)要么桩实现、要么为 NULL(走下一层驱动);
  • 路径类(BeginPath/EndPath/FillPath/StrokePath/WidenPath 等)全部返回 FALSE(mfdrv/dc.c 第 148~196 行)------传统 WMF 格式没有路径记录,主动失败而不是记录近似数据;
  • 查询回调 MFDRV_GetDeviceCaps(init.c 第 85~97 行):TECHNOLOGY 返回 DT_METAFILETEXTCAPS 返回 0,其余返回 0;
  • MFDRV_ExtEscape(init.c 第 40~59 行):把 Escape 调用记录成 META_ESCAPE 记录;需要输出的 Escape(cbOutput 非零)直接返回 0(元文件无法承载查询类 Escape)。

4.2 元文件 DC 的创建:MFDRV_AllocMetaFile

mfdrv/init.c(file:///d:/reactos/win32ss/gdi/gdi32/wine/mfdrv/init.c) 第 238~276 行:

  1. alloc_dc_ptr(OBJ_METADC) 分配用户态 Wine DC(魔数 OBJ_METADC,即 16 位元文件 DC,内核侧对应 GDILoObjType_LO_METADC16_TYPE);
  2. 分配 METAFILEDRV_PDEVICEMETAHEADER
  3. push_dc_driver(&dc->physDev, &physDev->dev, &MFDRV_Funcs) 把 mfdrv 挂上物理设备链;
  4. 初始化句柄表:handles_size = HANDLE_LIST_INC = 20cur_handles = 0
  5. 初始化头部:mtHeaderSize = 9mtVersion = 0x0300mtSize = 9mtNoObjects = 0mtMaxRecord = 0mtNoParameters = 0
  6. SetVirtualResolution(dev.hdc, 0, 0, 0, 0) 设置虚拟分辨率(使 GetDeviceCaps 等有合理默认值)。

4.3 CreateMetaFileW / CloseMetaFile / DeleteMetaFile

CreateMetaFileW(init.c 第 319~358 行)filename 为 NULL 时创建内存型元文件(mtType = METAFILE_MEMORY);非 NULL 时创建磁盘型元文件(mtType = METAFILE_DISK),先 CreateFileW 打开文件并把初始 METAHEADER 写盘,再用 MF_CreateMetaHeaderDisk 扩展头部追加文件名(METAHEADERDISK)。CreateMetaFileA 先把 ANSI 路径转宽字符再调用 W 版。

MFDRV_CloseMetaFile(init.c 第 388~438 行)

  1. 校验对象类型为 OBJ_METADC、引用计数为 1;
  2. 写入 META_EOF 结束记录(MFDRV_MetaParam0(dc->physDev, META_EOF),对应 MSDN KB Q99334 的要求);
  3. 磁盘型:把文件指针移到开头,mtType 临时改成 METAFILE_MEMORY 后回写头部(这是 Windows 的行为------磁盘元文件头里的类型也写 MEMORY,便于统一读取),随后关闭文件、改回 METAFILE_DISK。

CloseMetaFile(init.c 第 452~467 行) :调用 MFDRV_CloseMetaFile 后,把 METAHEADER 包装成 HMETAFILE(MF_Create_HMETAFILEalloc_gdi_handle(mh, OBJ_METAFILE, NULL),见 gdi32/wine/metafile.c 第 107~110 行),并把 physDev->mh 置 NULL 避免二次释放。

DeleteMetaFile(gdi32/wine/metafile.c 第 139~144 行)free_gdi_handle(hmf) 释放 GDI 句柄,再 HeapFree 释放 METAHEADER 缓冲。

4.4 记录写入核心:MFDRV_WriteRecord

mfdrv/init.c(file:///d:/reactos/win32ss/gdi/gdi32/wine/mfdrv/init.c) 第 475~511 行,是所有 MFDRV_* 记录的汇聚点:

c 复制代码
BOOL MFDRV_WriteRecord( PHYSDEV dev, METARECORD *mr, DWORD rlen)
  • 内存型len = mh->mtSize*2 + rlen;若超过当前 HeapSize,按 size += size/2 + rlen 扩容(HeapReAlloc,注意函数头注释"会改变 mh 指针");随后 memcpy((WORD*)mh + mh->mtSize, mr, rlen) 把记录追加到头部之后;
  • 磁盘型 :直接 WriteFile(hFile, mr, rlen, ...) 追加到文件;
  • 未知类型打印 ERR("Unknown metafile type")
  • 写完后更新 mh->mtSize += rlen/2mh->mtMaxRecord = max(...)

4.5 通用参数记录构造:MFDRV_MetaParam0/1/2/4/6/8

mfdrv/init.c(file:///d:/reactos/win32ss/gdi/gdi32/wine/mfdrv/init.c) 第 518~628 行,6 个函数分别构造 0/1/2/4/6/8 参数记录。统一模式:在栈上 分配 char 缓冲(8/10/14/18/22 字节),按 rdSize = 3 + 参数个数(WORD 数)填充 rdFunction,然后逆序填入参数:

c 复制代码
/* MFDRV_MetaParam4:rdParm[0]=param4, rdParm[1]=param3, ... */
mr->rdSize = 7;
mr->rdFunction = func;
params[0] = param4;
params[1] = param3;
params[2] = param2;
params[3] = param1;
return MFDRV_WriteRecord( dev, mr, mr->rdSize * 2);

这与 2.2 节的"参数逆序存放"约定一致,播放端按逆序还原。

4.6 对象句柄表管理(mfdrv/objects.c)

元文件内的 GDI 对象通过"句柄表索引"引用,而不是直接存句柄值(句柄值跨进程/跨机器无意义)。管理函数:

函数 行为
MFDRV_AddHandle(dev, obj)(第 39 行) 在 handles\[\] 找空槽(容量不足按 HANDLE_LIST_INC=20 扩容),存入 get_full_gdi_handle(obj),递增 cur_handles 并同步 mh->mtNoObjects(注意注释:索引 0 在元文件中不保留为特殊值)
MFDRV_RemoveHandle(dev, index)(第 64 行) 清空槽位、递减计数
MFDRV_FindObject(dev, obj)(第 81 行) 线性查找句柄返回索引,找不到返回 -1
MFDRV_DeleteObject(dev, obj)(第 98 行) META_DELETEOBJECT 记录(参数=索引),然后清空句柄槽
MFDRV_SelectObject(dev, index)(第 125 行,static) META_SELECTOBJECT 记录

对象惰性创建MFDRV_SelectBrush/SelectPen/SelectFontFindObject,找不到才写 META_CREATE*INDIRECT 创建记录并 AddHandle,再写选中记录;同时调用 GDI_hdc_using_object 告知对象被 DC 使用(防止用户态过早删除)。MFDRV_DeleteDC(init.c 第 292~304 行)遍历句柄表对每个对象调用 GDI_hdc_not_using_object

4.7 图形记录(mfdrv/graphics.c)

  • 简单图形MFDRV_MoveTo/LineTo → MetaParam2;MFDRV_Arc/Pie/Chord → MetaParam8;MFDRV_Ellipse/Rectangle → MetaParam4;MFDRV_RoundRect → MetaParam6;MFDRV_SetPixel → MetaParam4 且颜色拆为 HIWORD(color)/LOWORD(color)MFDRV_ExtFloodFill → MetaParam4。
  • 折线/多边形MFDRV_MetaPoly(第 121 行)把 POINTS(16 位)数组直接拷进记录;MFDRV_Polyline/Polygon 先把 32 位 POINT 数组转换为 POINTS(16 位) (逐元素截断);MFDRV_PolyPolygon 记录格式为 多边形数 + INT16 每多边形点数 + 全部 POINTS
  • 区域记录MFDRV_CreateRegion(第 257 行)用 GetRegionData 拿到矩形列表,压缩成"带(band)"格式写入 META_CREATEREGION 记录(格式说明见 gdi32/wine/metafile.c 的 MF_Play_MetaCreateRegion 注释),然后 MFDRV_AddHandle 返回索引;MFDRV_PaintRgn/InvertRgnMETA_PAINTREGION/META_INVERTREGIONMFDRV_FillRgn/FrameRgn 先创建区域和画刷再写 META_FILLREGION/META_FRAMEREGIONMFDRV_ExtSelectClipRgn 仅支持 RGN_COPY(写 SELECT/DELETE 两条记录后 RemoveHandle)。
  • 贝塞尔MFDRV_PolyBezier/PolyBezierTo(第 428/438 行)直接返回 FALSE------注释明确说明"元文件不记录贝塞尔曲线,连直线近似都不记"。

4.8 DC 状态记录(mfdrv/dc.c)

全部通过 MetaParam 系列写记录:SaveDC→META_SAVEDC(0 参)、RestoreDC→META_RESTOREDCSetMapMode→META_SETMAPMODESetBkMode/SetROP2/SetRelAbs/SetPolyFillMode/SetStretchBltMode/SetTextCharacterExtra→META_*(1 参)、SetBkColor/SetTextColor→META_*(颜色拆高低字,2 参)、IntersectClipRect/ExcludeClipRect→META_*(4 参)、SetViewportExtEx/OrgEx、SetWindowExtEx/OrgEx、OffsetViewportOrgEx/WindowOrgEx→META_*(2 参)、ScaleViewportExtEx/WindowExtEx→META_*(4 参)、SetTextJustification→META_*(2 参)、SetMapperFlags→META_SETMAPPERFLAGS(拆高低字)。

路径类(AbortPath/BeginPath/CloseFigure/EndPath/FillPath/FlattenPath/SelectClipPath/StrokeAndFillPath/StrokePath/WidenPath)全部返回 FALSE;SetDCBrushColor/SetDCPenColor 返回 CLR_INVALID

4.9 对象创建记录(mfdrv/objects.c)

  • 画笔MFDRV_CreatePenIndirectLOGPEN16(16 位 LOGPEN)直接拷入 META_CREATEPENINDIRECT 记录;MFDRV_SelectPen 处理两种情况:普通 LOGPEN 直接转换;EXTLOGPEN(扩展画笔)降级为 LOGPEN16(丢弃用户样式等扩展信息,源码 FIXME 注明)。
  • 字体MFDRV_CreateFontIndirectLOGFONTW 转成 LOGFONT16(面名字符串用 WideCharToMultiByte(CP_ACP) 转 ANSI 并零填充),写入 META_CREATEFONTINDIRECT
  • 画刷MFDRV_CreateBrushIndirect 分两类:BS_SOLID/BS_NULL/BS_HATCHED → META_CREATEBRUSHINDIRECT(LOGBRUSH16);BS_PATTERN/BS_DIBPATTERN → META_DIBCREATEPATTERNBRUSH(内嵌 BITMAPINFO + 位图数据,通过 get_brush_bitmap_info/get_dib_info_size 计算布局,统一转为自底向上 DIB)。
  • 调色板MFDRV_SelectPalette 通过 GetObjectA 取条目数,组装 LOGPALETTE 写 META_CREATEPALETTE 记录,紧接着写 META_SELECTPALETTEMFDRV_RealizePaletteMETA_REALIZEPALETTE 并恒返回 1(无法得知实际映射数)。

4.10 位块传输记录(mfdrv/bitblt.c)

  • MFDRV_PatBlt:写 META_PATBLT(6 参:x/y/w/h + ROP 高低字);
  • MFDRV_StretchBlt(第 47 行):关键约束------不允许把元文件 DC 作为源 DCdevSrc->funcs == devDst->funcs 直接返回 FALSE,因为元文件无法引用另一段记录作为位图源)。在 STRETCH_VIA_DIB 宏下走 META_DIBSTRETCHBLT:取源 DC 当前位图,用 GetDIBits 转成 DIB 嵌入记录(BITMAPINFOHEADER + 调色板 + 像素);否则走 META_STRETCHBLT(GetBitmapBits 内嵌位图位)。记录参数布局(rdParm0...9):ROP 低/高字、src h/w/y/x、dst h/w/y/x。
  • MFDRV_SetDIBitsToDevice/StretchDIBits 声明于头文件;播放端对应 META_SETDIBTODEV/META_STRETCHDIB

4.11 文本记录(mfdrv/text.c)

MFDRV_ExtTextOut(第 75 行)先把 RECT 转成 RECT16、INT 的 dx 数组转成 INT16 数组、宽字符串转 ANSI(内部 MFDRV_MetaExtTextOut 第 36 行组装 META_EXTTEXTOUT 记录):

记录布局:rdParm[0]=y, [1]=x, [2]=count, [3]=flags,若 flags 含 ETO_OPAQUE|ETO_CLIPPED 则紧跟 RECT16(4 个 SHORT),随后是 ANSI 字符串(对齐到 WORD),末尾可选 INT16 dx 数组。播放端 MF_Play_MetaExtTextOut 按同一布局解析(见 5.2)。

4.12 记录机制小结

步骤 位置 动作
1 应用 调用任意 GDI API(如 Rectangle)
2 gdi32 分发 按 DC 物理设备链路由到 MFDRV_Rectangle
3 mfdrv/graphics.c 组装 META_RECTANGLE 记录(参数逆序)
4 mfdrv/init.c MFDRV_WriteRecord 追加到内存缓冲/磁盘文件,更新 mtSize/mtMaxRecord
5 应用 CloseMetaFile 写 META_EOF,返回 HMETAFILE

4.13 录制端不支持的操作汇总

受 WMF 格式能力限制,mfdrv 对以下操作主动失败(返回 FALSE / 0 / CLR_INVALID),不会写入任何记录:

操作类别 具体函数 原因
路径 MFDRV_AbortPath/BeginPath/CloseFigure/EndPath/FillPath/FlattenPath/SelectClipPath/StrokeAndFillPath/StrokePath/WidenPath WMF 没有路径记录类型(EMF 才有 EMR_BEGINPATH 等)
贝塞尔 MFDRV_PolyBezier/PolyBezierTo WMF 无贝塞尔记录,也不做直线近似(源码注释明确说明)
复合 DC MFDRV_CreateCompatibleDC 元文件 DC 不能作为创建兼容 DC 的源
颜色扩展 MFDRV_SetDCBrushColor/SetDCPenColor WMF 时代没有 DC 画笔/画刷颜色概念
元文件互操作 MFDRV_StretchBlt(源为元文件 DC) devSrc->funcs == devDst->funcs 时拒绝------元文件不能引用另一段记录做位图源
查询类 Escape MFDRV_ExtEscape(cbOutput 非零) 元文件无法承载"要输出数据"的 Escape

这些桩的存在保证:应用在元文件 DC 上调用不支持的 API 时能立即得知失败(而不是静默丢失绘图),与 Windows 行为一致。


5. 元文件播放(gdi32:METARECORD → GDI 调用)

播放代码在 gdi32/wine/metafile.c(file:///d:/reactos/win32ss/gdi/gdi32/wine/metafile.c)(注意与内核 metafile.c 同名不同目录)。

5.1 数据获取:get_metafile_bits / MF_ReadMetaFile

  • MF_ReadMetaFile(hfile)(第 152 行):读 METAHEADER(18 字节),校验 mtType == METAFILE_MEMORYmtVersion == MFVERSION(0x300)mtHeaderSize == 9;然后按 mtSize*2 扩容缓冲并读入全部记录;磁盘型头部 mtType 若是 DISK 会被改回 MEMORY(第 182~185 行)。
  • get_metafile_bits(hmf)(第 289 行):GDI_GetObjPtr(hmf, OBJ_METAFILE) 取 METAHEADER;内存型直接 memcpy(mtSize*2) 拷贝;磁盘型调用 MF_LoadDiskBasedMetaFile(第 243 行:按 METAHEADERDISK 里的路径重新打开文件再 MF_ReadMetaFile)。返回的缓冲需调用方 HeapFree。

5.2 PlayMetaFile ------ 播放主循环

签名: BOOL WINAPI PlayMetaFile(HDC hdc, HMETAFILE hmf)

流程(第 380~451 行):

  1. get_metafile_bits(hmf) 取得内存副本;
  2. 保存目标 DC 现场GetCurrentObject 保存当前画笔/画刷/调色板;CreateRectRgn + GetClipRgn 保存剪裁区;
  3. mh->mtNoObjects 分配 HANDLETABLE(全零);
  4. 遍历记录offset = mh->mtHeaderSize * 2(跳过头部),循环直到 offset >= mh->mtSize * 2
    • mr = (METARECORD*)((char*)mh + offset)
    • 安全检查:mr->rdSize < 3 视为非法记录(记录最小 3 WORD),打印 TRACE 并 break(防畸形元文件越界);
    • offset += mr->rdSize * 2 前移;
    • 遇到 META_EOF 停止;
    • PlayMetaFileRecord(hdc, ht, mr, mh->mtNoObjects) 分发执行;
  5. 恢复现场 :重新选回画笔/画刷/调色板,ExtSelectClipRgn(hdc, hRgn, RGN_COPY) 恢复剪裁区,删除临时区域;
  6. 清理句柄表 :遍历 mtNoObjects 个槽位,非零对象 DeleteObject;释放 ht 与 mh 缓冲;返回 TRUE。

5.3 PlayMetaFileRecord ------ 单条记录分发

签名: BOOL WINAPI PlayMetaFileRecord(HDC hdc, HANDLETABLE *ht, METARECORD *mr, UINT handles)

第 540~1033 行的大 switch,把每种 META_* 还原成对目标 DC 的 GDI 调用(坐标/颜色按逆序取回并做 SHORT 符号扩展)。分组说明:

类别 记录 → 调用
DC 状态 SETBKCOLOR/SETTEXTCOLOR → MAKELONG 拼回 COLORREF;SETMAPMODE/SETBKMODE/SETROP2/SETRELABS/SETPOLYFILLMODE/SETSTRETCHBLTMODE/SETTEXTALIGN/SETTEXTCHAREXTRA/SETTEXTJUSTIFICATION;SAVEDC/RESTOREDC;SETWINDOWORG/EXT、SETVIEWPORTORG/EXT、OFFSET*、SCALE*
坐标/图形 MOVETO/LINETO、ARC/PIE/CHORD/ELLIPSE/RECTANGLE/ROUNDRECT/PATBLT/SETPIXEL/FLOODFILL/EXTFLOODFILL、POLYGON/POLYLINE(convert_points 把 POINTS 转回 POINT)、POLYPOLYGON(counts 升 32 位)
剪裁 EXCLUDECLIPRECT/INTERSECTCLIPRECT/OFFSETCLIPRGN/SELECTCLIPREGION
对象 DELETEOBJECT(删句柄表项)、SELECTOBJECT(按索引选入)、CREATEPENINDIRECT/CREATEFONTINDIRECT/CREATEBRUSHINDIRECT(用 16 位参数重建 LOGPEN/LOGFONTA/LOGBRUSH)、CREATEPALETTE、CREATEPATTERNBRUSH(BS_PATTERN 用 CreateBitmap+CreatePatternBrush;BS_DIBPATTERN 用 CreateDIBPatternBrushPt)、DIBCREATEPATTERNBRUSH、CREATEREGION(MF_Play_MetaCreateRegion 把 band 格式还原为 CombineRgn 序列)
文本 TEXTOUT(ANSI 字符串按 rdParm[0] 长度解析)、EXTTEXTOUT(MF_Play_MetaExtTextOut,见 4.11 布局逆向解析)
位块 BITBLT(CreateCompatibleDC+CreateBitmap+BitBlt)、STRETCHBLT(同上+StretchBlt)、DIBBITBLT(rdSize>12 判定有 DIB → StretchDIBits,否则退化为 PatBlt)、DIBSTRETCHBLT、STRETCHDIB、SETDIBTODEV
区域操作 FILLREGION/FRAMEREGION/INVERTREGION/PAINTREGION(按句柄表索引取 HRGN/HBRUSH)
ESCAPE 过滤 GETSCALINGFACTOR/GETPHYSPAGESIZE/GETPRINTINGOFFSET/SETABORTPROC(返回 FALSE),其余转发 Escape
未实现 META_UNIMP 宏列出的 13 个:DRAWTEXT、ANIMATEPALETTE、SETPALENTRIES、RESIZEPALETTE、RESETDC、STARTDOC、STARTPAGE、ENDPAGE、ABORTDOC、ENDDOC、CREATEBRUSH、CREATEBITMAPINDIRECT、CREATEBITMAP(打印 Escape 与 16 位位图创建在 WMF 中罕见,FIXME 记录)
未知 打印 WARN 并返回 FALSE

5.4 EnumMetaFile / GetMetaFileBitsEx / SetMetaFileBitsEx

  • EnumMetaFile (第 464 行):与 PlayMetaFile 相同的主循环,但不执行记录而是回调 lpEnumFunc(hdc, ht, mr, mh->mtNoObjects, lpData);回调返回 FALSE 即中止并返回 FALSE;同样保存/恢复画笔画刷字体、清理句柄表。
  • GetMetaFileBitsEx (第 1091 行):取原始字节。GDI_GetObjPtr(hmf, OBJ_METAFILE);磁盘型先 MF_LoadDiskBasedMetaFile 加载;返回 mh->mtSize * 2buf 非空时按 min(mfSize, nSize) 拷出(两段式:buf=NULL 查询大小)。
  • SetMetaFileBitsEx (第 1049 行):校验 size 为偶数、mtType==METAFILE_MEMORYmtVersion==MFVERSIONmtHeaderSize==9(失败 SetLastError(ERROR_INVALID_DATA)),拷贝整块并 mf_out->mtSize = size/2,返回 MF_Create_HMETAFILE

5.5 GetWinMetaFileBits ------ EMF → WMF 转换

(第 1257 行)用"录制 WMF"的方式把 EMF 降级为 WMF:CreateMetaFileW(NULL) 开一个新元文件 DC → add_mf_comment 把 EMF 原始字节作为 MFCOMMENT Escape 记录嵌入 → SetMapMode + set_window(把 ENHMETAHEADER.rclFrame 按映射模式换算为窗口原点/范围,用自实现 muldiv 避免 MulDiv 对负数的差异)→ PlayEnhMetaFile 重放到元文件 DC → CloseMetaFileGetMetaFileBitsEx 取位。若完整取回全部位,则对首条 MFCOMMENT 记录的校验和字段做修正(~checksum + 1)。

5.6 播放记录细节补充

META_ESCAPE 的安全过滤(PlayMetaFileRecord 第 843~854 行):播放时对 Escape 记录做白名单式检查------

  • GETSCALINGFACTOR/GETPHYSPAGESIZE/GETPRINTINGOFFSET 属于"查询类",直接返回 FALSE(源码注释:直接解引用会导致空指针崩溃);
  • SETABORTPROC 被过滤并打印 FIXME("Filtering Escape(SETABORTPROC), possible virus?\n")------因为恶意元文件可以通过 SETABORTPROC 注入回调地址,这是著名的元文件攻击向量之一;
  • 其余 Escape 才转发给 Escape(hdc, ...)

META_DIBBITBLT 的两种布局判定 (第 960~982 行):依据注释(Caolan.McNamara@ul.ie),META_DIBBITBLT 有两种变体------带源 DC 时内嵌 DIB,源 DC 为 NULL 时等价于 PatBlt。播放端用 mr->rdSize > 12 区分:大记录按 StretchDIBits 解析(BITMAPINFO 从 rdParm8 开始),小记录退化为 PatBlt

META_BITBLT / META_STRETCHBLT 的位图重建 (第 900~915 / 882~898 行):这两类旧式记录内嵌的是"设备相关位图"(宽/高/planes/bitspixel + 原始位),播放时先 CreateCompatibleDC(hdc) 建临时 DC,CreateBitmap(width, height, planes, bitspixel, &rdParm[15]) 重建位图并 SelectObject,再 BitBlt/StretchBlt 到目标 DC,最后 DeleteDC 清理。注意参数索引:STRETCHBLT 的位图参数从 rdParm10...14,BITBLT 从 rdParm7...11

META_TEXTOUT 的字符串定位 (第 696~701 行):ANSI 字符串从 rdParm + 1 开始(第 0 个 WORD 是长度 s1),字符串按 WORD 对齐(((s1+1)>>1)*2),x/y 坐标在字符串之后:x = rdParm[((s1+1)>>1)+2]y = rdParm[((s1+1)>>1)+1]

META_CREATEPATTERNBRUSH 的两种子类型 (第 755~779 行):rdParm[0]BS_PATTERN 时用 CreateBitmap + CreatePatternBrush(位图头 BITMAPINFOHEADER 在 rdParm+2,像素在其后);BS_DIBPATTERN 时用 CreateDIBPatternBrushPt(infohdr, rdParm[1])rdParm[1] 是颜色用法 DIB_RGB_COLORS 等)。

META_CREATEFONTINDIRECT 的位字段拆分 (第 792~811 行):16 位 LOGFONT16 中若干字段按"一 WORD 拆两字节"存放:lfItalic = LOBYTE(rdParm[5])lfUnderline = HIBYTE(rdParm[5])lfStrikeOut = LOBYTE(rdParm[6])lfCharSet = HIBYTE(rdParm[6])lfOutPrecision = LOBYTE(rdParm[7])lfClipPrecision = HIBYTE(rdParm[7])lfQuality = LOBYTE(rdParm[8])lfPitchAndFamily = HIBYTE(rdParm[8])lfFaceNamerdParm+9 拷贝 LF_FACESIZE 字节。

MF_Play_MetaCreateRegion 的 band 校验 (第 1331~1369 行):解析 META_CREATEREGION 时做三重防御------band 分隔符必须为偶数(*start/2 == (*start+1)/2)、band 结束指针不得越过记录尾(end > (WORD*)mr + mr->rdSize 则失败)、首尾分隔符必须相等;通过后逐对 x 坐标 SetRectRgn + CombineRgn(RGN_OR) 重建区域。


6. 服务器元文件对象的使用场景(剪贴板 / EMF↔WMF)

内核 NtGdiCreateServerMetaFile / NtGdiGetServerMetaFileBits唯一调用方是 gdi32 的剪贴板转换代码,构成"用户态对象 ↔ 内核快照"的双向桥:

6.1 把 EMF 存入内核(GdiConvertEnhMetaFile)

gdi32/objects/enhmfile.c(file:///d:/reactos/win32ss/gdi/gdi32/objects/enhmfile.c) 第 41~67 行:GetEnhMetaFileBits 取 EMF 原始字节 → NtGdiCreateServerMetaFile(GDITAG_TYPE_EMF, nSize, Buffer, 0, 0, 0) 得到内核句柄 hmo(mm/xExt/yExt 传 0,EMF 尺寸在自身头内)。

6.2 从内核取回并重建本地 EMF(GdiCreateLocalEnhMetaFile)

同上文件第 74~120 行:两段式 NtGdiGetServerMetaFileBits 取数据与 iType/mm/xExt/yExt;若 iType == GDITAG_TYPE_MFP(跨剪贴板从 WMF 来)则用 SetWinMetaFileBits(nSize, Buffer, NULL, &Info)(Info 携带 mm/xExt/yExt)转换回 EMF;否则直接 SetEnhMetaFileBits

6.3 METAFILEPICT:传统 WMF 剪贴板格式(GdiCreateLocalMetaFilePict)

gdi32/objects/metafile.c(file:///d:/reactos/win32ss/gdi/gdi32/objects/metafile.c) 第 95~194 行:分配 GlobalAlloc(GHND|GMEM_SHARE)METAFILEPICT({mm, xExt, yExt, hMF}),两段式取内核数据;若内核存的是 EMF (iType==GDITAG_TYPE_EMF),需要先 SetEnhMetaFileBits → 读 ENHMETAHEADER 计算 mm=MM_ANISOTROPIC、xExt/yExt = rclFrame 宽高CreateDCW(L"DISPLAY") + GetWinMetaFileBits 把 EMF 转成 WMF 字节,再 SetMetaFileBitsEx 得到传统 HMETAFILE 放进 METAFILEPICT。

6.4 把剪贴板 METAFILEPICT 存入内核(GdiConvertMetaFilePict)

同上文件第 201~241 行:GlobalLock 取 METAFILEPICT → GetMetaFileBitsEx 取 WMF 字节 → NtGdiCreateServerMetaFile(GDITAG_TYPE_MFP, nSize, Buffer, pInfo->mm, pInfo->xExt, pInfo->yExt)------把 mm/xExt/yExt 一并保存,供 6.2 反向转换使用。

6.5 剪贴板数据流

复制代码
进程 A(EMF 程序)                    进程 B(WMF 程序)
GetEnhMetaFileBits                       │
  ↓ NtGdiCreateServerMetaFile(EMF)       │
内核 METATYPEOBJ (iType='XEFM') ──剪贴板──→ NtGdiGetServerMetaFileBits
  (GDI_OBJ_HMGR_PUBLIC 公共句柄)         ↓
                                         GdiCreateLocalMetaFilePict
                                          → GetWinMetaFileBits(EMF→WMF)
                                          → SetMetaFileBitsEx → HMETAFILE

7. 调用链(mermaid)

win32k.sys gdi32.dll 应用程序 win32k.sys gdi32.dll 应用程序 #mermaid-svg-V8QDucQ2ZaTU6EJZ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-V8QDucQ2ZaTU6EJZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .error-icon{fill:#552222;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .marker.cross{stroke:#333333;}#mermaid-svg-V8QDucQ2ZaTU6EJZ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-V8QDucQ2ZaTU6EJZ p{margin:0;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-V8QDucQ2ZaTU6EJZ text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-V8QDucQ2ZaTU6EJZ .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-V8QDucQ2ZaTU6EJZ #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .sequenceNumber{fill:white;}#mermaid-svg-V8QDucQ2ZaTU6EJZ #sequencenumber{fill:#333;}#mermaid-svg-V8QDucQ2ZaTU6EJZ #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .messageText{fill:#333;stroke:none;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .labelText,#mermaid-svg-V8QDucQ2ZaTU6EJZ .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .loopText,#mermaid-svg-V8QDucQ2ZaTU6EJZ .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-V8QDucQ2ZaTU6EJZ .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .noteText,#mermaid-svg-V8QDucQ2ZaTU6EJZ .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .actorPopupMenu{position:absolute;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-V8QDucQ2ZaTU6EJZ .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-V8QDucQ2ZaTU6EJZ .actor-man circle,#mermaid-svg-V8QDucQ2ZaTU6EJZ line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-V8QDucQ2ZaTU6EJZ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 录制(WMF) 播放 剪贴板/EMF 服务器对象 CreateMetaFileW(NULL)MFDRV_AllocMetaFile(OBJ_METADC) + push_dc_driver(MFDRV_Funcs)Rectangle(hdcMF, ...)MFDRV_Rectangle → MFDRV_MetaParam4 → MFDRV_WriteRecordCloseMetaFile(hdcMF)MFDRV_MetaParam0(META_EOF) → MF_Create_HMETAFILEHMETAFILEPlayMetaFile(hdc, hmf)get_metafile_bits → 循环 METARECORDPlayMetaFileRecord → GDI APITRUEGdiConvertEnhMetaFile(hemf)NtGdiCreateServerMetaFile(EMF, data)hmo (META_TYPE, GDI_OBJ_HMGR_PUBLIC)GdiCreateLocalMetaFilePict(hmo)NtGdiGetServerMetaFileBits(hmo, ...)iType/mm/xExt/yExt + 数据HGLOBAL(METAFILEPICT)
#mermaid-svg-4NiX7aORvFvZfYG3{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4NiX7aORvFvZfYG3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4NiX7aORvFvZfYG3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4NiX7aORvFvZfYG3 .error-icon{fill:#552222;}#mermaid-svg-4NiX7aORvFvZfYG3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4NiX7aORvFvZfYG3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4NiX7aORvFvZfYG3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4NiX7aORvFvZfYG3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4NiX7aORvFvZfYG3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4NiX7aORvFvZfYG3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4NiX7aORvFvZfYG3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4NiX7aORvFvZfYG3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4NiX7aORvFvZfYG3 .marker.cross{stroke:#333333;}#mermaid-svg-4NiX7aORvFvZfYG3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4NiX7aORvFvZfYG3 p{margin:0;}#mermaid-svg-4NiX7aORvFvZfYG3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-4NiX7aORvFvZfYG3 .cluster-label text{fill:#333;}#mermaid-svg-4NiX7aORvFvZfYG3 .cluster-label span{color:#333;}#mermaid-svg-4NiX7aORvFvZfYG3 .cluster-label span p{background-color:transparent;}#mermaid-svg-4NiX7aORvFvZfYG3 .label text,#mermaid-svg-4NiX7aORvFvZfYG3 span{fill:#333;color:#333;}#mermaid-svg-4NiX7aORvFvZfYG3 .node rect,#mermaid-svg-4NiX7aORvFvZfYG3 .node circle,#mermaid-svg-4NiX7aORvFvZfYG3 .node ellipse,#mermaid-svg-4NiX7aORvFvZfYG3 .node polygon,#mermaid-svg-4NiX7aORvFvZfYG3 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4NiX7aORvFvZfYG3 .rough-node .label text,#mermaid-svg-4NiX7aORvFvZfYG3 .node .label text,#mermaid-svg-4NiX7aORvFvZfYG3 .image-shape .label,#mermaid-svg-4NiX7aORvFvZfYG3 .icon-shape .label{text-anchor:middle;}#mermaid-svg-4NiX7aORvFvZfYG3 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4NiX7aORvFvZfYG3 .rough-node .label,#mermaid-svg-4NiX7aORvFvZfYG3 .node .label,#mermaid-svg-4NiX7aORvFvZfYG3 .image-shape .label,#mermaid-svg-4NiX7aORvFvZfYG3 .icon-shape .label{text-align:center;}#mermaid-svg-4NiX7aORvFvZfYG3 .node.clickable{cursor:pointer;}#mermaid-svg-4NiX7aORvFvZfYG3 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4NiX7aORvFvZfYG3 .arrowheadPath{fill:#333333;}#mermaid-svg-4NiX7aORvFvZfYG3 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4NiX7aORvFvZfYG3 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4NiX7aORvFvZfYG3 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4NiX7aORvFvZfYG3 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4NiX7aORvFvZfYG3 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4NiX7aORvFvZfYG3 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4NiX7aORvFvZfYG3 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4NiX7aORvFvZfYG3 .cluster text{fill:#333;}#mermaid-svg-4NiX7aORvFvZfYG3 .cluster span{color:#333;}#mermaid-svg-4NiX7aORvFvZfYG3 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-4NiX7aORvFvZfYG3 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4NiX7aORvFvZfYG3 rect.text{fill:none;stroke-width:0;}#mermaid-svg-4NiX7aORvFvZfYG3 .icon-shape,#mermaid-svg-4NiX7aORvFvZfYG3 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4NiX7aORvFvZfYG3 .icon-shape p,#mermaid-svg-4NiX7aORvFvZfYG3 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4NiX7aORvFvZfYG3 .icon-shape .label rect,#mermaid-svg-4NiX7aORvFvZfYG3 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4NiX7aORvFvZfYG3 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4NiX7aORvFvZfYG3 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4NiX7aORvFvZfYG3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} MEMORY
DISK


CreateMetaFileW
MFDRV_AllocMetaFile
push_dc_driver MFDRV_Funcs
GDI 调用 → MFDRV_*
MFDRV_MetaParamN / 专用组装
MFDRV_WriteRecord
mtType
HeapReAlloc + memcpy 追加
WriteFile 追加
更新 mtSize / mtMaxRecord
CloseMetaFile 写 META_EOF
HMETAFILE
PlayMetaFile
get_metafile_bits
循环 METARECORD
rdSize<3 / META_EOF?
恢复 DC + 清理句柄表
PlayMetaFileRecord


8. 与其它分册的关系

关联项 说明
《分析_31》2.2 表(其余行)metafile.c 本文档展开
DC 生命周期 / dclife.c(《分析_49》) GreCreateCompatibleDCDC_vInitDc(DCTYPE_INFO)、DC_AllocDcWithHandle
GDI 对象管理 / gdiobj.c GDIObjType_META_TYPE(0x15) 分配/加锁/清理/进程回收
增强元文件(EMF) gdi32/wine/emfdc.c(录制 EMR*)、objects/enhmfile.c(转换)
LDEV_DEVICE_META / multidisp.c 桩(《分析_25》) 元文件设备模型的内核侧占位
剪贴板 CF_METAFILEPICTMETAFILEPICTOleSetClipboard 等上层使用

9. 源码索引

文件 关键内容
win32ss/gdi/ntgdi/metafile.c(file:///d:/reactos/win32ss/gdi/ntgdi/metafile.c) METATYPEOBJ;NtGdiCreateMetafileDC / NtGdiCreateServerMetaFile / NtGdiGetServerMetaFileBits / NtGdiConvertMetafileRect
win32ss/gdi/ntgdi/dclife.c(file:///d:/reactos/win32ss/gdi/ntgdi/dclife.c) GreCreateCompatibleDC(ALT DC + DCTYPE_INFO)
win32ss/gdi/ntgdi/dc.h(file:///d:/reactos/win32ss/gdi/ntgdi/dc.h) DCTYPE 枚举、DC/DCLEVEL 结构
win32ss/gdi/ntgdi/gdiobj.c(file:///d:/reactos/win32ss/gdi/ntgdi/gdiobj.c) GDIOBJ_AllocObjWithHandle / LockObject / vSetObjectOwner / apfnCleanupMETA
win32ss/include/ntgdityp.h(file:///d:/reactos/win32ss/include/ntgdityp.h) GDIObjType_META_TYPE(0x15)、GDILoObjType_LO_META_TYPE(0x150000)、GDITAG_TYPE_EMF/'XEFM'、GDITAG_TYPE_MFP/'_PFM'
win32ss/gdi/gdi32/wine/mfdrv/init.c(file:///d:/reactos/win32ss/gdi/gdi32/wine/mfdrv/init.c) MFDRV_Funcs、MFDRV_AllocMetaFile、CreateMetaFileW/A、CloseMetaFile、MFDRV_WriteRecord、MFDRV_MetaParam0/1/2/4/6/8、MFDRV_ExtEscape、MFDRV_GetDeviceCaps
win32ss/gdi/gdi32/wine/mfdrv/graphics.c(file:///d:/reactos/win32ss/gdi/gdi32/wine/mfdrv/graphics.c) MFDRV_MoveTo/LineTo/Arc/Pie/Chord/Ellipse/Rectangle/RoundRect/SetPixel、MFDRV_MetaPoly、MFDRV_CreateRegion、MFDRV_PaintRgn/InvertRgn/FillRgn/FrameRgn、MFDRV_PolyBezier(桩)
win32ss/gdi/gdi32/wine/mfdrv/dc.c(file:///d:/reactos/win32ss/gdi/gdi32/wine/mfdrv/dc.c) DC 状态类记录(SaveDC/RestoreDC/SetMapMode/SetBkColor/...)、路径类全部返回 FALSE
win32ss/gdi/gdi32/wine/mfdrv/objects.c(file:///d:/reactos/win32ss/gdi/gdi32/wine/mfdrv/objects.c) 句柄表管理、MFDRV_CreateBrushIndirect/CreateFontIndirect/CreatePenIndirect/CreatePalette/SelectPalette/RealizePalette
win32ss/gdi/gdi32/wine/mfdrv/bitblt.c(file:///d:/reactos/win32ss/gdi/gdi32/wine/mfdrv/bitblt.c) MFDRV_PatBlt、MFDRV_StretchBlt(STRETCH_VIA_DIB → META_DIBSTRETCHBLT)
win32ss/gdi/gdi32/wine/mfdrv/text.c(file:///d:/reactos/win32ss/gdi/gdi32/wine/mfdrv/text.c) MFDRV_MetaExtTextOut / MFDRV_ExtTextOut(META_EXTTEXTOUT 布局)
win32ss/gdi/gdi32/wine/mfdrv/metafiledrv.h(file:///d:/reactos/win32ss/gdi/gdi32/wine/mfdrv/metafiledrv.h) METAFILEDRV_PDEVICE、全部 MFDRV_* 原型
win32ss/gdi/gdi32/wine/metafile.c(file:///d:/reactos/win32ss/gdi/gdi32/wine/metafile.c) PlayMetaFile / PlayMetaFileRecord / EnumMetaFile / GetMetaFileBitsEx / SetMetaFileBitsEx / GetWinMetaFileBits / MF_ReadMetaFile / MF_Play_MetaCreateRegion / MF_Play_MetaExtTextOut / METAHEADERDISK
win32ss/gdi/gdi32/objects/metafile.c(file:///d:/reactos/win32ss/gdi/gdi32/objects/metafile.c) GdiIsPlayMetafileDC / GdiIsMetaFileDC / GdiIsMetaPrintDC、GdiCreateLocalMetaFilePict、GdiConvertMetaFilePict
win32ss/gdi/gdi32/objects/enhmfile.c(file:///d:/reactos/win32ss/gdi/gdi32/objects/enhmfile.c) GdiConvertEnhMetaFile、GdiCreateLocalEnhMetaFile(服务器元文件 ↔ EMF)
win32ss/gdi/gdi32/wine/rosglue.c(file:///d:/reactos/win32ss/gdi/gdi32/wine/rosglue.c) alloc_dc_ptr(OBJ_ENHMETADC) → NtGdiCreateMetafileDC(NULL) + GdiSetLDC
win32ss/gdi/gdi32/wine/gdi_private.h(file:///d:/reactos/win32ss/gdi/gdi32/wine/gdi_private.h) METAFILE_MEMORY(1) / METAFILE_DISK(2) / MFVERSION(0x300)
sdk/include/psdk/wingdi.h(file:///d:/reactos/sdk/include/psdk/wingdi.h) METAHEADER、METARECORD、HANDLETABLE、META_* 函数码

10. 记录格式字节级示例

为了直观理解"参数逆序 + WORD 单位"的约定,下面手工推导两条典型记录的内存布局。

10.1 META_MOVETO(无对象创建)

假设应用在元文件 DC 上调用 MoveToEx(hdcMF, 100, 50, NULL),gdi32 路由到 MFDRV_MoveTo(dev, x=100, y=50)

c 复制代码
MFDRV_MetaParam2(dev, META_MOVETO /* 0x0214 */, 100, 50);

MFDRV_MetaParam2 的构造逻辑:rdSize = 5(3 + 2 参数),params[0] = param2 = 50(y),params[1] = param1 = 100(x)。生成的内存字节(小端,WORD 序):

复制代码
偏移  内容(HEX)   含义
0x00  05 00 00 00 rdSize = 5(共 5 WORD = 10 字节)
0x04  14 02       rdFunction = META_MOVETO (0x0214)
0x06  32 00       rdParm[0] = y = 50      ← 注意:最后一个参数在最前
0x08  64 00       rdParm[1] = x = 100

播放端 PlayMetaFileRecord 的 META_MOVETO 分支按逆序还原:

c 复制代码
case META_MOVETO:
    MoveToEx(hdc, (SHORT)mr->rdParm[1] /* x=100 */, (SHORT)mr->rdParm[0] /* y=50 */, NULL);
    break;

10.2 META_EXTTEXTOUT(变长记录示例)

假设 ExtTextOut(hdcMF, x=10, y=20, ETO_OPAQUE, &rect, "A", 1, NULL)("A" 转 ANSI 后 1 字节)。MFDRV_MetaExtTextOut 计算:

复制代码
len = sizeof(METARECORD)             /* 8 */
    + ((count+1)>>1)*2               /* ((1+1)>>1)*2 = 2(字符串按 WORD 对齐) */
    + 2*sizeof(short)                /* x,y 两个 SHORT = 4 */
    + sizeof(UINT16)                 /* flags = 2 */
    + sizeof(RECT16)                 /* ETO_OPAQUE → 带矩形 = 8 */
      = 8+2+4+2+8 = 24 字节 → rdSize = 12

布局:rdParm[0]=y, [1]=x, [2]=count=1, [3]=flagsrdParm[4..7]=RECT16rdParm[8]=字符串 "A"(不足 WORD 部分由 HeapAlloc 的 ZERO_MEMORY 补齐)。播放端 MF_Play_MetaExtTextOut 通过"mr->rdSizelen 比较"判定是否存在 dx 数组(rdSize == len/2 说明没有;rdSize == (len + count*sizeof(INT16))/2 说明有),再调用 ExtTextOutA

10.3 内存型元文件的整体布局

复制代码
0x00  METAHEADER(18 字节 = 9 WORD)
        mtType=1(MEMORY)  mtHeaderSize=9  mtVersion=0x0300  mtSize=总WORD数
        mtNoObjects=N  mtMaxRecord=最大记录WORD数  mtNoParameters=0
0x12  第 1 条 METARECORD(rdSize/rdFunction/rdParm...)
...   第 2 条、第 3 条 ...
       META_EOF 记录(rdSize=3, rdFunction=0)

播放器从 mtHeaderSize*2 = 0x12 开始顺序扫描,直到 offset >= mtSize*2 或遇到 META_EOF。


11. 常见问题与陷阱

11.1 16 位坐标截断

WMF 的所有坐标都是 16 位(POINTS/SHORT)。MFDRV_Polyline/Polygon 把 32 位 POINT 逐元素赋给 POINTS静默截断(超出 ±32767 的坐标会回绕)。因此超大坐标的绘图在 WMF 中会失真,这是格式本身的上限,不是 bug。

11.2 为什么磁盘元文件头里 mtType 也是 MEMORY

MFDRV_CloseMetaFile 在回写磁盘文件头时把 mtType 临时改为 METAFILE_MEMORY(init.c 第 427 行注释"This is what windows does")。因此读取侧统一按内存型格式解析 :先读 18 字节头部,校验 mtType==MEMORY,再按 mtSize 读余下全部记录。MF_ReadMetaFile 遇到 DISK 类型的头也会把它纠正为 MEMORY(metafile.c 第 182~185 行)。

11.3 播放的健壮性

PlayMetaFile 对不可信元文件做了两道防护:

  1. mr->rdSize < 3 立即终止循环(防止畸形记录把 offset 算到缓冲外);
  2. 播放前保存目标 DC 的画笔/画刷/调色板/剪裁区,结束后全部恢复------即使中间有记录失败,DC 状态也不会被污染。

PlayMetaFileRecord 对未知记录返回 FALSE 并打 WARN,但 PlayMetaFile 主循环不检查返回值(保持 Windows 兼容行为:尽量播放能播放的记录)。

11.4 服务器元文件对象与 HMETAFILE 的区别

HMETAFILE(用户态) 服务器元文件对象(内核)
句柄类型 OBJ_METAFILE(LO_METAFILE_TYPE) GDIObjType_META_TYPE(LO_META_TYPE)
数据位置 gdi32 进程堆(METAHEADER 缓冲) win32k 内核池(METATYPEOBJ 尾部)
生命周期 进程内,DeleteMetaFile 释放 进程/会话,GDI_CleanupForProcess 回收
跨进程 不直接共享 公共句柄(GDI_OBJ_HMGR_PUBLIC)可跨进程
用途 录制/播放/枚举 剪贴板桥接(EMF↔WMF 中转)

两者通过 NtGdiCreateServerMetaFile(本地 → 内核)与 NtGdiGetServerMetaFileBits(内核 → 本地重建)互相转换。

11.5 大小限制

  • mh->mtSize 是 DWORD,按 WORD 计,因此内存型元文件理论上限约 8 GB,但实际受堆扩容策略(size += size/2 + rlen,每次扩 50%+本次记录)影响,大元文件会频繁 realloc;
  • mtNoObjects 是 WORD(上限 65535 个对象),MFDRV_AddHandleUINT16 索引,对象数超出会溢出。

12. ReactOS 与 Windows/Wine 的差异要点

  1. 内核侧更精简 :Windows 的 win32k 有完整的 metafile 内核支持;ReactOS 只保留了"服务器元文件对象"(剪贴板桥)与 ALT DC 底座,录制/播放全部下沉到 gdi32(源自 Wine 的实现),NtGdiConvertMetafileRect 未实现;
  2. CreateMetaFileW 的 DC 底座 :Wine 的 mfdrv DC 不依赖内核 DC 句柄(物理设备链完全在用户态);ReactOS 的 EMF DC(alloc_dc_ptr(OBJ_ENHMETADC))则额外向内核申请一个 ALT DC(NtGdiCreateMetafileDC)作为句柄底座,再通过 GdiSetLDC 关联,这是 ReactOS 特有的"用户态对象 + 内核句柄"桥接模式;
  3. EMF↔WMF 转换 :ReactOS 的 GdiConvertEnhMetaFile/GdiConvertMetaFilePict/GdiCreateLocalMetaFilePict/GdiCreateLocalEnhMetaFile 以"内核服务器元文件对象"为中转站,与 Wine 纯用户态的内存中转路径不同(Wine 直接传递全局内存句柄);
  4. 剪贴板METAFILEPICT 相关的 GdiCreateLocalMetaFilePict 中 EMF→WMF 使用 MM_ANISOTROPIC(源码注释注明 wine 用 MM_ISOTROPIC),属刻意取舍;
  5. 路径/贝塞尔:WMF 不支持路径与贝塞尔,mfdrv 一律返回 FALSE,与 Wine 一致(EMF 才支持这些特性,见 emfdc.c)。

附录 A:GDI32 元文件 API 速查

A.1 录制/管理

API 说明 实现位置
CreateMetaFileW/A 新建元文件 DC(内存型/磁盘型) mfdrv/init.c
CloseMetaFile 结束录制,返回 HMETAFILE(写 META_EOF) mfdrv/init.c
DeleteMetaFile 释放 HMETAFILE 及缓冲 wine/metafile.c
CopyMetaFileW/A 复制元文件(到新内存对象或磁盘文件) wine/metafile.c
GetMetaFileW/A 从磁盘文件读入元文件 wine/metafile.c
SetMetaFileBitsEx 从原始字节创建 HMETAFILE wine/metafile.c
GetMetaFileBitsEx 取 HMETAFILE 原始字节 wine/metafile.c

A.2 播放/枚举

API 说明 实现位置
PlayMetaFile 在目标 DC 上重放整个元文件 wine/metafile.c
PlayMetaFileRecord 重放单条 METARECORD(分发 switch) wine/metafile.c
EnumMetaFile 遍历记录并回调 MFENUMPROC wine/metafile.c
GetWinMetaFileBits EMF → WMF 位数据 wine/metafile.c

A.3 服务器元文件(内核桥接)

API 说明 实现位置
NtGdiCreateMetafileDC 创建 ALT DC(EMF DC 底座) ntgdi/metafile.c
NtGdiCreateServerMetaFile 创建内核服务器元文件对象 ntgdi/metafile.c
NtGdiGetServerMetaFileBits 读取内核服务器元文件数据 ntgdi/metafile.c
GdiConvertEnhMetaFile HENHMETAFILE → 内核对象(EMF) objects/enhmfile.c
GdiCreateLocalEnhMetaFile 内核对象 → HENHMETAFILE objects/enhmfile.c
GdiCreateLocalMetaFilePict 内核对象 → METAFILEPICT(WMF) objects/metafile.c
GdiConvertMetaFilePict METAFILEPICT → 内核对象(MFP) objects/metafile.c
GdiIsPlayMetafileDC 判断 DC 是否处于元文件播放态 objects/metafile.c
GdiIsMetaFileDC 判断 DC 是否为元文件 DC objects/metafile.c
GdiIsMetaPrintDC 判断 DC 是否为元文件打印 DC objects/metafile.c

A.4 对象类型检查

GetObjectType 可用来在运行时区分元文件相关对象:

GetObjectType 返回值 对象
OBJ_METADC 16 位元文件录制 DC
OBJ_METAFILE HMETAFILE
OBJ_ENHMETADC EMF 录制 DC(ReactOS 上内核底座为 ALT DC)
OBJ_ENHMETAFILE HENHMETAFILE

附录 B:调试要点

  1. 串口观察 UNIMPLEMENTEDNtGdiConvertMetafileRect 被调用时串口出现 WARNING: ... is UNIMPLEMENTED!
  2. 调试通道 :mfdrv 与播放代码统一使用 WINE_DEFAULT_DEBUG_CHANNEL(metafile),启用方式为设置调试通道 metafile(Wine 风格调试环境);内核侧 metafile.c 使用 DPRINT 家族;
  3. 录制内容检查GetMetaFileBitsEx(hmf, 0, NULL) 得到字节数,再取数据即可用十六进制工具核对 META_* 记录布局(参考第 10 节字节示例);
  4. 畸形元文件防护验证 :构造 rdSize < 3 的记录验证播放器安全终止;构造 SETABORTPROC Escape 验证过滤逻辑(串口应出现 FIXME 过滤提示);
  5. 句柄泄漏排查 :播放结束应看到句柄表 mtNoObjects 个对象全部被 DeleteObject;若进程 GDI 句柄持续增长,重点检查 PlayMetaFileRecordMF_AddHandle 失败路径。

13. 总结

metafile.c(file:///d:/reactos/win32ss/gdi/ntgdi/metafile.c) 是 win32k 中一个"小而精"的系统服务文件:

  • 4 个入口NtGdiCreateMetafileDC(ALT DC 底座)、NtGdiCreateServerMetaFile(创建内核元文件对象)、NtGdiGetServerMetaFileBits(读取内核元文件数据)、NtGdiConvertMetafileRect(未实现桩);
  • 1 个私有结构METATYPEOBJ(BASEOBJECT + iType/mm/xExt/yExt/cjData + 尾部数据),数据与结构一体分配;
  • 核心价值 :为剪贴板提供"跨进程、带标签、整块快照"的元文件数据容器,配合 gdi32 的 GdiConvert* 系列完成 EMF↔WMF 双向转换;
  • 真正的元文件语义在 gdi32 :录制由 mfdrv(MFDRV_* → METARECORD),播放由 PlayMetaFile/PlayMetaFileRecord 完成,格式约定为"WORD 单位、参数逆序、16 位坐标"。

理解本文件需要同时把握三条线:内核 GDI 对象机制(gdiobj.c)、用户态元文件物理驱动(mfdrv)、以及播放/转换层(gdi32/wine/metafile.c 与 objects/metafile.c)。


本文档基于 ReactOS 源代码 win32ss/gdi/ntgdi/metafile.c 及关联模块分析(2026 年 8 月)

相关推荐
学习星球2 小时前
Solid.js 实战:拆解官方 RealWorld 项目
开发语言·javascript·vue.js
OPEN-F2 小时前
Python进阶教程:正则表达式进阶与文本处理
开发语言·python·正则表达式
zhifou1234562 小时前
java 17升级安装
java·开发语言
for_ever_love__2 小时前
python基础语法学习: 面向对象的三大特性
开发语言·python·学习
不会代码的小猴3 小时前
7. JSON
开发语言·c++·笔记·qt·算法·json
沫璃染墨3 小时前
《Qt从零入门系列(六):信号与槽进阶——从多种连接方式到Lambda表达式》
开发语言·c++·qt·代码规范·设计规范·qt5
ZJU_统一阿萨姆4 小时前
【算子开发】环境搭建与第一个CUDA程序
开发语言·人工智能·系统架构
Evand J4 小时前
【MATLAB例程,车联网8】MPC网联车辆节能跟驰控制:速度跟踪、间距约束与能耗优化。附MATLAB完整代码的下载链接
开发语言·matlab·车联网·调度·mpc·节能·车辆控制
ttwuai4 小时前
Go 后台接入 SSO 后菜单正常但接口 403,怎么排查权限链路?
开发语言·后端·golang