1 /*
<lambda>null2  * Copyright 2024 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 package androidx.pdf
18 
19 import android.content.Context
20 import android.graphics.Bitmap
21 import android.graphics.Color
22 import android.graphics.PointF
23 import android.graphics.Rect
24 import android.os.Build
25 import android.util.Size
26 import androidx.annotation.RequiresExtension
27 import androidx.pdf.utils.TestUtils
28 import androidx.test.core.app.ApplicationProvider
29 import androidx.test.ext.junit.runners.AndroidJUnit4
30 import androidx.test.filters.SdkSuppress
31 import androidx.test.filters.SmallTest
32 import com.google.common.truth.Truth.assertThat
33 import junit.framework.TestCase.assertFalse
34 import junit.framework.TestCase.assertNotNull
35 import kotlinx.coroutines.Dispatchers
36 import kotlinx.coroutines.test.runTest
37 import org.junit.Assert.assertEquals
38 import org.junit.Test
39 import org.junit.runner.RunWith
40 
41 @SmallTest
42 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM, codeName = "VanillaIceCream")
43 @RunWith(AndroidJUnit4::class)
44 class SandboxedPdfDocumentTest {
45 
46     @Test
47     fun getPageInfo_validPageNumber_returnsValidPageInfo() = runTest {
48         withDocument(PDF_DOCUMENT) { document ->
49             val pageNumber = 0
50 
51             val pageInfo = document.getPageInfo(pageNumber)
52 
53             val expectedHeight = 792
54             val expectedWidth = 612
55             assertThat(pageInfo.pageNum == pageNumber).isTrue()
56             assertThat(pageInfo.height == expectedHeight).isTrue()
57             assertThat(pageInfo.width == expectedWidth).isTrue()
58         }
59     }
60 
61     @Test
62     fun getPageInfo_validDimension_onCorruptedPage() = runTest {
63         withDocument(PDF_DOCUMENT_PARTIALLY_CORRUPTED_FILE) { document ->
64             val pageNumber = 5
65 
66             val pageInfo = document.getPageInfo(pageNumber)
67 
68             val expectedHeight = 400
69             val expectedWidth = 400
70             assertThat(pageInfo.pageNum == pageNumber).isTrue()
71             assertThat(pageInfo.height == expectedHeight).isTrue()
72             assertThat(pageInfo.width == expectedWidth).isTrue()
73         }
74     }
75 
76     @Test(expected = IllegalArgumentException::class)
77     fun getPageInfo_invalidPage_throwsIllegalArgumentException() = runTest {
78         withDocument(PDF_DOCUMENT) { document ->
79             val pageNumber = 4
80 
81             document.getPageInfo(pageNumber)
82         }
83     }
84 
85     @Test
86     fun getPageInfos_partialPageRange_returnsValidPageInfos() = runTest {
87         withDocument(PDF_DOCUMENT) { document ->
88             val pageRange = 1..2
89 
90             val pageInfos = document.getPageInfos(pageRange)
91 
92             val expectedHeight = 792
93             val expectedWidth = 612
94             val pageIterator = pageInfos.iterator()
95 
96             assertThat(pageInfos.size == 2)
97             for (index: Int in pageRange) {
98                 assertThat(pageIterator.hasNext())
99                 val pageInfo = pageIterator.next()
100                 assertThat(pageInfo.pageNum == index).isTrue()
101                 assertThat(pageInfo.height == expectedHeight).isTrue()
102                 assertThat(pageInfo.width == expectedWidth).isTrue()
103             }
104         }
105     }
106 
107     @Test(expected = IllegalArgumentException::class)
108     fun getPageInfos_invalidPageRange_throwsIllegalArgumentException() = runTest {
109         withDocument(PDF_DOCUMENT) { document ->
110             val invalidPageRange = 2..4
111 
112             document.getPageInfos(invalidPageRange)
113         }
114     }
115 
116     @Test
117     fun searchDocument_singlePageSearch_returnsSparseArrayOfResults() = runTest {
118         withDocument(PDF_DOCUMENT) { document ->
119             val query = "lorem"
120             val pageRange = 0..0
121 
122             val results = document.searchDocument(query, pageRange)
123 
124             val expectedTotalPageResults = 1
125             val expectedFirstPageResults = 2
126 
127             assertThat(results.size() == expectedTotalPageResults).isTrue()
128             assertThat(results[0].size == expectedFirstPageResults).isTrue()
129         }
130     }
131 
132     @Test
133     fun searchDocument_partialDocumentSearch_returnsSparseArrayOfResults() = runTest {
134         withDocument(PDF_DOCUMENT) { document ->
135             val query = "lorem"
136             val pageRange = 1..2
137 
138             val results = document.searchDocument(query, pageRange)
139 
140             val expectedTotalPageResults = 2
141             val expectedSecondPageResults = 1
142             val expectedThirdPageResults = 1
143 
144             assertThat(results.size() == expectedTotalPageResults).isTrue()
145             assertThat(results[0] == null).isTrue()
146             assertThat(results[1].size == expectedSecondPageResults).isTrue()
147             assertThat(results[2].size == expectedThirdPageResults).isTrue()
148         }
149     }
150 
151     @Test
152     fun searchDocument_fullDocumentSearch_returnsSparseArrayOfResults() = runTest {
153         withDocument(PDF_DOCUMENT) { document ->
154             val query = "lorem"
155             val pageRange = 0..2
156 
157             val results = document.searchDocument(query, pageRange)
158 
159             val expectedTotalPageResults = 3
160             val expectedFirstPageResults = 2
161             val expectedSecondPageResults = 1
162             val expectedThirdPageResults = 1
163 
164             assertThat(results.size() == expectedTotalPageResults).isTrue()
165             assertThat(results[0].size == expectedFirstPageResults).isTrue()
166             assertThat(results[1].size == expectedSecondPageResults).isTrue()
167             assertThat(results[2].size == expectedThirdPageResults).isTrue()
168         }
169     }
170 
171     @Test
172     fun searchDocument_fullDocumentSearch_withSinglePageResults() = runTest {
173         withDocument(PDF_DOCUMENT) { document ->
174             val query = "pages are all the same size"
175             val pageRange = 0..2
176 
177             val results = document.searchDocument(query, pageRange)
178 
179             // Assert sparse array doesn't contain empty result lists
180             assertEquals(1, results.size())
181             // Assert single result on first page
182             assertEquals(1, results[0].size)
183         }
184     }
185 
186     @RequiresExtension(extension = Build.VERSION_CODES.S, version = 13)
187     @Test
188     fun getSelectionBounds_returnsPageSelection() = runTest {
189         withDocument(PDF_DOCUMENT) { document ->
190             val pageNumber = 0
191             val start = PointF(100f, 100f)
192             val stop = PointF(120f, 100f)
193 
194             val selection = document.getSelectionBounds(pageNumber, start, stop)
195 
196             val expectedSelectedText = "F i"
197             assertThat(selection != null).isTrue()
198             assertThat(selection!!.page == pageNumber).isTrue()
199             assertThat(selection.selectedTextContents.size == 1).isTrue()
200             assertThat(selection.selectedTextContents[0].text == expectedSelectedText).isTrue()
201         }
202     }
203 
204     @Test(expected = IllegalArgumentException::class)
205     fun getSelectionBounds_invalidPageNumber_throwsIllegalArgumentException() = runTest {
206         withDocument(PDF_DOCUMENT) { document ->
207             val pageNumber = -1
208             val start = PointF(100f, 100f)
209             val stop = PointF(200f, 200f)
210 
211             document.getSelectionBounds(pageNumber, start, stop)
212         }
213     }
214 
215     @RequiresExtension(extension = Build.VERSION_CODES.S, version = 13)
216     @Test
217     fun getSelectionBounds_emptySelection_returnsNull() = runTest {
218         withDocument(PDF_DOCUMENT) { document ->
219             val pageNumber = 0
220             val start = PointF(100f, 100f)
221             val stop = PointF(100f, 100f) // Empty selection
222 
223             val selection = document.getSelectionBounds(pageNumber, start, stop)
224 
225             assertThat(selection == null).isTrue()
226         }
227     }
228 
229     @RequiresExtension(extension = Build.VERSION_CODES.S, version = 13)
230     @Test
231     fun getSelectAllSelectionBounds() = runTest {
232         withDocument(PDF_DOCUMENT) { document ->
233             val pageNumber = 0
234 
235             val selection = document.getSelectAllSelectionBounds(pageNumber)?.selectedTextContents
236             val expectedSelection = document.getPageContent(pageNumber)?.textContents
237 
238             assertNotNull(selection)
239             assertNotNull(expectedSelection)
240             assertThat(selection?.size == expectedSelection?.size)
241             for (index: Int in 0..selection!!.size - 1) {
242                 assertThat(selection[index].text == expectedSelection!![index].text).isTrue()
243             }
244         }
245     }
246 
247     @Test
248     fun getPageContent_validPageNumber_returnsPageContentWithTextAndImages() = runTest {
249         withDocument(PDF_DOCUMENT_WITH_TEXT_AND_IMAGE) { document ->
250             val pageNumber = 0
251             val expectedImageContentSize = 1
252             val expectedAltText = "Social Security Administration Logo"
253             val expectedTextContentSize = 1
254 
255             val pageContent = document.getPageContent(pageNumber)
256 
257             assertThat(pageContent != null).isTrue()
258             assertThat(pageContent!!.textContents.size == expectedTextContentSize).isTrue()
259             assertThat(pageContent.imageContents.size == expectedImageContentSize).isTrue()
260             assertThat(pageContent.imageContents[0].altText == expectedAltText).isTrue()
261         }
262     }
263 
264     @Test
265     fun getPageContent_pageWithOnlyText_returnsPageContentWithText() = runTest {
266         withDocument(PDF_DOCUMENT) { document ->
267             val pageNumber = 0
268 
269             val pageContent = document.getPageContent(pageNumber)
270 
271             assertThat(pageContent != null).isTrue()
272             assertThat(pageContent!!.textContents.isNotEmpty()).isTrue()
273             assertThat(pageContent.imageContents.isEmpty()).isTrue()
274         }
275     }
276 
277     @Test
278     fun getPageLinks_validPageNumber_returnsPageLinksWithGotoAndExternalLinks() = runTest {
279         withDocument(PDF_DOCUMENT_WITH_LINKS) { document ->
280             val pageNumber = 0
281 
282             val pageLinks = document.getPageLinks(pageNumber)
283 
284             assertThat(pageLinks.gotoLinks.isNotEmpty()).isTrue()
285             assertThat(pageLinks.externalLinks.isNotEmpty()).isTrue()
286         }
287     }
288 
289     @Test
290     fun getBitmap_fullPage_returnsValidBitmap() = runTest {
291         val document = openDocument(PDF_DOCUMENT)
292         val pageNumber = 0
293         val scaledPageSizePx = Size(500, 600)
294 
295         val bitmapSource = document.getPageBitmapSource(pageNumber)
296         val bitmap = bitmapSource.getBitmap(scaledPageSizePx, tileRegion = null)
297 
298         assertThat(bitmap.width == scaledPageSizePx.width).isTrue()
299         assertThat(bitmap.height == scaledPageSizePx.height).isTrue()
300         assertFalse(bitmap.checkIsAllWhite())
301         // TODO(b/377922353): Update this test for a more accurate bitmap comparison
302     }
303 
304     @Test
305     fun getBitmap_tileRegion_returnsValidBitmap() = runTest {
306         val document = openDocument(PDF_DOCUMENT)
307         val pageNumber = 0
308         val scaledPageSizePx = Size(500, 600)
309         val tileRegion = Rect(100, 100, 300, 400) // Example tile region
310 
311         val bitmapSource = document.getPageBitmapSource(pageNumber)
312         bitmapSource.getBitmap(scaledPageSizePx, tileRegion = null)
313         val bitmap = bitmapSource.getBitmap(scaledPageSizePx, tileRegion)
314 
315         assertThat(bitmap.width == tileRegion.width()).isTrue()
316         assertThat(bitmap.height == tileRegion.height()).isTrue()
317         assertFalse(bitmap.checkIsAllWhite())
318         // TODO(b/377922353): Update this test for a more accurate bitmap comparison
319     }
320 
321     companion object {
322         private const val PDF_DOCUMENT = "sample.pdf"
323         private const val PDF_DOCUMENT_WITH_LINKS = "sample_links.pdf"
324         private const val PDF_DOCUMENT_PARTIALLY_CORRUPTED_FILE = "partially_corrupted.pdf"
325         private const val PDF_DOCUMENT_WITH_TEXT_AND_IMAGE = "alt_text.pdf"
326 
327         private suspend fun withDocument(filename: String, block: suspend (PdfDocument) -> Unit) {
328             val document = openDocument(filename)
329             try {
330                 block(document)
331             } catch (exception: Exception) {
332                 throw exception
333             } finally {
334                 runTest { document.close() }
335             }
336         }
337 
338         private suspend fun openDocument(filename: String): PdfDocument {
339             val context = ApplicationProvider.getApplicationContext<Context>()
340             val loader =
341                 SandboxedPdfLoader(
342                     context,
343                     Dispatchers.Main,
344                 )
345             val uri = TestUtils.openFile(context, filename)
346 
347             return loader.openDocument(uri)
348         }
349 
350         private fun Bitmap.checkIsAllWhite(): Boolean {
351             for (x in 0 until width) {
352                 for (y in 0 until height) {
353                     if (getPixel(x, y) != Color.WHITE) {
354                         return false
355                     }
356                 }
357             }
358             return true
359         }
360     }
361 }
362