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