Отключить сглаживание для определенного контекста устройства GDI

Я использую стороннюю библиотеку для рендеринга изображения в 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?

Даже если бы я мог просто внести изменения в конкретный процесс или поток, а не затрагивать всю операционную систему, это было бы большим шагом вперед! (Это дало бы мне возможность передать этот рендеринг в отдельный процесс — результаты все равно записываются на диск после рендеринга)

EDIT: я хотел бы добавить, что я не возражаю, если решение будет более сложным, чем несколько вызовов API. Я был бы даже рад решению, которое включало бы перехват системных dll, если бы на это ушло всего несколько дней.

РЕДАКТИРОВАТЬ: справочная информация. Сторонняя библиотека выполняет рендеринг с использованием палитры примерно из 70 цветов. После того, как изображение (которое на самом деле является фрагментом карты) визуализируется в DC, я конвертирую каждый пиксель из его 32-битного цвета обратно в его индекс палитры и сохраняю результат как изображение в оттенках серого 8bpp. Это загружается на видеокарту в виде текстуры. Во время рендеринга я повторно применяю палитру (также сохраненную как текстуру) с пиксельным шейдером, выполняемым на видеокарте. Это позволяет мне мгновенно переключаться между разными палитрами и исчезать, вместо того чтобы заново генерировать все необходимые плитки. Создание и загрузка всех плиток для типичного представления мира занимает от 10 до 60 секунд.

EDIT: переименование GraphicsDevice в Graphics Класс GraphicsDevice в предыдущей версии этого вопроса на самом деле является System.Drawing.Graphics. Я переименовал его (используя GraphicsDevice = ...), потому что рассматриваемый код находится в пространстве имен MyCompany.Graphics, и компилятор не смог правильно его разрешить.

EDIT: успех! Мне даже удалось перенести функцию 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];
}

person Jacob Stanley    schedule 13.03.2009    source источник


Ответы (4)


К сожалению, вы не можете. Возможность управления сглаживанием шрифтов осуществляется для каждого шрифта. Вызов GDI CreateFontIndirect обрабатывает элементы структуры LOGFONT, чтобы определить, разрешено ли использовать четкий тип, обычное сглаживание или отсутствие сглаживания.

Есть, как вы заметили, общесистемные настройки. К сожалению, изменение общесистемной настройки — практически единственный (задокументированный) способ понизить качество рендеринга шрифтов на контроллере домена, если вы не можете контролировать содержимое LOGFONT.


Этот код не мой. Является неуправляемым C. И перехватит любую функцию, импортированную из файла dll или exe, если вы знаете его 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 );
}

Вы бы вызвали это из своего кода, выполнив что-то вроде (я не проверял, что это каким-либо образом компилируется: P):

  1. Объявите тип указателя на функцию, которую вы хотите перехватить:

    typedef FARPROC (WINAPI* PFNCreateFontIndirect)(LOGFONT*);
    
  2. Реализовать функцию ловушки

    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. Перехватите функцию во время инициализации

    HMODULE h = LoadLibrary(TEXT("OtherDll"));
    PatchIat(h, "USER32.DLL", "CreateFontIndirectW", MyNewCreateFontIndirectProc, (void**)&OldCreateFontIndirectProc);
    

Конечно, если модуль, который вы подключаете, существует в .NET, очень неясно, откуда будет исходить вызов CreateFontIndirect. mscoree.dll? Фактический модуль вы называете? Удачи, я думаю :P

person Chris Becke    schedule 18.03.2009
comment
Примерно к такому же выводу пришел и я :( Я надеялся, что кто-то применит магию, чтобы решить мою проблему. Интересно, можно ли перехватить вызов CreateFontIndirect, а затем изменить LOGFONT, чтобы в нем не было сглаживания? - person Jacob Stanley; 19.03.2009
comment
Это, безусловно, возможно. Предполагая, что вы находитесь в процессе с модулем, который необходимо подключить, и вы знаете его дескриптор HMODULE, который является его базовым адресом, «легко» исправить таблицу адресов импорта, чтобы перехватить вызов API. - person Chris Becke; 19.03.2009
comment
Извините, я пропустил ваше редактирование до сих пор, я попробую это в ближайшие несколько дней. - person Jacob Stanley; 25.03.2009
comment
Сработало на славу! Библиотека, которую я использую, представляет собой оболочку .NET библиотеки DLL Win32, поэтому было легко найти модуль для исправления. Для удобства других разработчиков CreateFontIndirect находится в Gdi32.dll. - person Jacob Stanley; 03.04.2009

В соответствии с просьбой я упаковал написанный мной код для решения этой проблемы и поместил его в репозиторий github: http://github.com/jystic/patch-iat

Это выглядит как много кода, потому что мне пришлось воспроизвести все структуры Win32, чтобы все это заработало, и в то время я решил поместить каждую в отдельный файл.

Если вы хотите перейти прямо к сути кода, он находится в: ImportAddressTable.cs

Он имеет очень бесплатную лицензию и во всех смыслах является общественным достоянием, поэтому не стесняйтесь использовать его в любом проекте, который вам нравится.

person Jacob Stanley    schedule 31.05.2010
comment
если этот код решил проблему, вам, вероятно, следует пометить свой ответ как принятый ответ. - person Justin Grant; 25.03.2011
comment
@ Джастин, да, хорошая мысль. Как вы можете видеть по датам, я добавил код C# только через год, поэтому в то время я не думал об этом. Я надеюсь, что @Chris Becke не потеряет репутацию из-за переназначения ответа, я бы никогда не решил эту проблему без его помощи. - person Jacob Stanley; 25.03.2011
comment
Отличная работа! Обратите внимание, что это также работает на платформе x64, но вам нужно немного изменить структуру ImageOptionalHeader, так как поле BaseOfData отсутствует в IMAGE_OPTIONAL_HEADER64. - person Poustic; 19.02.2015

Вам нужно больше цветов, чем черно-белые шрифты? Если нет, вы можете сделать свой объект bitmap изображением с разрешением 1 бит на пиксель (Format1bppIndexed?).

Система, вероятно, не будет плавно отображать шрифты на изображениях 1bpp.

person Unbeknown    schedule 13.03.2009
comment
К сожалению, мне нужно около 70 цветов :( - person Jacob Stanley; 13.03.2009
comment
Можете ли вы создать растровый объект как индексированное изображение 8bpp, используя запрошенную предопределенную палитру? Отрисовка шрифта будет сглажена, но, по крайней мере, будет использоваться та палитра, которую вы хотите... - person Unbeknown; 13.03.2009
comment
Я на самом деле пробовал это, и я думал, что это работает отлично, пока я не переключился на другую палитру, и сглаженные части текста ссылались на цвета в новой палитре, которые полностью отличались от окружения. К сожалению, я не контролирую палитры, они предоставляются библиотекой. - person Jacob Stanley; 13.03.2009
comment
Я полагаю, нет никаких изменений, вы могли бы выполнять всю обработку в автономном режиме на другом компьютере, где ваша текущая техника не оскорбляет пользователей? - person Unbeknown; 13.03.2009
comment
Это то, что я мог бы рассмотреть. Но генерировать плитки онлайн весьма полезно, потому что это позволяет пользователям устанавливать новые пакеты с дополнительной детализацией, а также включать и выключать слои. Все должно быть перегенерировано, когда делается одно из таких изменений. - person Jacob Stanley; 13.03.2009

Является ли класс 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?)

person dkarzon    schedule 17.03.2009
comment
Спасибо за предложение, я попробовал, но похоже, что настройка SmoothingMode влияет только на рендеринг, выполняемый через сам объект Graphics. Поскольку сторонняя библиотека напрямую использует контекст устройства GDI, она игнорирует этот параметр. - person Jacob Stanley; 17.03.2009
comment
На самом деле код, который вы предоставляете, даже не обрабатывает текст, нарисованный с помощью самой графики. Для текста есть отдельное свойство TextRenderingHint. - person Marco Mp; 23.12.2013