Disable antialiasing for a specific GDI device context
Asked Answered
M

4

10

I'm using a third party library to render an image to a GDI DC and I need to ensure that any text is rendered without any smoothing/antialiasing so that I can convert the image to a predefined palette with indexed colors.

The third party library i'm using for rendering doesn't support this and just renders text as per the current windows settings for font rendering. They've also said that it's unlikely they'll add the ability to switch anti-aliasing off any time soon.

The best work around I've found so far is to call the third party library in this way (error handling and prior settings checks ommitted for brevity):

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);

This obviously has a horrible effect on the operating system, other applications flicker from cleartype enabled to disabled and back every time I render the image.

So the question is, does anyone know how I can alter the font rendering settings for a specific DC?

Even if I could just make the changes process or thread specific instead of affecting the whole operating system, that would be a big step forward! (That would give me the option of farming this rendering out to a separate process- the results are written to disk after rendering anyway)

EDIT: I'd like to add that I don't mind if the solution is more complex than just a few API calls. I'd even be happy with a solution that involved hooking system dlls if it was only about a days work.

EDIT: Background Information The third-party library renders using a palette of about 70 colors. After the image (which is actually a map tile) is rendered to the DC, I convert each pixel from it's 32-bit color back to it's palette index and store the result as an 8bpp greyscale image. This is uploaded to the video card as a texture. During rendering, I re-apply the palette (also stored as a texture) with a pixel shader executing on the video card. This allows me to switch and fade between different palettes instantaneously instead of needing to regenerate all the required tiles. It takes between 10-60 seconds to generate and upload all the tiles for a typical view of the world.

EDIT: Renamed GraphicsDevice to Graphics The class GraphicsDevice in the previous version of this question is actually System.Drawing.Graphics. I had renamed it (using GraphicsDevice = ...) because the code in question is in the namespace MyCompany.Graphics and the compiler wasn't able resolve it properly.

EDIT: Success! I even managed to port the PatchIat function below to C# with the help of Marshal.GetFunctionPointerForDelegate. The .NET interop team really did a fantastic job! I'm now using the following syntax, where Patch is an extension method on 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];
}
Mither answered 13/3, 2009 at 3:47 Comment(0)
M
3

As requested, I have packaged up the code I wrote to solve this problem and placed it in a github repository: http://github.com/jystic/patch-iat

It looks like a lot of code because I had to reproduce all the Win32 structures for this stuff to work, and at the time I chose to put each one in its own file.

If you want to go straight to the meat of of the code it's in: ImportAddressTable.cs

It's licensed very freely and is for all intents and purposes, public domain, so feel free to use it in any project that you like.

Mither answered 31/5, 2010 at 18:20 Comment(3)
if this code solved the problem, you should probably mark your own answer as the accepted answer.Osprey
@Justin, yeah good point. As you can see from the dates, I didn't put the C# code up until a year later, so I didn't think to do that at the time. I hope @Chris Becke doesn't lose rep for the answer being re-assigned, I never would have solved this problem without his help.Mither
Well done! Note that this also works on x64 platform but you need to modify the ImageOptionalHeader struct slightly as the BaseOfData field is not present in IMAGE_OPTIONAL_HEADER64.Dielu
C
5

Unfortunately you cant. The ability to control font anti aliasing is done per font. The GDI call CreateFontIndirect processes members of the LOGFONT struct to determine if its allowed to use cleartype, regular or no anti aliasing.

There are, as you noted, system wide settings. Unfortunately, changing the systemwide setting is pretty much the only (documented) way to downgrade the quality of font rendering on a DC if you cannot control the contents of the LOGFONT.


This code is not mine. Is unmanaged C. And will hook any function imported by a dll or exe file if you know its HMODULE.

#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 );
}

You would call this from your code by doing something like (I haven't checked that this in any way compiles :P):

  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);
    

Of course, if the module you are hooking exists in .NET land its very unclear as to where the CreateFontIndirect call is going to originate from. mscoree.dll? The actual module you call? Good luck I guess :P

Clougher answered 18/3, 2009 at 19:7 Comment(4)
This is pretty much the conclusion I came to as well :( I was hoping someone had some magic to solve my problem. I wonder if it's possible to hook the call to CreateFontIndirect and then modify LOGFONT so it has no anti-aliasing?Mither
It certainly is possible. Assuming that you are inprocess with the module that needs to be hooked, and you know its HMODULE handle - which happens to be its base address, its 'easy' to patch the Import Address Table to hook an api call.Clougher
Sorry, I missed your edit until now, I'll give this a try in the next few days.Mither
Worked a treat! The library I am using is a .NET wrapper of a Win32 dll, so it was easy to find the module to patch. For the benefit of other developers, CreateFontIndirect is in Gdi32.dll.Mither
M
3

As requested, I have packaged up the code I wrote to solve this problem and placed it in a github repository: http://github.com/jystic/patch-iat

It looks like a lot of code because I had to reproduce all the Win32 structures for this stuff to work, and at the time I chose to put each one in its own file.

If you want to go straight to the meat of of the code it's in: ImportAddressTable.cs

It's licensed very freely and is for all intents and purposes, public domain, so feel free to use it in any project that you like.

Mither answered 31/5, 2010 at 18:20 Comment(3)
if this code solved the problem, you should probably mark your own answer as the accepted answer.Osprey
@Justin, yeah good point. As you can see from the dates, I didn't put the C# code up until a year later, so I didn't think to do that at the time. I hope @Chris Becke doesn't lose rep for the answer being re-assigned, I never would have solved this problem without his help.Mither
Well done! Note that this also works on x64 platform but you need to modify the ImageOptionalHeader struct slightly as the BaseOfData field is not present in IMAGE_OPTIONAL_HEADER64.Dielu
S
0

Do you need more colours than black and white on your fonts? If not, you could make your bitmap object a 1 bit per pixel image (Format1bppIndexed?).

The system will probably not smooth font rendering on 1bpp images.

Stunner answered 13/3, 2009 at 12:20 Comment(5)
Unfortunately I need about ~70 colours :(Mither
Can you create the bitmap object as an indexed 8bpp image using the requested "predefined palette"? Font rendering will be smoothed but at least it would use the palette that you want...Stunner
I actually tried that, and I thought it was working great, until I switched to a different palette and the anti-aliased parts of the text referenced colours in the new palette which were totally different to the surrounds. Unfortunately I don't control the palettes, they're provided by the library.Mither
I suppose there is no change you could do all the processing off-line in a different computer where your current technique does not offend any users?Stunner
It's something that I might consider. But generating the tiles online is quite useful because it allows the users to install new packages with extra detail, and to switch layers on and off. Everything has to be regenerated when one of those type of changes is made.Mither
P
0

is the GraphicsDevice Class a 3rd party class?

the way i would do this is:

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

or in your case:

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

if the GraphicsDevice Class inherits the Graphics class (otherwise try using the Graphics class?)

Percy answered 17/3, 2009 at 1:42 Comment(2)
Thanks for the suggestion, I gave it a try, but it looks like setting the SmoothingMode only affects rendering done via the Graphics object itself. Because the third-party library uses the GDI device context directly, it bypasses this setting.Mither
Actually the code you provide doesn't even handle text drawn using Graphics itself. For text there is a separate TextRenderingHint property.Tuscan

© 2022 - 2024 — McMap. All rights reserved.