禁用特定GDI设备上下文的抗锯齿功能

10
我正在使用第三方库将图像渲染到GDI DC上,我需要确保任何文本都不被平滑处理/抗锯齿,这样我就可以将图像转换为预定义调色板中的索引颜色。
我使用的渲染第三方库不支持此功能,只按照当前Windows字体渲染设置呈现文本。他们还表示,他们不太可能很快添加关闭反走样的能力。
我目前找到的最佳解决方法是以这种方式调用第三方库(出于简洁起见省略了错误处理和先前的设置检查):
private static void SetFontSmoothing(bool enabled)
{
    int pv = 0;
    SystemParametersInfo(Spi.SetFontSmoothing, enabled ? 1 : 0, ref pv, Spif.None);
}

// snip
Graphics graphics = Graphics.FromImage(bitmap)
IntPtr deviceContext = graphics.GetHdc();

SetFontSmoothing(false);
thirdPartyComponent.Render(deviceContext);
SetFontSmoothing(true);
这显然对操作系统产生了可怕的影响,每次渲染图像时,其他应用程序都会从ClearType启用到禁用,然后再切换回来。 那么问题是,有没有人知道我如何为特定的DC更改字体呈现设置? 即使我只能使更改进程或线程特定而不影响整个操作系统,这也将是一个重要的步骤!(这将使我有机会将此呈现外包给单独的进程-无论如何,结果都将被写入磁盘) 编辑:我想补充一下,如果解决方案比几个API调用更复杂,我也不介意。即使涉及挂钩系统dll的解决方案,只要可以完成一天的工作,我也很高兴。 编辑:背景信息 第三方库使用大约70种颜色的调色板进行渲染。在图像(实际上是地图块)渲染到DC之后,我将每个像素从其32位颜色转换回其调色板索引,并将结果存储为8bpp灰度图像。这被上传到视频卡作为纹理。在渲染期间,我使用像素着色器在视频卡上重新应用调色板(也存储为纹理)。这使我能够立即在不需要重新生成所有所需的瓷砖的情况下在不同的调色板之间切换和淡入淡出。为典型的世界视图生成和上传所有瓦片需要花费10-60秒。 编辑:将GraphicsDevice重命名为Graphics 上一个版本中的GraphicsDevice类实际上是System.Drawing.Graphics。我已经重命名了它(使用GraphicsDevice = ...),因为涉及到的代码位于MyCompany.Graphics命名空间中,编译器无法正确解析它。 编辑:成功! 我甚至成功地将下面的PatchIat函数移植到C#中,并借助Marshal.GetFunctionPointerForDelegate的帮助。.NET互操作团队确实做得非常好!我现在正在使用以下语法,其中Patch是System.Diagnostics.ProcessModule的扩展方法:
module.Patch(
    "Gdi32.dll",
    "CreateFontIndirectA",
    (CreateFontIndirectA original) => font =>
    {
        font->lfQuality = NONANTIALIASED_QUALITY;
        return original(font);
    });

private unsafe delegate IntPtr CreateFontIndirectA(LOGFONTA* lplf);

private const int NONANTIALIASED_QUALITY = 3;

[StructLayout(LayoutKind.Sequential)]
private struct LOGFONTA
{
    public int lfHeight;
    public int lfWidth;
    public int lfEscapement;
    public int lfOrientation;
    public int lfWeight;
    public byte lfItalic;
    public byte lfUnderline;
    public byte lfStrikeOut;
    public byte lfCharSet;
    public byte lfOutPrecision;
    public byte lfClipPrecision;
    public byte lfQuality;
    public byte lfPitchAndFamily;
    public unsafe fixed sbyte lfFaceName [32];
}
4个回答

5
很遗憾,你不能。控制字体抗锯齿的能力是针对每种字体进行的。GDI调用CreateFontIndirect处理LOGFONT结构的成员,以确定是否允许使用ClearType、常规或无抗锯齿。
正如你所指出的,有系统范围的设置。不幸的是,如果你无法控制LOGFONT的内容,改变系统范围的设置几乎是降低DC字体渲染质量的唯一(记录在案的)方法。
这段代码不是我的。它是非托管的C语言。如果你知道它的HMODULE,它将钩取由dll或exe文件导入的任何函数。
#define PtrFromRva( base, rva ) ( ( ( PBYTE ) base ) + rva )

/*++
  Routine Description:
    Replace the function pointer in a module's IAT.

  Parameters:
    Module              - Module to use IAT from.
    ImportedModuleName  - Name of imported DLL from which
                          function is imported.
    ImportedProcName    - Name of imported function.
    AlternateProc       - Function to be written to IAT.
    OldProc             - Original function.

  Return Value:
    S_OK on success.
    (any HRESULT) on failure.
--*/
HRESULT PatchIat(
  __in HMODULE Module,
  __in PSTR ImportedModuleName,
  __in PSTR ImportedProcName,
  __in PVOID AlternateProc,
  __out_opt PVOID *OldProc
  )
{
  PIMAGE_DOS_HEADER DosHeader = ( PIMAGE_DOS_HEADER ) Module;
  PIMAGE_NT_HEADERS NtHeader;
  PIMAGE_IMPORT_DESCRIPTOR ImportDescriptor;
  UINT Index;

  assert( Module );
  assert( ImportedModuleName );
  assert( ImportedProcName );
  assert( AlternateProc );

  NtHeader = ( PIMAGE_NT_HEADERS )
    PtrFromRva( DosHeader, DosHeader->e_lfanew );
  if( IMAGE_NT_SIGNATURE != NtHeader->Signature )
  {
    return HRESULT_FROM_WIN32( ERROR_BAD_EXE_FORMAT );
  }

  ImportDescriptor = ( PIMAGE_IMPORT_DESCRIPTOR )
    PtrFromRva( DosHeader,
      NtHeader->OptionalHeader.DataDirectory
        [ IMAGE_DIRECTORY_ENTRY_IMPORT ].VirtualAddress );

  //
  // Iterate over import descriptors/DLLs.
  //
  for ( Index = 0;
        ImportDescriptor[ Index ].Characteristics != 0;
        Index++ )
  {
    PSTR dllName = ( PSTR )
      PtrFromRva( DosHeader, ImportDescriptor[ Index ].Name );

    if ( 0 == _strcmpi( dllName, ImportedModuleName ) )
    {
      //
      // This the DLL we are after.
      //
      PIMAGE_THUNK_DATA Thunk;
      PIMAGE_THUNK_DATA OrigThunk;

      if ( ! ImportDescriptor[ Index ].FirstThunk ||
         ! ImportDescriptor[ Index ].OriginalFirstThunk )
      {
        return E_INVALIDARG;
      }

      Thunk = ( PIMAGE_THUNK_DATA )
        PtrFromRva( DosHeader,
          ImportDescriptor[ Index ].FirstThunk );
      OrigThunk = ( PIMAGE_THUNK_DATA )
        PtrFromRva( DosHeader,
          ImportDescriptor[ Index ].OriginalFirstThunk );

      for ( ; OrigThunk->u1.Function != NULL;
              OrigThunk++, Thunk++ )
      {
        if ( OrigThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG )
        {
          //
          // Ordinal import - we can handle named imports
          // ony, so skip it.
          //
          continue;
        }

        PIMAGE_IMPORT_BY_NAME import = ( PIMAGE_IMPORT_BY_NAME )
          PtrFromRva( DosHeader, OrigThunk->u1.AddressOfData );

        if ( 0 == strcmp( ImportedProcName,
                              ( char* ) import->Name ) )
        {
          //
          // Proc found, patch it.
          //
          DWORD junk;
          MEMORY_BASIC_INFORMATION thunkMemInfo;

          //
          // Make page writable.
          //
          VirtualQuery(
            Thunk,
            &thunkMemInfo,
            sizeof( MEMORY_BASIC_INFORMATION ) );
          if ( ! VirtualProtect(
            thunkMemInfo.BaseAddress,
            thunkMemInfo.RegionSize,
            PAGE_EXECUTE_READWRITE,
            &thunkMemInfo.Protect ) )
          {
            return HRESULT_FROM_WIN32( GetLastError() );
          }

          //
          // Replace function pointers (non-atomically).
          //
          if ( OldProc )
          {
            *OldProc = ( PVOID ) ( DWORD_PTR )
                Thunk->u1.Function;
          }
#ifdef _WIN64
          Thunk->u1.Function = ( ULONGLONG ) ( DWORD_PTR )
              AlternateProc;
#else
          Thunk->u1.Function = ( DWORD ) ( DWORD_PTR )
              AlternateProc;
#endif
          //
          // Restore page protection.
          //
          if ( ! VirtualProtect(
            thunkMemInfo.BaseAddress,
            thunkMemInfo.RegionSize,
            thunkMemInfo.Protect,
            &junk ) )
          {
            return HRESULT_FROM_WIN32( GetLastError() );
          }

          return S_OK;
        }
      }

      //
      // Import not found.
      //
      return HRESULT_FROM_WIN32( ERROR_PROC_NOT_FOUND );
    }
  }

  //
  // DLL not found.
  //
  return HRESULT_FROM_WIN32( ERROR_MOD_NOT_FOUND );
}

您可以通过以下方式在代码中调用它(我没有检查它是否可以编译):

  1. Declare a pointer type to the funciton you want to hook:

    typedef FARPROC (WINAPI* PFNCreateFontIndirect)(LOGFONT*);
    
  2. Implement a hook function

    static PFNCreateFontIndirect OldCreateFontIndirect = NULL;
    
    WINAPI MyNewCreateFontIndirectCall(LOGFONT* plf)
    {
      // do stuff to plf (probably better to create a copy than tamper with passed in struct)
      // chain to old proc
      if(OldCreateFontIndirect)
        return OldCreateFontIndirect(plf);
    }
    
  3. Hook the function sometime during initialization

    HMODULE h = LoadLibrary(TEXT("OtherDll"));
    PatchIat(h, "USER32.DLL", "CreateFontIndirectW", MyNewCreateFontIndirectProc, (void**)&OldCreateFontIndirectProc);
    
当然,如果您要挂钩的模块存在于.NET环境中,那么很难确定CreateFontIndirect调用将从哪里发起。是mscoree.dll吗?还是您调用的实际模块?祝好运 :P

这基本上也是我得出的结论 :( 我希望有人能够用魔法解决我的问题。不知道是否可能钩取对CreateFontIndirect的调用,然后修改LOGFONT以使其没有反锯齿? - Jacob Stanley
当然是可能的。假设您正在处理需要挂钩的模块,并且您知道它的HMODULE句柄 - 它恰好是其基址,那么很容易通过打补丁导入地址表来挂钩API调用。 - Chris Becke
抱歉,我直到现在才看到你的编辑,我会在接下来的几天尝试一下。 - Jacob Stanley
非常成功!我使用的库是Win32 dll的.NET封装,所以很容易找到要修补的模块。为了其他开发人员的利益,CreateFontIndirect在Gdi32.dll中。 - Jacob Stanley

3
根据您的要求,我已经将我编写的解决此问题的代码打包并放置在github存储库中:http://github.com/jystic/patch-iat
看起来代码量很大,因为我不得不重现所有Win32结构,以使其正常工作,并且当时我选择将每个文件放在自己的文件中。
如果您想直接进入代码的主要部分,则在以下位置:ImportAddressTable.cs 它的许可非常自由,实际上是公共领域,因此请随意在任何您喜欢的项目中使用它。

如果这段代码解决了问题,你应该将自己的答案标记为被接受的答案。 - Justin Grant
@Justin,是的,说得好。从日期上可以看出,我直到一年后才上传了C#代码,所以当时没有想到这样做。我希望@Chris Becke不会因为答案被重新分配而失去声望,如果没有他的帮助,我永远无法解决这个问题。 - Jacob Stanley
干得好!请注意,这也适用于x64平台,但您需要稍微修改ImageOptionalHeader结构,因为_BaseOfData_字段在IMAGE_OPTIONAL_HEADER64中不存在。 - Poustic

0

GraphicsDevice类是第三方类吗?

我会这样做:

Graphics g = Graphics.FromImage(memImg);
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.None;

或者在你的情况下:

GraphicsDevice graphics = GraphicsDevice.FromImage(bitmap)
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.None;

如果GraphicsDevice类继承了Graphics类(否则尝试使用Graphics类)?


谢谢你的建议,我尝试了一下,但是看起来设置SmoothingMode只影响通过Graphics对象本身进行的呈现。因为第三方库直接使用GDI设备上下文,所以它会绕过这个设置。 - Jacob Stanley
实际上,您提供的代码甚至不能处理使用Graphics绘制的文本。对于文本,有一个单独的TextRenderingHint属性。 - Marco Mp

0

你的字体需要除了黑色和白色之外的更多颜色吗? 如果不需要,你可以将你的位图对象设置为每像素1位的图像(Format1bppIndexed?)。

系统可能无法对1bpp图像进行字体平滑渲染。


很不幸,我需要大约70种颜色 :( - Jacob Stanley
您能够创建一个索引为 8bpp 的位图对象,使用所请求的“预定义调色板”吗?字体呈现将被平滑处理,但至少会使用您想要的调色板... - Unbeknown
我实际上尝试过那个,一开始觉得效果很好,直到我切换到另一个调色板时,文本的反锯齿部分引用了新调色板中完全不同于周围颜色的颜色。不幸的是,我无法控制调色板,因为它们是由库提供的。 - Jacob Stanley
我猜你不能把所有的处理工作都放在一个不同的计算机上离线完成,这样你当前的技术就不会冒犯任何用户了? - Unbeknown
这是我可能会考虑的事情。但在线生成瓦片非常有用,因为它允许用户安装具有额外细节的新软件包,并打开或关闭图层。当进行这些类型的更改之一时,必须重新生成所有内容。 - Jacob Stanley

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接