다음을 통해 공유


글꼴 선택

IDWriteFontSet4 인터페이스는 글꼴 집합에서 글꼴을 선택하는 메서드를 노출합니다. 이러한 메서드를 사용하면 기존 애플리케이션, 문서 및 글꼴과의 호환성을 유지하면서 입력 글꼴 패밀리 모델로 전환할 수 있습니다.

글꼴 선택(글꼴 일치 또는 글꼴 매핑이라고도 함)은 애플리케이션에서 전달한 입력 매개 변수와 가장 일치하는 사용 가능한 글꼴을 선택하는 프로세스입니다. 입력 매개 변수를 논리적 글꼴로 통칭하는 경우도 있습니다. 논리 글꼴에는 글꼴 패밀리 이름과 패밀리 내의 특정 글꼴을 나타내는 다른 특성이 포함됩니다. 글꼴 선택 알고리즘은 논리 글꼴("원하는 글꼴")과 사용 가능한 실제 글꼴 ("있는 글꼴")과 일치합니다.

글꼴 패밀리는 일반적인 디자인을 공유하지만 가중치와 같은 특성이 다를 수 있는 명명된 글꼴 그룹입니다. 글꼴 패밀리 모델은 패밀리 내에서 글꼴을 구분하는 데 사용할 수 있는 특성을 정의합니다. 새 입력 글꼴 패밀리 모델은 Windows에서 사용된 두 개의 이전 글꼴 패밀리 모델에 비해 많은 이점이 있습니다. 그러나 글꼴 패밀리 모델을 변경하면 혼동과 호환성 문제가 발생할 수 있습니다. IDWriteFontSet4 인터페이스에서 노출하는 메서드는 호환성 문제를 완화하면서 입력 글꼴 패밀리 모델의 이점을 제공하는 하이브리드 접근 방식을 구현합니다.

이 항목에서는 이전 글꼴 패밀리 모델을 입력 글꼴 패밀리 모델과 비교합니다. 글꼴 패밀리 모델을 변경하여 제기되는 호환성 문제를 설명합니다. 마지막으로 [IDWriteFontSet4](/windows/win32/api/dwrite_3/nn-dwrite_3-idwritefontset4) 메서드를 사용하여 이러한 문제를 해결할 수 있는 방법을 설명합니다.

RBIZ 글꼴 패밀리 모델

GDI 애플리케이션 에코시스템에서 사용되는 사실상 글꼴 패밀리 모델을 "4 글꼴 모델" 또는 "RBIZ" 모델이라고도 합니다. 이 모델의 각 글꼴 패밀리에는 일반적으로 최대 4개의 글꼴이 있습니다. "RBIZ" 레이블은 일부 글꼴 파일에 사용되는 명명 규칙에서 제공됩니다. 예를 들면 다음과 같습니다.

파일 이름 글꼴 스타일
verdana.ttf Regular
verdanab.ttf 굵게
verdanai.ttf 기울임꼴
verdanaz.ttf 굵은 기울임꼴

GDI를 사용하면 글꼴을 선택하는 데 사용되는 입력 매개 변수는 가족 이름(), 가중치() 및 기울임꼴(lfFaceNamelfWeightlfItalic) 필드를 포함하는 LOGFONT 구조로 정의됩니다. lfItalic 필드는 TRUE 또는 FALSE입니다. GDI를 lfWeight 사용하면 필드가 FW_THIN (100)에서 FW_BLACK (900) 범위의 모든 값이 될 수 있지만, 기록적인 이유로 글꼴은 오랫동안 설계되어 동일한 GDI 글꼴 패밀리에 가중치가 두 개 이상 없도록 설계되었습니다.

초기에 널리 사용되는 애플리케이션 사용자 인터페이스에는 기울임꼴 단추(기울임꼴 켜기 및 끄기) 및 굵은 단추(일반 및 굵은 가중치 간 전환)가 포함되었습니다. 이러한 두 단추를 사용하여 패밀리 내에서 글꼴을 선택하는 경우 RBIZ 모델이 있다고 가정합니다. 따라서 GDI 자체가 두 개 이상의 가중치를 지원하더라도 애플리케이션 호환성으로 인해 글꼴 개발자는 GDI 패밀리 이름(OpenType 이름 ID 1)을 RBIZ 모델과 일치하는 방식으로 설정했습니다.

예를 들어 Arial 글꼴 패밀리에 더 무거운 "검정" 가중치를 추가하려는 경우를 가정해 보겠습니다. 논리적으로 이 글꼴은 Arial 패밀리의 일부이므로 "Arial"lfWeight으로 설정하고 lfFaceNameFW_BLACK 설정하여 선택할 수 있습니다. 그러나 애플리케이션 사용자가 2개 상태 굵게 단추를 사용하여 세 가지 가중치 중에서 선택할 수 있는 방법은 없습니다. 해결 방법은 새 글꼴에 다른 패밀리 이름을 지정하여 사용자가 글꼴 패밀리 목록에서 "Arial Black"을 선택하여 선택할 수 있도록 하는 것이었습니다. 마찬가지로 굵고 기울임꼴 단추만 사용하여 동일한 글꼴 패밀리의 여러 너비 중에서 선택할 수 있는 방법이 없으므로 좁은 버전의 Arial은 RBIZ 모델에서 서로 다른 패밀리 이름을 갖습니다. 따라서 RBIZ 모델에는 "Arial", "Arial Black", "Arial Narrow" 글꼴 familes가 있습니다.

이러한 예제에서는 글꼴 패밀리 모델의 제한 사항이 글꼴을 패밀리로 그룹화되는 방식에 어떤 영향을 줄 수 있는지 확인할 수 있습니다. 글꼴 패밀리는 이름으로 식별되므로 사용하는 글꼴 패밀리 모델에 따라 동일한 글꼴의 패밀리 이름이 다를 수 있습니다.

DirectWrite RBIZ 글꼴 패밀리 모델을 직접 지원하지는 않지만, IDWriteGdiInterop::CreateFontFromLOGFONT 및 IDWriteGdiInterop::ConvertFontToLOGFONT와 같은 RBIZ 모델로 변환하는 메서드를 제공합니다. IDWriteFont::GetInformationalStrings 메서드를 호출하고 DWRITE_INFORMATIONAL_STRING_WIN32_FAMILY_NAMES 지정하여 글꼴의 RBIZ 패밀리 이름을 가져올 수도 있습니다.

가중치 스트레치 스타일 글꼴 패밀리 모델

가중치 스트레치 스타일의 글꼴 패밀리 모델은 입력 글꼴 패밀리 모델이 도입되기 전에 DirectWrite 사용하는 원래 글꼴 패밀리 모델입니다. WWS(Weight-width-slope)라고도 합니다. WWS 모델에서 동일한 패밀리 내의 글꼴은 가중치(DWRITE_FONT_WEIGHT), 스트레치(DWRITE_FONT_STRETCH) 및 스타일(DWRITE_FONT_STYLE)의 세 가지 속성으로 다를 수 있습니다.

WWS 모델은 두 가지 방법으로 RBIZ 모델보다 더 유연합니다. 첫째, 동일한 패밀리의 글꼴은 늘이기(또는 너비)와 두께 및 스타일(일반, 기울임꼴 또는 경사)으로 구분할 수 있습니다. 둘째, 동일한 가족에 두 개 이상의 가중치가 있을 수 있습니다. 이러한 유연성은 Arial의 모든 변형을 동일한 WWS 제품군에 포함할 수 있도록 하기에 충분합니다. 다음 표에서는 RIZ 및 WWS 글꼴 속성을 비교하여 선택한 Arial 글꼴을 비교합니다.

전체 이름 RBIZ 가족 이름 lfWeight lfItalic WWS FamilyName 무게 Stretch 스타일
Arial Arial 400 0 Arial 400 5 0
Arial 굵게 Arial 700 0 Arial 700 5 0
Arial 검정 Arial 검정 900 0 Arial 900 5 0
Arial Narrow Arial Narrow 400 0 Arial 400 3 0
Arial 좁고 굵게 Arial Narrow 700 0 Arial 700 3 0

보듯이 "Arial Narrow"는 "Arial"과 같 lfWeightlfItalic 값이 같으므로 모호성을 방지하기 위해 다른 RBIZ 패밀리 이름이 있습니다. "Arial Black"은 "Arial" 가족에서 두 개 이상의 가중치를 사용하지 않도록 다른 RBIZ 가족 이름을 가지고 있습니다. 반면, 이러한 모든 글꼴은 동일한 가중치 스트레치 스타일 패밀리에 있습니다.

그럼에도 불구하고 가중치 스트레치 스타일 모델은 개방형이 아닙니다. 두 글꼴의 두께, 늘이기 및 스타일이 같지만 다른 방식(예: 광학 크기)이 다른 경우 동일한 WWS 글꼴 패밀리에 포함할 수 없습니다. 그러면 입력 글꼴 패밀리 모델이 표시됩니다.

입력 글꼴 패밀리 모델

선행 모델과 달리 입력 글꼴 패밀리 모델은 개방형 입니다 . 글꼴 패밀리 내에서 다양한 변형 축을 지원합니다.

글꼴 선택 매개 변수를 디자인 공간에서 좌표로 생각하는 경우 가중치 스트레치 스타일 모델은 두께, 스트레치 및 스타일을 축으로 사용하는 3차원 좌표계를 정의합니다. WWS 패밀리의 각 글꼴에는 해당 세 축을 따라 좌표로 정의된 고유한 위치가 있어야 합니다. 글꼴을 선택하려면 WWS 패밀리 이름과 가중치, 늘이기 및 스타일 매개 변수를 지정합니다.

반면, 입력 글꼴 패밀리 모델에는 N차원 디자인 공간이 있습니다. 글꼴 디자이너는 각각 4자 축 태그로 식별되는 다양한 디자인 축을 정의할 수 있습니다. N차원 디자인 공간에서 지정된 글꼴의 위치는 각 축 값이 축 태그와 부동 소수점 값으로 구성된 축 값의 배열에 의해 정의됩니다. 글꼴을 선택하려면 입력 체계 패밀리 이름과 축 값 배열(DWRITE_FONT_AXIS_VALUE 구조체)을 지정합니다.

글꼴 축 수는 개방형이지만 표준 의미가 있는 등록된 축이 몇 개 있으며 가중치, 늘이기 및 스타일 값을 등록된 축 값에 매핑할 수 있습니다. DWRITE_FONT_WEIGHT "wght"(DWRITE_FONT_AXIS_TAG_WEIGHT) 축 값에 매핑할 수 있습니다. DWRITE_FONT_STRETCH "wdth"(DWRITE_FONT_AXIS_TAG_WIDTH) 축 값에 매핑할 수 있습니다. DWRITE_FONT_STYLE "ital" 및 "slnt"(DWRITE_FONT_AXIS_TAG_ITALICDWRITE_FONT_AXIS_TAG_SLANT) 축 값의 조합에 매핑할 수 있습니다.

등록된 또 다른 축은 "opsz"(DWRITE_FONT_AXIS_TAG_OPTICAL_SIZE)입니다. Sitka와 같은 광학 글꼴 패밀리에는 "opsz" 축에 따라 다른 글꼴이 포함되어 있으므로 다양한 지점 크기로 사용하도록 설계되었습니다. WWS 글꼴 패밀리 모델에는 광학 크기 축이 없으므로 Sitka 글꼴 패밀리는 "Sitka Small", "Sitka Text", "Sitka Subheading" 등 여러 WWS 글꼴 패밀리로 분할되어야 합니다. 각 WWS 글꼴 패밀리는 다른 광학 크기에 해당하며, 지정된 글꼴 크기에 대해 오른쪽 WWS 패밀리 이름을 지정하는 것은 사용자에게 남아 있습니다. 입력 글꼴 패밀리 모델을 사용하면 사용자는 단순히 "Sitka"를 선택할 수 있으며 애플리케이션은 글꼴 크기에 따라 "opsz" 축 값을 자동으로 설정할 수 있습니다.

입력 글꼴 선택 및 가변 글꼴

변형 축의 개념은 종종 가변 글꼴과 연결되지만 정적 글꼴에도 적용됩니다. OpenType STAT(스타일 특성) 테이블은 글꼴에 있는 디자인 축과 해당 축의 값을 선언합니다. 이 표는 가변 글꼴에 필요하지만 정적 글꼴과도 관련이 있습니다.

DirectWrite API는 STAT 테이블에 없거나 STAT 테이블이 없는 경우에도 모든 글꼴에 대해 "wght", "wdth", "ital" 및 "slnt" 축 값을 노출합니다. 이러한 값은 가능하면 STAT 테이블에서 파생됩니다. 그렇지 않으면 글꼴 두께, 글꼴 늘이기 및 글꼴 스타일에서 파생됩니다.

글꼴 축은 변수이거나 변수가 아닐 수 있습니다. 정적 글꼴에는 변수가 없는 축만 있는 반면, 변수 글꼴에는 둘 다 있을 수 있습니다. 변수 글꼴을 사용하려면 모든 변수 축이 특정 값에 바인딩된 변수 글꼴 instance 만들어야 합니다. IDWriteFontFace 인터페이스는 정적 글꼴 또는 변수 글꼴의 특정 instance 나타냅니다. 지정된 축 값을 사용하여 변수 글꼴의 임의 instance 만들 수 있습니다. 또한 변수 글꼴은 축 값의 미리 정의된 조합을 사용하여 STAT 테이블에서 명명된 인스턴스를 선언할 수 있습니다. 명명된 인스턴스를 사용하면 변수 글꼴이 정적 글꼴 컬렉션처럼 동작할 수 있습니다. IDWriteFontFamily 또는 IDWriteFontSet의 요소를 열거하는 경우 각 정적 글꼴과 명명된 각 변수 글꼴 instance 대해 하나의 요소가 있습니다.

입력 글꼴 일치 알고리즘은 먼저 패밀리 이름에 따라 잠재적 일치 후보를 선택합니다. 일치 후보에게 변수 글꼴이 포함된 경우 동일한 변수 글꼴에 대한 모든 일치 후보가 하나의 일치 후보로 축소되어 각 변수 축에 대해 요청된 값에 최대한 가까운 특정 값이 할당됩니다. 변수 축에 대해 요청된 값이 없으면 해당 축의 기본값이 할당됩니다. 그런 다음, 일치 후보의 순서는 해당 축 값과 요청된 축 값을 비교하여 결정됩니다.

예를 들어 Windows의 Sitka 입력 체계 패밀리를 고려해 보세요. Sitka는 광학 글꼴 패밀리로, "opsz" 축이 있습니다. Windows 11 Sitka는 다음 축 값을 가진 두 개의 변수 글꼴로 구현됩니다. opszwght 축은 가변적이고 다른 축은 변수가 아닌 축입니다.

파일 이름 "opsz" "wght" "wdth" "ital" "slnt"
SitkaVF.ttf 6-27.5 400-700 100 0 0
SitkaVF-Italic.ttf 6-27.5 400-700 100 1 -12

요청된 축 값이 이라고 가정합니다 opsz:12 wght:475 wdth:100 ital:0 slnt:0. 각 변수 글꼴에 대해 각 변수 축에 특정 값이 할당되는 변수 글꼴 instance 대한 참조를 만듭니다. 즉, opszwght 축은 각각 및 47512 설정됩니다. 이렇게 하면 다음과 일치하는 글꼴이 생성되며, 기울임꼴이 아닌 글꼴이 및 slnt 축에 더 적합하기 때문에 순위가 ital 1위를 차지합니다.

SitkaVF.ttf opsz:12 wght:475 wdth:100 ital:0 slnt0
SitkaVF-Italic.ttf opsz:12 wght:475 wdth:100 ital:1 slnt:-12

위의 예제에서 일치하는 글꼴은 임의 변수 글꼴 인스턴스입니다. 무게가 475인 Sitka의 명명된 instance 없습니다. 반면 가중치 스트레치 스타일 일치 알고리즘은 명명된 인스턴스만 반환합니다.

글꼴 일치 순서

가중치 스트레치 스타일 글꼴 패밀리 모델(IDWriteFontFamily::GetMatchingFonts) 및 입력 글꼴 패밀리 모델(IDWriteFontCollection2::GetMatchingFonts)에 대해 다른 오버로드된 GetMatchingFonts 메서드가 있습니다. 두 경우 모두 출력은 각 후보 글꼴이 입력 속성과 얼마나 잘 일치하는지에 따라 우선 순위의 내림차순으로 일치하는 글꼴 목록입니다. 이 섹션에서는 우선 순위가 결정되는 방법을 설명합니다.

가중치 스트레치 스타일 모델에서 입력 매개 변수는 글꼴 두께(DWRITE_FONT_WEIGHT), 글꼴 스트레치(DWRITE_FONT_STRETCH) 및 글꼴 스타일(DWRITE_FONT_STYLE)입니다. 가장 일치하는 항목을 찾기 위한 알고리즘은 2006년 미하일 레오노프와 데이비드 브라운의 "WPF 글꼴 선택 모델"이라는 제목의 백서에 설명되어 있습니다. "후보 얼굴 목록에서 얼굴 일치" 섹션을 참조하세요. 이 논문은 WPF(Windows Presentation Foundation)에 관한 것이었지만 DirectWrite 나중에 동일한 접근 방식을 사용했습니다.

알고리즘은 다음과 같이 가중치, 늘이기 및 스타일의 지정된 조합에 대해 계산되는 글꼴 특성 벡터의 개념을 사용합니다.

FontAttributeVector.X = (stretch - 5) * 1100;
FontAttributeVector.Y = style * 700;
FontAttributeVector.Z = (weight - 400) * 5;

각 벡터 좌표는 해당 특성의 "normal" 값을 빼고 상수를 곱하여 정규화됩니다. 승수는 가중치, 늘이기 및 스타일에 대한 입력 값의 범위가 매우 다르다는 사실을 보완합니다. 그렇지 않으면 가중치(100..999)가 스타일(0..2)을 지배합니다.

각 일치 후보에 대해 일치 후보의 글꼴 특성 벡터와 입력 글꼴 특성 벡터 간에 벡터 거리와 점 제품이 계산됩니다. 두 개의 일치 후보를 비교할 때 벡터 거리가 작은 후보가 더 나은 일치 항목입니다. 거리가 같으면 점이 작은 후보가 더 잘 일치합니다. 점 제품도 동일한 경우 X, Y 및 Z 축을 따라의 거리가 해당 순서로 비교됩니다.

거리를 비교하는 것은 충분히 직관적이지만 점 제품을 보조 측정값으로 사용하려면 몇 가지 설명이 필요할 수 있습니다. 입력 가중치가 세미볼드(600)이고 두 후보 가중치가 검은색(900)과 반자율(300)이라고 가정합니다. 입력 가중치로부터의 각 후보 가중치의 거리는 동일하지만 검정 두께는 원점(즉, 400 또는 보통)과 같은 방향이므로 더 작은 점 제품이 있습니다.

입력 체계 일치 알고리즘은 가중치 스트레치 스타일의 일반화입니다. 각 축 값은 N차원 글꼴 특성 벡터에서 좌표로 처리됩니다. 각 일치 후보에 대해 일치 후보의 글꼴 특성 벡터와 입력 글꼴 특성 벡터 간에 벡터 거리와 점 제품이 계산됩니다. 벡터 거리가 더 작은 후보는 더 나은 일치입니다. 거리가 같으면 점이 작은 후보가 더 잘 일치합니다. 점 제품도 동일한 경우 지정된 가중치 스트레치 스타일 패밀리의 현재 상태를 타이 브레이커로 사용할 수 있습니다.

벡터 거리 및 점 제품을 계산하려면 일치 후보의 글꼴 특성 벡터와 입력 글꼴 특성 벡터에 동일한 축이 있어야 합니다. 따라서 두 벡터의 누락된 축 값은 해당 축의 표준 값을 대체하여 채워집니다. 벡터 좌표는 해당 축의 표준(또는 "normal") 값을 빼고 결과를 축별 승수로 곱하여 정규화됩니다. 다음은 각 축의 승수 및 표준 값입니다.

승수 표준 값
"wght" 5 400
"wdth" 55 100
"ital" 1400 0
"slnt" 35 0
"opsz" 1 12
other 1 0

승수는 가중치 스트레치 스타일 알고리즘에서 사용하는 값과 일치하지만 필요에 따라 크기가 조정됩니다. 예를 들어 일반 너비는 100이며 스트레치 5와 같습니다. 55와 1100의 승수를 생성합니다. 레거시 스타일 특성(0..2)은 기울임꼴 및 경사를 얽습니다. 이 특성은 입력 모델에서 "기울임꼴" 축(0..1) 및 "slnt" 축(-90..90)으로 분해됩니다. 이러한 두 축에 대해 선택한 승수는 오블리크 글꼴의 기본 20도 기울기를 가정하는 경우 레거시 알고리즘에 해당하는 결과를 제공합니다.

입력 글꼴 선택 및 광학 크기

입력 글꼴 패밀리 모델을 사용하는 애플리케이션은 축 값을 글꼴 선택 매개 변수로 지정하여 opsz 광학 크기 조정을 구현할 수 있습니다. 예를 들어, 워드 프로세싱 애플리케이션은 포인트의 opsz 글꼴 크기와 동일한 축 값을 지정할 수 있습니다. 이 경우 사용자는 글꼴 패밀리로 "Sitka"를 선택할 수 있으며 애플리케이션은 올바른 opsz 축 값으로 Sitka의 instance 자동으로 선택합니다. WWS 모델에서 각 광학 크기는 다른 패밀리 이름으로 노출되며 사용자가 올바른 이름을 선택해야 합니다.

이론적으로는 글꼴 선택 축 값을 별도의 단계로 재정의하여 opsz 가중치 스트레치 스타일 모델에서 자동 광학 크기 조정을 구현할 수 있습니다. 그러나 일치하는 첫 번째 글꼴이 변수 축이 있는 가변 글꼴인 opsz 경우에만 작동합니다. 글꼴 선택 매개 변수로 지정하면 opsz 정적 글꼴에도 동일하게 작동합니다. 예를 들어 Sitka 글꼴 패밀리는 Windows 11 가변 글꼴로 구현되지만 Windows 10 정적 글꼴 컬렉션으로 구현됩니다. 정적 글꼴에는 서로 다른 겹치지 않는 opsz 축 범위가 있습니다(글꼴 선택 목적으로 범위로 선언되지만 변수 축은 아님). opsz 글꼴 선택 매개 변수로 지정하면 광학 크기가 올바른 정적 글꼴을 선택할 수 있습니다.

입력 글꼴 선택 이점 및 호환성 문제

입력 글꼴 선택 모델은 이전 모델에 비해 몇 가지 장점이 있지만 순수한 형식에서는 호환성 문제가 발생할 수 있습니다. 이 섹션에서는 이점 및 호환성 문제에 대해 설명합니다. 다음 섹션에서는 호환성 문제를 완화하면서 장점을 유지하는 하이브리드 글꼴 선택 모델에 대해 설명합니다.

입력 글꼴 패밀리 모델의 장점은 다음과 같습니다.

  • 글꼴 패밀리 모델의 제한으로 인해 하위 파일로 분할되는 대신 디자이너가 의도한 대로 글꼴을 패밀리로 그룹화할 수 있습니다.

  • 애플리케이션은 사용자에게 다양한 광학 크기를 다른 글꼴 패밀리로 노출하는 대신 글꼴 크기에 따라 올바른 opsz 축 값을 자동으로 선택할 수 있습니다.

  • 변수 글꼴의 임의 인스턴스를 선택할 수 있습니다. 예를 들어 변수 글꼴이 연속 범위 100-900의 가중치를 지원하는 경우 입력 체계 모델은 이 범위의 모든 가중치를 선택할 수 있습니다. 이전 글꼴 패밀리 모델은 글꼴로 정의된 명명된 인스턴스 중에서 가장 가까운 가중치만 선택할 수 있습니다.

입력 글꼴 선택 모델과의 호환성 문제는 다음과 같습니다.

  • 일부 이전 글꼴은 입력 체계 패밀리 이름 및 축 값만 사용하여 명확하게 선택할 수 없습니다.

  • 기존 문서는 WWS 패밀리 이름 또는 RBIZ 패밀리 이름으로 글꼴을 참조할 수 있습니다. 사용자는 WWS 및 RBIZ 패밀리 이름을 사용할 수도 있습니다. 예를 들어 문서에서는 "Sitka" 대신 "Sitka 하위 머리글"(WWS 패밀리 이름)을 지정할 수 있습니다(오타학적 패밀리 이름).

  • 라이브러리 또는 프레임워크는 자동 광학 크기 조정을 활용하기 위해 입력 글꼴 패밀리 모델을 채택할 수 있지만 임의의 축 값을 지정하기 위한 API는 제공하지 않습니다. 새 API가 제공되더라도 프레임워크는 가중치, 스트레치 및 스타일 매개 변수만 지정하는 기존 애플리케이션에서 작업해야 할 수 있습니다.

이전 글꼴과의 호환성 문제는 타이포그래피 패밀리 이름의 개념이 OpenType 1.8의 가변 글꼴과 함께 도입된 글꼴 축 값의 개념보다 앞당기기 때문에 발생합니다. OpenType 1.8 이전에는 타이포그래피 패밀리 이름이 단지 글꼴 집합이 관련되어 있다는 디자이너의 의도를 표현했을 뿐, 해당 글꼴이 속성에 따라 프로그래밍 방식으로 구분될 수 있다는 보장은 없습니다. 가상의 예제로 다음 글꼴에 모두 입력 체계 패밀리 이름 "레거시"가 있다고 가정합니다.

전체 이름 WWS 제품군 무게 Stretch 스타일 오타 패밀리 wght wdth ital slnt
레거시 레거시 400 5 0 레거시 400 100 0 0
레거시 굵게 레거시 700 5 0 레거시 700 100 0 0
레거시 블랙 레거시 900 5 0 레거시 900 100 0 0
레거시 소프트 레거시 소프트 400 5 0 레거시 400 100 0 0
레거시 소프트 굵게 레거시 소프트 700 5 0 레거시 700 100 0 0
레거시 소프트 블랙 레거시 소프트 900 5 0 레거시 900 100 0 0

"레거시" 입력 패밀리에는 3개의 가중치가 있으며 각 가중치에는 일반 및 "소프트" 변형이 있습니다. 새 글꼴인 경우 SOFT 디자인 축을 선언하는 것으로 구현할 수 있습니다. 그러나 이러한 글꼴은 OpenType 1.8보다 이전이므로 유일한 디자인 축은 두께, 늘이기 및 스타일에서 파생된 축입니다. 각 두께에 대해 이 글꼴 패밀리에는 동일한 축 값이 있는 두 개의 글꼴이 있으므로 축 값만으로는 이 패밀리에서 글꼴을 명확하게 선택할 수 없습니다.

하이브리드 글꼴 선택 알고리즘

다음 섹션에서 설명하는 글꼴 선택 API는 호환성 문제를 완화하면서 입력 글꼴 선택의 이점을 유지하는 하이브리드 글꼴 선택 알고리즘을 사용합니다.

하이브리드 글꼴 선택은 글꼴 두께, 글꼴 늘이기 및 글꼴 스타일 값을 해당 글꼴 축 값에 매핑할 수 있도록 하여 이전 글꼴 패밀리 모델의 브리지를 제공합니다. 이를 통해 문서 및 애플리케이션 호환성 문제를 해결할 수 있습니다.

또한 하이브리드 글꼴 선택 알고리즘을 사용하면 지정된 패밀리 이름이 입력 체계 패밀리 이름, 가중치 스트레치 스타일 패밀리 이름, GDI/RBIZ 패밀리 이름 또는 전체 글꼴 이름이 될 수 있습니다. 일치는 우선 순위의 내림차순으로 다음 방법 중 하나로 발생합니다.

  1. 이름은 입력 체계 패밀리(예: Sitka)와 일치합니다. 일치는 입력 패밀리 내에서 발생하며 요청된 모든 축 값이 사용됩니다. 이름이 WWS 하위 패밀리(즉, 입력 패밀리보다 작음)와 일치하는 경우 WWS 하위 패밀리의 멤버 자격이 타이 브레이커로 사용됩니다.

  2. 이름은 WWS 패밀리와 일치합니다(예: Sitka Text). 일치는 WWS 패밀리 내에서 발생하며 "wght", "wdth", "ital" 및 "slnt" 이외의 요청된 축 값은 무시됩니다.

  3. 이름은 GDI 패밀리(예: Bahnschrift Condensed)와 일치합니다. 일치는 RBIZ 패밀리 내에서 발생하며 "wght", "ital" 및 "slnt" 이외의 요청된 축 값은 무시됩니다.

  4. 이름은 전체 이름(예: Bahnschrift Bold Condensed)과 일치합니다. 전체 이름과 일치하는 글꼴이 반환됩니다. 요청된 축 값은 무시됩니다. GDI가 지원하므로 전체 글꼴 이름으로 일치할 수 있습니다.

이전 섹션에서는 "레거시"라는 모호한 입력 체계 패밀리를 설명했습니다. 하이브리드 알고리즘을 사용하면 "레거시" 또는 "레거시 소프트"를 패밀리 이름으로 지정하여 모호성을 방지할 수 있습니다. "레거시 소프트"를 지정하면 WWS 제품군 내에서만 일치가 발생하므로 모호성이 없습니다. "레거시"가 지정되면 입력 체계 패밀리의 모든 글꼴이 일치 후보로 간주되지만 "레거시" WWS 제품군의 멤버 자격을 타이 브레이커로 사용하여 모호성을 방지할 수 있습니다.

문서에서 패밀리 이름 및 가중치, 늘이기 및 스타일 매개 변수를 지정하지만 축 값은 지정하지 않는다고 가정합니다. 애플리케이션은 먼저 IDWriteFontSet4::ConvertWeightStretchStyleToFontAxisValues를 호출하여 가중치, 늘이기, 스타일 및 글꼴 크기를 축 값으로 변환할 수 있습니다. 그런 다음 애플리케이션은 패밀리 이름과 축 값을 모두 IDWriteFontSet4::GetMatchingFonts에 전달할 수 있습니다. GetMatchingFonts 는 일치하는 글꼴 목록을 우선 순위순으로 반환하며, 지정된 패밀리 이름이 입력 체계 패밀리 이름, 가중치 스트레치 스타일 패밀리 이름, RBIZ 패밀리 이름 또는 전체 이름인지에 따라 결과가 적절합니다. 지정된 패밀리에 "opsz" 축이 있는 경우 글꼴 크기에 따라 적절한 광학 크기가 자동으로 선택됩니다.

문서에서 가중치, 늘이기 및 스타일을 지정하고 축 값 지정한다고 가정합니다. 이 경우 명시적 축 값을 IDWriteFontSet4::ConvertWeightStretchStyleToFontAxisValues에 전달할 수도 있으며 메서드에서 반환된 파생 축 값에는 명시적으로 지정되지 않은 글꼴 축만 포함됩니다. 따라서 문서(또는 응용 프로그램)에서 명시적으로 지정한 축 값이 두께, 늘이기, 스타일 및 글꼴 크기에서 파생된 축 값보다 우선합니다.

하이브리드 글꼴 선택 API

하이브리드 글꼴 선택 모델은 다음 IDWriteFontSet4 메서드에 의해 구현됩니다.

  • IDWriteFontSet4::ConvertWeightStretchStyleToFontAxisValues 메서드는 글꼴 크기, 가중치, 스트레치 및 스타일 매개 변수를 해당 축 값으로 변환합니다. 클라이언트가 전달한 명시적 축 값은 파생 축 값에서 제외됩니다.

  • IDWriteFontSet4::GetMatchingFonts 메서드는 패밀리 이름과 축 값 배열이 지정된 일치 글꼴의 우선 순위가 지정된 목록을 반환합니다. 위에서 설명한 대로 패밀리 이름 매개 변수는 입력 체계 패밀리 이름, WWS 패밀리 이름, RBIZ 패밀리 이름 또는 전체 이름이 될 수 있습니다. (전체 이름은 "Arial Bold Italic"과 같은 특정 글꼴 스타일을 식별합니다. GetMatchingFonts는 GDI를 사용하여 더 큰 comaptibiltiy를 위해 전체 이름으로 일치를 지원하며, 이를 허용합니다.)

다음 다른 DirectWrite API는 하이브리드 글꼴 선택 알고리즘도 사용합니다.

사용 중인 글꼴 선택 API의 코드 예제

이 섹션에서는 IDWriteFontSet4::GetMatchingFontsIDWriteFontSet4::ConvertWeightStretchStyleToFontAxisValues 메서드를 보여 주는 전체 콘솔 애플리케이션을 보여 줍니다. 먼저 몇 가지 헤더를 포함하겠습니다.

#include <dwrite_core.h>
#include <wil/com.h>
#include <iostream>
#include <string>
#include <vector>

IDWriteFontSet4::GetMatchingFonts 메서드는 지정된 패밀리 이름 및 축 값과 일치하는 글꼴 목록을 우선 순위순으로 반환합니다. 다음 MatchAxisValues 함수는 매개 변수를 IDWriteFontSet4::GetMatchingFonts 에 출력하고 반환된 글꼴 집합에서 일치하는 글꼴 목록을 출력합니다.

// Forward declarations of overloaded output operators used by MatchAxisValues.
std::wostream& operator<<(std::wostream& out, DWRITE_FONT_AXIS_VALUE const& axisValue);
std::wostream& operator<<(std::wostream& out, IDWriteFontFile* fileReference);
std::wostream& operator<<(std::wostream& out, IDWriteFontFaceReference1* faceReference);
//
// MatchAxisValues calls IDWriteFontSet4::GetMatchingFonts with the
// specified parameters and outputs the matching fonts.
//
void MatchAxisValues(
    IDWriteFontSet4* fontSet,
    _In_z_ WCHAR const* familyName,
    std::vector<DWRITE_FONT_AXIS_VALUE> const& axisValues,
    DWRITE_FONT_SIMULATIONS allowedSimulations
    )
{
    // Write the input parameters.
    std::wcout << L"GetMatchingFonts(\"" << familyName << L"\", {";
    for (DWRITE_FONT_AXIS_VALUE const& axisValue : axisValues)
    {
        std::wcout << L' ' << axisValue;
    }
    std::wcout << L" }, " << allowedSimulations << L"):\n";
    // Get the matching fonts for the specified family name and axis values.
    wil::com_ptr<IDWriteFontSet4> matchingFonts;
    THROW_IF_FAILED(fontSet->GetMatchingFonts(
        familyName,
        axisValues.data(),
        static_cast<UINT32>(axisValues.size()),
        allowedSimulations,
        &matchingFonts
    ));
    // Output the matching font face references.
    UINT32 const fontCount = matchingFonts->GetFontCount();
    for (UINT32 fontIndex = 0; fontIndex < fontCount; fontIndex++)
    {
        wil::com_ptr<IDWriteFontFaceReference1> faceReference;
        THROW_IF_FAILED(matchingFonts->GetFontFaceReference(fontIndex, &faceReference));
        std::wcout << L"    " << faceReference.get() << L'\n';
    }
    std::wcout << std::endl;
}

애플리케이션에는 축 값 대신 가중치, 늘이기 및 스타일 매개 변수가 있을 수 있습니다. 예를 들어 애플리케이션은 RBIZ 또는 가중치 스트레치 스타일 매개 변수를 사용하여 글꼴을 참조하는 문서로 작업해야 할 수 있습니다. 애플리케이션에서 임의의 축 값 지정에 대한 지원을 추가하더라도 이전 매개 변수도 지원해야 할 수 있습니다. 이를 위해 애플리케이션은 IDWriteFontSet4::GetMatchingFonts를 호출하기 전에 IDWriteFontSet4::ConvertWeightStretchStyleToFontAxisValues를 호출할 수 있습니다.

다음 MatchFont 함수는 축 값 외에도 가중치, 늘이기, 스타일 및 글꼴 크기 매개 변수를 사용합니다. IDWriteFontSet4::ConvertWeightStretchStyleToFontAxisValues 메서드에 이러한 매개 변수를 전달하여 입력 축 값에 추가되는 파생 축 값을 계산합니다. 결합된 축 값을 위의 MatchAxisValues 함수에 전달합니다.

struct FontStyleParams
{
    FontStyleParams() {}
    FontStyleParams(DWRITE_FONT_WEIGHT fontWeight) : fontWeight{ fontWeight } {}
    FontStyleParams(float fontSize) : fontSize{ fontSize } {}
    DWRITE_FONT_WEIGHT fontWeight = DWRITE_FONT_WEIGHT_NORMAL;
    DWRITE_FONT_STRETCH fontStretch = DWRITE_FONT_STRETCH_NORMAL;
    DWRITE_FONT_STYLE fontStyle = DWRITE_FONT_STYLE_NORMAL;
    float fontSize = 0.0f;
};
//
// MatchFont calls IDWriteFontSet4::ConvertWeightStretchStyleToFontAxisValues to convert
// the input parameters to axis values and then calls MatchAxisValues.
//
void MatchFont(
    IDWriteFactory7* factory,
    _In_z_ WCHAR const* familyName,
    FontStyleParams styleParams = {},
    std::vector<DWRITE_FONT_AXIS_VALUE> axisValues = {},
    DWRITE_FONT_SIMULATIONS allowedSimulations = DWRITE_FONT_SIMULATIONS_BOLD | DWRITE_FONT_SIMULATIONS_OBLIQUE
    )
{
    wil::com_ptr<IDWriteFontSet2> fontSet2;
    THROW_IF_FAILED(factory->GetSystemFontSet(/*includeDownloadableFonts*/ false, &fontSet2));
    wil::com_ptr<IDWriteFontSet4> fontSet;
    THROW_IF_FAILED(fontSet2->QueryInterface(&fontSet));
    // Ensure the total number of axis values can't overflow a UINT32.
    size_t const inputAxisCount = axisValues.size();
    if (inputAxisCount > UINT32_MAX - DWRITE_STANDARD_FONT_AXIS_COUNT)
    {
        THROW_HR(E_INVALIDARG);
    }
    // Reserve space at the end of axisValues vector for the derived axis values.
    // The maximum number of derived axis values is DWRITE_STANDARD_FONT_AXIS_COUNT.
    axisValues.resize(inputAxisCount + DWRITE_STANDARD_FONT_AXIS_COUNT);
    // Convert the weight, stretch, style, and font size to derived axis values.
    UINT32 derivedAxisCount = fontSet->ConvertWeightStretchStyleToFontAxisValues(
        /*inputAxisValues*/ axisValues.data(),
        static_cast<UINT32>(inputAxisCount),
        styleParams.fontWeight,
        styleParams.fontStretch,
        styleParams.fontStyle,
        styleParams.fontSize,
        /*out*/ axisValues.data() + inputAxisCount
        );
    // Resize the vector based on the actual number of derived axis values returned.
    axisValues.resize(inputAxisCount + derivedAxisCount);
    // Pass the combined axis values (explicit and derived) to MatchAxisValues.
    MatchAxisValues(fontSet.get(), familyName, axisValues, allowedSimulations);
}

다음 함수는 몇 가지 예제 입력을 사용하여 위의 MatchFont 함수를 호출한 결과를 보여 줍니다.

void TestFontSelection(IDWriteFactory7* factory)
{
    // Request "Cambria" with bold weight, with and without allowing simulations.
    //  - Matches a typographic/WWS family.
    //  - Result includes all fonts in the family ranked by priority.
    //  - Useful because Cambria Bold might have fewer glyphs than Cambria Regular.
    //  - Simulations may be applied if allowed.
    MatchFont(factory, L"Cambria", DWRITE_FONT_WEIGHT_BOLD);
    MatchFont(factory, L"Cambria", DWRITE_FONT_WEIGHT_BOLD, {}, DWRITE_FONT_SIMULATIONS_NONE);
    // Request "Cambria Bold".
    //  - Matches the full name of one static font.
    MatchFont(factory, L"Cambria Bold");
    // Request "Bahnschrift" with bold weight.
    //  - Matches a typographic/WWS family.
    //  - Returns an arbitrary instance of the variable font.
    MatchFont(factory, L"Bahnschrift", DWRITE_FONT_WEIGHT_BOLD);
    // Request "Segoe UI Variable" with two different font sizes.
    //  - Matches a typographic family name only (not a WWS family name).
    //  - Font size maps to "opsz" axis value (Note conversion from DIPs to points.)
    //  - Returns an arbitrary instance of the variable font.
    MatchFont(factory, L"Segoe UI Variable", 16.0f);
    MatchFont(factory, L"Segoe UI Variable", 32.0f);
    // Same as above with an explicit opsz axis value.
    // The explicit value is used instead of an "opsz" value derived from the font size.
    MatchFont(factory, L"Segoe UI Variable", 16.0f, { { DWRITE_FONT_AXIS_TAG_OPTICAL_SIZE, 32.0f } });
    // Request "Segoe UI Variable Display".
    //  - Matches a WWS family (NOT a typographic family).
    //  - The input "opsz" value is ignored; the optical size of the family is used.
    MatchFont(factory, L"Segoe UI Variable Display", 16.0f);
    // Request "Segoe UI Variable Display Bold".
    //  - Matches the full name of a variable font instance.
    //  - All input axes are ignored; the axis values of the matching font are used.
    MatchFont(factory, L"Segoe UI Variable Display Bold", 16.0f);
}

다음은 위의 TestFontSelection 함수의 출력입니다.

GetMatchingFonts("Cambria", { wght:700 wdth:100 ital:0 slnt:0 }, 3):
    cambriab.ttf wght:700 wdth:100 ital:0 slnt:0
    cambria.ttc wght:400 wdth:100 ital:0 slnt:0 BOLDSIM
    cambriaz.ttf wght:700 wdth:100 ital:1 slnt:-12.4
    cambriai.ttf wght:400 wdth:100 ital:1 slnt:-12.4 BOLDSIM
GetMatchingFonts("Cambria", { wght:700 wdth:100 ital:0 slnt:0 }, 0):
    cambriab.ttf wght:700 wdth:100 ital:0 slnt:0
    cambriaz.ttf wght:700 wdth:100 ital:1 slnt:-12.4
    cambria.ttc wght:400 wdth:100 ital:0 slnt:0
    cambriai.ttf wght:400 wdth:100 ital:1 slnt:-12.4
GetMatchingFonts("Cambria Bold", { wght:400 wdth:100 ital:0 slnt:0 }, 3):
    cambriab.ttf wght:700 wdth:100 ital:0 slnt:0
GetMatchingFonts("Bahnschrift", { wght:700 wdth:100 ital:0 slnt:0 }, 3):
    bahnschrift.ttf wght:700 wdth:100 ital:0 slnt:0
GetMatchingFonts("Segoe UI Variable", { wght:400 wdth:100 ital:0 slnt:0 opsz:12 }, 3):
    SegUIVar.ttf opsz:12 wght:400 wdth:100 ital:0 slnt:0
GetMatchingFonts("Segoe UI Variable", { wght:400 wdth:100 ital:0 slnt:0 opsz:24 }, 3):
    SegUIVar.ttf opsz:24 wght:400 wdth:100 ital:0 slnt:0
GetMatchingFonts("Segoe UI Variable", { opsz:32 wght:400 wdth:100 ital:0 slnt:0 }, 3):
    SegUIVar.ttf opsz:32 wght:400 wdth:100 ital:0 slnt:0
GetMatchingFonts("Segoe UI Variable Display", { wght:400 wdth:100 ital:0 slnt:0 opsz:12 }, 3):
    SegUIVar.ttf opsz:36 wght:400 wdth:100 ital:0 slnt:0
GetMatchingFonts("Segoe UI Variable Display Bold", { wght:400 wdth:100 ital:0 slnt:0 opsz:12 }, 3):
    SegUIVar.ttf opsz:36 wght:700 wdth:100 ital:0 slnt:0

다음은 위에서 선언한 오버로드된 연산자의 구현입니다. MatchAxisValues에서 입력 축 값과 결과 글꼴 얼굴 참조를 작성하는 데 사용됩니다.

// Output a font axis value.
std::wostream& operator<<(std::wostream& out, DWRITE_FONT_AXIS_VALUE const& axisValue)
{
    UINT32 tagValue = axisValue.axisTag;
    WCHAR tagChars[5] =
    {
        static_cast<WCHAR>(tagValue & 0xFF),
        static_cast<WCHAR>((tagValue >> 8) & 0xFF),
        static_cast<WCHAR>((tagValue >> 16) & 0xFF),
        static_cast<WCHAR>((tagValue >> 24) & 0xFF),
        '\0'
    };
    return out << tagChars << L':' << axisValue.value;
}
// Output a file name given a font file reference.
std::wostream& operator<<(std::wostream& out, IDWriteFontFile* fileReference)
{
    wil::com_ptr<IDWriteFontFileLoader> loader;
    THROW_IF_FAILED(fileReference->GetLoader(&loader));
    wil::com_ptr<IDWriteLocalFontFileLoader> localLoader;
    if (SUCCEEDED(loader->QueryInterface(&localLoader)))
    {
        const void* fileKey;
        UINT32 fileKeySize;
        THROW_IF_FAILED(fileReference->GetReferenceKey(&fileKey, &fileKeySize));
        WCHAR filePath[MAX_PATH];
        THROW_IF_FAILED(localLoader->GetFilePathFromKey(fileKey, fileKeySize, filePath, MAX_PATH));
        WCHAR* fileName = wcsrchr(filePath, L'\\');
        fileName = (fileName != nullptr) ? fileName + 1 : filePath;
        out << fileName;
    }
    return out;
}
// Output a font face reference.
std::wostream& operator<<(std::wostream& out, IDWriteFontFaceReference1* faceReference)
{
    // Output the font file name.
    wil::com_ptr<IDWriteFontFile> fileReference;
    THROW_IF_FAILED(faceReference->GetFontFile(&fileReference));
    std::wcout << fileReference.get();
    // Output the face index if nonzero.
    UINT32 const faceIndex = faceReference->GetFontFaceIndex();
    if (faceIndex != 0)
    {
        out << L'#' << faceIndex;
    }
    // Output the axis values.
    UINT32 const axisCount = faceReference->GetFontAxisValueCount();
    std::vector<DWRITE_FONT_AXIS_VALUE> axisValues(axisCount);
    THROW_IF_FAILED(faceReference->GetFontAxisValues(axisValues.data(), axisCount));
    for (DWRITE_FONT_AXIS_VALUE const& axisValue : axisValues)
    {
        out << L' ' << axisValue;
    }
    // Output the simulations.
    DWRITE_FONT_SIMULATIONS simulations = faceReference->GetSimulations();
    if (simulations & DWRITE_FONT_SIMULATIONS_BOLD)
    {
        out << L" BOLDSIM";
    }
    if (simulations & DWRITE_FONT_SIMULATIONS_OBLIQUE)
    {
        out << L" OBLIQUESIM";
    }
    return out;
}

샘플을 마무리하기 위해 명령줄 구문 분석 함수 및 기본 함수는 다음과 같습니다.

char const g_usage[] =
"ParseCmdLine <args>\n"
"\n"
" -test             Test sample font selection parameters.\n"
" <familyname>      Sets the font family name.\n"
" -size <value>     Sets the font size in DIPs.\n"
" -bold             Sets weight to bold (700).\n"
" -weight <value>   Sets a weight in the range 100-900.\n"
" -italic           Sets style to DWRITE_FONT_STYLE_ITALIC.\n"
" -oblique          Sets style to DWRITE_FONT_STYLE_OBLIQUE.\n"
" -stretch <value>  Sets a stretch in the range 1-9.\n"
" -<axis>:<value>   Sets an axis value (for example, -opsz:24).\n"
" -nosim            Disallow font simulations.\n"
"\n";
struct CmdArgs
{
    std::wstring familyName;
    FontStyleParams styleParams;
    std::vector<DWRITE_FONT_AXIS_VALUE> axisValues;
    DWRITE_FONT_SIMULATIONS allowedSimulations = DWRITE_FONT_SIMULATIONS_BOLD | DWRITE_FONT_SIMULATIONS_OBLIQUE;
    bool test = false;
};
template<typename T>
_Success_(return)
bool ParseEnum(_In_z_ WCHAR const* arg, long minValue, long maxValue, _Out_ T* result)
{
    WCHAR* endPtr;
    long value = wcstol(arg, &endPtr, 10);
    *result = static_cast<T>(value);
    return value >= minValue && value <= maxValue && *endPtr == L'\0';
}
_Success_(return)
bool ParseFloat(_In_z_ WCHAR const* arg, _Out_ float* value)
{
    WCHAR* endPtr;
    *value = wcstof(arg, &endPtr);
    return *arg != L'\0' && *endPtr == L'\0';
}
bool ParseCommandLine(int argc, WCHAR** argv, CmdArgs& cmd)
{
    for (int argIndex = 1; argIndex < argc; argIndex++)
    {
        WCHAR const* arg = argv[argIndex];
        if (*arg != L'-')
        {
            if (!cmd.familyName.empty())
                return false;
            cmd.familyName = argv[argIndex];
        }
        else if (!wcscmp(arg, L"-test"))
        {
            cmd.test = true;
        }
        else if (!wcscmp(arg, L"-size"))
        {
            if (++argIndex == argc)
                return false;
            if (!ParseFloat(argv[argIndex], &cmd.styleParams.fontSize))
                return false;
        }
        else if (!wcscmp(arg, L"-bold"))
        {
            cmd.styleParams.fontWeight = DWRITE_FONT_WEIGHT_BOLD;
        }
        else if (!wcscmp(arg, L"-weight"))
        {
            if (++argIndex == argc)
                return false;
            if (!ParseEnum(argv[argIndex], 100, 900, &cmd.styleParams.fontWeight))
                return false;
        }
        else if (!wcscmp(arg, L"-italic"))
        {
            cmd.styleParams.fontStyle = DWRITE_FONT_STYLE_ITALIC;
        }
        else if (!wcscmp(arg, L"-oblique"))
        {
            cmd.styleParams.fontStyle = DWRITE_FONT_STYLE_OBLIQUE;
        }
        else if (!wcscmp(arg, L"-stretch"))
        {
            if (++argIndex == argc)
                return false;
            if (!ParseEnum(argv[argIndex], 1, 9, &cmd.styleParams.fontStretch))
                return false;
        }
        else if (wcslen(arg) > 5 && arg[5] == L':')
        {
            // Example: -opsz:12
            DWRITE_FONT_AXIS_VALUE axisValue;
            axisValue.axisTag = DWRITE_MAKE_FONT_AXIS_TAG(arg[1], arg[2], arg[3], arg[4]);
            if (!ParseFloat(arg + 6, &axisValue.value))
                return false;
            cmd.axisValues.push_back(axisValue);
        }
        else if (!wcscmp(arg, L"-nosim"))
        {
            cmd.allowedSimulations = DWRITE_FONT_SIMULATIONS_NONE;
        }
        else
        {
            return false;
        }
    }
    return true;
}
int __cdecl wmain(int argc, WCHAR** argv)
{
    CmdArgs cmd;
    if (!ParseCommandLine(argc, argv, cmd))
    {
        std::cerr << "Invalid command. Type TestFontSelection with no arguments for usage.\n";
        return 1;
    }
    if (cmd.familyName.empty() && !cmd.test)
    {
        std::cout << g_usage;
        return 0;
    }
    wil::com_ptr<IDWriteFactory7> factory;
    THROW_IF_FAILED(DWriteCoreCreateFactory(
        DWRITE_FACTORY_TYPE_SHARED,
        __uuidof(IDWriteFactory7),
        (IUnknown**)&factory
    ));
    if (!cmd.familyName.empty())
    {
        MatchFont(
            factory.get(),
            cmd.familyName.c_str(),
            cmd.styleParams,
            std::move(cmd.axisValues),
            cmd.allowedSimulations
        );
    }
    if (cmd.test)
    {
        TestFontSelection(factory.get());
    }
}