1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 #include "minikin/Layout.h"
18 
19 #include <gtest/gtest.h>
20 
21 #include "minikin/LayoutCache.h"
22 
23 #include "FontTestUtils.h"
24 #include "LocaleListCache.h"
25 #include "UnicodeUtils.h"
26 
27 namespace minikin {
28 
29 class TestableLayoutCache : public LayoutCache {
30 public:
TestableLayoutCache(uint32_t maxEntries)31     TestableLayoutCache(uint32_t maxEntries) : LayoutCache(maxEntries) {}
32     using LayoutCache::getCacheSize;
33 };
34 
35 class LayoutCapture {
36 public:
LayoutCapture()37     LayoutCapture() {}
38 
operator ()(const LayoutPiece & layout,const MinikinPaint &,const MinikinRect & bounds)39     void operator()(const LayoutPiece& layout, const MinikinPaint& /* dir */,
40                     const MinikinRect& bounds) {
41         mLayout = &layout;
42         mBounds = bounds;
43     }
44 
get() const45     const LayoutPiece* get() const { return mLayout; }
bounds() const46     const MinikinRect& bounds() const { return mBounds; }
47 
48 private:
49     const LayoutPiece* mLayout;
50     MinikinRect mBounds;
51 };
52 
TEST(LayoutCacheTest,cacheHitTest)53 TEST(LayoutCacheTest, cacheHitTest) {
54     auto text = utf8ToUtf16("android");
55     Range range(0, text.size());
56     MinikinPaint paint(buildFontCollection("Ascii.ttf"));
57 
58     TestableLayoutCache layoutCache(10);
59 
60     LayoutCapture layout1;
61     layoutCache.getOrCreate(text, range, paint, false /* LTR */, StartHyphenEdit::NO_EDIT,
62                             EndHyphenEdit::NO_EDIT, false, layout1);
63 
64     LayoutCapture layout2;
65     layoutCache.getOrCreate(text, range, paint, false /* LTR */, StartHyphenEdit::NO_EDIT,
66                             EndHyphenEdit::NO_EDIT, false, layout2);
67 
68     EXPECT_EQ(layout1.get(), layout2.get());
69 }
70 
TEST(LayoutCacheTest,cacheMissTest)71 TEST(LayoutCacheTest, cacheMissTest) {
72     auto text1 = utf8ToUtf16("android");
73     auto text2 = utf8ToUtf16("ANDROID");
74     MinikinPaint paint(buildFontCollection("Ascii.ttf"));
75 
76     TestableLayoutCache layoutCache(10);
77 
78     LayoutCapture layout1;
79     LayoutCapture layout2;
80 
81     {
82         SCOPED_TRACE("Different text");
83         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint, false /* LTR */,
84                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT, false, layout1);
85         layoutCache.getOrCreate(text2, Range(0, text2.size()), paint, false /* LTR */,
86                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT, false, layout2);
87         EXPECT_NE(layout1.get(), layout2.get());
88     }
89     {
90         SCOPED_TRACE("Different range");
91         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint, false /* LTR */,
92                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT, false, layout1);
93         layoutCache.getOrCreate(text1, Range(1, text1.size()), paint, false /* LTR */,
94                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT, false, layout2);
95         EXPECT_NE(layout1.get(), layout2.get());
96     }
97     {
98         SCOPED_TRACE("Different text");
99         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint, false /* LTR */,
100                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT, false, layout1);
101         layoutCache.getOrCreate(text2, Range(0, text2.size()), paint, false /* LTR */,
102                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT, false, layout2);
103         EXPECT_NE(layout1.get(), layout2.get());
104     }
105     {
106         SCOPED_TRACE("Different direction");
107         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint, false /* LTR */,
108                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT, false, layout1);
109         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint, true /* RTL */,
110                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT, false, layout2);
111         EXPECT_NE(layout1.get(), layout2.get());
112     }
113     {
114         SCOPED_TRACE("Different start hyphenation");
115         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint, false /* LTR */,
116                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT, false, layout1);
117         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint, false /* LTR */,
118                                 StartHyphenEdit::INSERT_HYPHEN, EndHyphenEdit::NO_EDIT, false,
119                                 layout2);
120         EXPECT_NE(layout1.get(), layout2.get());
121     }
122     {
123         SCOPED_TRACE("Different end hyphen");
124         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint, false /* LTR */,
125                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT, false, layout1);
126         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint, false /* LTR */,
127                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::INSERT_HYPHEN, false,
128                                 layout2);
129         EXPECT_NE(layout1.get(), layout2.get());
130     }
131     {
132         SCOPED_TRACE("Different collection");
133         MinikinPaint paint1(buildFontCollection("Ascii.ttf"));
134         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint1, false /* LTR */,
135                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT, false, layout1);
136         MinikinPaint paint2(buildFontCollection("Emoji.ttf"));
137         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint2, false /* LTR */,
138                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT, false, layout2);
139         EXPECT_NE(layout1.get(), layout2.get());
140     }
141     {
142         SCOPED_TRACE("Different size");
143         auto collection = buildFontCollection("Ascii.ttf");
144         MinikinPaint paint1(collection);
145         paint1.size = 10.0f;
146         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint1, false /* LTR */,
147                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT, false, layout1);
148         MinikinPaint paint2(collection);
149         paint2.size = 20.0f;
150         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint2, false /* LTR */,
151                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT, false, layout2);
152         EXPECT_NE(layout1.get(), layout2.get());
153     }
154     {
155         SCOPED_TRACE("Different scale X");
156         auto collection = buildFontCollection("Ascii.ttf");
157         MinikinPaint paint1(collection);
158         paint1.scaleX = 1.0f;
159         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint1, false /* LTR */,
160                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT, false, layout1);
161         MinikinPaint paint2(collection);
162         paint2.scaleX = 2.0f;
163         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint2, false /* LTR */,
164                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT, false, layout2);
165         EXPECT_NE(layout1.get(), layout2.get());
166     }
167     {
168         SCOPED_TRACE("Different skew X");
169         auto collection = buildFontCollection("Ascii.ttf");
170         MinikinPaint paint1(collection);
171         paint1.skewX = 1.0f;
172         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint1, false /* LTR */,
173                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT, false, layout1);
174         MinikinPaint paint2(collection);
175         paint2.skewX = 2.0f;
176         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint2, false /* LTR */,
177                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT, false, layout2);
178         EXPECT_NE(layout1.get(), layout2.get());
179     }
180     {
181         SCOPED_TRACE("Different letter spacing");
182         auto collection = buildFontCollection("Ascii.ttf");
183         MinikinPaint paint1(collection);
184         paint1.letterSpacing = 0.0f;
185         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint1, false /* LTR */,
186                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT, false, layout1);
187         MinikinPaint paint2(collection);
188         paint2.letterSpacing = 1.0f;
189         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint2, false /* LTR */,
190                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT, false, layout2);
191         EXPECT_NE(layout1.get(), layout2.get());
192     }
193     {
194         SCOPED_TRACE("Different word spacing");
195         auto collection = buildFontCollection("Ascii.ttf");
196         MinikinPaint paint1(collection);
197         paint1.wordSpacing = 0.0f;
198         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint1, false /* LTR */,
199                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT, false, layout1);
200         MinikinPaint paint2(collection);
201         paint2.wordSpacing = 1.0f;
202         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint2, false /* LTR */,
203                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT, false, layout2);
204         EXPECT_NE(layout1.get(), layout2.get());
205     }
206     {
207         SCOPED_TRACE("Different paint flags");
208         auto collection = buildFontCollection("Ascii.ttf");
209         MinikinPaint paint1(collection);
210         paint1.fontFlags = 0;
211         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint1, false /* LTR */,
212                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT, false, layout1);
213         MinikinPaint paint2(collection);
214         paint2.fontFlags = LinearMetrics_Flag;
215         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint2, false /* LTR */,
216                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT, false, layout2);
217         EXPECT_NE(layout1.get(), layout2.get());
218     }
219     {
220         SCOPED_TRACE("Different locale list ID");
221         auto collection = buildFontCollection("Ascii.ttf");
222         MinikinPaint paint1(collection);
223         paint1.localeListId = LocaleListCache::getId("en-US");
224         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint1, false /* LTR */,
225                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT, false, layout1);
226         MinikinPaint paint2(collection);
227         paint2.localeListId = LocaleListCache::getId("ja-JP");
228         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint2, false /* LTR */,
229                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT, false, layout2);
230         EXPECT_NE(layout1.get(), layout2.get());
231     }
232     {
233         SCOPED_TRACE("Different family variant");
234         auto collection = buildFontCollection("Ascii.ttf");
235         MinikinPaint paint1(collection);
236         paint1.familyVariant = FamilyVariant::DEFAULT;
237         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint1, false /* LTR */,
238                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT, false, layout1);
239         MinikinPaint paint2(collection);
240         paint2.familyVariant = FamilyVariant::COMPACT;
241         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint2, false /* LTR */,
242                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT, false, layout2);
243         EXPECT_NE(layout1.get(), layout2.get());
244     }
245     {
246         SCOPED_TRACE("Different font feature settings");
247         auto collection = buildFontCollection("Ascii.ttf");
248         MinikinPaint paint1(collection);
249         paint1.fontFeatureSettings = FontFeature::parse("");
250         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint1, false /* LTR */,
251                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT, false, layout1);
252         MinikinPaint paint2(collection);
253         paint2.fontFeatureSettings = FontFeature::parse("'liga' on");
254         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint2, false /* LTR */,
255                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT, false, layout2);
256         EXPECT_NE(layout1.get(), layout2.get());
257     }
258 }
259 
TEST(LayoutCacheTest,cacheOverflowTest)260 TEST(LayoutCacheTest, cacheOverflowTest) {
261     auto text = utf8ToUtf16("android");
262     Range range(0, text.size());
263     MinikinPaint paint(buildFontCollection("Ascii.ttf"));
264 
265     TestableLayoutCache layoutCache(5);
266 
267     LayoutCapture layout1;
268     layoutCache.getOrCreate(text, range, paint, false /* LTR */, StartHyphenEdit::NO_EDIT,
269                             EndHyphenEdit::NO_EDIT, false, layout1);
270 
271     for (char c = 'a'; c <= 'z'; c++) {
272         auto text1 = utf8ToUtf16(std::string(10, c));
273         LayoutCapture layout2;
274         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint, false /* LTR */,
275                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT, false, layout2);
276     }
277 
278     LayoutCapture layout3;
279     layoutCache.getOrCreate(text, range, paint, false /* LTR */, StartHyphenEdit::NO_EDIT,
280                             EndHyphenEdit::NO_EDIT, false, layout3);
281     EXPECT_NE(layout1.get(), layout3.get());
282 }
283 
TEST(LayoutCacheTest,cacheLengthLimitTest)284 TEST(LayoutCacheTest, cacheLengthLimitTest) {
285     auto text = utf8ToUtf16(std::string(130, 'a'));
286     Range range(0, text.size());
287     MinikinPaint paint(buildFontCollection("Ascii.ttf"));
288 
289     TestableLayoutCache layoutCache(140);
290 
291     LayoutCapture layout;
292     layoutCache.getOrCreate(text, range, paint, false /* LTR */, StartHyphenEdit::NO_EDIT,
293                             EndHyphenEdit::NO_EDIT, false, layout);
294 
295     EXPECT_EQ(layoutCache.getCacheSize(), 0u);
296 }
297 
TEST(LayoutCacheTest,boundsCalculation)298 TEST(LayoutCacheTest, boundsCalculation) {
299     auto text1 = utf8ToUtf16("android");
300     MinikinPaint paint(buildFontCollection("Ascii.ttf"));
301 
302     TestableLayoutCache layoutCache(10);
303 
304     LayoutCapture layout1;
305     LayoutCapture layout2;
306 
307     {
308         SCOPED_TRACE("Bounds calculation should be treated different layout cache entry");
309         layoutCache.clear();
310         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint, false /* LTR */,
311                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT,
312                                 false /* calculateBounds */, layout1);
313         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint, false /* LTR */,
314                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT,
315                                 true /* calculateBounds */, layout2);
316         EXPECT_NE(layout1.get(), layout2.get());
317         EXPECT_FALSE(layout1.bounds().isValid());
318         EXPECT_TRUE(layout2.bounds().isValid());
319     }
320     {
321         SCOPED_TRACE("Bounds calculated entry can be used for the non-bounds request.");
322         layoutCache.clear();
323         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint, false /* LTR */,
324                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT,
325                                 true /* calculateBounds */, layout1);
326         layoutCache.getOrCreate(text1, Range(0, text1.size()), paint, false /* LTR */,
327                                 StartHyphenEdit::NO_EDIT, EndHyphenEdit::NO_EDIT,
328                                 false /* calculateBounds */, layout2);
329         EXPECT_EQ(layout1.get(), layout2.get());
330         EXPECT_TRUE(layout1.bounds().isValid());
331     }
332 }
333 
334 }  // namespace minikin
335