From 14158a71db73c4bfb4c293f67277fc2e79d310cd Mon Sep 17 00:00:00 2001 From: Nikolay Sivov Date: Mon, 27 Apr 2015 00:20:32 +0300 Subject: [PATCH] dwrite: Implement ApplyCharacterSpacing(). --- dlls/dwrite/analyzer.c | 219 ++++++++++++++++++++++++++++++++++- dlls/dwrite/tests/analyzer.c | 162 ++++++++++++++++++++++++++ 2 files changed, 379 insertions(+), 2 deletions(-) diff --git a/dlls/dwrite/analyzer.c b/dlls/dwrite/analyzer.c index b8bd5a852f0..b4be9f7cc1a 100644 --- a/dlls/dwrite/analyzer.c +++ b/dlls/dwrite/analyzer.c @@ -1076,14 +1076,229 @@ static HRESULT WINAPI dwritetextanalyzer_GetGdiCompatibleGlyphPlacements(IDWrite return E_NOTIMPL; } +static inline FLOAT get_cluster_advance(const FLOAT *advances, UINT32 start, UINT32 end) +{ + FLOAT advance = 0.0; + for (; start < end; start++) + advance += advances[start]; + return advance; +} + +static void apply_single_glyph_spacing(FLOAT leading_spacing, FLOAT trailing_spacing, + FLOAT min_advance_width, UINT32 g, FLOAT const *advances, DWRITE_GLYPH_OFFSET const *offsets, + DWRITE_SHAPING_GLYPH_PROPERTIES const *props, FLOAT *modified_advances, DWRITE_GLYPH_OFFSET *modified_offsets) +{ + BOOL reduced = leading_spacing < 0.0 || trailing_spacing < 0.0; + FLOAT advance = advances[g]; + FLOAT origin = 0.0; + + if (props[g].isZeroWidthSpace) { + modified_advances[g] = advances[g]; + modified_offsets[g] = offsets[g]; + return; + } + + /* first apply negative spacing and check if we hit minimum width */ + if (leading_spacing < 0.0) { + advance += leading_spacing; + origin -= leading_spacing; + } + if (trailing_spacing < 0.0) + advance += trailing_spacing; + + if (advance < min_advance_width) { + FLOAT half = (min_advance_width - advance) / 2.0; + + if (!reduced) + origin -= half; + else if (leading_spacing < 0.0 && trailing_spacing < 0.0) + origin -= half; + else if (leading_spacing < 0.0) + origin -= min_advance_width - advance; + + advance = min_advance_width; + } + + /* now apply positive spacing adjustments */ + if (leading_spacing > 0.0) { + advance += leading_spacing; + origin -= leading_spacing; + } + if (trailing_spacing > 0.0) + advance += trailing_spacing; + + modified_advances[g] = advance; + modified_offsets[g].advanceOffset = offsets[g].advanceOffset - origin; + /* ascender is never touched, it's orthogonal to reading direction and is not + affected by advance adjustments */ + modified_offsets[g].ascenderOffset = offsets[g].ascenderOffset; +} + +static void apply_cluster_spacing(FLOAT leading_spacing, FLOAT trailing_spacing, FLOAT min_advance_width, + UINT32 start, UINT32 end, FLOAT const *advances, DWRITE_GLYPH_OFFSET const *offsets, + FLOAT *modified_advances, DWRITE_GLYPH_OFFSET *modified_offsets) +{ + BOOL reduced = leading_spacing < 0.0 || trailing_spacing < 0.0; + FLOAT advance = get_cluster_advance(advances, start, end); + FLOAT origin = 0.0; + UINT16 g; + + modified_advances[start] = advances[start]; + modified_advances[end-1] = advances[end-1]; + + /* first apply negative spacing and check if we hit minimum width */ + if (leading_spacing < 0.0) { + advance += leading_spacing; + modified_advances[start] += leading_spacing; + origin -= leading_spacing; + } + if (trailing_spacing < 0.0) { + advance += trailing_spacing; + modified_advances[end-1] += trailing_spacing; + } + if (advance < min_advance_width) { + /* additional spacing is only applied to leading and trailing glyph */ + FLOAT half = (min_advance_width - advance) / 2.0; + + if (!reduced) { + origin -= half; + modified_advances[start] += half; + modified_advances[end-1] += half; + } + else if (leading_spacing < 0.0 && trailing_spacing < 0.0) { + origin -= half; + modified_advances[start] += half; + modified_advances[end-1] += half; + } + else if (leading_spacing < 0.0) { + origin -= min_advance_width - advance; + modified_advances[start] += min_advance_width - advance; + } + else + modified_advances[end-1] += min_advance_width - advance; + + advance = min_advance_width; + } + + /* now apply positive spacing adjustments */ + if (leading_spacing > 0.0) { + modified_advances[start] += leading_spacing; + origin -= leading_spacing; + } + if (trailing_spacing > 0.0) + modified_advances[end-1] += trailing_spacing; + + for (g = start; g < end; g++) { + if (g == start) { + modified_offsets[g].advanceOffset = offsets[g].advanceOffset - origin; + modified_offsets[g].ascenderOffset = offsets[g].ascenderOffset; + } + else if (g == end - 1) + /* trailing glyph offset is not adjusted */ + modified_offsets[g] = offsets[g]; + else { + /* for all glyphs within a cluster use original advances and offsets */ + modified_advances[g] = advances[g]; + modified_offsets[g] = offsets[g]; + } + } +} + +static inline UINT32 get_cluster_length(UINT16 const *clustermap, UINT32 start, UINT32 text_len) +{ + UINT16 g = clustermap[start]; + UINT32 length = 1; + + while (start < text_len && clustermap[++start] == g) + length++; + return length; +} + +/* Applies spacing adjustments to clusters. + + Adjustments are applied in the following order: + + 1. Negative adjustments + + Leading and trailing spacing could be negative, at this step + only negative ones are actually applied. Leading spacing is only + applied to leading glyph, trailing - to trailing glyph. + + 2. Minimum advance width + + Advances could only be reduced at this point or unchanged. In any + case it's checked if cluster advance width is less than minimum width. + If it's the case advance width is incremented up to minimum value. + + Important part is the direction in which this increment is applied; + it depends on from which directions total cluster advance was trimmed + at step 1. So it could be incremented from leading, trailing, or both + sides. When applied to both sides, each side gets half of difference + that bring advance to minimum width. + + 3. Positive adjustments + + After minimum width rule was applied, positive spacing is applied in same + way as negative ones on step 1. + + Glyph offset for leading glyph is adjusted too in a way that glyph origin + keeps its position in coordinate system where initial advance width is counted + from 0. + + Glyph properties + + It's known that isZeroWidthSpace property keeps initial advance from changing. + + TODO: test other properties; make isZeroWidthSpace work properly for clusters + with more than one glyph. + +*/ static HRESULT WINAPI dwritetextanalyzer1_ApplyCharacterSpacing(IDWriteTextAnalyzer2 *iface, FLOAT leading_spacing, FLOAT trailing_spacing, FLOAT min_advance_width, UINT32 len, UINT32 glyph_count, UINT16 const *clustermap, FLOAT const *advances, DWRITE_GLYPH_OFFSET const *offsets, DWRITE_SHAPING_GLYPH_PROPERTIES const *props, FLOAT *modified_advances, DWRITE_GLYPH_OFFSET *modified_offsets) { - FIXME("(%.2f %.2f %.2f %u %u %p %p %p %p %p %p): stub\n", leading_spacing, trailing_spacing, min_advance_width, + UINT16 start; + + TRACE("(%.2f %.2f %.2f %u %u %p %p %p %p %p %p)\n", leading_spacing, trailing_spacing, min_advance_width, len, glyph_count, clustermap, advances, offsets, props, modified_advances, modified_offsets); - return E_NOTIMPL; + + if (min_advance_width < 0.0) { + memset(modified_advances, 0, glyph_count*sizeof(*modified_advances)); + return E_INVALIDARG; + } + + /* minimum advance is not applied if no adjustments were made */ + if (leading_spacing == 0.0 && trailing_spacing == 0.0) { + memmove(modified_advances, advances, glyph_count*sizeof(*advances)); + memmove(modified_offsets, offsets, glyph_count*sizeof(*offsets)); + return S_OK; + } + + start = 0; + for (start = 0; start < len;) { + UINT32 length = get_cluster_length(clustermap, start, len); + + if (length == 1) { + UINT32 g = clustermap[start]; + + apply_single_glyph_spacing(leading_spacing, trailing_spacing, min_advance_width, + g, advances, offsets, props, modified_advances, modified_offsets); + } + else { + UINT32 g_start, g_end; + + g_start = clustermap[start]; + g_end = (start + length < len) ? clustermap[start + length] : glyph_count; + + apply_cluster_spacing(leading_spacing, trailing_spacing, min_advance_width, + g_start, g_end, advances, offsets, modified_advances, modified_offsets); + } + + start += length; + } + + return S_OK; } static HRESULT WINAPI dwritetextanalyzer1_GetBaseline(IDWriteTextAnalyzer2 *iface, IDWriteFontFace *face, diff --git a/dlls/dwrite/tests/analyzer.c b/dlls/dwrite/tests/analyzer.c index 3e3c51cb596..0043526b8a5 100644 --- a/dlls/dwrite/tests/analyzer.c +++ b/dlls/dwrite/tests/analyzer.c @@ -1449,6 +1449,167 @@ static void test_GetGlyphPlacements(void) DELETE_FONTFILE(path); } +struct spacing_test { + FLOAT leading; + FLOAT trailing; + FLOAT min_advance; + FLOAT advances[3]; + FLOAT offsets[3]; + FLOAT modified_advances[3]; + FLOAT modified_offsets[3]; + BOOL single_cluster; + BOOL is_ZWS[3]; +}; + +static const struct spacing_test spacing_tests[] = { + { 0.0, 0.0, 0.0, { 10.0, 11.0 }, { 2.0, 3.0 }, { 10.0, 11.0 }, { 2.0, 3.0 } }, /* 0 */ + { 0.0, 0.0, 2.0, { 10.0, 11.0 }, { 2.0, 3.0 }, { 10.0, 11.0 }, { 2.0, 3.0 } }, + { 1.0, 0.0, 0.0, { 10.0, 11.0 }, { 2.0, 3.0 }, { 11.0, 12.0 }, { 3.0, 4.0 } }, + { 1.0, 1.0, 0.0, { 10.0, 11.0 }, { 2.0, 3.0 }, { 12.0, 13.0 }, { 3.0, 4.0 } }, + { 1.0, -1.0, 0.0, { 10.0, 11.0 }, { 2.0, 3.0 }, { 10.0, 11.0 }, { 3.0, 4.0 } }, + { 0.0, -10.0, 0.0, { 10.0, 11.0 }, { 2.0, 3.0 }, { 0.0, 1.0 }, { 2.0, 3.0 } }, /* 5 */ + { -5.0, -4.0, 5.0, { 10.0, 11.0 }, { 2.0, 3.0 }, { 5.0, 5.0 }, { -1.0, -0.5 } }, + { -5.0, -5.0, 5.0, { 10.0, 11.0 }, { 2.0, 3.0 }, { 5.0, 5.0 }, { -0.5, 0.0 } }, + { 2.0, 0.0, 5.0, { 1.0, 2.0 }, { 2.0, 3.0 }, { 7.0, 7.0 }, { 6.0, 6.5 } }, + { 2.0, 1.0, 5.0, { 1.0, 2.0 }, { 2.0, 3.0 }, { 8.0, 8.0 }, { 6.0, 6.5 } }, + { 2.0, -10.0, 5.0, { 10.0, 11.0 }, { 2.0, 3.0 }, { 7.0, 7.0 }, { 4.0, 5.0 } }, /* 10 */ + { 1.0, -10.0, 5.0, { 10.0, 11.0 }, { 2.0, 3.0 }, { 6.0, 6.0 }, { 3.0, 4.0 } }, + { -10.0, 1.0, 5.0, { 10.0, 11.0 }, { 2.0, 3.0 }, { 6.0, 6.0 }, { -3.0, -3.0 } }, + { 0.0, -10.0, 5.0, { 10.0, 11.0 }, { 2.0, 3.0 }, { 5.0, 5.0 }, { 2.0, 3.0 } }, + { 1.0, -10.0, -5.0, { 10.0, 11.0 }, { 2.0, 3.0 }, { 0.0, 0.0 }, { 2.0, 3.0 } }, + { -10.0, 1.0, 5.0, { 8.0, 11.0 }, { 2.0, 3.0 }, { 6.0, 6.0 }, { -1.0, -3.0 } }, /* 15 */ + /* cluster of more than 1 glyph */ + { 0.0, 0.0, 0.0, { 10.0, 11.0 }, { 2.0, 3.0 }, { 10.0, 11.0 }, { 2.0, 3.0 }, TRUE }, + { 1.0, 0.0, 0.0, { 10.0, 11.0 }, { 2.0, 3.5 }, { 11.0, 11.0 }, { 3.0, 3.5 }, TRUE }, + { 1.0, 1.0, 0.0, { 10.0, 11.0 }, { 2.0, 3.0 }, { 11.0, 12.0 }, { 3.0, 3.0 }, TRUE }, + { 1.0, -1.0, 0.0, { 10.0, 11.0 }, { 2.0, 3.0 }, { 11.0, 10.0 }, { 3.0, 3.0 }, TRUE }, + { 0.0, -10.0, 0.0, { 10.0, 11.0 }, { 2.0, 3.0 }, { 10.0, 1.0 }, { 2.0, 3.0 }, TRUE }, /* 20 */ + { 0.0, -10.0, 5.0, { 10.0, 11.0 }, { 2.0, 3.0 }, { 10.0, 1.0 }, { 2.0, 3.0 }, TRUE }, + { 1.0, -10.0, -5.0, { 10.0, 11.0 }, { 2.0, 3.0 }, { 0.0, 0.0 }, { 2.0, 3.0 }, TRUE }, + { -5.0, -10.0, 4.0, { 10.0, 11.0, 12.0 }, { 2.0, 3.0, 4.0 }, { 5.0, 11.0, 2.0 }, { -3.0, 3.0, 4.0 }, TRUE }, + { -10.0, -10.0, 4.0, { 10.0, 11.0, 12.0 }, { 2.0, 3.0, 4.0 }, { 0.0, 11.0, 2.0 }, { -8.0, 3.0, 4.0 }, TRUE }, + { -10.0, -10.0, 5.0, { 10.0, 1.0, 12.0 }, { 2.0, 3.0, 4.0 }, { 1.0, 1.0, 3.0 }, { -7.0, 3.0, 4.0 }, TRUE }, /* 25 */ + { -10.0, 1.0, 5.0, { 10.0, 1.0, 2.0 }, { 2.0, 3.0, 4.0 }, { 2.0, 1.0, 3.0 }, { -6.0, 3.0, 4.0 }, TRUE }, + { 1.0, -10.0, 5.0, { 2.0, 1.0, 10.0 }, { 2.0, 3.0, 4.0 }, { 3.0, 1.0, 2.0 }, { 3.0, 3.0, 4.0 }, TRUE }, + { -10.0, -10.0, 5.0, { 11.0, 1.0, 11.0 }, { 2.0, 3.0, 4.0 }, { 2.0, 1.0, 2.0 }, { -7.0, 3.0, 4.0 }, TRUE }, + /* isZeroWidthSpace set */ + { 1.0, 0.0, 0.0, { 10.0, 11.0 }, { 2.0, 3.0 }, { 10.0, 12.0 }, { 2.0, 4.0 }, FALSE, { TRUE, FALSE } }, + { 1.0, 1.0, 0.0, { 10.0, 11.0 }, { 2.0, 3.0 }, { 10.0, 13.0 }, { 2.0, 4.0 }, FALSE, { TRUE, FALSE } }, /* 30 */ + { 1.0, -1.0, 0.0, { 10.0, 11.0 }, { 2.0, 3.0 }, { 10.0, 11.0 }, { 3.0, 3.0 }, FALSE, { FALSE, TRUE } }, + { 0.0, -10.0, 0.0, { 10.0, 11.0 }, { 2.0, 3.0 }, { 10.0, 1.0 }, { 2.0, 3.0 }, FALSE, { TRUE, FALSE } }, + { -5.0, -4.0, 5.0, { 10.0, 11.0 }, { 2.0, 3.0 }, { 5.0, 11.0 }, { -1.0, 3.0 }, FALSE, { FALSE, TRUE } }, + { -5.0, -5.0, 5.0, { 10.0, 11.0 }, { 2.0, 3.0 }, { 10.0, 11.0 }, { 2.0, 3.0 }, FALSE, { TRUE, TRUE } }, + { 2.0, 0.0, 5.0, { 1.0, 2.0 }, { 2.0, 3.0 }, { 7.0, 2.0 }, { 6.0, 3.0 }, FALSE, { FALSE, TRUE } }, /* 35 */ + { 2.0, 1.0, 5.0, { 1.0, 2.0 }, { 2.0, 3.0 }, { 8.0, 2.0 }, { 6.0, 3.0 }, FALSE, { FALSE, TRUE } }, + { 2.0, -10.0, 5.0, { 10.0, 11.0 }, { 2.0, 3.0 }, { 10.0, 11.0 }, { 2.0, 3.0 }, FALSE, { TRUE, TRUE } }, + { 1.0, -10.0, 5.0, { 10.0, 11.0 }, { 2.0, 3.0 }, { 6.0, 11.0 }, { 3.0, 3.0 }, FALSE, { FALSE, TRUE } }, + { -10.0, 1.0, 5.0, { 10.0, 11.0 }, { 2.0, 3.0 }, { 10.0, 11.0 }, { 2.0, 3.0 }, FALSE, { TRUE, TRUE } }, + { 0.0, -10.0, 5.0, { 10.0, 11.0 }, { 2.0, 3.0 }, { 5.0, 11.0 }, { 2.0, 3.0 }, FALSE, { FALSE, TRUE } }, /* 40 */ + { 1.0, -10.0, -5.0, { 10.0, 11.0 }, { 2.0, 3.0 }, { 0.0, 0.0 }, { 2.0, 3.0 }, FALSE, { TRUE, FALSE } }, + { -10.0, 1.0, 5.0, { 8.0, 11.0 }, { 2.0, 3.0 }, { 6.0, 11.0 }, { -1.0, 3.0 }, FALSE, { FALSE, TRUE } }, +}; + +static void test_ApplyCharacterSpacing(void) +{ + DWRITE_SHAPING_GLYPH_PROPERTIES props[3]; + IDWriteTextAnalyzer1 *analyzer1; + IDWriteTextAnalyzer *analyzer; + UINT16 clustermap[2]; + HRESULT hr; + int i; + + hr = IDWriteFactory_CreateTextAnalyzer(factory, &analyzer); + ok(hr == S_OK, "got 0x%08x\n", hr); + + hr = IDWriteTextAnalyzer_QueryInterface(analyzer, &IID_IDWriteTextAnalyzer1, (void**)&analyzer1); + IDWriteTextAnalyzer_Release(analyzer); + if (hr != S_OK) { + win_skip("ApplyCharacterSpacing() is not supported.\n"); + return; + } + + for (i = 0; i < sizeof(spacing_tests)/sizeof(spacing_tests[0]); i++) { + const struct spacing_test *ptr = spacing_tests + i; + DWRITE_GLYPH_OFFSET offsets[3]; + UINT32 glyph_count; + FLOAT advances[3]; + + offsets[0].advanceOffset = ptr->offsets[0]; + offsets[1].advanceOffset = ptr->offsets[1]; + offsets[2].advanceOffset = ptr->offsets[2]; + /* Ascender offsets are never thouched as spacing applies in reading direction only, + we'll only test them to see if they are not changed */ + offsets[0].ascenderOffset = 23.0; + offsets[1].ascenderOffset = 32.0; + offsets[2].ascenderOffset = 31.0; + + glyph_count = ptr->advances[2] > 0.0 ? 3 : 2; + if (ptr->single_cluster) { + clustermap[0] = 0; + clustermap[1] = 0; + } + else { + /* trivial case with one glyph per cluster */ + clustermap[0] = 0; + clustermap[1] = 1; + } + + advances[0] = advances[1] = 123.45; + memset(props, 0, sizeof(props)); + props[0].isZeroWidthSpace = ptr->is_ZWS[0]; + props[1].isZeroWidthSpace = ptr->is_ZWS[1]; + props[2].isZeroWidthSpace = ptr->is_ZWS[2]; + + hr = IDWriteTextAnalyzer1_ApplyCharacterSpacing(analyzer1, + ptr->leading, + ptr->trailing, + ptr->min_advance, + sizeof(clustermap)/sizeof(clustermap[0]), + glyph_count, + clustermap, + ptr->advances, + offsets, + props, + advances, + offsets); + /* invalid argument cases */ + if (ptr->min_advance < 0.0) + ok(hr == E_INVALIDARG, "%d: got 0x%08x\n", i, hr); + else + ok(hr == S_OK, "%d: got 0x%08x\n", i, hr); + if (hr == S_OK) { + ok(ptr->modified_advances[0] == advances[0], "%d: got advance[0] %.2f, expected %.2f\n", i, advances[0], ptr->modified_advances[0]); + ok(ptr->modified_advances[1] == advances[1], "%d: got advance[1] %.2f, expected %.2f\n", i, advances[1], ptr->modified_advances[1]); + if (glyph_count > 2) + ok(ptr->modified_advances[2] == advances[2], "%d: got advance[2] %.2f, expected %.2f\n", i, advances[2], ptr->modified_advances[2]); + + ok(ptr->modified_offsets[0] == offsets[0].advanceOffset, "%d: got offset[0] %.2f, expected %.2f\n", i, + offsets[0].advanceOffset, ptr->modified_offsets[0]); + ok(ptr->modified_offsets[1] == offsets[1].advanceOffset, "%d: got offset[1] %.2f, expected %.2f\n", i, + offsets[1].advanceOffset, ptr->modified_offsets[1]); + if (glyph_count > 2) + ok(ptr->modified_offsets[2] == offsets[2].advanceOffset, "%d: got offset[2] %.2f, expected %.2f\n", i, + offsets[2].advanceOffset, ptr->modified_offsets[2]); + + ok(offsets[0].ascenderOffset == 23.0, "%d: unexpected ascenderOffset %.2f\n", i, offsets[0].ascenderOffset); + ok(offsets[1].ascenderOffset == 32.0, "%d: unexpected ascenderOffset %.2f\n", i, offsets[1].ascenderOffset); + ok(offsets[2].ascenderOffset == 31.0, "%d: unexpected ascenderOffset %.2f\n", i, offsets[2].ascenderOffset); + } + else { + ok(ptr->modified_advances[0] == advances[0], "%d: got advance[0] %.2f, expected %.2f\n", i, advances[0], ptr->modified_advances[0]); + ok(ptr->modified_advances[1] == advances[1], "%d: got advance[1] %.2f, expected %.2f\n", i, advances[1], ptr->modified_advances[1]); + ok(ptr->offsets[0] == offsets[0].advanceOffset, "%d: got offset[0] %.2f, expected %.2f\n", i, + offsets[0].advanceOffset, ptr->modified_offsets[0]); + ok(ptr->offsets[1] == offsets[1].advanceOffset, "%d: got offset[1] %.2f, expected %.2f\n", i, + offsets[1].advanceOffset, ptr->modified_offsets[1]); + ok(offsets[0].ascenderOffset == 23.0, "%d: unexpected ascenderOffset %.2f\n", i, offsets[0].ascenderOffset); + ok(offsets[1].ascenderOffset == 32.0, "%d: unexpected ascenderOffset %.2f\n", i, offsets[1].ascenderOffset); + } + } + + IDWriteTextAnalyzer1_Release(analyzer1); +} + START_TEST(analyzer) { HRESULT hr; @@ -1472,6 +1633,7 @@ START_TEST(analyzer) test_numbersubstitution(); test_GetTypographicFeatures(); test_GetGlyphPlacements(); + test_ApplyCharacterSpacing(); IDWriteFactory_Release(factory); }