diff --git a/.github/workflows/android-ci.yml b/.github/workflows/android-ci.yml index 34af9b324..dc426afd2 100644 --- a/.github/workflows/android-ci.yml +++ b/.github/workflows/android-ci.yml @@ -18,8 +18,8 @@ jobs: - name: Setup Java uses: actions/setup-java@v3.11.0 with: - java-version: 11 - distribution: 'adopt' + java-version: 17 + distribution: 'temurin' - name: Build & Test run: ./gradlew build test @@ -42,8 +42,8 @@ jobs: - name: Set up JDK uses: actions/setup-java@v3.11.0 with: - java-version: 11 - distribution: 'adopt' + java-version: 17 + distribution: 'temurin' - name: Instrumentation Tests uses: reactivecircus/android-emulator-runner@v2 @@ -79,8 +79,8 @@ jobs: - name: Configure JDK uses: actions/setup-java@v3.11.0 with: - java-version: 11 - distribution: 'adopt' + java-version: 17 + distribution: 'temurin' - name: Upload Snapshot run: ./gradlew publish diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 13c48b3db..8b3f634d3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,8 +15,8 @@ jobs: - name: Configure JDK uses: actions/setup-java@v3.11.0 with: - java-version: 11 - distribution: 'adopt' + java-version: 17 + distribution: 'temurin' - name: Set Release Version Name run: sed -e s/-SNAPSHOT//g -i gradle.properties; diff --git a/README.md b/README.md index d408870fb..f80680b75 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Add the following to dependency to `build.gradle`: ```gradle dependencies { - implementation 'com.tom-roush:pdfbox-android:2.0.27.0' + implementation 'com.tom-roush:pdfbox-android:2.0.37.1' } ``` @@ -36,16 +36,23 @@ Reading JPX Images Android does not come with native support for handling JPX images. These images can be read using the [JP2Android library](https://github.com/ThalesGroup/JP2ForAndroid). As JPX is not a common image format, this library is not included with PdfBox-Android by default. If the JP2Android library is not on the classpath of your application, JPX images will be ignored and a warning will be logged. -To include the JP2Android library, add the following to your project's Gradle `dependencies` section. Note that this library is available in JCenter only, so you will need to add `jcenter()` to your repository list. -```gradle +To include the JP2Android library with 16 KB page-size alignment in your own application, add the Maven Central dependency: +```kotlin +dependencies { + implementation("dev.keiji.jp2:jp2-android:1.0.5") +} +``` +Or in Groovy DSL: +```groovy dependencies { - implementation 'com.gemalto.jp2:jp2-android:1.0.3' + implementation 'dev.keiji.jp2:jp2-android:1.0.5' } ``` +Note: `dev.keiji.jp2:jp2-android` is distributed through Maven Central and includes 16 KB page-size aligned ELF LOAD segments for modern Android (Android 15+) compatibility. Important notes ============== -* Currently based on PDFBox v2.0.27 +* Currently based on PDFBox v2.0.37 -* Requires API 19 or greater for full functionality +* Requires API 23 or greater for full functionality diff --git a/build.gradle b/build.gradle deleted file mode 100644 index bed855e79..000000000 --- a/build.gradle +++ /dev/null @@ -1,33 +0,0 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. -defaultTasks 'clean','build','connectedCheck' - -buildscript { - repositories { - mavenCentral() - google() - jcenter() // jp2-android - } - dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' - } -} - -allprojects { - version = VERSION_NAME - - repositories { -// Enable downloading snapshot versions -// maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots/" } - mavenCentral() - google() - jcenter() - } - -// Show more warnings if desired -// gradle.projectsEvaluated { -// tasks.withType(JavaCompile) { -// options.encoding = 'UTF-8' -// options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" -// } -// } -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 000000000..d35d59c3d --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.android.library) apply false +} + +allprojects { + version = project.findProperty("VERSION_NAME") as String? ?: "2.0.37.1" +} diff --git a/gradle.properties b/gradle.properties index dad4641be..67727bcf4 100755 --- a/gradle.properties +++ b/gradle.properties @@ -11,14 +11,16 @@ # The setting is particularly useful for tweaking memory settings. # Default value: -Xmx10248m -XX:MaxPermSize=256m # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m +org.gradle.jvmargs=-Xmx2048m org.gradle.parallel=true -VERSION_NAME=2.0.27.1-SNAPSHOT -VERSION_CODE=1 +VERSION_NAME=2.0.37.1 +VERSION_CODE=2 -ANDROID_BUILD_MIN_SDK_VERSION=14 -ANDROID_BUILD_TARGET_SDK_VERSION=33 -ANDROID_BUILD_SDK_VERSION=33 +ANDROID_BUILD_MIN_SDK_VERSION=23 +ANDROID_BUILD_TARGET_SDK_VERSION=36 +ANDROID_BUILD_SDK_VERSION=36 android.useAndroidX=true +android.nonTransitiveRClass=true +android.suppressUnsupportedCompileSdk=36 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 000000000..d5b1e3785 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,24 @@ +[versions] +agp = "8.8.2" +bouncycastle = "1.86" +appcompat = "1.8.0" +junit = "4.13.2" +mockito = "5.24.0" +diffutils = "1.3.0" +testRunner = "1.7.0" +jp2Android = "1.0.5" + +[libraries] +bouncycastle-bcprov = { module = "org.bouncycastle:bcprov-jdk15to18", version.ref = "bouncycastle" } +bouncycastle-bcpkix = { module = "org.bouncycastle:bcpkix-jdk15to18", version.ref = "bouncycastle" } +bouncycastle-bcutil = { module = "org.bouncycastle:bcutil-jdk15to18", version.ref = "bouncycastle" } +androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } +jp2-android = { module = "dev.keiji.jp2:jp2-android", version.ref = "jp2Android" } +junit = { module = "junit:junit", version.ref = "junit" } +mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" } +java-diffutils = { module = "com.googlecode.java-diff-utils:diffutils", version.ref = "diffutils" } +androidx-test-runner = { module = "androidx.test:runner", version.ref = "testRunner" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +android-library = { id = "com.android.library", version.ref = "agp" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7e62b17ac..709dee457 100755 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/library/build.gradle b/library/build.gradle deleted file mode 100644 index c23a2afee..000000000 --- a/library/build.gradle +++ /dev/null @@ -1,109 +0,0 @@ -plugins { - id 'com.android.library' -} - -ext { - PUBLISH_GROUP_ID = 'com.tom-roush' - PUBLISH_ARTIFACT_ID = 'pdfbox-android' - PUBLISH_VERSION = VERSION_NAME -} - -android { - compileSdkVersion Integer.parseInt(project.ANDROID_BUILD_SDK_VERSION) - compileOptions.encoding = 'UTF-8' - - defaultConfig { - testInstrumentationRunnerArguments notAnnotation: 'androidx.test.filters.FlakyTest' - minSdkVersion Integer.parseInt(project.ANDROID_BUILD_MIN_SDK_VERSION) - targetSdkVersion Integer.parseInt(project.ANDROID_BUILD_TARGET_SDK_VERSION) - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles 'consumer-proguard-rules.txt' - } - - buildTypes { - release { - minifyEnabled false - } - } - - - testOptions { - unitTests.returnDefaultValues = true - } - - sourceSets { - test { - resources.srcDirs = ['src/main/assets', 'src/test/resources'] - } - androidTest { - assets.srcDirs = ['src/test/resources'] - } - } - lint { - abortOnError false - } - namespace 'com.tom_roush.pdfbox' - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_7 - targetCompatibility JavaVersion.VERSION_1_7 - } - - // Rename the output aars - android.libraryVariants.all { variant -> - variant.outputs.all { output -> - if (outputFileName.endsWith('.aar')) { - outputFileName = "pdfbox-android-${variant.name}-${defaultConfig.versionName}.aar" - } - } - } -} - -// Test output from https://stackoverflow.com/questions/3963708/gradle-how-to-display-test-results-in-the-console-in-real-time -import org.gradle.api.tasks.testing.logging.TestExceptionFormat -import org.gradle.api.tasks.testing.logging.TestLogEvent - -tasks.withType(Test) { - // Performance improvements for tests - maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 - reports.html.enabled = false - reports.junitXml.enabled = false - - testLogging { - // set options for log level LIFECYCLE - events TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED, TestLogEvent.STANDARD_OUT, TestLogEvent.STANDARD_ERROR - showExceptions true - exceptionFormat TestExceptionFormat.FULL - showCauses true - showStackTraces true - - info.events = debug.events - info.exceptionFormat = debug.exceptionFormat - - afterSuite { desc, result -> - if (!desc.parent) { // will match the outermost suite - def output = "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} successes, ${result.failedTestCount} failures, ${result.skippedTestCount} skipped)" - def startItem = '| ', endItem = ' |' - def repeatLength = startItem.length() + output.length() + endItem.length() - println('\n' + ('-' * repeatLength) + '\n' + startItem + output + endItem + '\n' + ('-' * repeatLength)) - } - } - } -} - -dependencies { - api "org.bouncycastle:bcprov-jdk15to18:1.73" - api "org.bouncycastle:bcpkix-jdk15to18:1.73" - api "org.bouncycastle:bcutil-jdk15to18:1.73" - // for jpeg2000 decode/encode - compileOnly 'com.gemalto.jp2:jp2-android:1.0.3' - - // Test dependencies - testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-core:5.8.0' - testImplementation 'com.googlecode.java-diff-utils:diffutils:1.3.0' - - androidTestImplementation 'androidx.test:runner:1.5.1' -} - -apply from: 'maven-publish.gradle' diff --git a/library/build.gradle.kts b/library/build.gradle.kts new file mode 100644 index 000000000..7e87473cc --- /dev/null +++ b/library/build.gradle.kts @@ -0,0 +1,165 @@ +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.gradle.api.tasks.testing.logging.TestLogEvent + +plugins { + alias(libs.plugins.android.library) + `maven-publish` + signing +} + +val minSdkProp: String by project +val compileSdkProp: String by project +val versionNameProp = project.findProperty("VERSION_NAME") as String? ?: "2.0.37.1" + +android { + namespace = "com.tom_roush.pdfbox" + compileSdk = (project.findProperty("ANDROID_BUILD_SDK_VERSION") as String?)?.toInt() ?: 37 + + defaultConfig { + minSdk = (project.findProperty("ANDROID_BUILD_MIN_SDK_VERSION") as String?)?.toInt() ?: 23 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunnerArguments["notAnnotation"] = "androidx.test.filters.FlakyTest" + consumerProguardFiles("consumer-proguard-rules.txt") + } + + buildTypes { + release { + isMinifyEnabled = false + } + } + + testOptions { + unitTests.isReturnDefaultValues = true + } + + sourceSets { + getByName("test") { + resources.srcDirs("src/main/assets", "src/test/resources") + } + getByName("androidTest") { + assets.srcDirs("src/test/resources") + } + } + + lint { + abortOnError = false + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + encoding = "UTF-8" + } + + publishing { + singleVariant("release") { + withSourcesJar() + withJavadocJar() + } + } +} + +tasks.withType { + maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1) + + testLogging { + events( + TestLogEvent.PASSED, + TestLogEvent.SKIPPED, + TestLogEvent.FAILED, + TestLogEvent.STANDARD_OUT, + TestLogEvent.STANDARD_ERROR + ) + showExceptions = true + exceptionFormat = TestExceptionFormat.FULL + showCauses = true + showStackTraces = true + + afterSuite(KotlinClosure2({ desc, result -> + if (desc.parent == null) { + val output = "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} successes, ${result.failedTestCount} failures, ${result.skippedTestCount} skipped)" + val startItem = "| " + val endItem = " |" + val repeatLength = startItem.length + output.length + endItem.length + println("\n" + ("-".repeat(repeatLength)) + "\n" + startItem + output + endItem + "\n" + ("-".repeat(repeatLength))) + } + })) + } +} + +dependencies { + api(libs.bouncycastle.bcprov) + api(libs.bouncycastle.bcpkix) + api(libs.bouncycastle.bcutil) + + // for jpeg2000 decode/encode + compileOnly(libs.jp2.android) + + // Test dependencies + testImplementation(libs.junit) + testImplementation(libs.mockito.core) + testImplementation(libs.java.diffutils) + + androidTestImplementation(libs.androidx.test.runner) +} + +afterEvaluate { + publishing { + publications { + create("release") { + from(components["release"]) + groupId = "com.tom-roush" + artifactId = "pdfbox-android" + version = versionNameProp + + pom { + name.set("PdfBox-Android") + description.set("The Apache PdfBox project ported to work on Android") + url.set("https://github.com/TomRoush/PdfBox-Android") + licenses { + license { + name.set("The Apache Software License, Version 2.0") + url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") + distribution.set("repo") + } + } + developers { + developer { + name.set("Tom Roush") + email.set("tom@tom-roush.com") + organization.set("PdfBox-Android Developers") + organizationUrl.set("https://github.com/TomRoush/PdfBox-Android") + } + } + scm { + connection.set("scm:git:git@github.com:TomRoush/PdfBox-Android.git") + developerConnection.set("scm:git:ssh@github.com:TomRoush/PdfBox-Android.git") + url.set("https://github.com/TomRoush/PdfBox-Android") + } + } + } + } + repositories { + maven { + val isReleaseVersion = !versionNameProp.endsWith("SNAPSHOT") + val releaseRepo = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" + val snapshotRepo = "https://s01.oss.sonatype.org/content/repositories/snapshots/" + url = uri(if (isReleaseVersion) releaseRepo else snapshotRepo) + credentials { + username = project.findProperty("ossrhUsername") as String? ?: System.getenv("OSSRH_USERNAME") + password = project.findProperty("ossrhPassword") as String? ?: System.getenv("OSSRH_PASSWORD") + } + } + } + } + + signing { + val signingKeyId = project.findProperty("signingKeyId") as String? + val signingKey = project.findProperty("signingKey") as String? + val signingPassword = project.findProperty("signingPassword") as String? + if (!signingKey.isNullOrEmpty()) { + useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword) + sign(publishing.publications["release"]) + } + } +} diff --git a/library/maven-publish.gradle b/library/maven-publish.gradle deleted file mode 100644 index c34745ba3..000000000 --- a/library/maven-publish.gradle +++ /dev/null @@ -1,117 +0,0 @@ -apply plugin: 'maven-publish' -apply plugin: 'signing' - -ext.isReleaseVersion = !version.endsWith("SNAPSHOT") - -task androidJavadocs(type: Javadoc) { - failOnError false - source = android.sourceSets.main.java.sourceFiles - classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) - - // disable the crazy super-strict doclint tool in Java 8 - if (JavaVersion.current().isJava8Compatible()) { - options.addStringOption('Xdoclint:none', '-quiet') - } - if (JavaVersion.current().isJava9Compatible()) { - options.addBooleanOption('html5', true) - } - - options.encoding = "UTF-8" - options.docTitle("PdfBox-Android") - options.links("https://docs.oracle.com/en/java/javase/11/docs/api") - options.links("https://developer.android.com/reference/") - - exclude '**/BuildConfig.java' - exclude '**/R.java' -} - -afterEvaluate { - androidJavadocs.classpath += files(android.libraryVariants.collect { variant -> - variant.javaCompileProvider.get().classpath.files - }) -} - -task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { - archiveClassifier.set('javadoc') - from androidJavadocs.destinationDir -} - -task androidSourcesJar(type: Jar) { - archiveClassifier.set('sources') - from android.sourceSets.main.java.srcDirs -} - -def pomConfig = { - url "https://github.com/TomRoush/PdfBox-Android" - licenses { - license { - name "The Apache Software License, Version 2.0" - url "http://www.apache.org/licenses/LICENSE-2.0.txt" - distribution "repo" - } - } - developers { - developer { - name "Tom Roush" - email "tom@tom-roush.com" - organization "PdfBox-Android Developers" - organizationUrl "https://github.com/TomRoush/PdfBox-Android" - } - } - scm { - connection "scm:git:git@github.com:TomRoush/PdfBox-Android.git" - developerConnection "scm:git:ssh@github.com:TomRoush/PdfBox-Android.git" - url "https://github.com/TomRoush/PdfBox-Android" - } -} - -project.afterEvaluate { - publishing { - repositories { - maven { - def releaseRepo = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" - def snapshotRepo = "https://s01.oss.sonatype.org/content/repositories/snapshots/" - url = isReleaseVersion ? releaseRepo : snapshotRepo - credentials { - username = project.hasProperty('ossrhUsername') ? ossrhUsername : System.getenv('OSSRH_USERNAME') - password = project.hasProperty('ossrhPassword') ? ossrhPassword : System.getenv('OSSRH_PASSWORD') - } - } - } - publications { - maven(MavenPublication) { - artifact bundleReleaseAar - artifact androidJavadocsJar - artifact androidSourcesJar - - groupId project.PUBLISH_GROUP_ID - artifactId project.PUBLISH_ARTIFACT_ID - version project.PUBLISH_VERSION - - pom.withXml { - def root = asNode() - root.appendNode('name', 'PdfBox-Android') - root.appendNode('description', 'The Apache PdfBox project ported to work on Android') - root.children().last() + pomConfig - - // Manually add all the pom nodes for dependencies - def dependenciesNode = asNode().appendNode('dependencies') - configurations.api.allDependencies.each { - def dependencyNode = dependenciesNode.appendNode('dependency') - dependencyNode.appendNode('groupId', it.group) - dependencyNode.appendNode('artifactId', it.name) - dependencyNode.appendNode('version', it.version) - } - } - } - } - } - - signing { - def signingKeyId = findProperty("signingKeyId") - def signingKey = findProperty("signingKey") - def signingPassword = findProperty("signingPassword") - useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword) - sign publishing.publications.maven - } -} diff --git a/library/src/androidTest/java/com/tom_roush/pdfbox/encryption/TestPublicKeyEncryption.java b/library/src/androidTest/java/com/tom_roush/pdfbox/encryption/TestPublicKeyEncryption.java index d129558c2..6677d7172 100644 --- a/library/src/androidTest/java/com/tom_roush/pdfbox/encryption/TestPublicKeyEncryption.java +++ b/library/src/androidTest/java/com/tom_roush/pdfbox/encryption/TestPublicKeyEncryption.java @@ -46,6 +46,7 @@ import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; /** @@ -107,11 +108,9 @@ public TestPublicKeyEncryption() @Before public void setUp() throws Exception { - if (Cipher.getMaxAllowedKeyLength("AES") != Integer.MAX_VALUE) - { - // we need strong encryption for these tests - fail("JCE unlimited strength jurisdiction policy files are not installed"); - } + // we need strong encryption for these tests + assertEquals("JCE unlimited strength jurisdiction policy files are not installed", + Integer.MAX_VALUE, Cipher.getMaxAllowedKeyLength("AES")); testContext = InstrumentationRegistry.getInstrumentation().getContext(); PDFBoxResourceLoader.init(testContext); @@ -176,12 +175,10 @@ public void testProtectionError() throws Exception policy.setEncryptionKeyLength(keyLength); document.protect(policy); - PDDocument encryptedDoc = null; + File file = save("testProtectionError"); try { - File file = save("testProtectionError"); - encryptedDoc = reload(file, password2, getKeyStore(keyStore2)); - Assert.assertTrue(encryptedDoc.isEncrypted()); + reload(file, password2, getKeyStore(keyStore2)); fail("No exception when using an incorrect decryption key"); } catch (IOException ex) @@ -190,13 +187,6 @@ public void testProtectionError() throws Exception Assert.assertTrue("not the expected exception: " + msg, msg.contains("serial-#: rid 2 vs. cert 3")); } - finally - { - if (encryptedDoc != null) - { - encryptedDoc.close(); - } - } } diff --git a/library/src/androidTest/java/com/tom_roush/pdfbox/encryption/TestSymmetricKeyEncryption.java b/library/src/androidTest/java/com/tom_roush/pdfbox/encryption/TestSymmetricKeyEncryption.java index 5b7efd518..e4cb5c9fc 100644 --- a/library/src/androidTest/java/com/tom_roush/pdfbox/encryption/TestSymmetricKeyEncryption.java +++ b/library/src/androidTest/java/com/tom_roush/pdfbox/encryption/TestSymmetricKeyEncryption.java @@ -40,6 +40,7 @@ import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.io.IOUtils; import com.tom_roush.pdfbox.pdmodel.PDDocument; +import com.tom_roush.pdfbox.text.PDFTextStripper; import com.tom_roush.pdfbox.pdmodel.PDDocumentCatalog; import com.tom_roush.pdfbox.pdmodel.PDDocumentNameDictionary; import com.tom_roush.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode; @@ -47,6 +48,7 @@ import com.tom_roush.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification; import com.tom_roush.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile; import com.tom_roush.pdfbox.pdmodel.encryption.AccessPermission; +import com.tom_roush.pdfbox.pdmodel.encryption.InvalidPasswordException; import com.tom_roush.pdfbox.pdmodel.encryption.PDEncryption; import com.tom_roush.pdfbox.pdmodel.encryption.StandardProtectionPolicy; import com.tom_roush.pdfbox.pdmodel.encryption.StandardSecurityHandler; @@ -59,6 +61,7 @@ import org.junit.Test; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; @@ -96,11 +99,9 @@ public void setUp() throws Exception testResultsDir = new File(testContext.getCacheDir(), "pdfbox-test-output/crypto"); testResultsDir.mkdirs(); - if (Cipher.getMaxAllowedKeyLength("AES") != Integer.MAX_VALUE) - { - // we need strong encryption for these tests - fail("JCE unlimited strength jurisdiction policy files are not installed"); - } + // we need strong encryption for these tests + assertEquals("JCE unlimited strength jurisdiction policy files are not installed", + Integer.MAX_VALUE, Cipher.getMaxAllowedKeyLength("AES")); permission = new AccessPermission(); permission.setCanAssembleDocument(false); @@ -132,37 +133,20 @@ public void testPermissions() throws Exception restrAP.setCanExtractContent(false); restrAP.setCanModify(false); - byte[] inputFileAsByteArray = getFileResourceAsByteArray("PasswordSample-40bit.pdf"); - checkPerms(inputFileAsByteArray, "owner", fullAP); - checkPerms(inputFileAsByteArray, "user", restrAP); - try - { - checkPerms(inputFileAsByteArray, "", null); - fail("wrong password not detected"); - } - catch (IOException ex) - { - assertEquals("Cannot decrypt PDF, the password is incorrect", ex.getMessage()); - } + checkSeveralPerms(getFileResourceAsByteArray("PasswordSample-40bit.pdf"), fullAP, restrAP); restrAP.setCanAssembleDocument(false); restrAP.setCanExtractForAccessibility(false); restrAP.setCanPrintFaithful(false); - inputFileAsByteArray = getFileResourceAsByteArray("PasswordSample-128bit.pdf"); - checkPerms(inputFileAsByteArray, "owner", fullAP); - checkPerms(inputFileAsByteArray, "user", restrAP); - try - { - checkPerms(inputFileAsByteArray, "", null); - fail("wrong password not detected"); - } - catch (IOException ex) - { - assertEquals("Cannot decrypt PDF, the password is incorrect", ex.getMessage()); - } + checkSeveralPerms(getFileResourceAsByteArray("PasswordSample-128bit.pdf"), fullAP, restrAP); - inputFileAsByteArray = getFileResourceAsByteArray("PasswordSample-256bit.pdf"); + checkSeveralPerms(getFileResourceAsByteArray("PasswordSample-256bit.pdf"), fullAP, restrAP); + } + + private void checkSeveralPerms(byte[] inputFileAsByteArray, AccessPermission fullAP, + AccessPermission restrAP) throws IOException + { checkPerms(inputFileAsByteArray, "owner", fullAP); checkPerms(inputFileAsByteArray, "user", restrAP); try @@ -170,7 +154,7 @@ public void testPermissions() throws Exception checkPerms(inputFileAsByteArray, "", null); fail("wrong password not detected"); } - catch (IOException ex) + catch (InvalidPasswordException ex) { assertEquals("Cannot decrypt PDF, the password is incorrect", ex.getMessage()); } @@ -485,11 +469,85 @@ private void testSymmEncrForKeySizeInner(int keyLength, boolean preferAES, private byte[] getFileResourceAsByteArray(String testFileName) throws IOException { - return IOUtils.toByteArray(testContext.getAssets().open("pdfbox/com/tom_roush/pdfbox/pdmodel/encryption/" + testFileName)); + InputStream is = testContext.getAssets().open("pdfbox/com/tom_roush/pdfbox/pdmodel/encryption/" + testFileName); + try + { + return IOUtils.toByteArray(is); + } + finally + { + IOUtils.closeQuietly(is); + } } private byte[] getFileAsByteArray(File f) throws IOException { - return IOUtils.toByteArray(new FileInputStream(f)); + InputStream is = new FileInputStream(f); + try + { + return IOUtils.toByteArray(is); + } + finally + { + IOUtils.closeQuietly(is); + } + } + + /** + * PDFBOX-5955: test unusual RC4 encryption that has 40 or 48 bits instead of 128. + * + * @throws IOException + */ + @Test + public void testPDFBox5955() throws IOException + { + File TARGETPDFDIR = new File(testContext.getCacheDir(), "pdfs"); + TARGETPDFDIR.mkdirs(); + File file40bit = TestResourceGenerator.downloadTestResource(TARGETPDFDIR, "PDFBOX-5955-40bit.pdf", + "https://issues.apache.org/jira/secure/attachment/13074727/R%3D4%2C%20V%3D4%2C%2040-bit%20RC4.pdf"); + assumeTrue(file40bit.exists()); + File file48bit = TestResourceGenerator.downloadTestResource(TARGETPDFDIR, "PDFBOX-5955-48bit.pdf", + "https://issues.apache.org/jira/secure/attachment/13074728/R%3D4%2C%20V%3D4%2C%2048-bit%20RC4.pdf"); + assumeTrue(file48bit.exists()); + PDDocument doc = PDDocument.load(file40bit); + + PDFTextStripper stripper = new PDFTextStripper(); + String text = stripper.getText(doc); + assertTrue(text.contains("0x0446615747")); + doc.close(); + doc = PDDocument.load(file40bit); + stripper = new PDFTextStripper(); + text = stripper.getText(doc); + assertTrue(text.contains("0x0446615747")); + doc.close(); + doc = PDDocument.load(file48bit); + stripper = new PDFTextStripper(); + text = stripper.getText(doc); + assertTrue(text.contains("0x02988E82AFF8")); + doc.close(); + doc = PDDocument.load(file48bit); + stripper = new PDFTextStripper(); + text = stripper.getText(doc); + assertTrue(text.contains("0x02988E82AFF8")); + doc.close(); + } + + /** + * test AESV3 with R=5 and excess bytes. + * + * @throws IOException + */ + @Test + public void testPDFBox5639() throws IOException + { + File TARGETPDFDIR = new File(testContext.getCacheDir(), "pdfs"); + TARGETPDFDIR.mkdirs(); + File file = TestResourceGenerator.downloadTestResource(TARGETPDFDIR, "PDFBOX-5639.pdf", + "https://issues.apache.org/jira/secure/attachment/13061409/incorrect_password.pdf"); + assumeTrue(file.exists()); + PDDocument document = PDDocument.load(file, "JUL2023rfi"); + assertEquals(2, document.getNumberOfPages()); + document.close(); } } + diff --git a/library/src/androidTest/java/com/tom_roush/pdfbox/multipdf/PDFMergerUtilityTest.java b/library/src/androidTest/java/com/tom_roush/pdfbox/multipdf/PDFMergerUtilityTest.java index c59156aaf..28983d2e6 100644 --- a/library/src/androidTest/java/com/tom_roush/pdfbox/multipdf/PDFMergerUtilityTest.java +++ b/library/src/androidTest/java/com/tom_roush/pdfbox/multipdf/PDFMergerUtilityTest.java @@ -25,10 +25,13 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.util.Collections; import java.util.HashSet; +import java.util.List; +import java.util.TreeSet; import java.util.Map; import java.util.Set; @@ -44,24 +47,42 @@ import com.tom_roush.pdfbox.pdmodel.PDDocumentCatalog; import com.tom_roush.pdfbox.pdmodel.PDPage; import com.tom_roush.pdfbox.pdmodel.PDPageTree; +import com.tom_roush.pdfbox.pdmodel.PDResources; import com.tom_roush.pdfbox.pdmodel.common.COSObjectable; import com.tom_roush.pdfbox.pdmodel.common.PDNameTreeNode; import com.tom_roush.pdfbox.pdmodel.common.PDNumberTreeNode; +import com.tom_roush.pdfbox.pdmodel.documentinterchange.logicalstructure.PDMarkedContentReference; +import com.tom_roush.pdfbox.pdmodel.documentinterchange.logicalstructure.PDParentTreeValue; import com.tom_roush.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureElement; +import com.tom_roush.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureNode; import com.tom_roush.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureTreeRoot; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotation; +import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink; +import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationPopup; +import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationText; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; import com.tom_roush.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageDestination; +import com.tom_roush.pdfbox.pdmodel.interactive.documentnavigation.destination.PDNamedDestination; import com.tom_roush.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageFitDestination; +import com.tom_roush.pdfbox.pdmodel.documentinterchange.markedcontent.PDMarkedContent; +import com.tom_roush.pdfbox.pdmodel.graphics.image.PDImageXObject; +import com.tom_roush.pdfbox.pdmodel.interactive.action.PDActionGoTo; import com.tom_roush.pdfbox.pdmodel.interactive.form.PDAcroForm; import com.tom_roush.pdfbox.pdmodel.interactive.form.PDField; import com.tom_roush.pdfbox.rendering.PDFRenderer; +import com.tom_roush.pdfbox.text.PDFMarkedContentExtractor; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.function.ThrowingRunnable; +import static junit.framework.TestCase.assertFalse; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; @@ -680,6 +701,7 @@ void checkWithNumberTree(PDDocument document) throws IOException { PDDocumentCatalog documentCatalog = document.getDocumentCatalog(); PDNumberTreeNode parentTree = documentCatalog.getStructureTreeRoot().getParentTree(); + assertNotEquals(-1, documentCatalog.getStructureTreeRoot().getParentTreeNextKey()); Map numberTreeAsMap = PDFMergerUtility.getNumberTreeAsMap(parentTree); Set keySet = numberTreeAsMap.keySet(); PDAcroForm acroForm = documentCatalog.getAcroForm(); @@ -698,11 +720,65 @@ void checkWithNumberTree(PDDocument document) throws IOException } } } - for (PDPage page : document.getPages()) + PDPageTree pageTree = document.getPages(); + for (PDPage page : pageTree) { + int pageNum = pageTree.indexOf(page) + 1; if (page.getStructParents() >= 0) { - assertTrue(keySet.contains(page.getStructParents())); + assertTrue("/StructParents " + page.getStructParents() + " from page " + pageNum + " not found in /ParentTree", + keySet.contains(page.getStructParents())); + PDParentTreeValue obj = (PDParentTreeValue) numberTreeAsMap.get(page.getStructParents()); + assertTrue("Expected array in page " + pageNum + ", got " + obj.getClass(), + obj.getCOSObject() instanceof COSArray); + COSArray array = (COSArray) obj.getCOSObject(); + + PDFMarkedContentExtractor markedContentExtractor = new PDFMarkedContentExtractor(); + markedContentExtractor.processPage(page); + List markedContents = markedContentExtractor.getMarkedContents(); + TreeSet set = new TreeSet(); + for (PDMarkedContent pdMarkedContent : markedContents) + { + COSDictionary pdmcProperties = pdMarkedContent.getProperties(); + if (pdmcProperties == null) + { + continue; + } + int mcid = pdMarkedContent.getMCID(); + if (mcid >= 0) + { + // "For a page object (...), the value shall be an array of references + // to the parent elements of those marked-content sequences." + // this means that the /Pg entry doesn't have to match the page + COSDictionary dict = (COSDictionary) array.getObject(mcid); + org.junit.Assert.assertNotNull(dict); + set.add(mcid); + PDStructureElement structureElemen = (PDStructureElement) PDStructureNode.create(dict); + List kids = structureElemen.getKids(); + boolean found = false; + for (Object kid : kids) + { + if (kid instanceof Integer && ((Integer) kid) == mcid) + { + found = true; + break; + } + if (kid instanceof PDMarkedContentReference) + { + PDMarkedContentReference mcr = (PDMarkedContentReference) kid; + if (mcid == mcr.getMCID()) + { + found = true; + break; + } + } + } + assertTrue("page: " + pageNum + ", mcid: " + mcid + " not found", found); + } + } + // actual count may be larger if last element is null, e.g. PDFBOX-4408 + // set can be empty, see last page of pdf_32000_2008.pdf + assertTrue(set.isEmpty() || set.last() <= array.size() - 1); } for (PDAnnotation ann : page.getAnnotations()) { @@ -736,11 +812,22 @@ public void testFileDeletion() throws IOException OutputStream out = new FileOutputStream(outFile); PDFMergerUtility merger = new PDFMergerUtility(); merger.setDestinationStream(out); - merger.addSource(inFile1); - merger.addSource(inFile2); + + // Unrelated: increase test coverage by testing inputStream + InputStream is1 = new FileInputStream(inFile1); + InputStream is2 = new FileInputStream(inFile1); + assertEquals(out, merger.getDestinationStream()); + merger.addSource(is1); + merger.addSource(is2); merger.mergeDocuments(MemoryUsageSetting.setupMainMemoryOnly()); + is1.close(); + is2.close(); out.close(); + PDDocument doc = PDDocument.load(outFile); + assertEquals(2, doc.getNumberOfPages()); + doc.close(); + assertTrue(inFile1.delete()); assertTrue(inFile2.delete()); assertTrue(outFile.delete()); @@ -809,8 +896,9 @@ private void checkForPageOrphans(PDDocument doc) throws IOException // StructTreeRoot/IDTree trees. PDPageTree pageTree = doc.getPages(); PDStructureTreeRoot structureTreeRoot = doc.getDocumentCatalog().getStructureTreeRoot(); - checkElement(pageTree, structureTreeRoot.getParentTree().getCOSObject()); - checkElement(pageTree, structureTreeRoot.getK()); + checkElement(pageTree, structureTreeRoot.getParentTree().getCOSObject(), structureTreeRoot.getCOSObject()); + assertNotNull(structureTreeRoot.getK()); + checkElement(pageTree, structureTreeRoot.getK(), structureTreeRoot.getCOSObject()); checkForIDTreeOrphans(pageTree, structureTreeRoot); } @@ -831,7 +919,7 @@ private void checkForIDTreeOrphans(PDPageTree pageTree, PDStructureTreeRoot stru } if (!element.getKids().isEmpty()) { - checkElement(pageTree, element.getCOSObject().getDictionaryObject(COSName.K)); + checkElement(pageTree, element.getCOSObject().getDictionaryObject(COSName.K), element.getCOSObject()); } } } @@ -870,6 +958,27 @@ else if (base instanceof COSDictionary) ++cnt; set.add(kdict); } + else if (kdict.containsKey(COSName.K)) + { + // at least 1 kid with dict with /Pg, /MCID and type /MCR + // happens with confidential file from PDFBOX-6009 + COSArray kidArray = kdict.getCOSArray(COSName.K); + if (kidArray != null) + { + for (int i = 0; i < kidArray.size(); ++i) + { + COSBase base2 = kidArray.getObject(i); + if (base2 instanceof COSDictionary && + ((COSDictionary) base2).containsKey(COSName.PG) && + ((COSDictionary) base2).containsKey(COSName.MCID)) + { + ++cnt; + set.add(kdict); + break; + } + } + } + } if (kdict.containsKey(COSName.K)) { walk(kdict.getDictionaryObject(COSName.K)); @@ -885,7 +994,7 @@ else if (base instanceof COSDictionary) // See PDF specification Table 325 – Entries in an object reference dictionary // example of file with /Kids: 000153.pdf 000208.pdf 000314.pdf 000359.pdf 000671.pdf // from digitalcorpora site - private void checkElement(PDPageTree pageTree, COSBase base) throws IOException + private void checkElement(PDPageTree pageTree, COSBase base, COSDictionary parentDict) throws IOException { if (base instanceof COSArray) { @@ -895,7 +1004,7 @@ private void checkElement(PDPageTree pageTree, COSBase base) throws IOException { base2 = ((COSObject) base2).getObject(); } - checkElement(pageTree, base2); + checkElement(pageTree, base2, null); } } else if (base instanceof COSDictionary) @@ -908,18 +1017,35 @@ else if (base instanceof COSDictionary) } if (kdict.containsKey(COSName.K)) { - checkElement(pageTree, kdict.getDictionaryObject(COSName.K)); + checkElement(pageTree, kdict.getDictionaryObject(COSName.K), kdict); + + // Check that the /P entry points to the correct object + PDStructureNode node = PDStructureNode.create(kdict); + for (Object obj : node.getKids()) + { + if (obj instanceof PDStructureElement) + { + PDStructureNode parent = ((PDStructureElement) obj).getParent(); + assertTrue(parent.getCOSObject() == kdict); + } + } return; } // if we're in a number tree, check /Nums and /Kids if (kdict.containsKey(COSName.KIDS)) { - checkElement(pageTree, kdict.getDictionaryObject(COSName.KIDS)); + checkElement(pageTree, kdict.getDictionaryObject(COSName.KIDS), kdict); } else if (kdict.containsKey(COSName.NUMS)) { - checkElement(pageTree, kdict.getDictionaryObject(COSName.NUMS)); + checkElement(pageTree, kdict.getDictionaryObject(COSName.NUMS), kdict); + } + + if (COSName.OBJR.equals(kdict.getDictionaryObject(COSName.TYPE)) || + COSName.MCR.equals(kdict.getDictionaryObject(COSName.TYPE))) + { + assertFalse(kdict.getCOSDictionary(COSName.PG) == null && parentDict.getCOSDictionary(COSName.PG) == null); } // if we're an object reference dictionary (/OBJR), check the obj @@ -927,7 +1053,8 @@ else if (kdict.containsKey(COSName.NUMS)) { COSDictionary obj = (COSDictionary) kdict.getDictionaryObject(COSName.OBJ); COSBase type = obj.getDictionaryObject(COSName.TYPE); - if (COSName.ANNOT.equals(type)) + COSBase subtype = obj.getDictionaryObject(COSName.SUBTYPE); + if (COSName.ANNOT.equals(type) || COSName.LINK.equals(subtype)) { PDAnnotation annotation = PDAnnotation.createAnnotation(obj); PDPage page = annotation.getPage(); @@ -1032,4 +1159,357 @@ private void checkForPage(PDPageTree pageTree, PDStructureElement structureEleme Assert.assertNotEquals("Page is not in the page tree", -1, pageTree.indexOf(page)); } } + + @Test + public void testSplitWithStructureTree() throws IOException + { + PDDocument doc = PDDocument.load(testContext.getAssets().open(SRCDIR + "/" + "PDFBOX-4417-001031.pdf")); + Splitter splitter = new Splitter(); + splitter.setStartPage(1); + splitter.setEndPage(2); + splitter.setSplitAtPage(2); + List splitResult = splitter.split(doc); + assertEquals(1, splitResult.size()); + PDDocument dstDoc = splitResult.get(0); + assertEquals(2, dstDoc.getNumberOfPages()); + checkForPageOrphans(dstDoc); + // these tests just verify the status quo. Changes should be checked visually with + // a PDF viewer that can display structural information. + PDStructureTreeRoot structureTreeRoot = dstDoc.getDocumentCatalog().getStructureTreeRoot(); + assertEquals(126, PDFMergerUtility.getIDTreeAsMap(structureTreeRoot.getIDTree()).size()); + assertEquals(2, PDFMergerUtility.getNumberTreeAsMap(structureTreeRoot.getParentTree()).size()); + assertEquals(6, structureTreeRoot.getRoleMap().size()); + dstDoc.close(); + doc.close(); + } + + @Test + public void testSplitWithStructureTreeAndDestinations() throws IOException + { + PDDocument doc = PDDocument.load(testContext.getAssets().open(SRCDIR + "/" + "PDFBOX-5762-722238.pdf")); + Splitter splitter = new Splitter(); + splitter.setStartPage(1); + splitter.setEndPage(2); + splitter.setSplitAtPage(2); + List splitResult = splitter.split(doc); + assertEquals(1, splitResult.size()); + PDDocument dstDoc = splitResult.get(0); + assertEquals(2, dstDoc.getNumberOfPages()); + checkForPageOrphans(dstDoc); + // these tests just verify the status quo. Changes should be checked visually with + // a PDF viewer that can display structural information. + PDStructureTreeRoot structureTreeRoot = dstDoc.getDocumentCatalog().getStructureTreeRoot(); + assertEquals(7, PDFMergerUtility.getNumberTreeAsMap(structureTreeRoot.getParentTree()).size()); + assertEquals(4, structureTreeRoot.getRoleMap().size()); + + // check that destinations are fixed (only the two first point to the split doc) + List annotations = dstDoc.getPage(0).getAnnotations(); + assertEquals(5, annotations.size()); + PDAnnotationLink link1 = (PDAnnotationLink) annotations.get(0); + PDAnnotationLink link2 = (PDAnnotationLink) annotations.get(1); + PDAnnotationLink link3 = (PDAnnotationLink) annotations.get(2); + PDAnnotationLink link4 = (PDAnnotationLink) annotations.get(3); + PDAnnotationLink link5 = (PDAnnotationLink) annotations.get(4); + PDPageDestination pd1 = + (PDPageDestination) ((PDActionGoTo) link1.getAction()).getDestination(); + PDPageDestination pd2 = + (PDPageDestination) ((PDActionGoTo) link2.getAction()).getDestination(); + PDPageDestination pd3 = + (PDPageDestination) ((PDActionGoTo) link3.getAction()).getDestination(); + PDPageDestination pd4 = + (PDPageDestination) ((PDActionGoTo) link4.getAction()).getDestination(); + PDPageDestination pd5 = + (PDPageDestination) ((PDActionGoTo) link5.getAction()).getDestination(); + PDPageTree pageTree = dstDoc.getPages(); + assertEquals(0, pageTree.indexOf(pd1.getPage())); + assertEquals(1, pageTree.indexOf(pd2.getPage())); + assertNull(pd3.getPage()); + assertNull(pd4.getPage()); + assertNull(pd5.getPage()); + dstDoc.close(); + doc.close(); + } + + /** + * PDFBOX-5929: Check that orphan annotations are removed from the structure tree if annotations + * were removed from the pages (don't do that!). + * + * @throws IOException + */ + @Test + public void testSplitWithStructureTreeAndDestinationsAndRemovedAnnotations() throws IOException + { + PDDocument doc = PDDocument.load(testContext.getAssets().open(SRCDIR + "/" + "PDFBOX-5762-722238.pdf")); + Splitter splitter = new Splitter(); + for (PDPage page : doc.getPages()) + { + page.setAnnotations(Collections.EMPTY_LIST); + } + splitter.setStartPage(1); + splitter.setEndPage(2); + splitter.setSplitAtPage(2); + List splitResult = splitter.split(doc); + assertEquals(1, splitResult.size()); + PDDocument dstDoc = splitResult.get(0); + assertEquals(2, dstDoc.getNumberOfPages()); + checkForPageOrphans(dstDoc); + dstDoc.close(); + doc.close(); + } + + /** + * Check for the bug that happened in PDFBOX-5792, where a destination was outside a target + * document and hit an NPE in the next call of Splitter.fixDestinations(). + * + * @throws IOException + */ + @Test + public void testSinglePageSplit() throws IOException + { + PDDocument doc = PDDocument.load(testContext.getAssets().open(SRCDIR + "/" + "PDFBOX-5792-240045.pdf")); + Splitter splitter = new Splitter(); + splitter.setSplitAtPage(1); + List splitResult = splitter.split(doc); + assertEquals(6, splitResult.size()); + for (PDDocument dstDoc : splitResult) + { + assertEquals(1, dstDoc.getNumberOfPages()); + checkForPageOrphans(dstDoc); + for (PDAnnotation ann : dstDoc.getPage(0).getAnnotations()) + { + PDAnnotationLink link = (PDAnnotationLink) ann; + PDActionGoTo action = (PDActionGoTo) link.getAction(); + PDPageDestination destination = (PDPageDestination) action.getDestination(); + assertNull(destination.getPage()); + } + } + PDStructureTreeRoot structureTreeRoot1 = splitResult.get(0).getDocumentCatalog().getStructureTreeRoot(); + assertEquals(6, PDFMergerUtility.getNumberTreeAsMap(structureTreeRoot1.getParentTree()).size()); + assertEquals(3, structureTreeRoot1.getRoleMap().size()); + PDStructureTreeRoot structureTreeRoot2 = splitResult.get(1).getDocumentCatalog().getStructureTreeRoot(); + assertEquals(6, PDFMergerUtility.getNumberTreeAsMap(structureTreeRoot2.getParentTree()).size()); + assertEquals(3, structureTreeRoot2.getRoleMap().size()); + PDStructureTreeRoot structureTreeRoot3 = splitResult.get(2).getDocumentCatalog().getStructureTreeRoot(); + assertEquals(6, PDFMergerUtility.getNumberTreeAsMap(structureTreeRoot3.getParentTree()).size()); + assertEquals(4, structureTreeRoot3.getRoleMap().size()); + PDStructureTreeRoot structureTreeRoot4 = splitResult.get(3).getDocumentCatalog().getStructureTreeRoot(); + assertEquals(5, PDFMergerUtility.getNumberTreeAsMap(structureTreeRoot4.getParentTree()).size()); + assertEquals(4, structureTreeRoot4.getRoleMap().size()); + PDStructureTreeRoot structureTreeRoot5 = splitResult.get(4).getDocumentCatalog().getStructureTreeRoot(); + assertEquals(1, PDFMergerUtility.getNumberTreeAsMap(structureTreeRoot5.getParentTree()).size()); + assertEquals(6, structureTreeRoot5.getRoleMap().size()); + PDStructureTreeRoot structureTreeRoot6 = splitResult.get(5).getDocumentCatalog().getStructureTreeRoot(); + assertEquals(1, PDFMergerUtility.getNumberTreeAsMap(structureTreeRoot6.getParentTree()).size()); + assertEquals(7, structureTreeRoot6.getRoleMap().size()); + for (PDDocument dstDoc : splitResult) + { + dstDoc.close(); + } + doc.close(); + } + + @Test + public void testSplitWithPopupAnnotations() throws IOException + { + PDDocument doc = PDDocument.load(testContext.getAssets().open(SRCDIR + "/" + "PDFBOX-5809-509329.pdf")); + Splitter splitter = new Splitter(); + splitter.setStartPage(3); + splitter.setEndPage(3); + splitter.setSplitAtPage(1); + List splitResult = splitter.split(doc); + assertEquals(1, splitResult.size()); + PDDocument dstDoc = splitResult.get(0); + checkForPageOrphans(dstDoc); + assertEquals(1, dstDoc.getNumberOfPages()); + List annotations = dstDoc.getPage(0).getAnnotations(); + assertEquals(5, annotations.size()); + PDAnnotationText annotationText3 = (PDAnnotationText) annotations.get(3); + PDAnnotationPopup annotationPopup4 = (PDAnnotationPopup) annotations.get(4); + assertEquals(annotationText3.getPopup(), annotationPopup4); + assertEquals(annotationPopup4.getParent(), annotationText3); + assertEquals(annotationText3.getPage(), dstDoc.getPage(0)); + dstDoc.close(); + + // Check that source document is ok + annotations = doc.getPage(2).getAnnotations(); + assertEquals(5, annotations.size()); + annotationText3 = (PDAnnotationText) annotations.get(3); + annotationPopup4 = (PDAnnotationPopup) annotations.get(4); + assertEquals(annotationText3.getPopup(), annotationPopup4); + assertEquals(annotationPopup4.getParent(), annotationText3); + assertEquals(annotationText3.getPage(), doc.getPage(2)); + + doc.close(); + } + + @Test + public void testSplitWithBrokenDestination() throws IOException + { + PDDocument doc = PDDocument.load(testContext.getAssets().open(SRCDIR + "/" + "PDFBOX-5811-362972.pdf")); + Splitter splitter = new Splitter(); + splitter.setStartPage(2); + splitter.setEndPage(2); + List splitResult = splitter.split(doc); + assertEquals(1, splitResult.size()); + List annotations; + PDDocument dstDoc = splitResult.get(0); + checkForPageOrphans(dstDoc); + assertEquals(1, dstDoc.getNumberOfPages()); + annotations = dstDoc.getPage(0).getAnnotations(); + assertEquals(1, annotations.size()); + PDAnnotationLink link = (PDAnnotationLink) annotations.get(0); + assertNull(link.getDestination()); + dstDoc.close(); + // Check source document + annotations = doc.getPage(1).getAnnotations(); + assertEquals(1, annotations.size()); + final PDAnnotationLink link2 = (PDAnnotationLink) annotations.get(0); + assertThrows(IOException.class, new ThrowingRunnable() + { + @Override + public void run() throws Throwable + { + link2.getDestination(); + } + }); + doc.close(); + } + + @Test + public void testSplitWithNamedDestinations() throws IOException + { + PDDocument doc = PDDocument.load(testContext.getAssets().open(SRCDIR + "/" + "PDFBOX-5840-410609.pdf")); + Splitter splitter = new Splitter(); + splitter.setSplitAtPage(6); + List splitResult = splitter.split(doc); + assertEquals(1, splitResult.size()); + List annotations; + PDDocument dstDoc = splitResult.get(0); + checkForPageOrphans(dstDoc); + assertEquals(6, dstDoc.getNumberOfPages()); + annotations = dstDoc.getPage(0).getAnnotations(); + assertEquals(5, annotations.size()); + PDAnnotationLink link1 = (PDAnnotationLink) annotations.get(0); + PDAnnotationLink link2 = (PDAnnotationLink) annotations.get(1); + PDAnnotationLink link3 = (PDAnnotationLink) annotations.get(2); + PDAnnotationLink link4 = (PDAnnotationLink) annotations.get(3); + PDAnnotationLink link5 = (PDAnnotationLink) annotations.get(4); + PDPageDestination pd1 = + (PDPageDestination) ((PDActionGoTo) link1.getAction()).getDestination(); + PDPageDestination pd2 = + (PDPageDestination) ((PDActionGoTo) link2.getAction()).getDestination(); + PDPageDestination pd3 = + (PDPageDestination) ((PDActionGoTo) link3.getAction()).getDestination(); + PDPageDestination pd4 = + (PDPageDestination) ((PDActionGoTo) link4.getAction()).getDestination(); + PDPageDestination pd5 = + (PDPageDestination) ((PDActionGoTo) link5.getAction()).getDestination(); + PDPageTree pageTree = dstDoc.getPages(); + assertEquals(0, pageTree.indexOf(pd1.getPage())); + assertEquals(1, pageTree.indexOf(pd2.getPage())); + assertEquals(3, pageTree.indexOf(pd3.getPage())); + assertEquals(3, pageTree.indexOf(pd4.getPage())); + assertEquals(5, pageTree.indexOf(pd5.getPage())); + dstDoc.close(); + // Check that source document is unchanged + annotations = doc.getPage(0).getAnnotations(); + assertEquals(5, annotations.size()); + PDAnnotationLink link = (PDAnnotationLink) annotations.get(0); + assertTrue(((PDActionGoTo) link.getAction()).getDestination() instanceof PDNamedDestination); + doc.close(); + } + + /** + * PDFBOX-6009: This test verifies that the destination PDF has a /K tree. Before the change, + * nodes with the "wrong" /Pg entries were deleted entirely and because this file has a /Pg + * entry with page 1 at the top, the entire /K tree would be missing. + * + * @throws IOException + */ + @Test + public void testSplitWithPgEntryAtTheTop() throws IOException + { + File inputPdf = TestResourceGenerator.downloadTestResource(TARGETPDFDIR, "PDFBOX-6009.pdf", + "https://issues.apache.org/jira/secure/attachment/13076529/pdfbox-split-missing-tags_mail%2015.5.2025.pdf"); + assumeTrue(inputPdf.exists()); + PDDocument doc = PDDocument.load(inputPdf); + Splitter splitter = new Splitter(); + splitter.setSplitAtPage(1); + List splitResult = splitter.split(doc); + assertEquals(3, splitResult.size()); + for (PDDocument dstDoc : splitResult) + { + assertEquals(1, dstDoc.getNumberOfPages()); + checkWithNumberTree(dstDoc); + checkForPageOrphans(dstDoc); + } + for (PDDocument dstDoc : splitResult) + { + dstDoc.close(); + } + doc.close(); + } + + /** + * PDFBOX-6018: Test split a PDF with popup annotations that are not in the annotations list. + * Verify that after splitting, they still link back to their markup annotation and these to the + * page. + * + * @throws IOException + */ + @Test + public void testSplitWithOrphanPopupAnnotation() throws IOException + { + PDDocument doc = PDDocument.load(testContext.getAssets().open(SRCDIR + "/" + "PDFBOX-6018-099267-p9-OrphanPopups.pdf")); + Splitter splitter = new Splitter(); + List splitResult = splitter.split(doc); + assertEquals(1, splitResult.size()); + PDDocument dstDoc = splitResult.get(0); + assertEquals(1, dstDoc.getNumberOfPages()); + PDPage page = dstDoc.getPage(0); + List annotations = page.getAnnotations(); + assertEquals(2, annotations.size()); + PDAnnotationText ann0 = (PDAnnotationText) annotations.get(0); + PDAnnotationText ann1 = (PDAnnotationText) annotations.get(1); + assertEquals(page, ann0.getPage()); + assertEquals(page, ann1.getPage()); + assertEquals(ann0, ann0.getPopup().getParent()); + assertEquals(ann1, ann1.getPopup().getParent()); + dstDoc.close(); + doc.close(); + } + + /** + * PDFBOX-5939: merge a file with an outline that has itself as a parent without producing a + * stack overflow. + * + * @throws IOException + */ + @Test + public void testOutlinesSelfParent() throws IOException + { + File inputPdf = TestResourceGenerator.downloadTestResource(TARGETPDFDIR, "PDFBOX-5939-google-docs-1.pdf", + "https://issues.apache.org/jira/secure/attachment/13074264/google-docs-1.pdf"); + assumeTrue(inputPdf.exists()); + PDFMergerUtility pdfMergerUtility = new PDFMergerUtility(); + pdfMergerUtility.addSource(new File(TARGETPDFDIR, "PDFBOX-5939-google-docs-1.pdf")); + pdfMergerUtility.addSource(new File(TARGETPDFDIR, "PDFBOX-5939-google-docs-1.pdf")); + pdfMergerUtility.setDestinationFileName(TARGETTESTDIR + "PDFBOX-5939-google-docs-result.pdf"); + pdfMergerUtility.mergeDocuments(MemoryUsageSetting.setupMainMemoryOnly()); + + PDDocument mergedDoc = + PDDocument.load(new File(TARGETTESTDIR, "PDFBOX-5939-google-docs-result.pdf")); + assertEquals(2, mergedDoc.getNumberOfPages()); + mergedDoc.close(); + } + +// /** +// * PDFBOX-515 / PDFBOX-5950: test merging of two files where one file has a stream deep down in +// * the info dictionary (Info/ImPDF/Images/Kids/[0]). +// * TODO: PdfBox-Android not ported: sources ship as a zip (pdfboxpdfs.zip) that maven unpacks +// * into target/pdfs; TestResourceGenerator cannot unpack archives. +// */ +// @Test +// public void testPDFBox515() throws IOException } + diff --git a/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/font/PDFontTest.java b/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/font/PDFontTest.java index 2c7614c70..6428c8c94 100644 --- a/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/font/PDFontTest.java +++ b/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/font/PDFontTest.java @@ -37,6 +37,8 @@ import java.util.ArrayList; import java.util.List; +import com.tom_roush.fontbox.ttf.OTFParser; +import com.tom_roush.fontbox.ttf.OpenTypeFont; import com.tom_roush.fontbox.ttf.TTFParser; import com.tom_roush.fontbox.ttf.TrueTypeCollection; import com.tom_roush.fontbox.ttf.TrueTypeFont; @@ -156,7 +158,9 @@ public void testPDFBOX4115() throws IOException PDPage page = new PDPage(); PDPageContentStream contentStream = new PDPageContentStream(doc, page); - PDType1Font font = new PDType1Font(doc, new FileInputStream(fontFile), WinAnsiEncoding.INSTANCE); + InputStream is = new FileInputStream(fontFile); + PDType1Font font = new PDType1Font(doc, is, WinAnsiEncoding.INSTANCE); + is.close(); contentStream.beginText(); contentStream.setFont(font, 10); @@ -459,4 +463,94 @@ public void testPDFBox5484() throws IOException Assert.assertTrue(area1.equals(area2)); // assertEquals does not test equals() doc.close(); } + + /** + * Check space width. + * + * @throws IOException + */ + @Test + public void PDFBOX5920Type0() throws IOException + { + InputStream is = testContext.getAssets().open( + "com/tom_roush/pdfbox/resources/ttf/LiberationSans-Regular.ttf"); + PDDocument document = new PDDocument(); + PDFont font = PDType0Font.load(document, is, false); + Assert.assertEquals(20064.0f, + font.getStringWidth("The quick brown fox jumps over the lazy dog."), 0.001f); + Assert.assertEquals(278.0f, font.getSpaceWidth(), 0.001f); + document.close(); + } + + /** + * Check space width. + * + * @throws IOException + */ + @Test + public void PDFBOX5920TrueType() throws IOException + { + InputStream is = testContext.getAssets().open( + "com/tom_roush/pdfbox/resources/ttf/LiberationSans-Regular.ttf"); + PDDocument document = new PDDocument(); + PDFont font = PDTrueTypeFont.load(document, is, WinAnsiEncoding.INSTANCE); + Assert.assertEquals(20064.0f, + font.getStringWidth("The quick brown fox jumps over the lazy dog."), 0.001f); + Assert.assertEquals(278.0f, font.getSpaceWidth(), 0.001f); + document.close(); + } + + @Test + public void testSymbol() throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + PDDocument doc = new PDDocument(); + PDPage page = new PDPage(); + PDPageContentStream contentStream = new PDPageContentStream(doc, page); + contentStream.beginText(); + contentStream.setFont(PDType1Font.SYMBOL, 10); + contentStream.newLineAtOffset(10, 700); + // Note that the Alpha is the greek alpha, but the Omega is the Ohm symbol + // (Tested on Windows) + contentStream.showText("\u0391 \u2126"); + contentStream.endText(); + contentStream.close(); + doc.addPage(page); + doc.save(baos); + doc.close(); + + doc = PDDocument.load(baos.toByteArray()); + PDFTextStripper stripper = new PDFTextStripper(); + String text = stripper.getText(doc); + Assert.assertEquals("\u0391 \u2126", text.trim()); + doc.close(); + } + + // PDFBOX-6172: test that exception is thrown on otf font with CID and GID not identical. + @Test + public void testPDFBox6172() throws IOException + { + PDDocument document = new PDDocument(); + File fontFile = TestResourceGenerator.downloadTestResource(IN_DIR, "NotoSansSC-Regular.otf", + "https://issues.apache.org/jira/secure/attachment/13081021/NotoSansSC-Regular.otf"); + assumeTrue(fontFile.exists()); + InputStream is = new FileInputStream(fontFile); + OpenTypeFont otf = new OTFParser().parse(is); + try + { + PDType0Font.load(document, otf, false); + } + catch (IllegalStateException ex) + { + Assert.assertEquals("CID and GID not identical: CID 628 != GID 372, use a ttf font instead", ex.getMessage()); + return; + } + finally + { + document.close(); + } + Assert.fail("should have thrown IllegalStateException"); + } } + diff --git a/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/graphics/image/CCITTFactoryTest.java b/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/graphics/image/CCITTFactoryTest.java index 537fa99e7..34ae9db51 100644 --- a/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/graphics/image/CCITTFactoryTest.java +++ b/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/graphics/image/CCITTFactoryTest.java @@ -283,4 +283,11 @@ public void testByteShortPaddedWithGarbage() throws IOException } document.close(); } + +// /** +// * PDFBOX-6164: test support of TIFF-files with FillOrder=2 +// * TODO: PdfBox-Android not ported: downloads Wing.tif from issues.apache.org and +// * compares against a javax.imageio-decoded reference image +// */ +// public void testFillOrder2() throws IOException, URISyntaxException } diff --git a/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/graphics/image/JPEGFactoryTest.java b/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/graphics/image/JPEGFactoryTest.java index b237d5f95..a6cc4dc46 100644 --- a/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/graphics/image/JPEGFactoryTest.java +++ b/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/graphics/image/JPEGFactoryTest.java @@ -23,7 +23,6 @@ import androidx.test.platform.app.InstrumentationRegistry; import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -85,6 +84,7 @@ public void testCreateFromStream() throws IOException doWritePDF(document, ximage, testResultsDir, "jpegrgbstream.pdf"); checkJpegStream(testResultsDir, "jpegrgbstream.pdf", testContext.getAssets().open( "pdfbox/com/tom_roush/pdfbox/pdmodel/graphics/image/jpeg.jpg")); + stream.close(); } /* @@ -101,6 +101,7 @@ public void testCreateFromStreamCMYK() throws IOException doWritePDF(document, ximage, testResultsDir, "jpegcmykstream.pdf"); checkJpegStream(testResultsDir, "jpegcmykstream.pdf", testContext.getAssets().open("pdfbox/com/tom_roush/pdfbox/pdmodel/graphics/image/jpegcmyk.jpg")); + stream.close(); } /** @@ -119,6 +120,7 @@ public void testCreateFromStream256() throws IOException doWritePDF(document, ximage, testResultsDir, "jpeg256stream.pdf"); checkJpegStream(testResultsDir, "jpeg256stream.pdf", testContext.getAssets().open( "pdfbox/com/tom_roush/pdfbox/pdmodel/graphics/image/jpeg256.jpg")); + stream.close(); } /** @@ -330,20 +332,16 @@ public void testPDFBox5137() throws IOException // check whether it is possible to extract the jpeg stream exactly // as it was passed to createFromStream - private void checkJpegStream(File testResultsDir, String filename, InputStream resourceStream) + private void checkJpegStream(File testResultsDir, String filename, InputStream expected) throws IOException { PDDocument doc = PDDocument.load(new File(testResultsDir, filename)); PDImageXObject img = (PDImageXObject) doc.getPage(0).getResources().getXObject(COSName.getPDFName("Im1")); InputStream dctStream = img.createInputStream(Arrays.asList(COSName.DCT_DECODE.getName())); - ByteArrayOutputStream baos1 = new ByteArrayOutputStream(); - ByteArrayOutputStream baos2 = new ByteArrayOutputStream(); - IOUtils.copy(resourceStream, baos1); - IOUtils.copy(dctStream, baos2); - resourceStream.close(); + assertArrayEquals(IOUtils.toByteArray(expected), IOUtils.toByteArray(dctStream)); + expected.close(); dctStream.close(); - assertArrayEquals(baos1.toByteArray(), baos2.toByteArray()); doc.close(); } } diff --git a/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/graphics/image/LosslessFactoryTest.java b/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/graphics/image/LosslessFactoryTest.java index b7a930540..92c017588 100644 --- a/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/graphics/image/LosslessFactoryTest.java +++ b/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/graphics/image/LosslessFactoryTest.java @@ -172,7 +172,7 @@ public void testCreateLosslessFromImageINT_ARGB() throws IOException validate(ximage, 8, argbImage.getWidth(), argbImage.getHeight(), "png", PDDeviceRGB.INSTANCE.getName()); checkIdent(argbImage, ximage.getImage()); - checkIdentRGB(argbImage, ximage.getOpaqueImage()); + checkIdentRGB(argbImage, ximage.getOpaqueImage(null, 1)); assertNotNull(ximage.getSoftMask()); validate(ximage.getSoftMask(), 8, argbImage.getWidth(), argbImage.getHeight(), "png", @@ -180,6 +180,7 @@ public void testCreateLosslessFromImageINT_ARGB() throws IOException assertTrue(colorCount(ximage.getSoftMask().getImage()) > image.getHeight() / 10); doWritePDF(document, ximage, testResultsDir, "intargb.pdf"); + document.close(); } // testCreateLosslessFromImageBITMASK_INT_ARGB: Android does not have bitmask transparency @@ -224,13 +225,14 @@ public void testCreateLosslessFromImage4BYTE_ABGR() throws IOException validate(ximage, 8, w, h, "png", PDDeviceRGB.INSTANCE.getName()); checkIdent(argbImage, ximage.getImage()); - checkIdentRGB(argbImage, ximage.getOpaqueImage()); + checkIdentRGB(argbImage, ximage.getOpaqueImage(null, 1)); assertNotNull(ximage.getSoftMask()); validate(ximage.getSoftMask(), 8, w, h, "png", PDDeviceGray.INSTANCE.getName()); assertTrue(colorCount(ximage.getSoftMask().getImage()) > image.getHeight() / 10); doWritePDF(document, ximage, testResultsDir, "4babgr.pdf"); + document.close(); } /** @@ -268,11 +270,12 @@ public void testCreateLosslessFromImageUSHORT_555_RGB() throws IOException validate(ximage, 8, w, h, "png", PDDeviceRGB.INSTANCE.getName()); checkIdent(rgbImage, ximage.getImage()); - checkIdentRGB(rgbImage, ximage.getOpaqueImage()); + checkIdentRGB(rgbImage, ximage.getOpaqueImage(null, 1)); assertNull(ximage.getSoftMask()); doWritePDF(document, ximage, testResultsDir, "ushort555rgb.pdf"); + document.close(); } // TODO: PdfBox-Android : testCreateLosslessFromTransparentGIF: GIF images not currently supported @@ -385,12 +388,13 @@ public void testCreateLosslessFrom16BitPNG() throws IOException int h = image.getHeight(); validate(ximage, 8, w, h, "png", PDDeviceRGB.INSTANCE.getName()); checkIdent(compareImage, ximage.getImage()); - checkIdentRGB(compareImage, ximage.getOpaqueImage()); + checkIdentRGB(compareImage, ximage.getOpaqueImage(null, 1)); assertNotNull(ximage.getSoftMask()); validate(ximage.getSoftMask(), 8, w, h, "png", PDDeviceGray.INSTANCE.getName()); // assertEquals(35, colorCount(ximage.getSoftMask().getImage())); TODO: PdfBox-Android doWritePDF(document, ximage, testResultsDir, "png16bit.pdf"); + document.close(); } } diff --git a/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImageXObjectTest.java b/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImageXObjectTest.java index 6d6b0bcc7..1c1ad0011 100644 --- a/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImageXObjectTest.java +++ b/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImageXObjectTest.java @@ -24,6 +24,7 @@ import java.io.File; import java.io.FileInputStream; +import java.io.InputStream; import java.io.IOException; import java.net.URISyntaxException; @@ -173,7 +174,9 @@ private void testCompareCreatedFileByExtensionWithCreatedByJPEGFactory(String fi File file = TestResourceGenerator.copyStreamToFile(CACHE_DIR, filename, testContext.getAssets().open(inPath + filename)); PDImageXObject image = PDImageXObject.createFromFileByExtension(file, doc); - PDImageXObject expectedImage = JPEGFactory.createFromStream(doc, new FileInputStream(file)); + InputStream is = new FileInputStream(file); + PDImageXObject expectedImage = JPEGFactory.createFromStream(doc, is); + is.close(); Assert.assertEquals(expectedImage.getSuffix(), image.getSuffix()); checkIdentARGB(image.getImage(), expectedImage.getImage()); @@ -219,7 +222,9 @@ private void testCompareCreatedFileWithCreatedByJPEGFactory(String filename) File file = TestResourceGenerator.copyStreamToFile(CACHE_DIR, filename, testContext.getAssets().open(inPath + filename)); PDImageXObject image = PDImageXObject.createFromFile(file.getAbsolutePath(), doc); - PDImageXObject expectedImage = JPEGFactory.createFromStream(doc, new FileInputStream(file)); + InputStream is = new FileInputStream(file); + PDImageXObject expectedImage = JPEGFactory.createFromStream(doc, is); + is.close(); Assert.assertEquals(expectedImage.getSuffix(), image.getSuffix()); checkIdentARGB(image.getImage(), expectedImage.getImage()); @@ -265,7 +270,9 @@ private void testCompareCreatedByContentWithCreatedByJPEGFactory(String filename File file = TestResourceGenerator.copyStreamToFile(CACHE_DIR, filename, testContext.getAssets().open(inPath + filename)); PDImageXObject image = PDImageXObject.createFromFileByContent(file, doc); - PDImageXObject expectedImage = JPEGFactory.createFromStream(doc, new FileInputStream(file)); + InputStream is = new FileInputStream(file); + PDImageXObject expectedImage = JPEGFactory.createFromStream(doc, is); + is.close(); Assert.assertEquals(expectedImage.getSuffix(), image.getSuffix()); checkIdentARGB(image.getImage(), expectedImage.getImage()); @@ -281,7 +288,9 @@ private void testCompareCreatedFromByteArrayWithCreatedByLosslessFactory(String { PDDocument doc = new PDDocument(); File file = TestResourceGenerator.copyStreamToFile(CACHE_DIR, filename, testContext.getAssets().open(inPath + filename)); - byte[] byteArray = IOUtils.toByteArray(new FileInputStream(file)); + InputStream is = new FileInputStream(file); + byte[] byteArray = IOUtils.toByteArray(is); + is.close(); PDImageXObject image = PDImageXObject.createFromByteArray(doc, byteArray, null); Bitmap bim = BitmapFactory.decodeStream(testContext.getAssets().open(inPath + filename)); @@ -298,7 +307,9 @@ private void testCompareCreatedFromByteArrayWithCreatedByCCITTFactory(String fil { PDDocument doc = new PDDocument(); File file = TestResourceGenerator.copyStreamToFile(CACHE_DIR, filename, testContext.getAssets().open(inPath + filename)); - byte[] byteArray = IOUtils.toByteArray(new FileInputStream(file)); + InputStream is = new FileInputStream(file); + byte[] byteArray = IOUtils.toByteArray(is); + is.close(); PDImageXObject image = PDImageXObject.createFromByteArray(doc, byteArray, null); PDImageXObject expectedImage = CCITTFactory.createFromFile(doc, file); @@ -314,10 +325,14 @@ private void testCompareCreatedFromByteArrayWithCreatedByJPEGFactory(String file { PDDocument doc = new PDDocument(); File file = TestResourceGenerator.copyStreamToFile(CACHE_DIR, filename, testContext.getAssets().open(inPath + filename)); - byte[] byteArray = IOUtils.toByteArray(new FileInputStream(file)); + InputStream is1 = new FileInputStream(file); + byte[] byteArray = IOUtils.toByteArray(is1); + is1.close(); PDImageXObject image = PDImageXObject.createFromByteArray(doc, byteArray, null); - PDImageXObject expectedImage = JPEGFactory.createFromStream(doc, new FileInputStream(file)); + InputStream is2 = new FileInputStream(file); + PDImageXObject expectedImage = JPEGFactory.createFromStream(doc, is2); + is2.close(); Assert.assertEquals(expectedImage.getSuffix(), image.getSuffix()); checkIdentARGB(image.getImage(), expectedImage.getImage()); diff --git a/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/graphics/image/SampledImageReaderTest.java b/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/graphics/image/SampledImageReaderTest.java new file mode 100644 index 000000000..c67e7ab7e --- /dev/null +++ b/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/graphics/image/SampledImageReaderTest.java @@ -0,0 +1,128 @@ +package com.tom_roush.pdfbox.pdmodel.graphics.image; + +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Bitmap; + +import java.io.InputStream; +import java.io.IOException; +import java.util.List; + +import com.tom_roush.pdfbox.cos.COSArray; +import com.tom_roush.pdfbox.cos.COSBase; +import com.tom_roush.pdfbox.filter.DecodeOptions; +import com.tom_roush.pdfbox.pdmodel.common.PDStream; +import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColorSpace; + +import org.junit.Assert; +import org.junit.Test; + +public class SampledImageReaderTest +{ + private static PDImage createDummyPDImage(final int width, final int height) + { + return new PDImage() + { + @Override + public Bitmap getImage() throws IOException { return null; } + + @Override + public Bitmap getImage(Rect region, int subsampling) throws IOException { return null; } + + @Override + public Bitmap getStencilImage(Paint paint) throws IOException { return null; } + + @Override + public InputStream createInputStream() throws IOException { return null; } + + @Override + public InputStream createInputStream(DecodeOptions options) throws IOException { return null; } + + @Override + public InputStream createInputStream(List stopFilters) throws IOException { return null; } + + @Override + public boolean isEmpty() { return false; } + + @Override + public boolean isStencil() { return false; } + + @Override + public void setStencil(boolean isStencil) {} + + @Override + public int getBitsPerComponent() { return 8; } + + @Override + public void setBitsPerComponent(int bpc) {} + + @Override + public PDColorSpace getColorSpace() throws IOException { return null; } + + @Override + public void setColorSpace(PDColorSpace cs) {} + + @Override + public int getHeight() { return height; } + + @Override + public void setHeight(int h) {} + + @Override + public int getWidth() { return width; } + + @Override + public void setWidth(int w) {} + + @Override + public boolean getInterpolate() { return false; } + + @Override + public void setInterpolate(boolean value) {} + + @Override + public COSArray getDecode() { return null; } + + @Override + public void setDecode(COSArray decode) {} + + @Override + public String getSuffix() { return "png"; } + + @Override + public COSBase getCOSObject() { return null; } + }; + } + + @Test + public void testClipRegionNull() + { + PDImage pdImage = createDummyPDImage(400, 300); + + Rect clipped = SampledImageReader.clipRegion(pdImage, null); + Assert.assertNotNull(clipped); + Assert.assertEquals(0, clipped.left); + Assert.assertEquals(0, clipped.top); + Assert.assertEquals(400, clipped.right); + Assert.assertEquals(300, clipped.bottom); + } + + @Test + public void testClipRegionNonZeroOffset() + { + PDImage pdImage = createDummyPDImage(500, 500); + + // Rect(left, top, right, bottom) + Rect sourceRegion = new Rect(50, 60, 250, 360); + // sourceRegion width is 200, height is 300 + + Rect clipped = SampledImageReader.clipRegion(pdImage, sourceRegion); + Assert.assertNotNull(clipped); + Assert.assertEquals(50, clipped.left); + Assert.assertEquals(60, clipped.top); + // right must be left + width = 50 + 200 = 250 + Assert.assertEquals(250, clipped.right); + // bottom must be top + height = 60 + 300 = 360 + Assert.assertEquals(360, clipped.bottom); + } +} diff --git a/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/graphics/image/ValidateXImage.java b/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/graphics/image/ValidateXImage.java index 112b21ea5..1eb7ae33a 100644 --- a/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/graphics/image/ValidateXImage.java +++ b/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/graphics/image/ValidateXImage.java @@ -80,7 +80,7 @@ else if ("png".equals(format)) writeOk = ximage.getImage().compress(compressFormat, 100, new NullOutputStream()); assertTrue(writeOk); - writeOk = ximage.getOpaqueImage().compress(compressFormat, 100, + writeOk = ximage.getOpaqueImage(null, 1).compress(compressFormat, 100, new NullOutputStream()); assertTrue(writeOk); } diff --git a/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/interactive/form/CombAlignmentTest.java b/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/interactive/form/CombAlignmentTest.java index f3a25ee54..15d480320 100644 --- a/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/interactive/form/CombAlignmentTest.java +++ b/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/interactive/form/CombAlignmentTest.java @@ -59,10 +59,13 @@ public void testCombFields() throws IOException PDDocument document = PDDocument.load(testContext.getAssets().open(IN_DIR + "/" + NAME_OF_PDF)); PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm(); PDField field = acroForm.getField("PDFBoxCombLeft"); + field.setValue(""); field.setValue(TEST_VALUE); field = acroForm.getField("PDFBoxCombMiddle"); + field.setValue(""); field.setValue(TEST_VALUE); field = acroForm.getField("PDFBoxCombRight"); + field.setValue(""); field.setValue(TEST_VALUE); // compare rendering File file = new File(OUT_DIR, NAME_OF_PDF); @@ -72,4 +75,28 @@ public void testCombFields() throws IOException testRendering.setUp(); testRendering.render(file); } + + // PDFBOX-5784 + @Test + public void testPDFBOX5784() throws IOException + { + final String NAME_OF_PDF = "PDFBOX-5784.pdf"; + + PDDocument document = PDDocument.load(testContext.getAssets().open(IN_DIR + "/" + NAME_OF_PDF)); + PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm(); + for (PDField field : acroForm.getFieldTree()) + { + if (!field.getPartialName().contains("acrobat")) + { + field.setValue("WIaqg"); + } + } + // compare rendering + File file = new File(OUT_DIR, NAME_OF_PDF); + document.save(file); + document.close(); + TestRendering testRendering = new TestRendering(); + testRendering.setUp(); + testRendering.render(file); + } } diff --git a/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDAcroFormFlattenTest.java b/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDAcroFormFlattenTest.java index 7b3793f7a..89b336f0f 100644 --- a/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDAcroFormFlattenTest.java +++ b/library/src/androidTest/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDAcroFormFlattenTest.java @@ -26,17 +26,22 @@ import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; import java.io.OutputStream; import java.net.URL; import com.tom_roush.pdfbox.android.PDFBoxResourceLoader; import com.tom_roush.pdfbox.pdmodel.PDDocument; +import com.tom_roush.pdfbox.pdmodel.interactive.form.PDAcroForm; import com.tom_roush.pdfbox.rendering.PDFRenderer; import com.tom_roush.pdfbox.rendering.TestRendering; import org.junit.Before; import org.junit.Test; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; /** @@ -281,12 +286,77 @@ public void testFlattenPDFBox4788() throws IOException * @throws IOException */ @Test - public void testFlattenPDFBox4889() throws IOException + public void flattenTestPDFBOX5254() throws IOException { String sourceUrl = "https://issues.apache.org/jira/secure/attachment/13005793/f1040sb%20test.pdf"; - String targetFileName = "PDFBOX-4889.pdf"; + String targetFileName = "PDFBOX-4889-5254.pdf"; - flattenAndCompare(sourceUrl, targetFileName); + generateSamples(sourceUrl, targetFileName); + + File inputFile = new File(IN_DIR, targetFileName); + File outputFile = new File(OUT_DIR, targetFileName); + + PDDocument testPdf = PDDocument.load(inputFile); + testPdf.getDocumentCatalog().getAcroForm().flatten(); + testPdf.setAllSecurityToBeRemoved(true); + testPdf.save(outputFile); + assertTrue(testPdf.getDocumentCatalog().getAcroForm().getFields().isEmpty()); + assertEquals(72, testPdf.getPage(0).getAnnotations().size()); + testPdf.close(); + + // compare rendering + TestRendering testRendering = new TestRendering(); + testRendering.setUp(); + testRendering.render(outputFile); + + removeAllRenditions(inputFile); + inputFile.delete(); + outputFile.delete(); + } + + /** + * Check that only VN_Name is removed in the field tree and in the annotations list. That field + * has an "orphan" widget that belongs to no page. + * + * @throws IOException + */ + @Test + public void flattenTestPDFBOX5225() throws IOException + { + String sourceUrl = "https://issues.apache.org/jira/secure/attachment/13027311/SourceFailure.pdf"; + String targetFileName = "PDFBOX-5225.pdf"; + + generateSamples(sourceUrl, targetFileName); + + File inputFile = new File(IN_DIR, targetFileName); + File outputFile = new File(OUT_DIR, targetFileName); + + PDDocument testPdf = PDDocument.load(inputFile); + PDAcroForm acroForm = testPdf.getDocumentCatalog().getAcroForm(); + List list = new ArrayList(); + list.add(acroForm.getField("VN_NAME")); + acroForm.flatten(list, false); + testPdf.setAllSecurityToBeRemoved(true); + testPdf.save(outputFile); + int count = 0; + Iterator iterator = acroForm.getFieldTree().iterator(); + while (iterator.hasNext()) + { + iterator.next(); + ++count; + } + assertEquals(76, count); + assertEquals(59, testPdf.getPage(0).getAnnotations().size()); + testPdf.close(); + + // compare rendering + TestRendering testRendering = new TestRendering(); + testRendering.setUp(); + testRendering.render(outputFile); + + removeAllRenditions(inputFile); + inputFile.delete(); + outputFile.delete(); } /** diff --git a/library/src/main/assets/com/tom_roush/fontbox/resources/unicode/Scripts.txt b/library/src/main/assets/com/tom_roush/fontbox/resources/unicode/Scripts.txt index a0bb2068a..0b3f717cb 100644 --- a/library/src/main/assets/com/tom_roush/fontbox/resources/unicode/Scripts.txt +++ b/library/src/main/assets/com/tom_roush/fontbox/resources/unicode/Scripts.txt @@ -1,2693 +1,3033 @@ -# Scripts-10.0.0.txt -# Date: 2017-03-11, 06:40:37 GMT -# © 2017 Unicode®, Inc. -# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries. -# For terms of use, see http://www.unicode.org/terms_of_use.html -# -# Unicode Character Database -# For documentation, see http://www.unicode.org/reports/tr44/ -# For more information, see: -# UAX #24, Unicode Script Property: http://www.unicode.org/reports/tr24/ -# Especially the sections: -# http://www.unicode.org/reports/tr24/#Assignment_Script_Values -# http://www.unicode.org/reports/tr24/#Assignment_ScriptX_Values -# - -# ================================================ - -# Property: Script - -# All code points not explicitly listed for Script -# have the value Unknown (Zzzz). - -# @missing: 0000..10FFFF; Unknown - -# ================================================ - -0000..001F ; Common # Cc [32] .. -0020 ; Common # Zs SPACE -0021..0023 ; Common # Po [3] EXCLAMATION MARK..NUMBER SIGN -0024 ; Common # Sc DOLLAR SIGN -0025..0027 ; Common # Po [3] PERCENT SIGN..APOSTROPHE -0028 ; Common # Ps LEFT PARENTHESIS -0029 ; Common # Pe RIGHT PARENTHESIS -002A ; Common # Po ASTERISK -002B ; Common # Sm PLUS SIGN -002C ; Common # Po COMMA -002D ; Common # Pd HYPHEN-MINUS -002E..002F ; Common # Po [2] FULL STOP..SOLIDUS -0030..0039 ; Common # Nd [10] DIGIT ZERO..DIGIT NINE -003A..003B ; Common # Po [2] COLON..SEMICOLON -003C..003E ; Common # Sm [3] LESS-THAN SIGN..GREATER-THAN SIGN -003F..0040 ; Common # Po [2] QUESTION MARK..COMMERCIAL AT -005B ; Common # Ps LEFT SQUARE BRACKET -005C ; Common # Po REVERSE SOLIDUS -005D ; Common # Pe RIGHT SQUARE BRACKET -005E ; Common # Sk CIRCUMFLEX ACCENT -005F ; Common # Pc LOW LINE -0060 ; Common # Sk GRAVE ACCENT -007B ; Common # Ps LEFT CURLY BRACKET -007C ; Common # Sm VERTICAL LINE -007D ; Common # Pe RIGHT CURLY BRACKET -007E ; Common # Sm TILDE -007F..009F ; Common # Cc [33] .. -00A0 ; Common # Zs NO-BREAK SPACE -00A1 ; Common # Po INVERTED EXCLAMATION MARK -00A2..00A5 ; Common # Sc [4] CENT SIGN..YEN SIGN -00A6 ; Common # So BROKEN BAR -00A7 ; Common # Po SECTION SIGN -00A8 ; Common # Sk DIAERESIS -00A9 ; Common # So COPYRIGHT SIGN -00AB ; Common # Pi LEFT-POINTING DOUBLE ANGLE QUOTATION MARK -00AC ; Common # Sm NOT SIGN -00AD ; Common # Cf SOFT HYPHEN -00AE ; Common # So REGISTERED SIGN -00AF ; Common # Sk MACRON -00B0 ; Common # So DEGREE SIGN -00B1 ; Common # Sm PLUS-MINUS SIGN -00B2..00B3 ; Common # No [2] SUPERSCRIPT TWO..SUPERSCRIPT THREE -00B4 ; Common # Sk ACUTE ACCENT -00B5 ; Common # L& MICRO SIGN -00B6..00B7 ; Common # Po [2] PILCROW SIGN..MIDDLE DOT -00B8 ; Common # Sk CEDILLA -00B9 ; Common # No SUPERSCRIPT ONE -00BB ; Common # Pf RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK -00BC..00BE ; Common # No [3] VULGAR FRACTION ONE QUARTER..VULGAR FRACTION THREE QUARTERS -00BF ; Common # Po INVERTED QUESTION MARK -00D7 ; Common # Sm MULTIPLICATION SIGN -00F7 ; Common # Sm DIVISION SIGN -02B9..02C1 ; Common # Lm [9] MODIFIER LETTER PRIME..MODIFIER LETTER REVERSED GLOTTAL STOP -02C2..02C5 ; Common # Sk [4] MODIFIER LETTER LEFT ARROWHEAD..MODIFIER LETTER DOWN ARROWHEAD -02C6..02D1 ; Common # Lm [12] MODIFIER LETTER CIRCUMFLEX ACCENT..MODIFIER LETTER HALF TRIANGULAR COLON -02D2..02DF ; Common # Sk [14] MODIFIER LETTER CENTRED RIGHT HALF RING..MODIFIER LETTER CROSS ACCENT -02E5..02E9 ; Common # Sk [5] MODIFIER LETTER EXTRA-HIGH TONE BAR..MODIFIER LETTER EXTRA-LOW TONE BAR -02EC ; Common # Lm MODIFIER LETTER VOICING -02ED ; Common # Sk MODIFIER LETTER UNASPIRATED -02EE ; Common # Lm MODIFIER LETTER DOUBLE APOSTROPHE -02EF..02FF ; Common # Sk [17] MODIFIER LETTER LOW DOWN ARROWHEAD..MODIFIER LETTER LOW LEFT ARROW -0374 ; Common # Lm GREEK NUMERAL SIGN -037E ; Common # Po GREEK QUESTION MARK -0385 ; Common # Sk GREEK DIALYTIKA TONOS -0387 ; Common # Po GREEK ANO TELEIA -0589 ; Common # Po ARMENIAN FULL STOP -0605 ; Common # Cf ARABIC NUMBER MARK ABOVE -060C ; Common # Po ARABIC COMMA -061B ; Common # Po ARABIC SEMICOLON -061F ; Common # Po ARABIC QUESTION MARK -0640 ; Common # Lm ARABIC TATWEEL -06DD ; Common # Cf ARABIC END OF AYAH -08E2 ; Common # Cf ARABIC DISPUTED END OF AYAH -0964..0965 ; Common # Po [2] DEVANAGARI DANDA..DEVANAGARI DOUBLE DANDA -0E3F ; Common # Sc THAI CURRENCY SYMBOL BAHT -0FD5..0FD8 ; Common # So [4] RIGHT-FACING SVASTI SIGN..LEFT-FACING SVASTI SIGN WITH DOTS -10FB ; Common # Po GEORGIAN PARAGRAPH SEPARATOR -16EB..16ED ; Common # Po [3] RUNIC SINGLE PUNCTUATION..RUNIC CROSS PUNCTUATION -1735..1736 ; Common # Po [2] PHILIPPINE SINGLE PUNCTUATION..PHILIPPINE DOUBLE PUNCTUATION -1802..1803 ; Common # Po [2] MONGOLIAN COMMA..MONGOLIAN FULL STOP -1805 ; Common # Po MONGOLIAN FOUR DOTS -1CD3 ; Common # Po VEDIC SIGN NIHSHVASA -1CE1 ; Common # Mc VEDIC TONE ATHARVAVEDIC INDEPENDENT SVARITA -1CE9..1CEC ; Common # Lo [4] VEDIC SIGN ANUSVARA ANTARGOMUKHA..VEDIC SIGN ANUSVARA VAMAGOMUKHA WITH TAIL -1CEE..1CF1 ; Common # Lo [4] VEDIC SIGN HEXIFORM LONG ANUSVARA..VEDIC SIGN ANUSVARA UBHAYATO MUKHA -1CF2..1CF3 ; Common # Mc [2] VEDIC SIGN ARDHAVISARGA..VEDIC SIGN ROTATED ARDHAVISARGA -1CF5..1CF6 ; Common # Lo [2] VEDIC SIGN JIHVAMULIYA..VEDIC SIGN UPADHMANIYA -1CF7 ; Common # Mc VEDIC SIGN ATIKRAMA -2000..200A ; Common # Zs [11] EN QUAD..HAIR SPACE -200B ; Common # Cf ZERO WIDTH SPACE -200E..200F ; Common # Cf [2] LEFT-TO-RIGHT MARK..RIGHT-TO-LEFT MARK -2010..2015 ; Common # Pd [6] HYPHEN..HORIZONTAL BAR -2016..2017 ; Common # Po [2] DOUBLE VERTICAL LINE..DOUBLE LOW LINE -2018 ; Common # Pi LEFT SINGLE QUOTATION MARK -2019 ; Common # Pf RIGHT SINGLE QUOTATION MARK -201A ; Common # Ps SINGLE LOW-9 QUOTATION MARK -201B..201C ; Common # Pi [2] SINGLE HIGH-REVERSED-9 QUOTATION MARK..LEFT DOUBLE QUOTATION MARK -201D ; Common # Pf RIGHT DOUBLE QUOTATION MARK -201E ; Common # Ps DOUBLE LOW-9 QUOTATION MARK -201F ; Common # Pi DOUBLE HIGH-REVERSED-9 QUOTATION MARK -2020..2027 ; Common # Po [8] DAGGER..HYPHENATION POINT -2028 ; Common # Zl LINE SEPARATOR -2029 ; Common # Zp PARAGRAPH SEPARATOR -202A..202E ; Common # Cf [5] LEFT-TO-RIGHT EMBEDDING..RIGHT-TO-LEFT OVERRIDE -202F ; Common # Zs NARROW NO-BREAK SPACE -2030..2038 ; Common # Po [9] PER MILLE SIGN..CARET -2039 ; Common # Pi SINGLE LEFT-POINTING ANGLE QUOTATION MARK -203A ; Common # Pf SINGLE RIGHT-POINTING ANGLE QUOTATION MARK -203B..203E ; Common # Po [4] REFERENCE MARK..OVERLINE -203F..2040 ; Common # Pc [2] UNDERTIE..CHARACTER TIE -2041..2043 ; Common # Po [3] CARET INSERTION POINT..HYPHEN BULLET -2044 ; Common # Sm FRACTION SLASH -2045 ; Common # Ps LEFT SQUARE BRACKET WITH QUILL -2046 ; Common # Pe RIGHT SQUARE BRACKET WITH QUILL -2047..2051 ; Common # Po [11] DOUBLE QUESTION MARK..TWO ASTERISKS ALIGNED VERTICALLY -2052 ; Common # Sm COMMERCIAL MINUS SIGN -2053 ; Common # Po SWUNG DASH -2054 ; Common # Pc INVERTED UNDERTIE -2055..205E ; Common # Po [10] FLOWER PUNCTUATION MARK..VERTICAL FOUR DOTS -205F ; Common # Zs MEDIUM MATHEMATICAL SPACE -2060..2064 ; Common # Cf [5] WORD JOINER..INVISIBLE PLUS -2066..206F ; Common # Cf [10] LEFT-TO-RIGHT ISOLATE..NOMINAL DIGIT SHAPES -2070 ; Common # No SUPERSCRIPT ZERO -2074..2079 ; Common # No [6] SUPERSCRIPT FOUR..SUPERSCRIPT NINE -207A..207C ; Common # Sm [3] SUPERSCRIPT PLUS SIGN..SUPERSCRIPT EQUALS SIGN -207D ; Common # Ps SUPERSCRIPT LEFT PARENTHESIS -207E ; Common # Pe SUPERSCRIPT RIGHT PARENTHESIS -2080..2089 ; Common # No [10] SUBSCRIPT ZERO..SUBSCRIPT NINE -208A..208C ; Common # Sm [3] SUBSCRIPT PLUS SIGN..SUBSCRIPT EQUALS SIGN -208D ; Common # Ps SUBSCRIPT LEFT PARENTHESIS -208E ; Common # Pe SUBSCRIPT RIGHT PARENTHESIS -20A0..20BF ; Common # Sc [32] EURO-CURRENCY SIGN..BITCOIN SIGN -2100..2101 ; Common # So [2] ACCOUNT OF..ADDRESSED TO THE SUBJECT -2102 ; Common # L& DOUBLE-STRUCK CAPITAL C -2103..2106 ; Common # So [4] DEGREE CELSIUS..CADA UNA -2107 ; Common # L& EULER CONSTANT -2108..2109 ; Common # So [2] SCRUPLE..DEGREE FAHRENHEIT -210A..2113 ; Common # L& [10] SCRIPT SMALL G..SCRIPT SMALL L -2114 ; Common # So L B BAR SYMBOL -2115 ; Common # L& DOUBLE-STRUCK CAPITAL N -2116..2117 ; Common # So [2] NUMERO SIGN..SOUND RECORDING COPYRIGHT -2118 ; Common # Sm SCRIPT CAPITAL P -2119..211D ; Common # L& [5] DOUBLE-STRUCK CAPITAL P..DOUBLE-STRUCK CAPITAL R -211E..2123 ; Common # So [6] PRESCRIPTION TAKE..VERSICLE -2124 ; Common # L& DOUBLE-STRUCK CAPITAL Z -2125 ; Common # So OUNCE SIGN -2127 ; Common # So INVERTED OHM SIGN -2128 ; Common # L& BLACK-LETTER CAPITAL Z -2129 ; Common # So TURNED GREEK SMALL LETTER IOTA -212C..212D ; Common # L& [2] SCRIPT CAPITAL B..BLACK-LETTER CAPITAL C -212E ; Common # So ESTIMATED SYMBOL -212F..2131 ; Common # L& [3] SCRIPT SMALL E..SCRIPT CAPITAL F -2133..2134 ; Common # L& [2] SCRIPT CAPITAL M..SCRIPT SMALL O -2135..2138 ; Common # Lo [4] ALEF SYMBOL..DALET SYMBOL -2139 ; Common # L& INFORMATION SOURCE -213A..213B ; Common # So [2] ROTATED CAPITAL Q..FACSIMILE SIGN -213C..213F ; Common # L& [4] DOUBLE-STRUCK SMALL PI..DOUBLE-STRUCK CAPITAL PI -2140..2144 ; Common # Sm [5] DOUBLE-STRUCK N-ARY SUMMATION..TURNED SANS-SERIF CAPITAL Y -2145..2149 ; Common # L& [5] DOUBLE-STRUCK ITALIC CAPITAL D..DOUBLE-STRUCK ITALIC SMALL J -214A ; Common # So PROPERTY LINE -214B ; Common # Sm TURNED AMPERSAND -214C..214D ; Common # So [2] PER SIGN..AKTIESELSKAB -214F ; Common # So SYMBOL FOR SAMARITAN SOURCE -2150..215F ; Common # No [16] VULGAR FRACTION ONE SEVENTH..FRACTION NUMERATOR ONE -2189 ; Common # No VULGAR FRACTION ZERO THIRDS -218A..218B ; Common # So [2] TURNED DIGIT TWO..TURNED DIGIT THREE -2190..2194 ; Common # Sm [5] LEFTWARDS ARROW..LEFT RIGHT ARROW -2195..2199 ; Common # So [5] UP DOWN ARROW..SOUTH WEST ARROW -219A..219B ; Common # Sm [2] LEFTWARDS ARROW WITH STROKE..RIGHTWARDS ARROW WITH STROKE -219C..219F ; Common # So [4] LEFTWARDS WAVE ARROW..UPWARDS TWO HEADED ARROW -21A0 ; Common # Sm RIGHTWARDS TWO HEADED ARROW -21A1..21A2 ; Common # So [2] DOWNWARDS TWO HEADED ARROW..LEFTWARDS ARROW WITH TAIL -21A3 ; Common # Sm RIGHTWARDS ARROW WITH TAIL -21A4..21A5 ; Common # So [2] LEFTWARDS ARROW FROM BAR..UPWARDS ARROW FROM BAR -21A6 ; Common # Sm RIGHTWARDS ARROW FROM BAR -21A7..21AD ; Common # So [7] DOWNWARDS ARROW FROM BAR..LEFT RIGHT WAVE ARROW -21AE ; Common # Sm LEFT RIGHT ARROW WITH STROKE -21AF..21CD ; Common # So [31] DOWNWARDS ZIGZAG ARROW..LEFTWARDS DOUBLE ARROW WITH STROKE -21CE..21CF ; Common # Sm [2] LEFT RIGHT DOUBLE ARROW WITH STROKE..RIGHTWARDS DOUBLE ARROW WITH STROKE -21D0..21D1 ; Common # So [2] LEFTWARDS DOUBLE ARROW..UPWARDS DOUBLE ARROW -21D2 ; Common # Sm RIGHTWARDS DOUBLE ARROW -21D3 ; Common # So DOWNWARDS DOUBLE ARROW -21D4 ; Common # Sm LEFT RIGHT DOUBLE ARROW -21D5..21F3 ; Common # So [31] UP DOWN DOUBLE ARROW..UP DOWN WHITE ARROW -21F4..22FF ; Common # Sm [268] RIGHT ARROW WITH SMALL CIRCLE..Z NOTATION BAG MEMBERSHIP -2300..2307 ; Common # So [8] DIAMETER SIGN..WAVY LINE -2308 ; Common # Ps LEFT CEILING -2309 ; Common # Pe RIGHT CEILING -230A ; Common # Ps LEFT FLOOR -230B ; Common # Pe RIGHT FLOOR -230C..231F ; Common # So [20] BOTTOM RIGHT CROP..BOTTOM RIGHT CORNER -2320..2321 ; Common # Sm [2] TOP HALF INTEGRAL..BOTTOM HALF INTEGRAL -2322..2328 ; Common # So [7] FROWN..KEYBOARD -2329 ; Common # Ps LEFT-POINTING ANGLE BRACKET -232A ; Common # Pe RIGHT-POINTING ANGLE BRACKET -232B..237B ; Common # So [81] ERASE TO THE LEFT..NOT CHECK MARK -237C ; Common # Sm RIGHT ANGLE WITH DOWNWARDS ZIGZAG ARROW -237D..239A ; Common # So [30] SHOULDERED OPEN BOX..CLEAR SCREEN SYMBOL -239B..23B3 ; Common # Sm [25] LEFT PARENTHESIS UPPER HOOK..SUMMATION BOTTOM -23B4..23DB ; Common # So [40] TOP SQUARE BRACKET..FUSE -23DC..23E1 ; Common # Sm [6] TOP PARENTHESIS..BOTTOM TORTOISE SHELL BRACKET -23E2..2426 ; Common # So [69] WHITE TRAPEZIUM..SYMBOL FOR SUBSTITUTE FORM TWO -2440..244A ; Common # So [11] OCR HOOK..OCR DOUBLE BACKSLASH -2460..249B ; Common # No [60] CIRCLED DIGIT ONE..NUMBER TWENTY FULL STOP -249C..24E9 ; Common # So [78] PARENTHESIZED LATIN SMALL LETTER A..CIRCLED LATIN SMALL LETTER Z -24EA..24FF ; Common # No [22] CIRCLED DIGIT ZERO..NEGATIVE CIRCLED DIGIT ZERO -2500..25B6 ; Common # So [183] BOX DRAWINGS LIGHT HORIZONTAL..BLACK RIGHT-POINTING TRIANGLE -25B7 ; Common # Sm WHITE RIGHT-POINTING TRIANGLE -25B8..25C0 ; Common # So [9] BLACK RIGHT-POINTING SMALL TRIANGLE..BLACK LEFT-POINTING TRIANGLE -25C1 ; Common # Sm WHITE LEFT-POINTING TRIANGLE -25C2..25F7 ; Common # So [54] BLACK LEFT-POINTING SMALL TRIANGLE..WHITE CIRCLE WITH UPPER RIGHT QUADRANT -25F8..25FF ; Common # Sm [8] UPPER LEFT TRIANGLE..LOWER RIGHT TRIANGLE -2600..266E ; Common # So [111] BLACK SUN WITH RAYS..MUSIC NATURAL SIGN -266F ; Common # Sm MUSIC SHARP SIGN -2670..2767 ; Common # So [248] WEST SYRIAC CROSS..ROTATED FLORAL HEART BULLET -2768 ; Common # Ps MEDIUM LEFT PARENTHESIS ORNAMENT -2769 ; Common # Pe MEDIUM RIGHT PARENTHESIS ORNAMENT -276A ; Common # Ps MEDIUM FLATTENED LEFT PARENTHESIS ORNAMENT -276B ; Common # Pe MEDIUM FLATTENED RIGHT PARENTHESIS ORNAMENT -276C ; Common # Ps MEDIUM LEFT-POINTING ANGLE BRACKET ORNAMENT -276D ; Common # Pe MEDIUM RIGHT-POINTING ANGLE BRACKET ORNAMENT -276E ; Common # Ps HEAVY LEFT-POINTING ANGLE QUOTATION MARK ORNAMENT -276F ; Common # Pe HEAVY RIGHT-POINTING ANGLE QUOTATION MARK ORNAMENT -2770 ; Common # Ps HEAVY LEFT-POINTING ANGLE BRACKET ORNAMENT -2771 ; Common # Pe HEAVY RIGHT-POINTING ANGLE BRACKET ORNAMENT -2772 ; Common # Ps LIGHT LEFT TORTOISE SHELL BRACKET ORNAMENT -2773 ; Common # Pe LIGHT RIGHT TORTOISE SHELL BRACKET ORNAMENT -2774 ; Common # Ps MEDIUM LEFT CURLY BRACKET ORNAMENT -2775 ; Common # Pe MEDIUM RIGHT CURLY BRACKET ORNAMENT -2776..2793 ; Common # No [30] DINGBAT NEGATIVE CIRCLED DIGIT ONE..DINGBAT NEGATIVE CIRCLED SANS-SERIF NUMBER TEN -2794..27BF ; Common # So [44] HEAVY WIDE-HEADED RIGHTWARDS ARROW..DOUBLE CURLY LOOP -27C0..27C4 ; Common # Sm [5] THREE DIMENSIONAL ANGLE..OPEN SUPERSET -27C5 ; Common # Ps LEFT S-SHAPED BAG DELIMITER -27C6 ; Common # Pe RIGHT S-SHAPED BAG DELIMITER -27C7..27E5 ; Common # Sm [31] OR WITH DOT INSIDE..WHITE SQUARE WITH RIGHTWARDS TICK -27E6 ; Common # Ps MATHEMATICAL LEFT WHITE SQUARE BRACKET -27E7 ; Common # Pe MATHEMATICAL RIGHT WHITE SQUARE BRACKET -27E8 ; Common # Ps MATHEMATICAL LEFT ANGLE BRACKET -27E9 ; Common # Pe MATHEMATICAL RIGHT ANGLE BRACKET -27EA ; Common # Ps MATHEMATICAL LEFT DOUBLE ANGLE BRACKET -27EB ; Common # Pe MATHEMATICAL RIGHT DOUBLE ANGLE BRACKET -27EC ; Common # Ps MATHEMATICAL LEFT WHITE TORTOISE SHELL BRACKET -27ED ; Common # Pe MATHEMATICAL RIGHT WHITE TORTOISE SHELL BRACKET -27EE ; Common # Ps MATHEMATICAL LEFT FLATTENED PARENTHESIS -27EF ; Common # Pe MATHEMATICAL RIGHT FLATTENED PARENTHESIS -27F0..27FF ; Common # Sm [16] UPWARDS QUADRUPLE ARROW..LONG RIGHTWARDS SQUIGGLE ARROW -2900..2982 ; Common # Sm [131] RIGHTWARDS TWO-HEADED ARROW WITH VERTICAL STROKE..Z NOTATION TYPE COLON -2983 ; Common # Ps LEFT WHITE CURLY BRACKET -2984 ; Common # Pe RIGHT WHITE CURLY BRACKET -2985 ; Common # Ps LEFT WHITE PARENTHESIS -2986 ; Common # Pe RIGHT WHITE PARENTHESIS -2987 ; Common # Ps Z NOTATION LEFT IMAGE BRACKET -2988 ; Common # Pe Z NOTATION RIGHT IMAGE BRACKET -2989 ; Common # Ps Z NOTATION LEFT BINDING BRACKET -298A ; Common # Pe Z NOTATION RIGHT BINDING BRACKET -298B ; Common # Ps LEFT SQUARE BRACKET WITH UNDERBAR -298C ; Common # Pe RIGHT SQUARE BRACKET WITH UNDERBAR -298D ; Common # Ps LEFT SQUARE BRACKET WITH TICK IN TOP CORNER -298E ; Common # Pe RIGHT SQUARE BRACKET WITH TICK IN BOTTOM CORNER -298F ; Common # Ps LEFT SQUARE BRACKET WITH TICK IN BOTTOM CORNER -2990 ; Common # Pe RIGHT SQUARE BRACKET WITH TICK IN TOP CORNER -2991 ; Common # Ps LEFT ANGLE BRACKET WITH DOT -2992 ; Common # Pe RIGHT ANGLE BRACKET WITH DOT -2993 ; Common # Ps LEFT ARC LESS-THAN BRACKET -2994 ; Common # Pe RIGHT ARC GREATER-THAN BRACKET -2995 ; Common # Ps DOUBLE LEFT ARC GREATER-THAN BRACKET -2996 ; Common # Pe DOUBLE RIGHT ARC LESS-THAN BRACKET -2997 ; Common # Ps LEFT BLACK TORTOISE SHELL BRACKET -2998 ; Common # Pe RIGHT BLACK TORTOISE SHELL BRACKET -2999..29D7 ; Common # Sm [63] DOTTED FENCE..BLACK HOURGLASS -29D8 ; Common # Ps LEFT WIGGLY FENCE -29D9 ; Common # Pe RIGHT WIGGLY FENCE -29DA ; Common # Ps LEFT DOUBLE WIGGLY FENCE -29DB ; Common # Pe RIGHT DOUBLE WIGGLY FENCE -29DC..29FB ; Common # Sm [32] INCOMPLETE INFINITY..TRIPLE PLUS -29FC ; Common # Ps LEFT-POINTING CURVED ANGLE BRACKET -29FD ; Common # Pe RIGHT-POINTING CURVED ANGLE BRACKET -29FE..2AFF ; Common # Sm [258] TINY..N-ARY WHITE VERTICAL BAR -2B00..2B2F ; Common # So [48] NORTH EAST WHITE ARROW..WHITE VERTICAL ELLIPSE -2B30..2B44 ; Common # Sm [21] LEFT ARROW WITH SMALL CIRCLE..RIGHTWARDS ARROW THROUGH SUPERSET -2B45..2B46 ; Common # So [2] LEFTWARDS QUADRUPLE ARROW..RIGHTWARDS QUADRUPLE ARROW -2B47..2B4C ; Common # Sm [6] REVERSE TILDE OPERATOR ABOVE RIGHTWARDS ARROW..RIGHTWARDS ARROW ABOVE REVERSE TILDE OPERATOR -2B4D..2B73 ; Common # So [39] DOWNWARDS TRIANGLE-HEADED ZIGZAG ARROW..DOWNWARDS TRIANGLE-HEADED ARROW TO BAR -2B76..2B95 ; Common # So [32] NORTH WEST TRIANGLE-HEADED ARROW TO BAR..RIGHTWARDS BLACK ARROW -2B98..2BB9 ; Common # So [34] THREE-D TOP-LIGHTED LEFTWARDS EQUILATERAL ARROWHEAD..UP ARROWHEAD IN A RECTANGLE BOX -2BBD..2BC8 ; Common # So [12] BALLOT BOX WITH LIGHT X..BLACK MEDIUM RIGHT-POINTING TRIANGLE CENTRED -2BCA..2BD2 ; Common # So [9] TOP HALF BLACK CIRCLE..GROUP MARK -2BEC..2BEF ; Common # So [4] LEFTWARDS TWO-HEADED ARROW WITH TRIANGLE ARROWHEADS..DOWNWARDS TWO-HEADED ARROW WITH TRIANGLE ARROWHEADS -2E00..2E01 ; Common # Po [2] RIGHT ANGLE SUBSTITUTION MARKER..RIGHT ANGLE DOTTED SUBSTITUTION MARKER -2E02 ; Common # Pi LEFT SUBSTITUTION BRACKET -2E03 ; Common # Pf RIGHT SUBSTITUTION BRACKET -2E04 ; Common # Pi LEFT DOTTED SUBSTITUTION BRACKET -2E05 ; Common # Pf RIGHT DOTTED SUBSTITUTION BRACKET -2E06..2E08 ; Common # Po [3] RAISED INTERPOLATION MARKER..DOTTED TRANSPOSITION MARKER -2E09 ; Common # Pi LEFT TRANSPOSITION BRACKET -2E0A ; Common # Pf RIGHT TRANSPOSITION BRACKET -2E0B ; Common # Po RAISED SQUARE -2E0C ; Common # Pi LEFT RAISED OMISSION BRACKET -2E0D ; Common # Pf RIGHT RAISED OMISSION BRACKET -2E0E..2E16 ; Common # Po [9] EDITORIAL CORONIS..DOTTED RIGHT-POINTING ANGLE -2E17 ; Common # Pd DOUBLE OBLIQUE HYPHEN -2E18..2E19 ; Common # Po [2] INVERTED INTERROBANG..PALM BRANCH -2E1A ; Common # Pd HYPHEN WITH DIAERESIS -2E1B ; Common # Po TILDE WITH RING ABOVE -2E1C ; Common # Pi LEFT LOW PARAPHRASE BRACKET -2E1D ; Common # Pf RIGHT LOW PARAPHRASE BRACKET -2E1E..2E1F ; Common # Po [2] TILDE WITH DOT ABOVE..TILDE WITH DOT BELOW -2E20 ; Common # Pi LEFT VERTICAL BAR WITH QUILL -2E21 ; Common # Pf RIGHT VERTICAL BAR WITH QUILL -2E22 ; Common # Ps TOP LEFT HALF BRACKET -2E23 ; Common # Pe TOP RIGHT HALF BRACKET -2E24 ; Common # Ps BOTTOM LEFT HALF BRACKET -2E25 ; Common # Pe BOTTOM RIGHT HALF BRACKET -2E26 ; Common # Ps LEFT SIDEWAYS U BRACKET -2E27 ; Common # Pe RIGHT SIDEWAYS U BRACKET -2E28 ; Common # Ps LEFT DOUBLE PARENTHESIS -2E29 ; Common # Pe RIGHT DOUBLE PARENTHESIS -2E2A..2E2E ; Common # Po [5] TWO DOTS OVER ONE DOT PUNCTUATION..REVERSED QUESTION MARK -2E2F ; Common # Lm VERTICAL TILDE -2E30..2E39 ; Common # Po [10] RING POINT..TOP HALF SECTION SIGN -2E3A..2E3B ; Common # Pd [2] TWO-EM DASH..THREE-EM DASH -2E3C..2E3F ; Common # Po [4] STENOGRAPHIC FULL STOP..CAPITULUM -2E40 ; Common # Pd DOUBLE HYPHEN -2E41 ; Common # Po REVERSED COMMA -2E42 ; Common # Ps DOUBLE LOW-REVERSED-9 QUOTATION MARK -2E43..2E49 ; Common # Po [7] DASH WITH LEFT UPTURN..DOUBLE STACKED COMMA -2FF0..2FFB ; Common # So [12] IDEOGRAPHIC DESCRIPTION CHARACTER LEFT TO RIGHT..IDEOGRAPHIC DESCRIPTION CHARACTER OVERLAID -3000 ; Common # Zs IDEOGRAPHIC SPACE -3001..3003 ; Common # Po [3] IDEOGRAPHIC COMMA..DITTO MARK -3004 ; Common # So JAPANESE INDUSTRIAL STANDARD SYMBOL -3006 ; Common # Lo IDEOGRAPHIC CLOSING MARK -3008 ; Common # Ps LEFT ANGLE BRACKET -3009 ; Common # Pe RIGHT ANGLE BRACKET -300A ; Common # Ps LEFT DOUBLE ANGLE BRACKET -300B ; Common # Pe RIGHT DOUBLE ANGLE BRACKET -300C ; Common # Ps LEFT CORNER BRACKET -300D ; Common # Pe RIGHT CORNER BRACKET -300E ; Common # Ps LEFT WHITE CORNER BRACKET -300F ; Common # Pe RIGHT WHITE CORNER BRACKET -3010 ; Common # Ps LEFT BLACK LENTICULAR BRACKET -3011 ; Common # Pe RIGHT BLACK LENTICULAR BRACKET -3012..3013 ; Common # So [2] POSTAL MARK..GETA MARK -3014 ; Common # Ps LEFT TORTOISE SHELL BRACKET -3015 ; Common # Pe RIGHT TORTOISE SHELL BRACKET -3016 ; Common # Ps LEFT WHITE LENTICULAR BRACKET -3017 ; Common # Pe RIGHT WHITE LENTICULAR BRACKET -3018 ; Common # Ps LEFT WHITE TORTOISE SHELL BRACKET -3019 ; Common # Pe RIGHT WHITE TORTOISE SHELL BRACKET -301A ; Common # Ps LEFT WHITE SQUARE BRACKET -301B ; Common # Pe RIGHT WHITE SQUARE BRACKET -301C ; Common # Pd WAVE DASH -301D ; Common # Ps REVERSED DOUBLE PRIME QUOTATION MARK -301E..301F ; Common # Pe [2] DOUBLE PRIME QUOTATION MARK..LOW DOUBLE PRIME QUOTATION MARK -3020 ; Common # So POSTAL MARK FACE -3030 ; Common # Pd WAVY DASH -3031..3035 ; Common # Lm [5] VERTICAL KANA REPEAT MARK..VERTICAL KANA REPEAT MARK LOWER HALF -3036..3037 ; Common # So [2] CIRCLED POSTAL MARK..IDEOGRAPHIC TELEGRAPH LINE FEED SEPARATOR SYMBOL -303C ; Common # Lo MASU MARK -303D ; Common # Po PART ALTERNATION MARK -303E..303F ; Common # So [2] IDEOGRAPHIC VARIATION INDICATOR..IDEOGRAPHIC HALF FILL SPACE -309B..309C ; Common # Sk [2] KATAKANA-HIRAGANA VOICED SOUND MARK..KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK -30A0 ; Common # Pd KATAKANA-HIRAGANA DOUBLE HYPHEN -30FB ; Common # Po KATAKANA MIDDLE DOT -30FC ; Common # Lm KATAKANA-HIRAGANA PROLONGED SOUND MARK -3190..3191 ; Common # So [2] IDEOGRAPHIC ANNOTATION LINKING MARK..IDEOGRAPHIC ANNOTATION REVERSE MARK -3192..3195 ; Common # No [4] IDEOGRAPHIC ANNOTATION ONE MARK..IDEOGRAPHIC ANNOTATION FOUR MARK -3196..319F ; Common # So [10] IDEOGRAPHIC ANNOTATION TOP MARK..IDEOGRAPHIC ANNOTATION MAN MARK -31C0..31E3 ; Common # So [36] CJK STROKE T..CJK STROKE Q -3220..3229 ; Common # No [10] PARENTHESIZED IDEOGRAPH ONE..PARENTHESIZED IDEOGRAPH TEN -322A..3247 ; Common # So [30] PARENTHESIZED IDEOGRAPH MOON..CIRCLED IDEOGRAPH KOTO -3248..324F ; Common # No [8] CIRCLED NUMBER TEN ON BLACK SQUARE..CIRCLED NUMBER EIGHTY ON BLACK SQUARE -3250 ; Common # So PARTNERSHIP SIGN -3251..325F ; Common # No [15] CIRCLED NUMBER TWENTY ONE..CIRCLED NUMBER THIRTY FIVE -327F ; Common # So KOREAN STANDARD SYMBOL -3280..3289 ; Common # No [10] CIRCLED IDEOGRAPH ONE..CIRCLED IDEOGRAPH TEN -328A..32B0 ; Common # So [39] CIRCLED IDEOGRAPH MOON..CIRCLED IDEOGRAPH NIGHT -32B1..32BF ; Common # No [15] CIRCLED NUMBER THIRTY SIX..CIRCLED NUMBER FIFTY -32C0..32CF ; Common # So [16] IDEOGRAPHIC TELEGRAPH SYMBOL FOR JANUARY..LIMITED LIABILITY SIGN -3358..33FF ; Common # So [168] IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR ZERO..SQUARE GAL -4DC0..4DFF ; Common # So [64] HEXAGRAM FOR THE CREATIVE HEAVEN..HEXAGRAM FOR BEFORE COMPLETION -A700..A716 ; Common # Sk [23] MODIFIER LETTER CHINESE TONE YIN PING..MODIFIER LETTER EXTRA-LOW LEFT-STEM TONE BAR -A717..A71F ; Common # Lm [9] MODIFIER LETTER DOT VERTICAL BAR..MODIFIER LETTER LOW INVERTED EXCLAMATION MARK -A720..A721 ; Common # Sk [2] MODIFIER LETTER STRESS AND HIGH TONE..MODIFIER LETTER STRESS AND LOW TONE -A788 ; Common # Lm MODIFIER LETTER LOW CIRCUMFLEX ACCENT -A789..A78A ; Common # Sk [2] MODIFIER LETTER COLON..MODIFIER LETTER SHORT EQUALS SIGN -A830..A835 ; Common # No [6] NORTH INDIC FRACTION ONE QUARTER..NORTH INDIC FRACTION THREE SIXTEENTHS -A836..A837 ; Common # So [2] NORTH INDIC QUARTER MARK..NORTH INDIC PLACEHOLDER MARK -A838 ; Common # Sc NORTH INDIC RUPEE MARK -A839 ; Common # So NORTH INDIC QUANTITY MARK -A92E ; Common # Po KAYAH LI SIGN CWI -A9CF ; Common # Lm JAVANESE PANGRANGKEP -AB5B ; Common # Sk MODIFIER BREVE WITH INVERTED BREVE -FD3E ; Common # Pe ORNATE LEFT PARENTHESIS -FD3F ; Common # Ps ORNATE RIGHT PARENTHESIS -FE10..FE16 ; Common # Po [7] PRESENTATION FORM FOR VERTICAL COMMA..PRESENTATION FORM FOR VERTICAL QUESTION MARK -FE17 ; Common # Ps PRESENTATION FORM FOR VERTICAL LEFT WHITE LENTICULAR BRACKET -FE18 ; Common # Pe PRESENTATION FORM FOR VERTICAL RIGHT WHITE LENTICULAR BRAKCET -FE19 ; Common # Po PRESENTATION FORM FOR VERTICAL HORIZONTAL ELLIPSIS -FE30 ; Common # Po PRESENTATION FORM FOR VERTICAL TWO DOT LEADER -FE31..FE32 ; Common # Pd [2] PRESENTATION FORM FOR VERTICAL EM DASH..PRESENTATION FORM FOR VERTICAL EN DASH -FE33..FE34 ; Common # Pc [2] PRESENTATION FORM FOR VERTICAL LOW LINE..PRESENTATION FORM FOR VERTICAL WAVY LOW LINE -FE35 ; Common # Ps PRESENTATION FORM FOR VERTICAL LEFT PARENTHESIS -FE36 ; Common # Pe PRESENTATION FORM FOR VERTICAL RIGHT PARENTHESIS -FE37 ; Common # Ps PRESENTATION FORM FOR VERTICAL LEFT CURLY BRACKET -FE38 ; Common # Pe PRESENTATION FORM FOR VERTICAL RIGHT CURLY BRACKET -FE39 ; Common # Ps PRESENTATION FORM FOR VERTICAL LEFT TORTOISE SHELL BRACKET -FE3A ; Common # Pe PRESENTATION FORM FOR VERTICAL RIGHT TORTOISE SHELL BRACKET -FE3B ; Common # Ps PRESENTATION FORM FOR VERTICAL LEFT BLACK LENTICULAR BRACKET -FE3C ; Common # Pe PRESENTATION FORM FOR VERTICAL RIGHT BLACK LENTICULAR BRACKET -FE3D ; Common # Ps PRESENTATION FORM FOR VERTICAL LEFT DOUBLE ANGLE BRACKET -FE3E ; Common # Pe PRESENTATION FORM FOR VERTICAL RIGHT DOUBLE ANGLE BRACKET -FE3F ; Common # Ps PRESENTATION FORM FOR VERTICAL LEFT ANGLE BRACKET -FE40 ; Common # Pe PRESENTATION FORM FOR VERTICAL RIGHT ANGLE BRACKET -FE41 ; Common # Ps PRESENTATION FORM FOR VERTICAL LEFT CORNER BRACKET -FE42 ; Common # Pe PRESENTATION FORM FOR VERTICAL RIGHT CORNER BRACKET -FE43 ; Common # Ps PRESENTATION FORM FOR VERTICAL LEFT WHITE CORNER BRACKET -FE44 ; Common # Pe PRESENTATION FORM FOR VERTICAL RIGHT WHITE CORNER BRACKET -FE45..FE46 ; Common # Po [2] SESAME DOT..WHITE SESAME DOT -FE47 ; Common # Ps PRESENTATION FORM FOR VERTICAL LEFT SQUARE BRACKET -FE48 ; Common # Pe PRESENTATION FORM FOR VERTICAL RIGHT SQUARE BRACKET -FE49..FE4C ; Common # Po [4] DASHED OVERLINE..DOUBLE WAVY OVERLINE -FE4D..FE4F ; Common # Pc [3] DASHED LOW LINE..WAVY LOW LINE -FE50..FE52 ; Common # Po [3] SMALL COMMA..SMALL FULL STOP -FE54..FE57 ; Common # Po [4] SMALL SEMICOLON..SMALL EXCLAMATION MARK -FE58 ; Common # Pd SMALL EM DASH -FE59 ; Common # Ps SMALL LEFT PARENTHESIS -FE5A ; Common # Pe SMALL RIGHT PARENTHESIS -FE5B ; Common # Ps SMALL LEFT CURLY BRACKET -FE5C ; Common # Pe SMALL RIGHT CURLY BRACKET -FE5D ; Common # Ps SMALL LEFT TORTOISE SHELL BRACKET -FE5E ; Common # Pe SMALL RIGHT TORTOISE SHELL BRACKET -FE5F..FE61 ; Common # Po [3] SMALL NUMBER SIGN..SMALL ASTERISK -FE62 ; Common # Sm SMALL PLUS SIGN -FE63 ; Common # Pd SMALL HYPHEN-MINUS -FE64..FE66 ; Common # Sm [3] SMALL LESS-THAN SIGN..SMALL EQUALS SIGN -FE68 ; Common # Po SMALL REVERSE SOLIDUS -FE69 ; Common # Sc SMALL DOLLAR SIGN -FE6A..FE6B ; Common # Po [2] SMALL PERCENT SIGN..SMALL COMMERCIAL AT -FEFF ; Common # Cf ZERO WIDTH NO-BREAK SPACE -FF01..FF03 ; Common # Po [3] FULLWIDTH EXCLAMATION MARK..FULLWIDTH NUMBER SIGN -FF04 ; Common # Sc FULLWIDTH DOLLAR SIGN -FF05..FF07 ; Common # Po [3] FULLWIDTH PERCENT SIGN..FULLWIDTH APOSTROPHE -FF08 ; Common # Ps FULLWIDTH LEFT PARENTHESIS -FF09 ; Common # Pe FULLWIDTH RIGHT PARENTHESIS -FF0A ; Common # Po FULLWIDTH ASTERISK -FF0B ; Common # Sm FULLWIDTH PLUS SIGN -FF0C ; Common # Po FULLWIDTH COMMA -FF0D ; Common # Pd FULLWIDTH HYPHEN-MINUS -FF0E..FF0F ; Common # Po [2] FULLWIDTH FULL STOP..FULLWIDTH SOLIDUS -FF10..FF19 ; Common # Nd [10] FULLWIDTH DIGIT ZERO..FULLWIDTH DIGIT NINE -FF1A..FF1B ; Common # Po [2] FULLWIDTH COLON..FULLWIDTH SEMICOLON -FF1C..FF1E ; Common # Sm [3] FULLWIDTH LESS-THAN SIGN..FULLWIDTH GREATER-THAN SIGN -FF1F..FF20 ; Common # Po [2] FULLWIDTH QUESTION MARK..FULLWIDTH COMMERCIAL AT -FF3B ; Common # Ps FULLWIDTH LEFT SQUARE BRACKET -FF3C ; Common # Po FULLWIDTH REVERSE SOLIDUS -FF3D ; Common # Pe FULLWIDTH RIGHT SQUARE BRACKET -FF3E ; Common # Sk FULLWIDTH CIRCUMFLEX ACCENT -FF3F ; Common # Pc FULLWIDTH LOW LINE -FF40 ; Common # Sk FULLWIDTH GRAVE ACCENT -FF5B ; Common # Ps FULLWIDTH LEFT CURLY BRACKET -FF5C ; Common # Sm FULLWIDTH VERTICAL LINE -FF5D ; Common # Pe FULLWIDTH RIGHT CURLY BRACKET -FF5E ; Common # Sm FULLWIDTH TILDE -FF5F ; Common # Ps FULLWIDTH LEFT WHITE PARENTHESIS -FF60 ; Common # Pe FULLWIDTH RIGHT WHITE PARENTHESIS -FF61 ; Common # Po HALFWIDTH IDEOGRAPHIC FULL STOP -FF62 ; Common # Ps HALFWIDTH LEFT CORNER BRACKET -FF63 ; Common # Pe HALFWIDTH RIGHT CORNER BRACKET -FF64..FF65 ; Common # Po [2] HALFWIDTH IDEOGRAPHIC COMMA..HALFWIDTH KATAKANA MIDDLE DOT -FF70 ; Common # Lm HALFWIDTH KATAKANA-HIRAGANA PROLONGED SOUND MARK -FF9E..FF9F ; Common # Lm [2] HALFWIDTH KATAKANA VOICED SOUND MARK..HALFWIDTH KATAKANA SEMI-VOICED SOUND MARK -FFE0..FFE1 ; Common # Sc [2] FULLWIDTH CENT SIGN..FULLWIDTH POUND SIGN -FFE2 ; Common # Sm FULLWIDTH NOT SIGN -FFE3 ; Common # Sk FULLWIDTH MACRON -FFE4 ; Common # So FULLWIDTH BROKEN BAR -FFE5..FFE6 ; Common # Sc [2] FULLWIDTH YEN SIGN..FULLWIDTH WON SIGN -FFE8 ; Common # So HALFWIDTH FORMS LIGHT VERTICAL -FFE9..FFEC ; Common # Sm [4] HALFWIDTH LEFTWARDS ARROW..HALFWIDTH DOWNWARDS ARROW -FFED..FFEE ; Common # So [2] HALFWIDTH BLACK SQUARE..HALFWIDTH WHITE CIRCLE -FFF9..FFFB ; Common # Cf [3] INTERLINEAR ANNOTATION ANCHOR..INTERLINEAR ANNOTATION TERMINATOR -FFFC..FFFD ; Common # So [2] OBJECT REPLACEMENT CHARACTER..REPLACEMENT CHARACTER -10100..10102 ; Common # Po [3] AEGEAN WORD SEPARATOR LINE..AEGEAN CHECK MARK -10107..10133 ; Common # No [45] AEGEAN NUMBER ONE..AEGEAN NUMBER NINETY THOUSAND -10137..1013F ; Common # So [9] AEGEAN WEIGHT BASE UNIT..AEGEAN MEASURE THIRD SUBUNIT -10190..1019B ; Common # So [12] ROMAN SEXTANS SIGN..ROMAN CENTURIAL SIGN -101D0..101FC ; Common # So [45] PHAISTOS DISC SIGN PEDESTRIAN..PHAISTOS DISC SIGN WAVY BAND -102E1..102FB ; Common # No [27] COPTIC EPACT DIGIT ONE..COPTIC EPACT NUMBER NINE HUNDRED -1BCA0..1BCA3 ; Common # Cf [4] SHORTHAND FORMAT LETTER OVERLAP..SHORTHAND FORMAT UP STEP -1D000..1D0F5 ; Common # So [246] BYZANTINE MUSICAL SYMBOL PSILI..BYZANTINE MUSICAL SYMBOL GORGON NEO KATO -1D100..1D126 ; Common # So [39] MUSICAL SYMBOL SINGLE BARLINE..MUSICAL SYMBOL DRUM CLEF-2 -1D129..1D164 ; Common # So [60] MUSICAL SYMBOL MULTIPLE MEASURE REST..MUSICAL SYMBOL ONE HUNDRED TWENTY-EIGHTH NOTE -1D165..1D166 ; Common # Mc [2] MUSICAL SYMBOL COMBINING STEM..MUSICAL SYMBOL COMBINING SPRECHGESANG STEM -1D16A..1D16C ; Common # So [3] MUSICAL SYMBOL FINGERED TREMOLO-1..MUSICAL SYMBOL FINGERED TREMOLO-3 -1D16D..1D172 ; Common # Mc [6] MUSICAL SYMBOL COMBINING AUGMENTATION DOT..MUSICAL SYMBOL COMBINING FLAG-5 -1D173..1D17A ; Common # Cf [8] MUSICAL SYMBOL BEGIN BEAM..MUSICAL SYMBOL END PHRASE -1D183..1D184 ; Common # So [2] MUSICAL SYMBOL ARPEGGIATO UP..MUSICAL SYMBOL ARPEGGIATO DOWN -1D18C..1D1A9 ; Common # So [30] MUSICAL SYMBOL RINFORZANDO..MUSICAL SYMBOL DEGREE SLASH -1D1AE..1D1E8 ; Common # So [59] MUSICAL SYMBOL PEDAL MARK..MUSICAL SYMBOL KIEVAN FLAT SIGN -1D300..1D356 ; Common # So [87] MONOGRAM FOR EARTH..TETRAGRAM FOR FOSTERING -1D360..1D371 ; Common # No [18] COUNTING ROD UNIT DIGIT ONE..COUNTING ROD TENS DIGIT NINE -1D400..1D454 ; Common # L& [85] MATHEMATICAL BOLD CAPITAL A..MATHEMATICAL ITALIC SMALL G -1D456..1D49C ; Common # L& [71] MATHEMATICAL ITALIC SMALL I..MATHEMATICAL SCRIPT CAPITAL A -1D49E..1D49F ; Common # L& [2] MATHEMATICAL SCRIPT CAPITAL C..MATHEMATICAL SCRIPT CAPITAL D -1D4A2 ; Common # L& MATHEMATICAL SCRIPT CAPITAL G -1D4A5..1D4A6 ; Common # L& [2] MATHEMATICAL SCRIPT CAPITAL J..MATHEMATICAL SCRIPT CAPITAL K -1D4A9..1D4AC ; Common # L& [4] MATHEMATICAL SCRIPT CAPITAL N..MATHEMATICAL SCRIPT CAPITAL Q -1D4AE..1D4B9 ; Common # L& [12] MATHEMATICAL SCRIPT CAPITAL S..MATHEMATICAL SCRIPT SMALL D -1D4BB ; Common # L& MATHEMATICAL SCRIPT SMALL F -1D4BD..1D4C3 ; Common # L& [7] MATHEMATICAL SCRIPT SMALL H..MATHEMATICAL SCRIPT SMALL N -1D4C5..1D505 ; Common # L& [65] MATHEMATICAL SCRIPT SMALL P..MATHEMATICAL FRAKTUR CAPITAL B -1D507..1D50A ; Common # L& [4] MATHEMATICAL FRAKTUR CAPITAL D..MATHEMATICAL FRAKTUR CAPITAL G -1D50D..1D514 ; Common # L& [8] MATHEMATICAL FRAKTUR CAPITAL J..MATHEMATICAL FRAKTUR CAPITAL Q -1D516..1D51C ; Common # L& [7] MATHEMATICAL FRAKTUR CAPITAL S..MATHEMATICAL FRAKTUR CAPITAL Y -1D51E..1D539 ; Common # L& [28] MATHEMATICAL FRAKTUR SMALL A..MATHEMATICAL DOUBLE-STRUCK CAPITAL B -1D53B..1D53E ; Common # L& [4] MATHEMATICAL DOUBLE-STRUCK CAPITAL D..MATHEMATICAL DOUBLE-STRUCK CAPITAL G -1D540..1D544 ; Common # L& [5] MATHEMATICAL DOUBLE-STRUCK CAPITAL I..MATHEMATICAL DOUBLE-STRUCK CAPITAL M -1D546 ; Common # L& MATHEMATICAL DOUBLE-STRUCK CAPITAL O -1D54A..1D550 ; Common # L& [7] MATHEMATICAL DOUBLE-STRUCK CAPITAL S..MATHEMATICAL DOUBLE-STRUCK CAPITAL Y -1D552..1D6A5 ; Common # L& [340] MATHEMATICAL DOUBLE-STRUCK SMALL A..MATHEMATICAL ITALIC SMALL DOTLESS J -1D6A8..1D6C0 ; Common # L& [25] MATHEMATICAL BOLD CAPITAL ALPHA..MATHEMATICAL BOLD CAPITAL OMEGA -1D6C1 ; Common # Sm MATHEMATICAL BOLD NABLA -1D6C2..1D6DA ; Common # L& [25] MATHEMATICAL BOLD SMALL ALPHA..MATHEMATICAL BOLD SMALL OMEGA -1D6DB ; Common # Sm MATHEMATICAL BOLD PARTIAL DIFFERENTIAL -1D6DC..1D6FA ; Common # L& [31] MATHEMATICAL BOLD EPSILON SYMBOL..MATHEMATICAL ITALIC CAPITAL OMEGA -1D6FB ; Common # Sm MATHEMATICAL ITALIC NABLA -1D6FC..1D714 ; Common # L& [25] MATHEMATICAL ITALIC SMALL ALPHA..MATHEMATICAL ITALIC SMALL OMEGA -1D715 ; Common # Sm MATHEMATICAL ITALIC PARTIAL DIFFERENTIAL -1D716..1D734 ; Common # L& [31] MATHEMATICAL ITALIC EPSILON SYMBOL..MATHEMATICAL BOLD ITALIC CAPITAL OMEGA -1D735 ; Common # Sm MATHEMATICAL BOLD ITALIC NABLA -1D736..1D74E ; Common # L& [25] MATHEMATICAL BOLD ITALIC SMALL ALPHA..MATHEMATICAL BOLD ITALIC SMALL OMEGA -1D74F ; Common # Sm MATHEMATICAL BOLD ITALIC PARTIAL DIFFERENTIAL -1D750..1D76E ; Common # L& [31] MATHEMATICAL BOLD ITALIC EPSILON SYMBOL..MATHEMATICAL SANS-SERIF BOLD CAPITAL OMEGA -1D76F ; Common # Sm MATHEMATICAL SANS-SERIF BOLD NABLA -1D770..1D788 ; Common # L& [25] MATHEMATICAL SANS-SERIF BOLD SMALL ALPHA..MATHEMATICAL SANS-SERIF BOLD SMALL OMEGA -1D789 ; Common # Sm MATHEMATICAL SANS-SERIF BOLD PARTIAL DIFFERENTIAL -1D78A..1D7A8 ; Common # L& [31] MATHEMATICAL SANS-SERIF BOLD EPSILON SYMBOL..MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL OMEGA -1D7A9 ; Common # Sm MATHEMATICAL SANS-SERIF BOLD ITALIC NABLA -1D7AA..1D7C2 ; Common # L& [25] MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL ALPHA..MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL OMEGA -1D7C3 ; Common # Sm MATHEMATICAL SANS-SERIF BOLD ITALIC PARTIAL DIFFERENTIAL -1D7C4..1D7CB ; Common # L& [8] MATHEMATICAL SANS-SERIF BOLD ITALIC EPSILON SYMBOL..MATHEMATICAL BOLD SMALL DIGAMMA -1D7CE..1D7FF ; Common # Nd [50] MATHEMATICAL BOLD DIGIT ZERO..MATHEMATICAL MONOSPACE DIGIT NINE -1F000..1F02B ; Common # So [44] MAHJONG TILE EAST WIND..MAHJONG TILE BACK -1F030..1F093 ; Common # So [100] DOMINO TILE HORIZONTAL BACK..DOMINO TILE VERTICAL-06-06 -1F0A0..1F0AE ; Common # So [15] PLAYING CARD BACK..PLAYING CARD KING OF SPADES -1F0B1..1F0BF ; Common # So [15] PLAYING CARD ACE OF HEARTS..PLAYING CARD RED JOKER -1F0C1..1F0CF ; Common # So [15] PLAYING CARD ACE OF DIAMONDS..PLAYING CARD BLACK JOKER -1F0D1..1F0F5 ; Common # So [37] PLAYING CARD ACE OF CLUBS..PLAYING CARD TRUMP-21 -1F100..1F10C ; Common # No [13] DIGIT ZERO FULL STOP..DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT ZERO -1F110..1F12E ; Common # So [31] PARENTHESIZED LATIN CAPITAL LETTER A..CIRCLED WZ -1F130..1F16B ; Common # So [60] SQUARED LATIN CAPITAL LETTER A..RAISED MD SIGN -1F170..1F1AC ; Common # So [61] NEGATIVE SQUARED LATIN CAPITAL LETTER A..SQUARED VOD -1F1E6..1F1FF ; Common # So [26] REGIONAL INDICATOR SYMBOL LETTER A..REGIONAL INDICATOR SYMBOL LETTER Z -1F201..1F202 ; Common # So [2] SQUARED KATAKANA KOKO..SQUARED KATAKANA SA -1F210..1F23B ; Common # So [44] SQUARED CJK UNIFIED IDEOGRAPH-624B..SQUARED CJK UNIFIED IDEOGRAPH-914D -1F240..1F248 ; Common # So [9] TORTOISE SHELL BRACKETED CJK UNIFIED IDEOGRAPH-672C..TORTOISE SHELL BRACKETED CJK UNIFIED IDEOGRAPH-6557 -1F250..1F251 ; Common # So [2] CIRCLED IDEOGRAPH ADVANTAGE..CIRCLED IDEOGRAPH ACCEPT -1F260..1F265 ; Common # So [6] ROUNDED SYMBOL FOR FU..ROUNDED SYMBOL FOR CAI -1F300..1F3FA ; Common # So [251] CYCLONE..AMPHORA -1F3FB..1F3FF ; Common # Sk [5] EMOJI MODIFIER FITZPATRICK TYPE-1-2..EMOJI MODIFIER FITZPATRICK TYPE-6 -1F400..1F6D4 ; Common # So [725] RAT..PAGODA -1F6E0..1F6EC ; Common # So [13] HAMMER AND WRENCH..AIRPLANE ARRIVING -1F6F0..1F6F8 ; Common # So [9] SATELLITE..FLYING SAUCER -1F700..1F773 ; Common # So [116] ALCHEMICAL SYMBOL FOR QUINTESSENCE..ALCHEMICAL SYMBOL FOR HALF OUNCE -1F780..1F7D4 ; Common # So [85] BLACK LEFT-POINTING ISOSCELES RIGHT TRIANGLE..HEAVY TWELVE POINTED PINWHEEL STAR -1F800..1F80B ; Common # So [12] LEFTWARDS ARROW WITH SMALL TRIANGLE ARROWHEAD..DOWNWARDS ARROW WITH LARGE TRIANGLE ARROWHEAD -1F810..1F847 ; Common # So [56] LEFTWARDS ARROW WITH SMALL EQUILATERAL ARROWHEAD..DOWNWARDS HEAVY ARROW -1F850..1F859 ; Common # So [10] LEFTWARDS SANS-SERIF ARROW..UP DOWN SANS-SERIF ARROW -1F860..1F887 ; Common # So [40] WIDE-HEADED LEFTWARDS LIGHT BARB ARROW..WIDE-HEADED SOUTH WEST VERY HEAVY BARB ARROW -1F890..1F8AD ; Common # So [30] LEFTWARDS TRIANGLE ARROWHEAD..WHITE ARROW SHAFT WIDTH TWO THIRDS -1F900..1F90B ; Common # So [12] CIRCLED CROSS FORMEE WITH FOUR DOTS..DOWNWARD FACING NOTCHED HOOK WITH DOT -1F910..1F93E ; Common # So [47] ZIPPER-MOUTH FACE..HANDBALL -1F940..1F94C ; Common # So [13] WILTED FLOWER..CURLING STONE -1F950..1F96B ; Common # So [28] CROISSANT..CANNED FOOD -1F980..1F997 ; Common # So [24] CRAB..CRICKET -1F9C0 ; Common # So CHEESE WEDGE -1F9D0..1F9E6 ; Common # So [23] FACE WITH MONOCLE..SOCKS -E0001 ; Common # Cf LANGUAGE TAG -E0020..E007F ; Common # Cf [96] TAG SPACE..CANCEL TAG - -# Total code points: 7363 - -# ================================================ - -0041..005A ; Latin # L& [26] LATIN CAPITAL LETTER A..LATIN CAPITAL LETTER Z -0061..007A ; Latin # L& [26] LATIN SMALL LETTER A..LATIN SMALL LETTER Z -00AA ; Latin # Lo FEMININE ORDINAL INDICATOR -00BA ; Latin # Lo MASCULINE ORDINAL INDICATOR -00C0..00D6 ; Latin # L& [23] LATIN CAPITAL LETTER A WITH GRAVE..LATIN CAPITAL LETTER O WITH DIAERESIS -00D8..00F6 ; Latin # L& [31] LATIN CAPITAL LETTER O WITH STROKE..LATIN SMALL LETTER O WITH DIAERESIS -00F8..01BA ; Latin # L& [195] LATIN SMALL LETTER O WITH STROKE..LATIN SMALL LETTER EZH WITH TAIL -01BB ; Latin # Lo LATIN LETTER TWO WITH STROKE -01BC..01BF ; Latin # L& [4] LATIN CAPITAL LETTER TONE FIVE..LATIN LETTER WYNN -01C0..01C3 ; Latin # Lo [4] LATIN LETTER DENTAL CLICK..LATIN LETTER RETROFLEX CLICK -01C4..0293 ; Latin # L& [208] LATIN CAPITAL LETTER DZ WITH CARON..LATIN SMALL LETTER EZH WITH CURL -0294 ; Latin # Lo LATIN LETTER GLOTTAL STOP -0295..02AF ; Latin # L& [27] LATIN LETTER PHARYNGEAL VOICED FRICATIVE..LATIN SMALL LETTER TURNED H WITH FISHHOOK AND TAIL -02B0..02B8 ; Latin # Lm [9] MODIFIER LETTER SMALL H..MODIFIER LETTER SMALL Y -02E0..02E4 ; Latin # Lm [5] MODIFIER LETTER SMALL GAMMA..MODIFIER LETTER SMALL REVERSED GLOTTAL STOP -1D00..1D25 ; Latin # L& [38] LATIN LETTER SMALL CAPITAL A..LATIN LETTER AIN -1D2C..1D5C ; Latin # Lm [49] MODIFIER LETTER CAPITAL A..MODIFIER LETTER SMALL AIN -1D62..1D65 ; Latin # Lm [4] LATIN SUBSCRIPT SMALL LETTER I..LATIN SUBSCRIPT SMALL LETTER V -1D6B..1D77 ; Latin # L& [13] LATIN SMALL LETTER UE..LATIN SMALL LETTER TURNED G -1D79..1D9A ; Latin # L& [34] LATIN SMALL LETTER INSULAR G..LATIN SMALL LETTER EZH WITH RETROFLEX HOOK -1D9B..1DBE ; Latin # Lm [36] MODIFIER LETTER SMALL TURNED ALPHA..MODIFIER LETTER SMALL EZH -1E00..1EFF ; Latin # L& [256] LATIN CAPITAL LETTER A WITH RING BELOW..LATIN SMALL LETTER Y WITH LOOP -2071 ; Latin # Lm SUPERSCRIPT LATIN SMALL LETTER I -207F ; Latin # Lm SUPERSCRIPT LATIN SMALL LETTER N -2090..209C ; Latin # Lm [13] LATIN SUBSCRIPT SMALL LETTER A..LATIN SUBSCRIPT SMALL LETTER T -212A..212B ; Latin # L& [2] KELVIN SIGN..ANGSTROM SIGN -2132 ; Latin # L& TURNED CAPITAL F -214E ; Latin # L& TURNED SMALL F -2160..2182 ; Latin # Nl [35] ROMAN NUMERAL ONE..ROMAN NUMERAL TEN THOUSAND -2183..2184 ; Latin # L& [2] ROMAN NUMERAL REVERSED ONE HUNDRED..LATIN SMALL LETTER REVERSED C -2185..2188 ; Latin # Nl [4] ROMAN NUMERAL SIX LATE FORM..ROMAN NUMERAL ONE HUNDRED THOUSAND -2C60..2C7B ; Latin # L& [28] LATIN CAPITAL LETTER L WITH DOUBLE BAR..LATIN LETTER SMALL CAPITAL TURNED E -2C7C..2C7D ; Latin # Lm [2] LATIN SUBSCRIPT SMALL LETTER J..MODIFIER LETTER CAPITAL V -2C7E..2C7F ; Latin # L& [2] LATIN CAPITAL LETTER S WITH SWASH TAIL..LATIN CAPITAL LETTER Z WITH SWASH TAIL -A722..A76F ; Latin # L& [78] LATIN CAPITAL LETTER EGYPTOLOGICAL ALEF..LATIN SMALL LETTER CON -A770 ; Latin # Lm MODIFIER LETTER US -A771..A787 ; Latin # L& [23] LATIN SMALL LETTER DUM..LATIN SMALL LETTER INSULAR T -A78B..A78E ; Latin # L& [4] LATIN CAPITAL LETTER SALTILLO..LATIN SMALL LETTER L WITH RETROFLEX HOOK AND BELT -A78F ; Latin # Lo LATIN LETTER SINOLOGICAL DOT -A790..A7AE ; Latin # L& [31] LATIN CAPITAL LETTER N WITH DESCENDER..LATIN CAPITAL LETTER SMALL CAPITAL I -A7B0..A7B7 ; Latin # L& [8] LATIN CAPITAL LETTER TURNED K..LATIN SMALL LETTER OMEGA -A7F7 ; Latin # Lo LATIN EPIGRAPHIC LETTER SIDEWAYS I -A7F8..A7F9 ; Latin # Lm [2] MODIFIER LETTER CAPITAL H WITH STROKE..MODIFIER LETTER SMALL LIGATURE OE -A7FA ; Latin # L& LATIN LETTER SMALL CAPITAL TURNED M -A7FB..A7FF ; Latin # Lo [5] LATIN EPIGRAPHIC LETTER REVERSED F..LATIN EPIGRAPHIC LETTER ARCHAIC M -AB30..AB5A ; Latin # L& [43] LATIN SMALL LETTER BARRED ALPHA..LATIN SMALL LETTER Y WITH SHORT RIGHT LEG -AB5C..AB5F ; Latin # Lm [4] MODIFIER LETTER SMALL HENG..MODIFIER LETTER SMALL U WITH LEFT HOOK -AB60..AB64 ; Latin # L& [5] LATIN SMALL LETTER SAKHA YAT..LATIN SMALL LETTER INVERTED ALPHA -FB00..FB06 ; Latin # L& [7] LATIN SMALL LIGATURE FF..LATIN SMALL LIGATURE ST -FF21..FF3A ; Latin # L& [26] FULLWIDTH LATIN CAPITAL LETTER A..FULLWIDTH LATIN CAPITAL LETTER Z -FF41..FF5A ; Latin # L& [26] FULLWIDTH LATIN SMALL LETTER A..FULLWIDTH LATIN SMALL LETTER Z - -# Total code points: 1350 - -# ================================================ - -0370..0373 ; Greek # L& [4] GREEK CAPITAL LETTER HETA..GREEK SMALL LETTER ARCHAIC SAMPI -0375 ; Greek # Sk GREEK LOWER NUMERAL SIGN -0376..0377 ; Greek # L& [2] GREEK CAPITAL LETTER PAMPHYLIAN DIGAMMA..GREEK SMALL LETTER PAMPHYLIAN DIGAMMA -037A ; Greek # Lm GREEK YPOGEGRAMMENI -037B..037D ; Greek # L& [3] GREEK SMALL REVERSED LUNATE SIGMA SYMBOL..GREEK SMALL REVERSED DOTTED LUNATE SIGMA SYMBOL -037F ; Greek # L& GREEK CAPITAL LETTER YOT -0384 ; Greek # Sk GREEK TONOS -0386 ; Greek # L& GREEK CAPITAL LETTER ALPHA WITH TONOS -0388..038A ; Greek # L& [3] GREEK CAPITAL LETTER EPSILON WITH TONOS..GREEK CAPITAL LETTER IOTA WITH TONOS -038C ; Greek # L& GREEK CAPITAL LETTER OMICRON WITH TONOS -038E..03A1 ; Greek # L& [20] GREEK CAPITAL LETTER UPSILON WITH TONOS..GREEK CAPITAL LETTER RHO -03A3..03E1 ; Greek # L& [63] GREEK CAPITAL LETTER SIGMA..GREEK SMALL LETTER SAMPI -03F0..03F5 ; Greek # L& [6] GREEK KAPPA SYMBOL..GREEK LUNATE EPSILON SYMBOL -03F6 ; Greek # Sm GREEK REVERSED LUNATE EPSILON SYMBOL -03F7..03FF ; Greek # L& [9] GREEK CAPITAL LETTER SHO..GREEK CAPITAL REVERSED DOTTED LUNATE SIGMA SYMBOL -1D26..1D2A ; Greek # L& [5] GREEK LETTER SMALL CAPITAL GAMMA..GREEK LETTER SMALL CAPITAL PSI -1D5D..1D61 ; Greek # Lm [5] MODIFIER LETTER SMALL BETA..MODIFIER LETTER SMALL CHI -1D66..1D6A ; Greek # Lm [5] GREEK SUBSCRIPT SMALL LETTER BETA..GREEK SUBSCRIPT SMALL LETTER CHI -1DBF ; Greek # Lm MODIFIER LETTER SMALL THETA -1F00..1F15 ; Greek # L& [22] GREEK SMALL LETTER ALPHA WITH PSILI..GREEK SMALL LETTER EPSILON WITH DASIA AND OXIA -1F18..1F1D ; Greek # L& [6] GREEK CAPITAL LETTER EPSILON WITH PSILI..GREEK CAPITAL LETTER EPSILON WITH DASIA AND OXIA -1F20..1F45 ; Greek # L& [38] GREEK SMALL LETTER ETA WITH PSILI..GREEK SMALL LETTER OMICRON WITH DASIA AND OXIA -1F48..1F4D ; Greek # L& [6] GREEK CAPITAL LETTER OMICRON WITH PSILI..GREEK CAPITAL LETTER OMICRON WITH DASIA AND OXIA -1F50..1F57 ; Greek # L& [8] GREEK SMALL LETTER UPSILON WITH PSILI..GREEK SMALL LETTER UPSILON WITH DASIA AND PERISPOMENI -1F59 ; Greek # L& GREEK CAPITAL LETTER UPSILON WITH DASIA -1F5B ; Greek # L& GREEK CAPITAL LETTER UPSILON WITH DASIA AND VARIA -1F5D ; Greek # L& GREEK CAPITAL LETTER UPSILON WITH DASIA AND OXIA -1F5F..1F7D ; Greek # L& [31] GREEK CAPITAL LETTER UPSILON WITH DASIA AND PERISPOMENI..GREEK SMALL LETTER OMEGA WITH OXIA -1F80..1FB4 ; Greek # L& [53] GREEK SMALL LETTER ALPHA WITH PSILI AND YPOGEGRAMMENI..GREEK SMALL LETTER ALPHA WITH OXIA AND YPOGEGRAMMENI -1FB6..1FBC ; Greek # L& [7] GREEK SMALL LETTER ALPHA WITH PERISPOMENI..GREEK CAPITAL LETTER ALPHA WITH PROSGEGRAMMENI -1FBD ; Greek # Sk GREEK KORONIS -1FBE ; Greek # L& GREEK PROSGEGRAMMENI -1FBF..1FC1 ; Greek # Sk [3] GREEK PSILI..GREEK DIALYTIKA AND PERISPOMENI -1FC2..1FC4 ; Greek # L& [3] GREEK SMALL LETTER ETA WITH VARIA AND YPOGEGRAMMENI..GREEK SMALL LETTER ETA WITH OXIA AND YPOGEGRAMMENI -1FC6..1FCC ; Greek # L& [7] GREEK SMALL LETTER ETA WITH PERISPOMENI..GREEK CAPITAL LETTER ETA WITH PROSGEGRAMMENI -1FCD..1FCF ; Greek # Sk [3] GREEK PSILI AND VARIA..GREEK PSILI AND PERISPOMENI -1FD0..1FD3 ; Greek # L& [4] GREEK SMALL LETTER IOTA WITH VRACHY..GREEK SMALL LETTER IOTA WITH DIALYTIKA AND OXIA -1FD6..1FDB ; Greek # L& [6] GREEK SMALL LETTER IOTA WITH PERISPOMENI..GREEK CAPITAL LETTER IOTA WITH OXIA -1FDD..1FDF ; Greek # Sk [3] GREEK DASIA AND VARIA..GREEK DASIA AND PERISPOMENI -1FE0..1FEC ; Greek # L& [13] GREEK SMALL LETTER UPSILON WITH VRACHY..GREEK CAPITAL LETTER RHO WITH DASIA -1FED..1FEF ; Greek # Sk [3] GREEK DIALYTIKA AND VARIA..GREEK VARIA -1FF2..1FF4 ; Greek # L& [3] GREEK SMALL LETTER OMEGA WITH VARIA AND YPOGEGRAMMENI..GREEK SMALL LETTER OMEGA WITH OXIA AND YPOGEGRAMMENI -1FF6..1FFC ; Greek # L& [7] GREEK SMALL LETTER OMEGA WITH PERISPOMENI..GREEK CAPITAL LETTER OMEGA WITH PROSGEGRAMMENI -1FFD..1FFE ; Greek # Sk [2] GREEK OXIA..GREEK DASIA -2126 ; Greek # L& OHM SIGN -AB65 ; Greek # L& GREEK LETTER SMALL CAPITAL OMEGA -10140..10174 ; Greek # Nl [53] GREEK ACROPHONIC ATTIC ONE QUARTER..GREEK ACROPHONIC STRATIAN FIFTY MNAS -10175..10178 ; Greek # No [4] GREEK ONE HALF SIGN..GREEK THREE QUARTERS SIGN -10179..10189 ; Greek # So [17] GREEK YEAR SIGN..GREEK TRYBLION BASE SIGN -1018A..1018B ; Greek # No [2] GREEK ZERO SIGN..GREEK ONE QUARTER SIGN -1018C..1018E ; Greek # So [3] GREEK SINUSOID SIGN..NOMISMA SIGN -101A0 ; Greek # So GREEK SYMBOL TAU RHO -1D200..1D241 ; Greek # So [66] GREEK VOCAL NOTATION SYMBOL-1..GREEK INSTRUMENTAL NOTATION SYMBOL-54 -1D242..1D244 ; Greek # Mn [3] COMBINING GREEK MUSICAL TRISEME..COMBINING GREEK MUSICAL PENTASEME -1D245 ; Greek # So GREEK MUSICAL LEIMMA - -# Total code points: 518 - -# ================================================ - -0400..0481 ; Cyrillic # L& [130] CYRILLIC CAPITAL LETTER IE WITH GRAVE..CYRILLIC SMALL LETTER KOPPA -0482 ; Cyrillic # So CYRILLIC THOUSANDS SIGN -0483..0484 ; Cyrillic # Mn [2] COMBINING CYRILLIC TITLO..COMBINING CYRILLIC PALATALIZATION -0487 ; Cyrillic # Mn COMBINING CYRILLIC POKRYTIE -0488..0489 ; Cyrillic # Me [2] COMBINING CYRILLIC HUNDRED THOUSANDS SIGN..COMBINING CYRILLIC MILLIONS SIGN -048A..052F ; Cyrillic # L& [166] CYRILLIC CAPITAL LETTER SHORT I WITH TAIL..CYRILLIC SMALL LETTER EL WITH DESCENDER -1C80..1C88 ; Cyrillic # L& [9] CYRILLIC SMALL LETTER ROUNDED VE..CYRILLIC SMALL LETTER UNBLENDED UK -1D2B ; Cyrillic # L& CYRILLIC LETTER SMALL CAPITAL EL -1D78 ; Cyrillic # Lm MODIFIER LETTER CYRILLIC EN -2DE0..2DFF ; Cyrillic # Mn [32] COMBINING CYRILLIC LETTER BE..COMBINING CYRILLIC LETTER IOTIFIED BIG YUS -A640..A66D ; Cyrillic # L& [46] CYRILLIC CAPITAL LETTER ZEMLYA..CYRILLIC SMALL LETTER DOUBLE MONOCULAR O -A66E ; Cyrillic # Lo CYRILLIC LETTER MULTIOCULAR O -A66F ; Cyrillic # Mn COMBINING CYRILLIC VZMET -A670..A672 ; Cyrillic # Me [3] COMBINING CYRILLIC TEN MILLIONS SIGN..COMBINING CYRILLIC THOUSAND MILLIONS SIGN -A673 ; Cyrillic # Po SLAVONIC ASTERISK -A674..A67D ; Cyrillic # Mn [10] COMBINING CYRILLIC LETTER UKRAINIAN IE..COMBINING CYRILLIC PAYEROK -A67E ; Cyrillic # Po CYRILLIC KAVYKA -A67F ; Cyrillic # Lm CYRILLIC PAYEROK -A680..A69B ; Cyrillic # L& [28] CYRILLIC CAPITAL LETTER DWE..CYRILLIC SMALL LETTER CROSSED O -A69C..A69D ; Cyrillic # Lm [2] MODIFIER LETTER CYRILLIC HARD SIGN..MODIFIER LETTER CYRILLIC SOFT SIGN -A69E..A69F ; Cyrillic # Mn [2] COMBINING CYRILLIC LETTER EF..COMBINING CYRILLIC LETTER IOTIFIED E -FE2E..FE2F ; Cyrillic # Mn [2] COMBINING CYRILLIC TITLO LEFT HALF..COMBINING CYRILLIC TITLO RIGHT HALF - -# Total code points: 443 - -# ================================================ - -0531..0556 ; Armenian # L& [38] ARMENIAN CAPITAL LETTER AYB..ARMENIAN CAPITAL LETTER FEH -0559 ; Armenian # Lm ARMENIAN MODIFIER LETTER LEFT HALF RING -055A..055F ; Armenian # Po [6] ARMENIAN APOSTROPHE..ARMENIAN ABBREVIATION MARK -0561..0587 ; Armenian # L& [39] ARMENIAN SMALL LETTER AYB..ARMENIAN SMALL LIGATURE ECH YIWN -058A ; Armenian # Pd ARMENIAN HYPHEN -058D..058E ; Armenian # So [2] RIGHT-FACING ARMENIAN ETERNITY SIGN..LEFT-FACING ARMENIAN ETERNITY SIGN -058F ; Armenian # Sc ARMENIAN DRAM SIGN -FB13..FB17 ; Armenian # L& [5] ARMENIAN SMALL LIGATURE MEN NOW..ARMENIAN SMALL LIGATURE MEN XEH - -# Total code points: 93 - -# ================================================ - -0591..05BD ; Hebrew # Mn [45] HEBREW ACCENT ETNAHTA..HEBREW POINT METEG -05BE ; Hebrew # Pd HEBREW PUNCTUATION MAQAF -05BF ; Hebrew # Mn HEBREW POINT RAFE -05C0 ; Hebrew # Po HEBREW PUNCTUATION PASEQ -05C1..05C2 ; Hebrew # Mn [2] HEBREW POINT SHIN DOT..HEBREW POINT SIN DOT -05C3 ; Hebrew # Po HEBREW PUNCTUATION SOF PASUQ -05C4..05C5 ; Hebrew # Mn [2] HEBREW MARK UPPER DOT..HEBREW MARK LOWER DOT -05C6 ; Hebrew # Po HEBREW PUNCTUATION NUN HAFUKHA -05C7 ; Hebrew # Mn HEBREW POINT QAMATS QATAN -05D0..05EA ; Hebrew # Lo [27] HEBREW LETTER ALEF..HEBREW LETTER TAV -05F0..05F2 ; Hebrew # Lo [3] HEBREW LIGATURE YIDDISH DOUBLE VAV..HEBREW LIGATURE YIDDISH DOUBLE YOD -05F3..05F4 ; Hebrew # Po [2] HEBREW PUNCTUATION GERESH..HEBREW PUNCTUATION GERSHAYIM -FB1D ; Hebrew # Lo HEBREW LETTER YOD WITH HIRIQ -FB1E ; Hebrew # Mn HEBREW POINT JUDEO-SPANISH VARIKA -FB1F..FB28 ; Hebrew # Lo [10] HEBREW LIGATURE YIDDISH YOD YOD PATAH..HEBREW LETTER WIDE TAV -FB29 ; Hebrew # Sm HEBREW LETTER ALTERNATIVE PLUS SIGN -FB2A..FB36 ; Hebrew # Lo [13] HEBREW LETTER SHIN WITH SHIN DOT..HEBREW LETTER ZAYIN WITH DAGESH -FB38..FB3C ; Hebrew # Lo [5] HEBREW LETTER TET WITH DAGESH..HEBREW LETTER LAMED WITH DAGESH -FB3E ; Hebrew # Lo HEBREW LETTER MEM WITH DAGESH -FB40..FB41 ; Hebrew # Lo [2] HEBREW LETTER NUN WITH DAGESH..HEBREW LETTER SAMEKH WITH DAGESH -FB43..FB44 ; Hebrew # Lo [2] HEBREW LETTER FINAL PE WITH DAGESH..HEBREW LETTER PE WITH DAGESH -FB46..FB4F ; Hebrew # Lo [10] HEBREW LETTER TSADI WITH DAGESH..HEBREW LIGATURE ALEF LAMED - -# Total code points: 133 - -# ================================================ - -0600..0604 ; Arabic # Cf [5] ARABIC NUMBER SIGN..ARABIC SIGN SAMVAT -0606..0608 ; Arabic # Sm [3] ARABIC-INDIC CUBE ROOT..ARABIC RAY -0609..060A ; Arabic # Po [2] ARABIC-INDIC PER MILLE SIGN..ARABIC-INDIC PER TEN THOUSAND SIGN -060B ; Arabic # Sc AFGHANI SIGN -060D ; Arabic # Po ARABIC DATE SEPARATOR -060E..060F ; Arabic # So [2] ARABIC POETIC VERSE SIGN..ARABIC SIGN MISRA -0610..061A ; Arabic # Mn [11] ARABIC SIGN SALLALLAHOU ALAYHE WASSALLAM..ARABIC SMALL KASRA -061C ; Arabic # Cf ARABIC LETTER MARK -061E ; Arabic # Po ARABIC TRIPLE DOT PUNCTUATION MARK -0620..063F ; Arabic # Lo [32] ARABIC LETTER KASHMIRI YEH..ARABIC LETTER FARSI YEH WITH THREE DOTS ABOVE -0641..064A ; Arabic # Lo [10] ARABIC LETTER FEH..ARABIC LETTER YEH -0656..065F ; Arabic # Mn [10] ARABIC SUBSCRIPT ALEF..ARABIC WAVY HAMZA BELOW -0660..0669 ; Arabic # Nd [10] ARABIC-INDIC DIGIT ZERO..ARABIC-INDIC DIGIT NINE -066A..066D ; Arabic # Po [4] ARABIC PERCENT SIGN..ARABIC FIVE POINTED STAR -066E..066F ; Arabic # Lo [2] ARABIC LETTER DOTLESS BEH..ARABIC LETTER DOTLESS QAF -0671..06D3 ; Arabic # Lo [99] ARABIC LETTER ALEF WASLA..ARABIC LETTER YEH BARREE WITH HAMZA ABOVE -06D4 ; Arabic # Po ARABIC FULL STOP -06D5 ; Arabic # Lo ARABIC LETTER AE -06D6..06DC ; Arabic # Mn [7] ARABIC SMALL HIGH LIGATURE SAD WITH LAM WITH ALEF MAKSURA..ARABIC SMALL HIGH SEEN -06DE ; Arabic # So ARABIC START OF RUB EL HIZB -06DF..06E4 ; Arabic # Mn [6] ARABIC SMALL HIGH ROUNDED ZERO..ARABIC SMALL HIGH MADDA -06E5..06E6 ; Arabic # Lm [2] ARABIC SMALL WAW..ARABIC SMALL YEH -06E7..06E8 ; Arabic # Mn [2] ARABIC SMALL HIGH YEH..ARABIC SMALL HIGH NOON -06E9 ; Arabic # So ARABIC PLACE OF SAJDAH -06EA..06ED ; Arabic # Mn [4] ARABIC EMPTY CENTRE LOW STOP..ARABIC SMALL LOW MEEM -06EE..06EF ; Arabic # Lo [2] ARABIC LETTER DAL WITH INVERTED V..ARABIC LETTER REH WITH INVERTED V -06F0..06F9 ; Arabic # Nd [10] EXTENDED ARABIC-INDIC DIGIT ZERO..EXTENDED ARABIC-INDIC DIGIT NINE -06FA..06FC ; Arabic # Lo [3] ARABIC LETTER SHEEN WITH DOT BELOW..ARABIC LETTER GHAIN WITH DOT BELOW -06FD..06FE ; Arabic # So [2] ARABIC SIGN SINDHI AMPERSAND..ARABIC SIGN SINDHI POSTPOSITION MEN -06FF ; Arabic # Lo ARABIC LETTER HEH WITH INVERTED V -0750..077F ; Arabic # Lo [48] ARABIC LETTER BEH WITH THREE DOTS HORIZONTALLY BELOW..ARABIC LETTER KAF WITH TWO DOTS ABOVE -08A0..08B4 ; Arabic # Lo [21] ARABIC LETTER BEH WITH SMALL V BELOW..ARABIC LETTER KAF WITH DOT BELOW -08B6..08BD ; Arabic # Lo [8] ARABIC LETTER BEH WITH SMALL MEEM ABOVE..ARABIC LETTER AFRICAN NOON -08D4..08E1 ; Arabic # Mn [14] ARABIC SMALL HIGH WORD AR-RUB..ARABIC SMALL HIGH SIGN SAFHA -08E3..08FF ; Arabic # Mn [29] ARABIC TURNED DAMMA BELOW..ARABIC MARK SIDEWAYS NOON GHUNNA -FB50..FBB1 ; Arabic # Lo [98] ARABIC LETTER ALEF WASLA ISOLATED FORM..ARABIC LETTER YEH BARREE WITH HAMZA ABOVE FINAL FORM -FBB2..FBC1 ; Arabic # Sk [16] ARABIC SYMBOL DOT ABOVE..ARABIC SYMBOL SMALL TAH BELOW -FBD3..FD3D ; Arabic # Lo [363] ARABIC LETTER NG ISOLATED FORM..ARABIC LIGATURE ALEF WITH FATHATAN ISOLATED FORM -FD50..FD8F ; Arabic # Lo [64] ARABIC LIGATURE TEH WITH JEEM WITH MEEM INITIAL FORM..ARABIC LIGATURE MEEM WITH KHAH WITH MEEM INITIAL FORM -FD92..FDC7 ; Arabic # Lo [54] ARABIC LIGATURE MEEM WITH JEEM WITH KHAH INITIAL FORM..ARABIC LIGATURE NOON WITH JEEM WITH YEH FINAL FORM -FDF0..FDFB ; Arabic # Lo [12] ARABIC LIGATURE SALLA USED AS KORANIC STOP SIGN ISOLATED FORM..ARABIC LIGATURE JALLAJALALOUHOU -FDFC ; Arabic # Sc RIAL SIGN -FDFD ; Arabic # So ARABIC LIGATURE BISMILLAH AR-RAHMAN AR-RAHEEM -FE70..FE74 ; Arabic # Lo [5] ARABIC FATHATAN ISOLATED FORM..ARABIC KASRATAN ISOLATED FORM -FE76..FEFC ; Arabic # Lo [135] ARABIC FATHA ISOLATED FORM..ARABIC LIGATURE LAM WITH ALEF FINAL FORM -10E60..10E7E ; Arabic # No [31] RUMI DIGIT ONE..RUMI FRACTION TWO THIRDS -1EE00..1EE03 ; Arabic # Lo [4] ARABIC MATHEMATICAL ALEF..ARABIC MATHEMATICAL DAL -1EE05..1EE1F ; Arabic # Lo [27] ARABIC MATHEMATICAL WAW..ARABIC MATHEMATICAL DOTLESS QAF -1EE21..1EE22 ; Arabic # Lo [2] ARABIC MATHEMATICAL INITIAL BEH..ARABIC MATHEMATICAL INITIAL JEEM -1EE24 ; Arabic # Lo ARABIC MATHEMATICAL INITIAL HEH -1EE27 ; Arabic # Lo ARABIC MATHEMATICAL INITIAL HAH -1EE29..1EE32 ; Arabic # Lo [10] ARABIC MATHEMATICAL INITIAL YEH..ARABIC MATHEMATICAL INITIAL QAF -1EE34..1EE37 ; Arabic # Lo [4] ARABIC MATHEMATICAL INITIAL SHEEN..ARABIC MATHEMATICAL INITIAL KHAH -1EE39 ; Arabic # Lo ARABIC MATHEMATICAL INITIAL DAD -1EE3B ; Arabic # Lo ARABIC MATHEMATICAL INITIAL GHAIN -1EE42 ; Arabic # Lo ARABIC MATHEMATICAL TAILED JEEM -1EE47 ; Arabic # Lo ARABIC MATHEMATICAL TAILED HAH -1EE49 ; Arabic # Lo ARABIC MATHEMATICAL TAILED YEH -1EE4B ; Arabic # Lo ARABIC MATHEMATICAL TAILED LAM -1EE4D..1EE4F ; Arabic # Lo [3] ARABIC MATHEMATICAL TAILED NOON..ARABIC MATHEMATICAL TAILED AIN -1EE51..1EE52 ; Arabic # Lo [2] ARABIC MATHEMATICAL TAILED SAD..ARABIC MATHEMATICAL TAILED QAF -1EE54 ; Arabic # Lo ARABIC MATHEMATICAL TAILED SHEEN -1EE57 ; Arabic # Lo ARABIC MATHEMATICAL TAILED KHAH -1EE59 ; Arabic # Lo ARABIC MATHEMATICAL TAILED DAD -1EE5B ; Arabic # Lo ARABIC MATHEMATICAL TAILED GHAIN -1EE5D ; Arabic # Lo ARABIC MATHEMATICAL TAILED DOTLESS NOON -1EE5F ; Arabic # Lo ARABIC MATHEMATICAL TAILED DOTLESS QAF -1EE61..1EE62 ; Arabic # Lo [2] ARABIC MATHEMATICAL STRETCHED BEH..ARABIC MATHEMATICAL STRETCHED JEEM -1EE64 ; Arabic # Lo ARABIC MATHEMATICAL STRETCHED HEH -1EE67..1EE6A ; Arabic # Lo [4] ARABIC MATHEMATICAL STRETCHED HAH..ARABIC MATHEMATICAL STRETCHED KAF -1EE6C..1EE72 ; Arabic # Lo [7] ARABIC MATHEMATICAL STRETCHED MEEM..ARABIC MATHEMATICAL STRETCHED QAF -1EE74..1EE77 ; Arabic # Lo [4] ARABIC MATHEMATICAL STRETCHED SHEEN..ARABIC MATHEMATICAL STRETCHED KHAH -1EE79..1EE7C ; Arabic # Lo [4] ARABIC MATHEMATICAL STRETCHED DAD..ARABIC MATHEMATICAL STRETCHED DOTLESS BEH -1EE7E ; Arabic # Lo ARABIC MATHEMATICAL STRETCHED DOTLESS FEH -1EE80..1EE89 ; Arabic # Lo [10] ARABIC MATHEMATICAL LOOPED ALEF..ARABIC MATHEMATICAL LOOPED YEH -1EE8B..1EE9B ; Arabic # Lo [17] ARABIC MATHEMATICAL LOOPED LAM..ARABIC MATHEMATICAL LOOPED GHAIN -1EEA1..1EEA3 ; Arabic # Lo [3] ARABIC MATHEMATICAL DOUBLE-STRUCK BEH..ARABIC MATHEMATICAL DOUBLE-STRUCK DAL -1EEA5..1EEA9 ; Arabic # Lo [5] ARABIC MATHEMATICAL DOUBLE-STRUCK WAW..ARABIC MATHEMATICAL DOUBLE-STRUCK YEH -1EEAB..1EEBB ; Arabic # Lo [17] ARABIC MATHEMATICAL DOUBLE-STRUCK LAM..ARABIC MATHEMATICAL DOUBLE-STRUCK GHAIN -1EEF0..1EEF1 ; Arabic # Sm [2] ARABIC MATHEMATICAL OPERATOR MEEM WITH HAH WITH TATWEEL..ARABIC MATHEMATICAL OPERATOR HAH WITH DAL - -# Total code points: 1280 - -# ================================================ - -0700..070D ; Syriac # Po [14] SYRIAC END OF PARAGRAPH..SYRIAC HARKLEAN ASTERISCUS -070F ; Syriac # Cf SYRIAC ABBREVIATION MARK -0710 ; Syriac # Lo SYRIAC LETTER ALAPH -0711 ; Syriac # Mn SYRIAC LETTER SUPERSCRIPT ALAPH -0712..072F ; Syriac # Lo [30] SYRIAC LETTER BETH..SYRIAC LETTER PERSIAN DHALATH -0730..074A ; Syriac # Mn [27] SYRIAC PTHAHA ABOVE..SYRIAC BARREKH -074D..074F ; Syriac # Lo [3] SYRIAC LETTER SOGDIAN ZHAIN..SYRIAC LETTER SOGDIAN FE -0860..086A ; Syriac # Lo [11] SYRIAC LETTER MALAYALAM NGA..SYRIAC LETTER MALAYALAM SSA - -# Total code points: 88 - -# ================================================ - -0780..07A5 ; Thaana # Lo [38] THAANA LETTER HAA..THAANA LETTER WAAVU -07A6..07B0 ; Thaana # Mn [11] THAANA ABAFILI..THAANA SUKUN -07B1 ; Thaana # Lo THAANA LETTER NAA - -# Total code points: 50 - -# ================================================ - -0900..0902 ; Devanagari # Mn [3] DEVANAGARI SIGN INVERTED CANDRABINDU..DEVANAGARI SIGN ANUSVARA -0903 ; Devanagari # Mc DEVANAGARI SIGN VISARGA -0904..0939 ; Devanagari # Lo [54] DEVANAGARI LETTER SHORT A..DEVANAGARI LETTER HA -093A ; Devanagari # Mn DEVANAGARI VOWEL SIGN OE -093B ; Devanagari # Mc DEVANAGARI VOWEL SIGN OOE -093C ; Devanagari # Mn DEVANAGARI SIGN NUKTA -093D ; Devanagari # Lo DEVANAGARI SIGN AVAGRAHA -093E..0940 ; Devanagari # Mc [3] DEVANAGARI VOWEL SIGN AA..DEVANAGARI VOWEL SIGN II -0941..0948 ; Devanagari # Mn [8] DEVANAGARI VOWEL SIGN U..DEVANAGARI VOWEL SIGN AI -0949..094C ; Devanagari # Mc [4] DEVANAGARI VOWEL SIGN CANDRA O..DEVANAGARI VOWEL SIGN AU -094D ; Devanagari # Mn DEVANAGARI SIGN VIRAMA -094E..094F ; Devanagari # Mc [2] DEVANAGARI VOWEL SIGN PRISHTHAMATRA E..DEVANAGARI VOWEL SIGN AW -0950 ; Devanagari # Lo DEVANAGARI OM -0953..0957 ; Devanagari # Mn [5] DEVANAGARI GRAVE ACCENT..DEVANAGARI VOWEL SIGN UUE -0958..0961 ; Devanagari # Lo [10] DEVANAGARI LETTER QA..DEVANAGARI LETTER VOCALIC LL -0962..0963 ; Devanagari # Mn [2] DEVANAGARI VOWEL SIGN VOCALIC L..DEVANAGARI VOWEL SIGN VOCALIC LL -0966..096F ; Devanagari # Nd [10] DEVANAGARI DIGIT ZERO..DEVANAGARI DIGIT NINE -0970 ; Devanagari # Po DEVANAGARI ABBREVIATION SIGN -0971 ; Devanagari # Lm DEVANAGARI SIGN HIGH SPACING DOT -0972..097F ; Devanagari # Lo [14] DEVANAGARI LETTER CANDRA A..DEVANAGARI LETTER BBA -A8E0..A8F1 ; Devanagari # Mn [18] COMBINING DEVANAGARI DIGIT ZERO..COMBINING DEVANAGARI SIGN AVAGRAHA -A8F2..A8F7 ; Devanagari # Lo [6] DEVANAGARI SIGN SPACING CANDRABINDU..DEVANAGARI SIGN CANDRABINDU AVAGRAHA -A8F8..A8FA ; Devanagari # Po [3] DEVANAGARI SIGN PUSHPIKA..DEVANAGARI CARET -A8FB ; Devanagari # Lo DEVANAGARI HEADSTROKE -A8FC ; Devanagari # Po DEVANAGARI SIGN SIDDHAM -A8FD ; Devanagari # Lo DEVANAGARI JAIN OM - -# Total code points: 154 - -# ================================================ - -0980 ; Bengali # Lo BENGALI ANJI -0981 ; Bengali # Mn BENGALI SIGN CANDRABINDU -0982..0983 ; Bengali # Mc [2] BENGALI SIGN ANUSVARA..BENGALI SIGN VISARGA -0985..098C ; Bengali # Lo [8] BENGALI LETTER A..BENGALI LETTER VOCALIC L -098F..0990 ; Bengali # Lo [2] BENGALI LETTER E..BENGALI LETTER AI -0993..09A8 ; Bengali # Lo [22] BENGALI LETTER O..BENGALI LETTER NA -09AA..09B0 ; Bengali # Lo [7] BENGALI LETTER PA..BENGALI LETTER RA -09B2 ; Bengali # Lo BENGALI LETTER LA -09B6..09B9 ; Bengali # Lo [4] BENGALI LETTER SHA..BENGALI LETTER HA -09BC ; Bengali # Mn BENGALI SIGN NUKTA -09BD ; Bengali # Lo BENGALI SIGN AVAGRAHA -09BE..09C0 ; Bengali # Mc [3] BENGALI VOWEL SIGN AA..BENGALI VOWEL SIGN II -09C1..09C4 ; Bengali # Mn [4] BENGALI VOWEL SIGN U..BENGALI VOWEL SIGN VOCALIC RR -09C7..09C8 ; Bengali # Mc [2] BENGALI VOWEL SIGN E..BENGALI VOWEL SIGN AI -09CB..09CC ; Bengali # Mc [2] BENGALI VOWEL SIGN O..BENGALI VOWEL SIGN AU -09CD ; Bengali # Mn BENGALI SIGN VIRAMA -09CE ; Bengali # Lo BENGALI LETTER KHANDA TA -09D7 ; Bengali # Mc BENGALI AU LENGTH MARK -09DC..09DD ; Bengali # Lo [2] BENGALI LETTER RRA..BENGALI LETTER RHA -09DF..09E1 ; Bengali # Lo [3] BENGALI LETTER YYA..BENGALI LETTER VOCALIC LL -09E2..09E3 ; Bengali # Mn [2] BENGALI VOWEL SIGN VOCALIC L..BENGALI VOWEL SIGN VOCALIC LL -09E6..09EF ; Bengali # Nd [10] BENGALI DIGIT ZERO..BENGALI DIGIT NINE -09F0..09F1 ; Bengali # Lo [2] BENGALI LETTER RA WITH MIDDLE DIAGONAL..BENGALI LETTER RA WITH LOWER DIAGONAL -09F2..09F3 ; Bengali # Sc [2] BENGALI RUPEE MARK..BENGALI RUPEE SIGN -09F4..09F9 ; Bengali # No [6] BENGALI CURRENCY NUMERATOR ONE..BENGALI CURRENCY DENOMINATOR SIXTEEN -09FA ; Bengali # So BENGALI ISSHAR -09FB ; Bengali # Sc BENGALI GANDA MARK -09FC ; Bengali # Lo BENGALI LETTER VEDIC ANUSVARA -09FD ; Bengali # Po BENGALI ABBREVIATION SIGN - -# Total code points: 95 - -# ================================================ - -0A01..0A02 ; Gurmukhi # Mn [2] GURMUKHI SIGN ADAK BINDI..GURMUKHI SIGN BINDI -0A03 ; Gurmukhi # Mc GURMUKHI SIGN VISARGA -0A05..0A0A ; Gurmukhi # Lo [6] GURMUKHI LETTER A..GURMUKHI LETTER UU -0A0F..0A10 ; Gurmukhi # Lo [2] GURMUKHI LETTER EE..GURMUKHI LETTER AI -0A13..0A28 ; Gurmukhi # Lo [22] GURMUKHI LETTER OO..GURMUKHI LETTER NA -0A2A..0A30 ; Gurmukhi # Lo [7] GURMUKHI LETTER PA..GURMUKHI LETTER RA -0A32..0A33 ; Gurmukhi # Lo [2] GURMUKHI LETTER LA..GURMUKHI LETTER LLA -0A35..0A36 ; Gurmukhi # Lo [2] GURMUKHI LETTER VA..GURMUKHI LETTER SHA -0A38..0A39 ; Gurmukhi # Lo [2] GURMUKHI LETTER SA..GURMUKHI LETTER HA -0A3C ; Gurmukhi # Mn GURMUKHI SIGN NUKTA -0A3E..0A40 ; Gurmukhi # Mc [3] GURMUKHI VOWEL SIGN AA..GURMUKHI VOWEL SIGN II -0A41..0A42 ; Gurmukhi # Mn [2] GURMUKHI VOWEL SIGN U..GURMUKHI VOWEL SIGN UU -0A47..0A48 ; Gurmukhi # Mn [2] GURMUKHI VOWEL SIGN EE..GURMUKHI VOWEL SIGN AI -0A4B..0A4D ; Gurmukhi # Mn [3] GURMUKHI VOWEL SIGN OO..GURMUKHI SIGN VIRAMA -0A51 ; Gurmukhi # Mn GURMUKHI SIGN UDAAT -0A59..0A5C ; Gurmukhi # Lo [4] GURMUKHI LETTER KHHA..GURMUKHI LETTER RRA -0A5E ; Gurmukhi # Lo GURMUKHI LETTER FA -0A66..0A6F ; Gurmukhi # Nd [10] GURMUKHI DIGIT ZERO..GURMUKHI DIGIT NINE -0A70..0A71 ; Gurmukhi # Mn [2] GURMUKHI TIPPI..GURMUKHI ADDAK -0A72..0A74 ; Gurmukhi # Lo [3] GURMUKHI IRI..GURMUKHI EK ONKAR -0A75 ; Gurmukhi # Mn GURMUKHI SIGN YAKASH - -# Total code points: 79 - -# ================================================ - -0A81..0A82 ; Gujarati # Mn [2] GUJARATI SIGN CANDRABINDU..GUJARATI SIGN ANUSVARA -0A83 ; Gujarati # Mc GUJARATI SIGN VISARGA -0A85..0A8D ; Gujarati # Lo [9] GUJARATI LETTER A..GUJARATI VOWEL CANDRA E -0A8F..0A91 ; Gujarati # Lo [3] GUJARATI LETTER E..GUJARATI VOWEL CANDRA O -0A93..0AA8 ; Gujarati # Lo [22] GUJARATI LETTER O..GUJARATI LETTER NA -0AAA..0AB0 ; Gujarati # Lo [7] GUJARATI LETTER PA..GUJARATI LETTER RA -0AB2..0AB3 ; Gujarati # Lo [2] GUJARATI LETTER LA..GUJARATI LETTER LLA -0AB5..0AB9 ; Gujarati # Lo [5] GUJARATI LETTER VA..GUJARATI LETTER HA -0ABC ; Gujarati # Mn GUJARATI SIGN NUKTA -0ABD ; Gujarati # Lo GUJARATI SIGN AVAGRAHA -0ABE..0AC0 ; Gujarati # Mc [3] GUJARATI VOWEL SIGN AA..GUJARATI VOWEL SIGN II -0AC1..0AC5 ; Gujarati # Mn [5] GUJARATI VOWEL SIGN U..GUJARATI VOWEL SIGN CANDRA E -0AC7..0AC8 ; Gujarati # Mn [2] GUJARATI VOWEL SIGN E..GUJARATI VOWEL SIGN AI -0AC9 ; Gujarati # Mc GUJARATI VOWEL SIGN CANDRA O -0ACB..0ACC ; Gujarati # Mc [2] GUJARATI VOWEL SIGN O..GUJARATI VOWEL SIGN AU -0ACD ; Gujarati # Mn GUJARATI SIGN VIRAMA -0AD0 ; Gujarati # Lo GUJARATI OM -0AE0..0AE1 ; Gujarati # Lo [2] GUJARATI LETTER VOCALIC RR..GUJARATI LETTER VOCALIC LL -0AE2..0AE3 ; Gujarati # Mn [2] GUJARATI VOWEL SIGN VOCALIC L..GUJARATI VOWEL SIGN VOCALIC LL -0AE6..0AEF ; Gujarati # Nd [10] GUJARATI DIGIT ZERO..GUJARATI DIGIT NINE -0AF0 ; Gujarati # Po GUJARATI ABBREVIATION SIGN -0AF1 ; Gujarati # Sc GUJARATI RUPEE SIGN -0AF9 ; Gujarati # Lo GUJARATI LETTER ZHA -0AFA..0AFF ; Gujarati # Mn [6] GUJARATI SIGN SUKUN..GUJARATI SIGN TWO-CIRCLE NUKTA ABOVE - -# Total code points: 91 - -# ================================================ - -0B01 ; Oriya # Mn ORIYA SIGN CANDRABINDU -0B02..0B03 ; Oriya # Mc [2] ORIYA SIGN ANUSVARA..ORIYA SIGN VISARGA -0B05..0B0C ; Oriya # Lo [8] ORIYA LETTER A..ORIYA LETTER VOCALIC L -0B0F..0B10 ; Oriya # Lo [2] ORIYA LETTER E..ORIYA LETTER AI -0B13..0B28 ; Oriya # Lo [22] ORIYA LETTER O..ORIYA LETTER NA -0B2A..0B30 ; Oriya # Lo [7] ORIYA LETTER PA..ORIYA LETTER RA -0B32..0B33 ; Oriya # Lo [2] ORIYA LETTER LA..ORIYA LETTER LLA -0B35..0B39 ; Oriya # Lo [5] ORIYA LETTER VA..ORIYA LETTER HA -0B3C ; Oriya # Mn ORIYA SIGN NUKTA -0B3D ; Oriya # Lo ORIYA SIGN AVAGRAHA -0B3E ; Oriya # Mc ORIYA VOWEL SIGN AA -0B3F ; Oriya # Mn ORIYA VOWEL SIGN I -0B40 ; Oriya # Mc ORIYA VOWEL SIGN II -0B41..0B44 ; Oriya # Mn [4] ORIYA VOWEL SIGN U..ORIYA VOWEL SIGN VOCALIC RR -0B47..0B48 ; Oriya # Mc [2] ORIYA VOWEL SIGN E..ORIYA VOWEL SIGN AI -0B4B..0B4C ; Oriya # Mc [2] ORIYA VOWEL SIGN O..ORIYA VOWEL SIGN AU -0B4D ; Oriya # Mn ORIYA SIGN VIRAMA -0B56 ; Oriya # Mn ORIYA AI LENGTH MARK -0B57 ; Oriya # Mc ORIYA AU LENGTH MARK -0B5C..0B5D ; Oriya # Lo [2] ORIYA LETTER RRA..ORIYA LETTER RHA -0B5F..0B61 ; Oriya # Lo [3] ORIYA LETTER YYA..ORIYA LETTER VOCALIC LL -0B62..0B63 ; Oriya # Mn [2] ORIYA VOWEL SIGN VOCALIC L..ORIYA VOWEL SIGN VOCALIC LL -0B66..0B6F ; Oriya # Nd [10] ORIYA DIGIT ZERO..ORIYA DIGIT NINE -0B70 ; Oriya # So ORIYA ISSHAR -0B71 ; Oriya # Lo ORIYA LETTER WA -0B72..0B77 ; Oriya # No [6] ORIYA FRACTION ONE QUARTER..ORIYA FRACTION THREE SIXTEENTHS - -# Total code points: 90 - -# ================================================ - -0B82 ; Tamil # Mn TAMIL SIGN ANUSVARA -0B83 ; Tamil # Lo TAMIL SIGN VISARGA -0B85..0B8A ; Tamil # Lo [6] TAMIL LETTER A..TAMIL LETTER UU -0B8E..0B90 ; Tamil # Lo [3] TAMIL LETTER E..TAMIL LETTER AI -0B92..0B95 ; Tamil # Lo [4] TAMIL LETTER O..TAMIL LETTER KA -0B99..0B9A ; Tamil # Lo [2] TAMIL LETTER NGA..TAMIL LETTER CA -0B9C ; Tamil # Lo TAMIL LETTER JA -0B9E..0B9F ; Tamil # Lo [2] TAMIL LETTER NYA..TAMIL LETTER TTA -0BA3..0BA4 ; Tamil # Lo [2] TAMIL LETTER NNA..TAMIL LETTER TA -0BA8..0BAA ; Tamil # Lo [3] TAMIL LETTER NA..TAMIL LETTER PA -0BAE..0BB9 ; Tamil # Lo [12] TAMIL LETTER MA..TAMIL LETTER HA -0BBE..0BBF ; Tamil # Mc [2] TAMIL VOWEL SIGN AA..TAMIL VOWEL SIGN I -0BC0 ; Tamil # Mn TAMIL VOWEL SIGN II -0BC1..0BC2 ; Tamil # Mc [2] TAMIL VOWEL SIGN U..TAMIL VOWEL SIGN UU -0BC6..0BC8 ; Tamil # Mc [3] TAMIL VOWEL SIGN E..TAMIL VOWEL SIGN AI -0BCA..0BCC ; Tamil # Mc [3] TAMIL VOWEL SIGN O..TAMIL VOWEL SIGN AU -0BCD ; Tamil # Mn TAMIL SIGN VIRAMA -0BD0 ; Tamil # Lo TAMIL OM -0BD7 ; Tamil # Mc TAMIL AU LENGTH MARK -0BE6..0BEF ; Tamil # Nd [10] TAMIL DIGIT ZERO..TAMIL DIGIT NINE -0BF0..0BF2 ; Tamil # No [3] TAMIL NUMBER TEN..TAMIL NUMBER ONE THOUSAND -0BF3..0BF8 ; Tamil # So [6] TAMIL DAY SIGN..TAMIL AS ABOVE SIGN -0BF9 ; Tamil # Sc TAMIL RUPEE SIGN -0BFA ; Tamil # So TAMIL NUMBER SIGN - -# Total code points: 72 - -# ================================================ - -0C00 ; Telugu # Mn TELUGU SIGN COMBINING CANDRABINDU ABOVE -0C01..0C03 ; Telugu # Mc [3] TELUGU SIGN CANDRABINDU..TELUGU SIGN VISARGA -0C05..0C0C ; Telugu # Lo [8] TELUGU LETTER A..TELUGU LETTER VOCALIC L -0C0E..0C10 ; Telugu # Lo [3] TELUGU LETTER E..TELUGU LETTER AI -0C12..0C28 ; Telugu # Lo [23] TELUGU LETTER O..TELUGU LETTER NA -0C2A..0C39 ; Telugu # Lo [16] TELUGU LETTER PA..TELUGU LETTER HA -0C3D ; Telugu # Lo TELUGU SIGN AVAGRAHA -0C3E..0C40 ; Telugu # Mn [3] TELUGU VOWEL SIGN AA..TELUGU VOWEL SIGN II -0C41..0C44 ; Telugu # Mc [4] TELUGU VOWEL SIGN U..TELUGU VOWEL SIGN VOCALIC RR -0C46..0C48 ; Telugu # Mn [3] TELUGU VOWEL SIGN E..TELUGU VOWEL SIGN AI -0C4A..0C4D ; Telugu # Mn [4] TELUGU VOWEL SIGN O..TELUGU SIGN VIRAMA -0C55..0C56 ; Telugu # Mn [2] TELUGU LENGTH MARK..TELUGU AI LENGTH MARK -0C58..0C5A ; Telugu # Lo [3] TELUGU LETTER TSA..TELUGU LETTER RRRA -0C60..0C61 ; Telugu # Lo [2] TELUGU LETTER VOCALIC RR..TELUGU LETTER VOCALIC LL -0C62..0C63 ; Telugu # Mn [2] TELUGU VOWEL SIGN VOCALIC L..TELUGU VOWEL SIGN VOCALIC LL -0C66..0C6F ; Telugu # Nd [10] TELUGU DIGIT ZERO..TELUGU DIGIT NINE -0C78..0C7E ; Telugu # No [7] TELUGU FRACTION DIGIT ZERO FOR ODD POWERS OF FOUR..TELUGU FRACTION DIGIT THREE FOR EVEN POWERS OF FOUR -0C7F ; Telugu # So TELUGU SIGN TUUMU - -# Total code points: 96 - -# ================================================ - -0C80 ; Kannada # Lo KANNADA SIGN SPACING CANDRABINDU -0C81 ; Kannada # Mn KANNADA SIGN CANDRABINDU -0C82..0C83 ; Kannada # Mc [2] KANNADA SIGN ANUSVARA..KANNADA SIGN VISARGA -0C85..0C8C ; Kannada # Lo [8] KANNADA LETTER A..KANNADA LETTER VOCALIC L -0C8E..0C90 ; Kannada # Lo [3] KANNADA LETTER E..KANNADA LETTER AI -0C92..0CA8 ; Kannada # Lo [23] KANNADA LETTER O..KANNADA LETTER NA -0CAA..0CB3 ; Kannada # Lo [10] KANNADA LETTER PA..KANNADA LETTER LLA -0CB5..0CB9 ; Kannada # Lo [5] KANNADA LETTER VA..KANNADA LETTER HA -0CBC ; Kannada # Mn KANNADA SIGN NUKTA -0CBD ; Kannada # Lo KANNADA SIGN AVAGRAHA -0CBE ; Kannada # Mc KANNADA VOWEL SIGN AA -0CBF ; Kannada # Mn KANNADA VOWEL SIGN I -0CC0..0CC4 ; Kannada # Mc [5] KANNADA VOWEL SIGN II..KANNADA VOWEL SIGN VOCALIC RR -0CC6 ; Kannada # Mn KANNADA VOWEL SIGN E -0CC7..0CC8 ; Kannada # Mc [2] KANNADA VOWEL SIGN EE..KANNADA VOWEL SIGN AI -0CCA..0CCB ; Kannada # Mc [2] KANNADA VOWEL SIGN O..KANNADA VOWEL SIGN OO -0CCC..0CCD ; Kannada # Mn [2] KANNADA VOWEL SIGN AU..KANNADA SIGN VIRAMA -0CD5..0CD6 ; Kannada # Mc [2] KANNADA LENGTH MARK..KANNADA AI LENGTH MARK -0CDE ; Kannada # Lo KANNADA LETTER FA -0CE0..0CE1 ; Kannada # Lo [2] KANNADA LETTER VOCALIC RR..KANNADA LETTER VOCALIC LL -0CE2..0CE3 ; Kannada # Mn [2] KANNADA VOWEL SIGN VOCALIC L..KANNADA VOWEL SIGN VOCALIC LL -0CE6..0CEF ; Kannada # Nd [10] KANNADA DIGIT ZERO..KANNADA DIGIT NINE -0CF1..0CF2 ; Kannada # Lo [2] KANNADA SIGN JIHVAMULIYA..KANNADA SIGN UPADHMANIYA - -# Total code points: 88 - -# ================================================ - -0D00..0D01 ; Malayalam # Mn [2] MALAYALAM SIGN COMBINING ANUSVARA ABOVE..MALAYALAM SIGN CANDRABINDU -0D02..0D03 ; Malayalam # Mc [2] MALAYALAM SIGN ANUSVARA..MALAYALAM SIGN VISARGA -0D05..0D0C ; Malayalam # Lo [8] MALAYALAM LETTER A..MALAYALAM LETTER VOCALIC L -0D0E..0D10 ; Malayalam # Lo [3] MALAYALAM LETTER E..MALAYALAM LETTER AI -0D12..0D3A ; Malayalam # Lo [41] MALAYALAM LETTER O..MALAYALAM LETTER TTTA -0D3B..0D3C ; Malayalam # Mn [2] MALAYALAM SIGN VERTICAL BAR VIRAMA..MALAYALAM SIGN CIRCULAR VIRAMA -0D3D ; Malayalam # Lo MALAYALAM SIGN AVAGRAHA -0D3E..0D40 ; Malayalam # Mc [3] MALAYALAM VOWEL SIGN AA..MALAYALAM VOWEL SIGN II -0D41..0D44 ; Malayalam # Mn [4] MALAYALAM VOWEL SIGN U..MALAYALAM VOWEL SIGN VOCALIC RR -0D46..0D48 ; Malayalam # Mc [3] MALAYALAM VOWEL SIGN E..MALAYALAM VOWEL SIGN AI -0D4A..0D4C ; Malayalam # Mc [3] MALAYALAM VOWEL SIGN O..MALAYALAM VOWEL SIGN AU -0D4D ; Malayalam # Mn MALAYALAM SIGN VIRAMA -0D4E ; Malayalam # Lo MALAYALAM LETTER DOT REPH -0D4F ; Malayalam # So MALAYALAM SIGN PARA -0D54..0D56 ; Malayalam # Lo [3] MALAYALAM LETTER CHILLU M..MALAYALAM LETTER CHILLU LLL -0D57 ; Malayalam # Mc MALAYALAM AU LENGTH MARK -0D58..0D5E ; Malayalam # No [7] MALAYALAM FRACTION ONE ONE-HUNDRED-AND-SIXTIETH..MALAYALAM FRACTION ONE FIFTH -0D5F..0D61 ; Malayalam # Lo [3] MALAYALAM LETTER ARCHAIC II..MALAYALAM LETTER VOCALIC LL -0D62..0D63 ; Malayalam # Mn [2] MALAYALAM VOWEL SIGN VOCALIC L..MALAYALAM VOWEL SIGN VOCALIC LL -0D66..0D6F ; Malayalam # Nd [10] MALAYALAM DIGIT ZERO..MALAYALAM DIGIT NINE -0D70..0D78 ; Malayalam # No [9] MALAYALAM NUMBER TEN..MALAYALAM FRACTION THREE SIXTEENTHS -0D79 ; Malayalam # So MALAYALAM DATE MARK -0D7A..0D7F ; Malayalam # Lo [6] MALAYALAM LETTER CHILLU NN..MALAYALAM LETTER CHILLU K - -# Total code points: 117 - -# ================================================ - -0D82..0D83 ; Sinhala # Mc [2] SINHALA SIGN ANUSVARAYA..SINHALA SIGN VISARGAYA -0D85..0D96 ; Sinhala # Lo [18] SINHALA LETTER AYANNA..SINHALA LETTER AUYANNA -0D9A..0DB1 ; Sinhala # Lo [24] SINHALA LETTER ALPAPRAANA KAYANNA..SINHALA LETTER DANTAJA NAYANNA -0DB3..0DBB ; Sinhala # Lo [9] SINHALA LETTER SANYAKA DAYANNA..SINHALA LETTER RAYANNA -0DBD ; Sinhala # Lo SINHALA LETTER DANTAJA LAYANNA -0DC0..0DC6 ; Sinhala # Lo [7] SINHALA LETTER VAYANNA..SINHALA LETTER FAYANNA -0DCA ; Sinhala # Mn SINHALA SIGN AL-LAKUNA -0DCF..0DD1 ; Sinhala # Mc [3] SINHALA VOWEL SIGN AELA-PILLA..SINHALA VOWEL SIGN DIGA AEDA-PILLA -0DD2..0DD4 ; Sinhala # Mn [3] SINHALA VOWEL SIGN KETTI IS-PILLA..SINHALA VOWEL SIGN KETTI PAA-PILLA -0DD6 ; Sinhala # Mn SINHALA VOWEL SIGN DIGA PAA-PILLA -0DD8..0DDF ; Sinhala # Mc [8] SINHALA VOWEL SIGN GAETTA-PILLA..SINHALA VOWEL SIGN GAYANUKITTA -0DE6..0DEF ; Sinhala # Nd [10] SINHALA LITH DIGIT ZERO..SINHALA LITH DIGIT NINE -0DF2..0DF3 ; Sinhala # Mc [2] SINHALA VOWEL SIGN DIGA GAETTA-PILLA..SINHALA VOWEL SIGN DIGA GAYANUKITTA -0DF4 ; Sinhala # Po SINHALA PUNCTUATION KUNDDALIYA -111E1..111F4 ; Sinhala # No [20] SINHALA ARCHAIC DIGIT ONE..SINHALA ARCHAIC NUMBER ONE THOUSAND - -# Total code points: 110 - -# ================================================ - -0E01..0E30 ; Thai # Lo [48] THAI CHARACTER KO KAI..THAI CHARACTER SARA A -0E31 ; Thai # Mn THAI CHARACTER MAI HAN-AKAT -0E32..0E33 ; Thai # Lo [2] THAI CHARACTER SARA AA..THAI CHARACTER SARA AM -0E34..0E3A ; Thai # Mn [7] THAI CHARACTER SARA I..THAI CHARACTER PHINTHU -0E40..0E45 ; Thai # Lo [6] THAI CHARACTER SARA E..THAI CHARACTER LAKKHANGYAO -0E46 ; Thai # Lm THAI CHARACTER MAIYAMOK -0E47..0E4E ; Thai # Mn [8] THAI CHARACTER MAITAIKHU..THAI CHARACTER YAMAKKAN -0E4F ; Thai # Po THAI CHARACTER FONGMAN -0E50..0E59 ; Thai # Nd [10] THAI DIGIT ZERO..THAI DIGIT NINE -0E5A..0E5B ; Thai # Po [2] THAI CHARACTER ANGKHANKHU..THAI CHARACTER KHOMUT - -# Total code points: 86 - -# ================================================ - -0E81..0E82 ; Lao # Lo [2] LAO LETTER KO..LAO LETTER KHO SUNG -0E84 ; Lao # Lo LAO LETTER KHO TAM -0E87..0E88 ; Lao # Lo [2] LAO LETTER NGO..LAO LETTER CO -0E8A ; Lao # Lo LAO LETTER SO TAM -0E8D ; Lao # Lo LAO LETTER NYO -0E94..0E97 ; Lao # Lo [4] LAO LETTER DO..LAO LETTER THO TAM -0E99..0E9F ; Lao # Lo [7] LAO LETTER NO..LAO LETTER FO SUNG -0EA1..0EA3 ; Lao # Lo [3] LAO LETTER MO..LAO LETTER LO LING -0EA5 ; Lao # Lo LAO LETTER LO LOOT -0EA7 ; Lao # Lo LAO LETTER WO -0EAA..0EAB ; Lao # Lo [2] LAO LETTER SO SUNG..LAO LETTER HO SUNG -0EAD..0EB0 ; Lao # Lo [4] LAO LETTER O..LAO VOWEL SIGN A -0EB1 ; Lao # Mn LAO VOWEL SIGN MAI KAN -0EB2..0EB3 ; Lao # Lo [2] LAO VOWEL SIGN AA..LAO VOWEL SIGN AM -0EB4..0EB9 ; Lao # Mn [6] LAO VOWEL SIGN I..LAO VOWEL SIGN UU -0EBB..0EBC ; Lao # Mn [2] LAO VOWEL SIGN MAI KON..LAO SEMIVOWEL SIGN LO -0EBD ; Lao # Lo LAO SEMIVOWEL SIGN NYO -0EC0..0EC4 ; Lao # Lo [5] LAO VOWEL SIGN E..LAO VOWEL SIGN AI -0EC6 ; Lao # Lm LAO KO LA -0EC8..0ECD ; Lao # Mn [6] LAO TONE MAI EK..LAO NIGGAHITA -0ED0..0ED9 ; Lao # Nd [10] LAO DIGIT ZERO..LAO DIGIT NINE -0EDC..0EDF ; Lao # Lo [4] LAO HO NO..LAO LETTER KHMU NYO - -# Total code points: 67 - -# ================================================ - -0F00 ; Tibetan # Lo TIBETAN SYLLABLE OM -0F01..0F03 ; Tibetan # So [3] TIBETAN MARK GTER YIG MGO TRUNCATED A..TIBETAN MARK GTER YIG MGO -UM GTER TSHEG MA -0F04..0F12 ; Tibetan # Po [15] TIBETAN MARK INITIAL YIG MGO MDUN MA..TIBETAN MARK RGYA GRAM SHAD -0F13 ; Tibetan # So TIBETAN MARK CARET -DZUD RTAGS ME LONG CAN -0F14 ; Tibetan # Po TIBETAN MARK GTER TSHEG -0F15..0F17 ; Tibetan # So [3] TIBETAN LOGOTYPE SIGN CHAD RTAGS..TIBETAN ASTROLOGICAL SIGN SGRA GCAN -CHAR RTAGS -0F18..0F19 ; Tibetan # Mn [2] TIBETAN ASTROLOGICAL SIGN -KHYUD PA..TIBETAN ASTROLOGICAL SIGN SDONG TSHUGS -0F1A..0F1F ; Tibetan # So [6] TIBETAN SIGN RDEL DKAR GCIG..TIBETAN SIGN RDEL DKAR RDEL NAG -0F20..0F29 ; Tibetan # Nd [10] TIBETAN DIGIT ZERO..TIBETAN DIGIT NINE -0F2A..0F33 ; Tibetan # No [10] TIBETAN DIGIT HALF ONE..TIBETAN DIGIT HALF ZERO -0F34 ; Tibetan # So TIBETAN MARK BSDUS RTAGS -0F35 ; Tibetan # Mn TIBETAN MARK NGAS BZUNG NYI ZLA -0F36 ; Tibetan # So TIBETAN MARK CARET -DZUD RTAGS BZHI MIG CAN -0F37 ; Tibetan # Mn TIBETAN MARK NGAS BZUNG SGOR RTAGS -0F38 ; Tibetan # So TIBETAN MARK CHE MGO -0F39 ; Tibetan # Mn TIBETAN MARK TSA -PHRU -0F3A ; Tibetan # Ps TIBETAN MARK GUG RTAGS GYON -0F3B ; Tibetan # Pe TIBETAN MARK GUG RTAGS GYAS -0F3C ; Tibetan # Ps TIBETAN MARK ANG KHANG GYON -0F3D ; Tibetan # Pe TIBETAN MARK ANG KHANG GYAS -0F3E..0F3F ; Tibetan # Mc [2] TIBETAN SIGN YAR TSHES..TIBETAN SIGN MAR TSHES -0F40..0F47 ; Tibetan # Lo [8] TIBETAN LETTER KA..TIBETAN LETTER JA -0F49..0F6C ; Tibetan # Lo [36] TIBETAN LETTER NYA..TIBETAN LETTER RRA -0F71..0F7E ; Tibetan # Mn [14] TIBETAN VOWEL SIGN AA..TIBETAN SIGN RJES SU NGA RO -0F7F ; Tibetan # Mc TIBETAN SIGN RNAM BCAD -0F80..0F84 ; Tibetan # Mn [5] TIBETAN VOWEL SIGN REVERSED I..TIBETAN MARK HALANTA -0F85 ; Tibetan # Po TIBETAN MARK PALUTA -0F86..0F87 ; Tibetan # Mn [2] TIBETAN SIGN LCI RTAGS..TIBETAN SIGN YANG RTAGS -0F88..0F8C ; Tibetan # Lo [5] TIBETAN SIGN LCE TSA CAN..TIBETAN SIGN INVERTED MCHU CAN -0F8D..0F97 ; Tibetan # Mn [11] TIBETAN SUBJOINED SIGN LCE TSA CAN..TIBETAN SUBJOINED LETTER JA -0F99..0FBC ; Tibetan # Mn [36] TIBETAN SUBJOINED LETTER NYA..TIBETAN SUBJOINED LETTER FIXED-FORM RA -0FBE..0FC5 ; Tibetan # So [8] TIBETAN KU RU KHA..TIBETAN SYMBOL RDO RJE -0FC6 ; Tibetan # Mn TIBETAN SYMBOL PADMA GDAN -0FC7..0FCC ; Tibetan # So [6] TIBETAN SYMBOL RDO RJE RGYA GRAM..TIBETAN SYMBOL NOR BU BZHI -KHYIL -0FCE..0FCF ; Tibetan # So [2] TIBETAN SIGN RDEL NAG RDEL DKAR..TIBETAN SIGN RDEL NAG GSUM -0FD0..0FD4 ; Tibetan # Po [5] TIBETAN MARK BSKA- SHOG GI MGO RGYAN..TIBETAN MARK CLOSING BRDA RNYING YIG MGO SGAB MA -0FD9..0FDA ; Tibetan # Po [2] TIBETAN MARK LEADING MCHAN RTAGS..TIBETAN MARK TRAILING MCHAN RTAGS - -# Total code points: 207 - -# ================================================ - -1000..102A ; Myanmar # Lo [43] MYANMAR LETTER KA..MYANMAR LETTER AU -102B..102C ; Myanmar # Mc [2] MYANMAR VOWEL SIGN TALL AA..MYANMAR VOWEL SIGN AA -102D..1030 ; Myanmar # Mn [4] MYANMAR VOWEL SIGN I..MYANMAR VOWEL SIGN UU -1031 ; Myanmar # Mc MYANMAR VOWEL SIGN E -1032..1037 ; Myanmar # Mn [6] MYANMAR VOWEL SIGN AI..MYANMAR SIGN DOT BELOW -1038 ; Myanmar # Mc MYANMAR SIGN VISARGA -1039..103A ; Myanmar # Mn [2] MYANMAR SIGN VIRAMA..MYANMAR SIGN ASAT -103B..103C ; Myanmar # Mc [2] MYANMAR CONSONANT SIGN MEDIAL YA..MYANMAR CONSONANT SIGN MEDIAL RA -103D..103E ; Myanmar # Mn [2] MYANMAR CONSONANT SIGN MEDIAL WA..MYANMAR CONSONANT SIGN MEDIAL HA -103F ; Myanmar # Lo MYANMAR LETTER GREAT SA -1040..1049 ; Myanmar # Nd [10] MYANMAR DIGIT ZERO..MYANMAR DIGIT NINE -104A..104F ; Myanmar # Po [6] MYANMAR SIGN LITTLE SECTION..MYANMAR SYMBOL GENITIVE -1050..1055 ; Myanmar # Lo [6] MYANMAR LETTER SHA..MYANMAR LETTER VOCALIC LL -1056..1057 ; Myanmar # Mc [2] MYANMAR VOWEL SIGN VOCALIC R..MYANMAR VOWEL SIGN VOCALIC RR -1058..1059 ; Myanmar # Mn [2] MYANMAR VOWEL SIGN VOCALIC L..MYANMAR VOWEL SIGN VOCALIC LL -105A..105D ; Myanmar # Lo [4] MYANMAR LETTER MON NGA..MYANMAR LETTER MON BBE -105E..1060 ; Myanmar # Mn [3] MYANMAR CONSONANT SIGN MON MEDIAL NA..MYANMAR CONSONANT SIGN MON MEDIAL LA -1061 ; Myanmar # Lo MYANMAR LETTER SGAW KAREN SHA -1062..1064 ; Myanmar # Mc [3] MYANMAR VOWEL SIGN SGAW KAREN EU..MYANMAR TONE MARK SGAW KAREN KE PHO -1065..1066 ; Myanmar # Lo [2] MYANMAR LETTER WESTERN PWO KAREN THA..MYANMAR LETTER WESTERN PWO KAREN PWA -1067..106D ; Myanmar # Mc [7] MYANMAR VOWEL SIGN WESTERN PWO KAREN EU..MYANMAR SIGN WESTERN PWO KAREN TONE-5 -106E..1070 ; Myanmar # Lo [3] MYANMAR LETTER EASTERN PWO KAREN NNA..MYANMAR LETTER EASTERN PWO KAREN GHWA -1071..1074 ; Myanmar # Mn [4] MYANMAR VOWEL SIGN GEBA KAREN I..MYANMAR VOWEL SIGN KAYAH EE -1075..1081 ; Myanmar # Lo [13] MYANMAR LETTER SHAN KA..MYANMAR LETTER SHAN HA -1082 ; Myanmar # Mn MYANMAR CONSONANT SIGN SHAN MEDIAL WA -1083..1084 ; Myanmar # Mc [2] MYANMAR VOWEL SIGN SHAN AA..MYANMAR VOWEL SIGN SHAN E -1085..1086 ; Myanmar # Mn [2] MYANMAR VOWEL SIGN SHAN E ABOVE..MYANMAR VOWEL SIGN SHAN FINAL Y -1087..108C ; Myanmar # Mc [6] MYANMAR SIGN SHAN TONE-2..MYANMAR SIGN SHAN COUNCIL TONE-3 -108D ; Myanmar # Mn MYANMAR SIGN SHAN COUNCIL EMPHATIC TONE -108E ; Myanmar # Lo MYANMAR LETTER RUMAI PALAUNG FA -108F ; Myanmar # Mc MYANMAR SIGN RUMAI PALAUNG TONE-5 -1090..1099 ; Myanmar # Nd [10] MYANMAR SHAN DIGIT ZERO..MYANMAR SHAN DIGIT NINE -109A..109C ; Myanmar # Mc [3] MYANMAR SIGN KHAMTI TONE-1..MYANMAR VOWEL SIGN AITON A -109D ; Myanmar # Mn MYANMAR VOWEL SIGN AITON AI -109E..109F ; Myanmar # So [2] MYANMAR SYMBOL SHAN ONE..MYANMAR SYMBOL SHAN EXCLAMATION -A9E0..A9E4 ; Myanmar # Lo [5] MYANMAR LETTER SHAN GHA..MYANMAR LETTER SHAN BHA -A9E5 ; Myanmar # Mn MYANMAR SIGN SHAN SAW -A9E6 ; Myanmar # Lm MYANMAR MODIFIER LETTER SHAN REDUPLICATION -A9E7..A9EF ; Myanmar # Lo [9] MYANMAR LETTER TAI LAING NYA..MYANMAR LETTER TAI LAING NNA -A9F0..A9F9 ; Myanmar # Nd [10] MYANMAR TAI LAING DIGIT ZERO..MYANMAR TAI LAING DIGIT NINE -A9FA..A9FE ; Myanmar # Lo [5] MYANMAR LETTER TAI LAING LLA..MYANMAR LETTER TAI LAING BHA -AA60..AA6F ; Myanmar # Lo [16] MYANMAR LETTER KHAMTI GA..MYANMAR LETTER KHAMTI FA -AA70 ; Myanmar # Lm MYANMAR MODIFIER LETTER KHAMTI REDUPLICATION -AA71..AA76 ; Myanmar # Lo [6] MYANMAR LETTER KHAMTI XA..MYANMAR LOGOGRAM KHAMTI HM -AA77..AA79 ; Myanmar # So [3] MYANMAR SYMBOL AITON EXCLAMATION..MYANMAR SYMBOL AITON TWO -AA7A ; Myanmar # Lo MYANMAR LETTER AITON RA -AA7B ; Myanmar # Mc MYANMAR SIGN PAO KAREN TONE -AA7C ; Myanmar # Mn MYANMAR SIGN TAI LAING TONE-2 -AA7D ; Myanmar # Mc MYANMAR SIGN TAI LAING TONE-5 -AA7E..AA7F ; Myanmar # Lo [2] MYANMAR LETTER SHWE PALAUNG CHA..MYANMAR LETTER SHWE PALAUNG SHA - -# Total code points: 223 - -# ================================================ - -10A0..10C5 ; Georgian # L& [38] GEORGIAN CAPITAL LETTER AN..GEORGIAN CAPITAL LETTER HOE -10C7 ; Georgian # L& GEORGIAN CAPITAL LETTER YN -10CD ; Georgian # L& GEORGIAN CAPITAL LETTER AEN -10D0..10FA ; Georgian # Lo [43] GEORGIAN LETTER AN..GEORGIAN LETTER AIN -10FC ; Georgian # Lm MODIFIER LETTER GEORGIAN NAR -10FD..10FF ; Georgian # Lo [3] GEORGIAN LETTER AEN..GEORGIAN LETTER LABIAL SIGN -2D00..2D25 ; Georgian # L& [38] GEORGIAN SMALL LETTER AN..GEORGIAN SMALL LETTER HOE -2D27 ; Georgian # L& GEORGIAN SMALL LETTER YN -2D2D ; Georgian # L& GEORGIAN SMALL LETTER AEN - -# Total code points: 127 - -# ================================================ - -1100..11FF ; Hangul # Lo [256] HANGUL CHOSEONG KIYEOK..HANGUL JONGSEONG SSANGNIEUN -302E..302F ; Hangul # Mc [2] HANGUL SINGLE DOT TONE MARK..HANGUL DOUBLE DOT TONE MARK -3131..318E ; Hangul # Lo [94] HANGUL LETTER KIYEOK..HANGUL LETTER ARAEAE -3200..321E ; Hangul # So [31] PARENTHESIZED HANGUL KIYEOK..PARENTHESIZED KOREAN CHARACTER O HU -3260..327E ; Hangul # So [31] CIRCLED HANGUL KIYEOK..CIRCLED HANGUL IEUNG U -A960..A97C ; Hangul # Lo [29] HANGUL CHOSEONG TIKEUT-MIEUM..HANGUL CHOSEONG SSANGYEORINHIEUH -AC00..D7A3 ; Hangul # Lo [11172] HANGUL SYLLABLE GA..HANGUL SYLLABLE HIH -D7B0..D7C6 ; Hangul # Lo [23] HANGUL JUNGSEONG O-YEO..HANGUL JUNGSEONG ARAEA-E -D7CB..D7FB ; Hangul # Lo [49] HANGUL JONGSEONG NIEUN-RIEUL..HANGUL JONGSEONG PHIEUPH-THIEUTH -FFA0..FFBE ; Hangul # Lo [31] HALFWIDTH HANGUL FILLER..HALFWIDTH HANGUL LETTER HIEUH -FFC2..FFC7 ; Hangul # Lo [6] HALFWIDTH HANGUL LETTER A..HALFWIDTH HANGUL LETTER E -FFCA..FFCF ; Hangul # Lo [6] HALFWIDTH HANGUL LETTER YEO..HALFWIDTH HANGUL LETTER OE -FFD2..FFD7 ; Hangul # Lo [6] HALFWIDTH HANGUL LETTER YO..HALFWIDTH HANGUL LETTER YU -FFDA..FFDC ; Hangul # Lo [3] HALFWIDTH HANGUL LETTER EU..HALFWIDTH HANGUL LETTER I - -# Total code points: 11739 - -# ================================================ - -1200..1248 ; Ethiopic # Lo [73] ETHIOPIC SYLLABLE HA..ETHIOPIC SYLLABLE QWA -124A..124D ; Ethiopic # Lo [4] ETHIOPIC SYLLABLE QWI..ETHIOPIC SYLLABLE QWE -1250..1256 ; Ethiopic # Lo [7] ETHIOPIC SYLLABLE QHA..ETHIOPIC SYLLABLE QHO -1258 ; Ethiopic # Lo ETHIOPIC SYLLABLE QHWA -125A..125D ; Ethiopic # Lo [4] ETHIOPIC SYLLABLE QHWI..ETHIOPIC SYLLABLE QHWE -1260..1288 ; Ethiopic # Lo [41] ETHIOPIC SYLLABLE BA..ETHIOPIC SYLLABLE XWA -128A..128D ; Ethiopic # Lo [4] ETHIOPIC SYLLABLE XWI..ETHIOPIC SYLLABLE XWE -1290..12B0 ; Ethiopic # Lo [33] ETHIOPIC SYLLABLE NA..ETHIOPIC SYLLABLE KWA -12B2..12B5 ; Ethiopic # Lo [4] ETHIOPIC SYLLABLE KWI..ETHIOPIC SYLLABLE KWE -12B8..12BE ; Ethiopic # Lo [7] ETHIOPIC SYLLABLE KXA..ETHIOPIC SYLLABLE KXO -12C0 ; Ethiopic # Lo ETHIOPIC SYLLABLE KXWA -12C2..12C5 ; Ethiopic # Lo [4] ETHIOPIC SYLLABLE KXWI..ETHIOPIC SYLLABLE KXWE -12C8..12D6 ; Ethiopic # Lo [15] ETHIOPIC SYLLABLE WA..ETHIOPIC SYLLABLE PHARYNGEAL O -12D8..1310 ; Ethiopic # Lo [57] ETHIOPIC SYLLABLE ZA..ETHIOPIC SYLLABLE GWA -1312..1315 ; Ethiopic # Lo [4] ETHIOPIC SYLLABLE GWI..ETHIOPIC SYLLABLE GWE -1318..135A ; Ethiopic # Lo [67] ETHIOPIC SYLLABLE GGA..ETHIOPIC SYLLABLE FYA -135D..135F ; Ethiopic # Mn [3] ETHIOPIC COMBINING GEMINATION AND VOWEL LENGTH MARK..ETHIOPIC COMBINING GEMINATION MARK -1360..1368 ; Ethiopic # Po [9] ETHIOPIC SECTION MARK..ETHIOPIC PARAGRAPH SEPARATOR -1369..137C ; Ethiopic # No [20] ETHIOPIC DIGIT ONE..ETHIOPIC NUMBER TEN THOUSAND -1380..138F ; Ethiopic # Lo [16] ETHIOPIC SYLLABLE SEBATBEIT MWA..ETHIOPIC SYLLABLE PWE -1390..1399 ; Ethiopic # So [10] ETHIOPIC TONAL MARK YIZET..ETHIOPIC TONAL MARK KURT -2D80..2D96 ; Ethiopic # Lo [23] ETHIOPIC SYLLABLE LOA..ETHIOPIC SYLLABLE GGWE -2DA0..2DA6 ; Ethiopic # Lo [7] ETHIOPIC SYLLABLE SSA..ETHIOPIC SYLLABLE SSO -2DA8..2DAE ; Ethiopic # Lo [7] ETHIOPIC SYLLABLE CCA..ETHIOPIC SYLLABLE CCO -2DB0..2DB6 ; Ethiopic # Lo [7] ETHIOPIC SYLLABLE ZZA..ETHIOPIC SYLLABLE ZZO -2DB8..2DBE ; Ethiopic # Lo [7] ETHIOPIC SYLLABLE CCHA..ETHIOPIC SYLLABLE CCHO -2DC0..2DC6 ; Ethiopic # Lo [7] ETHIOPIC SYLLABLE QYA..ETHIOPIC SYLLABLE QYO -2DC8..2DCE ; Ethiopic # Lo [7] ETHIOPIC SYLLABLE KYA..ETHIOPIC SYLLABLE KYO -2DD0..2DD6 ; Ethiopic # Lo [7] ETHIOPIC SYLLABLE XYA..ETHIOPIC SYLLABLE XYO -2DD8..2DDE ; Ethiopic # Lo [7] ETHIOPIC SYLLABLE GYA..ETHIOPIC SYLLABLE GYO -AB01..AB06 ; Ethiopic # Lo [6] ETHIOPIC SYLLABLE TTHU..ETHIOPIC SYLLABLE TTHO -AB09..AB0E ; Ethiopic # Lo [6] ETHIOPIC SYLLABLE DDHU..ETHIOPIC SYLLABLE DDHO -AB11..AB16 ; Ethiopic # Lo [6] ETHIOPIC SYLLABLE DZU..ETHIOPIC SYLLABLE DZO -AB20..AB26 ; Ethiopic # Lo [7] ETHIOPIC SYLLABLE CCHHA..ETHIOPIC SYLLABLE CCHHO -AB28..AB2E ; Ethiopic # Lo [7] ETHIOPIC SYLLABLE BBA..ETHIOPIC SYLLABLE BBO - -# Total code points: 495 - -# ================================================ - -13A0..13F5 ; Cherokee # L& [86] CHEROKEE LETTER A..CHEROKEE LETTER MV -13F8..13FD ; Cherokee # L& [6] CHEROKEE SMALL LETTER YE..CHEROKEE SMALL LETTER MV -AB70..ABBF ; Cherokee # L& [80] CHEROKEE SMALL LETTER A..CHEROKEE SMALL LETTER YA - -# Total code points: 172 - -# ================================================ - -1400 ; Canadian_Aboriginal # Pd CANADIAN SYLLABICS HYPHEN -1401..166C ; Canadian_Aboriginal # Lo [620] CANADIAN SYLLABICS E..CANADIAN SYLLABICS CARRIER TTSA -166D..166E ; Canadian_Aboriginal # Po [2] CANADIAN SYLLABICS CHI SIGN..CANADIAN SYLLABICS FULL STOP -166F..167F ; Canadian_Aboriginal # Lo [17] CANADIAN SYLLABICS QAI..CANADIAN SYLLABICS BLACKFOOT W -18B0..18F5 ; Canadian_Aboriginal # Lo [70] CANADIAN SYLLABICS OY..CANADIAN SYLLABICS CARRIER DENTAL S - -# Total code points: 710 - -# ================================================ - -1680 ; Ogham # Zs OGHAM SPACE MARK -1681..169A ; Ogham # Lo [26] OGHAM LETTER BEITH..OGHAM LETTER PEITH -169B ; Ogham # Ps OGHAM FEATHER MARK -169C ; Ogham # Pe OGHAM REVERSED FEATHER MARK - -# Total code points: 29 - -# ================================================ - -16A0..16EA ; Runic # Lo [75] RUNIC LETTER FEHU FEOH FE F..RUNIC LETTER X -16EE..16F0 ; Runic # Nl [3] RUNIC ARLAUG SYMBOL..RUNIC BELGTHOR SYMBOL -16F1..16F8 ; Runic # Lo [8] RUNIC LETTER K..RUNIC LETTER FRANKS CASKET AESC - -# Total code points: 86 - -# ================================================ - -1780..17B3 ; Khmer # Lo [52] KHMER LETTER KA..KHMER INDEPENDENT VOWEL QAU -17B4..17B5 ; Khmer # Mn [2] KHMER VOWEL INHERENT AQ..KHMER VOWEL INHERENT AA -17B6 ; Khmer # Mc KHMER VOWEL SIGN AA -17B7..17BD ; Khmer # Mn [7] KHMER VOWEL SIGN I..KHMER VOWEL SIGN UA -17BE..17C5 ; Khmer # Mc [8] KHMER VOWEL SIGN OE..KHMER VOWEL SIGN AU -17C6 ; Khmer # Mn KHMER SIGN NIKAHIT -17C7..17C8 ; Khmer # Mc [2] KHMER SIGN REAHMUK..KHMER SIGN YUUKALEAPINTU -17C9..17D3 ; Khmer # Mn [11] KHMER SIGN MUUSIKATOAN..KHMER SIGN BATHAMASAT -17D4..17D6 ; Khmer # Po [3] KHMER SIGN KHAN..KHMER SIGN CAMNUC PII KUUH -17D7 ; Khmer # Lm KHMER SIGN LEK TOO -17D8..17DA ; Khmer # Po [3] KHMER SIGN BEYYAL..KHMER SIGN KOOMUUT -17DB ; Khmer # Sc KHMER CURRENCY SYMBOL RIEL -17DC ; Khmer # Lo KHMER SIGN AVAKRAHASANYA -17DD ; Khmer # Mn KHMER SIGN ATTHACAN -17E0..17E9 ; Khmer # Nd [10] KHMER DIGIT ZERO..KHMER DIGIT NINE -17F0..17F9 ; Khmer # No [10] KHMER SYMBOL LEK ATTAK SON..KHMER SYMBOL LEK ATTAK PRAM-BUON -19E0..19FF ; Khmer # So [32] KHMER SYMBOL PATHAMASAT..KHMER SYMBOL DAP-PRAM ROC - -# Total code points: 146 - -# ================================================ - -1800..1801 ; Mongolian # Po [2] MONGOLIAN BIRGA..MONGOLIAN ELLIPSIS -1804 ; Mongolian # Po MONGOLIAN COLON -1806 ; Mongolian # Pd MONGOLIAN TODO SOFT HYPHEN -1807..180A ; Mongolian # Po [4] MONGOLIAN SIBE SYLLABLE BOUNDARY MARKER..MONGOLIAN NIRUGU -180B..180D ; Mongolian # Mn [3] MONGOLIAN FREE VARIATION SELECTOR ONE..MONGOLIAN FREE VARIATION SELECTOR THREE -180E ; Mongolian # Cf MONGOLIAN VOWEL SEPARATOR -1810..1819 ; Mongolian # Nd [10] MONGOLIAN DIGIT ZERO..MONGOLIAN DIGIT NINE -1820..1842 ; Mongolian # Lo [35] MONGOLIAN LETTER A..MONGOLIAN LETTER CHI -1843 ; Mongolian # Lm MONGOLIAN LETTER TODO LONG VOWEL SIGN -1844..1877 ; Mongolian # Lo [52] MONGOLIAN LETTER TODO E..MONGOLIAN LETTER MANCHU ZHA -1880..1884 ; Mongolian # Lo [5] MONGOLIAN LETTER ALI GALI ANUSVARA ONE..MONGOLIAN LETTER ALI GALI INVERTED UBADAMA -1885..1886 ; Mongolian # Mn [2] MONGOLIAN LETTER ALI GALI BALUDA..MONGOLIAN LETTER ALI GALI THREE BALUDA -1887..18A8 ; Mongolian # Lo [34] MONGOLIAN LETTER ALI GALI A..MONGOLIAN LETTER MANCHU ALI GALI BHA -18A9 ; Mongolian # Mn MONGOLIAN LETTER ALI GALI DAGALGA -18AA ; Mongolian # Lo MONGOLIAN LETTER MANCHU ALI GALI LHA -11660..1166C ; Mongolian # Po [13] MONGOLIAN BIRGA WITH ORNAMENT..MONGOLIAN TURNED SWIRL BIRGA WITH DOUBLE ORNAMENT - -# Total code points: 166 - -# ================================================ - -3041..3096 ; Hiragana # Lo [86] HIRAGANA LETTER SMALL A..HIRAGANA LETTER SMALL KE -309D..309E ; Hiragana # Lm [2] HIRAGANA ITERATION MARK..HIRAGANA VOICED ITERATION MARK -309F ; Hiragana # Lo HIRAGANA DIGRAPH YORI -1B001..1B11E ; Hiragana # Lo [286] HIRAGANA LETTER ARCHAIC YE..HENTAIGANA LETTER N-MU-MO-2 -1F200 ; Hiragana # So SQUARE HIRAGANA HOKA - -# Total code points: 376 - -# ================================================ - -30A1..30FA ; Katakana # Lo [90] KATAKANA LETTER SMALL A..KATAKANA LETTER VO -30FD..30FE ; Katakana # Lm [2] KATAKANA ITERATION MARK..KATAKANA VOICED ITERATION MARK -30FF ; Katakana # Lo KATAKANA DIGRAPH KOTO -31F0..31FF ; Katakana # Lo [16] KATAKANA LETTER SMALL KU..KATAKANA LETTER SMALL RO -32D0..32FE ; Katakana # So [47] CIRCLED KATAKANA A..CIRCLED KATAKANA WO -3300..3357 ; Katakana # So [88] SQUARE APAATO..SQUARE WATTO -FF66..FF6F ; Katakana # Lo [10] HALFWIDTH KATAKANA LETTER WO..HALFWIDTH KATAKANA LETTER SMALL TU -FF71..FF9D ; Katakana # Lo [45] HALFWIDTH KATAKANA LETTER A..HALFWIDTH KATAKANA LETTER N -1B000 ; Katakana # Lo KATAKANA LETTER ARCHAIC E - -# Total code points: 300 - -# ================================================ - -02EA..02EB ; Bopomofo # Sk [2] MODIFIER LETTER YIN DEPARTING TONE MARK..MODIFIER LETTER YANG DEPARTING TONE MARK -3105..312E ; Bopomofo # Lo [42] BOPOMOFO LETTER B..BOPOMOFO LETTER O WITH DOT ABOVE -31A0..31BA ; Bopomofo # Lo [27] BOPOMOFO LETTER BU..BOPOMOFO LETTER ZY - -# Total code points: 71 - -# ================================================ - -2E80..2E99 ; Han # So [26] CJK RADICAL REPEAT..CJK RADICAL RAP -2E9B..2EF3 ; Han # So [89] CJK RADICAL CHOKE..CJK RADICAL C-SIMPLIFIED TURTLE -2F00..2FD5 ; Han # So [214] KANGXI RADICAL ONE..KANGXI RADICAL FLUTE -3005 ; Han # Lm IDEOGRAPHIC ITERATION MARK -3007 ; Han # Nl IDEOGRAPHIC NUMBER ZERO -3021..3029 ; Han # Nl [9] HANGZHOU NUMERAL ONE..HANGZHOU NUMERAL NINE -3038..303A ; Han # Nl [3] HANGZHOU NUMERAL TEN..HANGZHOU NUMERAL THIRTY -303B ; Han # Lm VERTICAL IDEOGRAPHIC ITERATION MARK -3400..4DB5 ; Han # Lo [6582] CJK UNIFIED IDEOGRAPH-3400..CJK UNIFIED IDEOGRAPH-4DB5 -4E00..9FEA ; Han # Lo [20971] CJK UNIFIED IDEOGRAPH-4E00..CJK UNIFIED IDEOGRAPH-9FEA -F900..FA6D ; Han # Lo [366] CJK COMPATIBILITY IDEOGRAPH-F900..CJK COMPATIBILITY IDEOGRAPH-FA6D -FA70..FAD9 ; Han # Lo [106] CJK COMPATIBILITY IDEOGRAPH-FA70..CJK COMPATIBILITY IDEOGRAPH-FAD9 -20000..2A6D6 ; Han # Lo [42711] CJK UNIFIED IDEOGRAPH-20000..CJK UNIFIED IDEOGRAPH-2A6D6 -2A700..2B734 ; Han # Lo [4149] CJK UNIFIED IDEOGRAPH-2A700..CJK UNIFIED IDEOGRAPH-2B734 -2B740..2B81D ; Han # Lo [222] CJK UNIFIED IDEOGRAPH-2B740..CJK UNIFIED IDEOGRAPH-2B81D -2B820..2CEA1 ; Han # Lo [5762] CJK UNIFIED IDEOGRAPH-2B820..CJK UNIFIED IDEOGRAPH-2CEA1 -2CEB0..2EBE0 ; Han # Lo [7473] CJK UNIFIED IDEOGRAPH-2CEB0..CJK UNIFIED IDEOGRAPH-2EBE0 -2F800..2FA1D ; Han # Lo [542] CJK COMPATIBILITY IDEOGRAPH-2F800..CJK COMPATIBILITY IDEOGRAPH-2FA1D - -# Total code points: 89228 - -# ================================================ - -A000..A014 ; Yi # Lo [21] YI SYLLABLE IT..YI SYLLABLE E -A015 ; Yi # Lm YI SYLLABLE WU -A016..A48C ; Yi # Lo [1143] YI SYLLABLE BIT..YI SYLLABLE YYR -A490..A4C6 ; Yi # So [55] YI RADICAL QOT..YI RADICAL KE - -# Total code points: 1220 - -# ================================================ - -10300..1031F ; Old_Italic # Lo [32] OLD ITALIC LETTER A..OLD ITALIC LETTER ESS -10320..10323 ; Old_Italic # No [4] OLD ITALIC NUMERAL ONE..OLD ITALIC NUMERAL FIFTY -1032D..1032F ; Old_Italic # Lo [3] OLD ITALIC LETTER YE..OLD ITALIC LETTER SOUTHERN TSE - -# Total code points: 39 - -# ================================================ - -10330..10340 ; Gothic # Lo [17] GOTHIC LETTER AHSA..GOTHIC LETTER PAIRTHRA -10341 ; Gothic # Nl GOTHIC LETTER NINETY -10342..10349 ; Gothic # Lo [8] GOTHIC LETTER RAIDA..GOTHIC LETTER OTHAL -1034A ; Gothic # Nl GOTHIC LETTER NINE HUNDRED - -# Total code points: 27 - -# ================================================ - -10400..1044F ; Deseret # L& [80] DESERET CAPITAL LETTER LONG I..DESERET SMALL LETTER EW - -# Total code points: 80 - -# ================================================ - -0300..036F ; Inherited # Mn [112] COMBINING GRAVE ACCENT..COMBINING LATIN SMALL LETTER X -0485..0486 ; Inherited # Mn [2] COMBINING CYRILLIC DASIA PNEUMATA..COMBINING CYRILLIC PSILI PNEUMATA -064B..0655 ; Inherited # Mn [11] ARABIC FATHATAN..ARABIC HAMZA BELOW -0670 ; Inherited # Mn ARABIC LETTER SUPERSCRIPT ALEF -0951..0952 ; Inherited # Mn [2] DEVANAGARI STRESS SIGN UDATTA..DEVANAGARI STRESS SIGN ANUDATTA -1AB0..1ABD ; Inherited # Mn [14] COMBINING DOUBLED CIRCUMFLEX ACCENT..COMBINING PARENTHESES BELOW -1ABE ; Inherited # Me COMBINING PARENTHESES OVERLAY -1CD0..1CD2 ; Inherited # Mn [3] VEDIC TONE KARSHANA..VEDIC TONE PRENKHA -1CD4..1CE0 ; Inherited # Mn [13] VEDIC SIGN YAJURVEDIC MIDLINE SVARITA..VEDIC TONE RIGVEDIC KASHMIRI INDEPENDENT SVARITA -1CE2..1CE8 ; Inherited # Mn [7] VEDIC SIGN VISARGA SVARITA..VEDIC SIGN VISARGA ANUDATTA WITH TAIL -1CED ; Inherited # Mn VEDIC SIGN TIRYAK -1CF4 ; Inherited # Mn VEDIC TONE CANDRA ABOVE -1CF8..1CF9 ; Inherited # Mn [2] VEDIC TONE RING ABOVE..VEDIC TONE DOUBLE RING ABOVE -1DC0..1DF9 ; Inherited # Mn [58] COMBINING DOTTED GRAVE ACCENT..COMBINING WIDE INVERTED BRIDGE BELOW -1DFB..1DFF ; Inherited # Mn [5] COMBINING DELETION MARK..COMBINING RIGHT ARROWHEAD AND DOWN ARROWHEAD BELOW -200C..200D ; Inherited # Cf [2] ZERO WIDTH NON-JOINER..ZERO WIDTH JOINER -20D0..20DC ; Inherited # Mn [13] COMBINING LEFT HARPOON ABOVE..COMBINING FOUR DOTS ABOVE -20DD..20E0 ; Inherited # Me [4] COMBINING ENCLOSING CIRCLE..COMBINING ENCLOSING CIRCLE BACKSLASH -20E1 ; Inherited # Mn COMBINING LEFT RIGHT ARROW ABOVE -20E2..20E4 ; Inherited # Me [3] COMBINING ENCLOSING SCREEN..COMBINING ENCLOSING UPWARD POINTING TRIANGLE -20E5..20F0 ; Inherited # Mn [12] COMBINING REVERSE SOLIDUS OVERLAY..COMBINING ASTERISK ABOVE -302A..302D ; Inherited # Mn [4] IDEOGRAPHIC LEVEL TONE MARK..IDEOGRAPHIC ENTERING TONE MARK -3099..309A ; Inherited # Mn [2] COMBINING KATAKANA-HIRAGANA VOICED SOUND MARK..COMBINING KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK -FE00..FE0F ; Inherited # Mn [16] VARIATION SELECTOR-1..VARIATION SELECTOR-16 -FE20..FE2D ; Inherited # Mn [14] COMBINING LIGATURE LEFT HALF..COMBINING CONJOINING MACRON BELOW -101FD ; Inherited # Mn PHAISTOS DISC SIGN COMBINING OBLIQUE STROKE -102E0 ; Inherited # Mn COPTIC EPACT THOUSANDS MARK -1D167..1D169 ; Inherited # Mn [3] MUSICAL SYMBOL COMBINING TREMOLO-1..MUSICAL SYMBOL COMBINING TREMOLO-3 -1D17B..1D182 ; Inherited # Mn [8] MUSICAL SYMBOL COMBINING ACCENT..MUSICAL SYMBOL COMBINING LOURE -1D185..1D18B ; Inherited # Mn [7] MUSICAL SYMBOL COMBINING DOIT..MUSICAL SYMBOL COMBINING TRIPLE TONGUE -1D1AA..1D1AD ; Inherited # Mn [4] MUSICAL SYMBOL COMBINING DOWN BOW..MUSICAL SYMBOL COMBINING SNAP PIZZICATO -E0100..E01EF ; Inherited # Mn [240] VARIATION SELECTOR-17..VARIATION SELECTOR-256 - -# Total code points: 568 - -# ================================================ - -1700..170C ; Tagalog # Lo [13] TAGALOG LETTER A..TAGALOG LETTER YA -170E..1711 ; Tagalog # Lo [4] TAGALOG LETTER LA..TAGALOG LETTER HA -1712..1714 ; Tagalog # Mn [3] TAGALOG VOWEL SIGN I..TAGALOG SIGN VIRAMA - -# Total code points: 20 - -# ================================================ - -1720..1731 ; Hanunoo # Lo [18] HANUNOO LETTER A..HANUNOO LETTER HA -1732..1734 ; Hanunoo # Mn [3] HANUNOO VOWEL SIGN I..HANUNOO SIGN PAMUDPOD - -# Total code points: 21 - -# ================================================ - -1740..1751 ; Buhid # Lo [18] BUHID LETTER A..BUHID LETTER HA -1752..1753 ; Buhid # Mn [2] BUHID VOWEL SIGN I..BUHID VOWEL SIGN U - -# Total code points: 20 - -# ================================================ - -1760..176C ; Tagbanwa # Lo [13] TAGBANWA LETTER A..TAGBANWA LETTER YA -176E..1770 ; Tagbanwa # Lo [3] TAGBANWA LETTER LA..TAGBANWA LETTER SA -1772..1773 ; Tagbanwa # Mn [2] TAGBANWA VOWEL SIGN I..TAGBANWA VOWEL SIGN U - -# Total code points: 18 - -# ================================================ - -1900..191E ; Limbu # Lo [31] LIMBU VOWEL-CARRIER LETTER..LIMBU LETTER TRA -1920..1922 ; Limbu # Mn [3] LIMBU VOWEL SIGN A..LIMBU VOWEL SIGN U -1923..1926 ; Limbu # Mc [4] LIMBU VOWEL SIGN EE..LIMBU VOWEL SIGN AU -1927..1928 ; Limbu # Mn [2] LIMBU VOWEL SIGN E..LIMBU VOWEL SIGN O -1929..192B ; Limbu # Mc [3] LIMBU SUBJOINED LETTER YA..LIMBU SUBJOINED LETTER WA -1930..1931 ; Limbu # Mc [2] LIMBU SMALL LETTER KA..LIMBU SMALL LETTER NGA -1932 ; Limbu # Mn LIMBU SMALL LETTER ANUSVARA -1933..1938 ; Limbu # Mc [6] LIMBU SMALL LETTER TA..LIMBU SMALL LETTER LA -1939..193B ; Limbu # Mn [3] LIMBU SIGN MUKPHRENG..LIMBU SIGN SA-I -1940 ; Limbu # So LIMBU SIGN LOO -1944..1945 ; Limbu # Po [2] LIMBU EXCLAMATION MARK..LIMBU QUESTION MARK -1946..194F ; Limbu # Nd [10] LIMBU DIGIT ZERO..LIMBU DIGIT NINE - -# Total code points: 68 - -# ================================================ - -1950..196D ; Tai_Le # Lo [30] TAI LE LETTER KA..TAI LE LETTER AI -1970..1974 ; Tai_Le # Lo [5] TAI LE LETTER TONE-2..TAI LE LETTER TONE-6 - -# Total code points: 35 - -# ================================================ - -10000..1000B ; Linear_B # Lo [12] LINEAR B SYLLABLE B008 A..LINEAR B SYLLABLE B046 JE -1000D..10026 ; Linear_B # Lo [26] LINEAR B SYLLABLE B036 JO..LINEAR B SYLLABLE B032 QO -10028..1003A ; Linear_B # Lo [19] LINEAR B SYLLABLE B060 RA..LINEAR B SYLLABLE B042 WO -1003C..1003D ; Linear_B # Lo [2] LINEAR B SYLLABLE B017 ZA..LINEAR B SYLLABLE B074 ZE -1003F..1004D ; Linear_B # Lo [15] LINEAR B SYLLABLE B020 ZO..LINEAR B SYLLABLE B091 TWO -10050..1005D ; Linear_B # Lo [14] LINEAR B SYMBOL B018..LINEAR B SYMBOL B089 -10080..100FA ; Linear_B # Lo [123] LINEAR B IDEOGRAM B100 MAN..LINEAR B IDEOGRAM VESSEL B305 - -# Total code points: 211 - -# ================================================ - -10380..1039D ; Ugaritic # Lo [30] UGARITIC LETTER ALPA..UGARITIC LETTER SSU -1039F ; Ugaritic # Po UGARITIC WORD DIVIDER - -# Total code points: 31 - -# ================================================ - -10450..1047F ; Shavian # Lo [48] SHAVIAN LETTER PEEP..SHAVIAN LETTER YEW - -# Total code points: 48 - -# ================================================ - -10480..1049D ; Osmanya # Lo [30] OSMANYA LETTER ALEF..OSMANYA LETTER OO -104A0..104A9 ; Osmanya # Nd [10] OSMANYA DIGIT ZERO..OSMANYA DIGIT NINE - -# Total code points: 40 - -# ================================================ - -10800..10805 ; Cypriot # Lo [6] CYPRIOT SYLLABLE A..CYPRIOT SYLLABLE JA -10808 ; Cypriot # Lo CYPRIOT SYLLABLE JO -1080A..10835 ; Cypriot # Lo [44] CYPRIOT SYLLABLE KA..CYPRIOT SYLLABLE WO -10837..10838 ; Cypriot # Lo [2] CYPRIOT SYLLABLE XA..CYPRIOT SYLLABLE XE -1083C ; Cypriot # Lo CYPRIOT SYLLABLE ZA -1083F ; Cypriot # Lo CYPRIOT SYLLABLE ZO - -# Total code points: 55 - -# ================================================ - -2800..28FF ; Braille # So [256] BRAILLE PATTERN BLANK..BRAILLE PATTERN DOTS-12345678 - -# Total code points: 256 - -# ================================================ - -1A00..1A16 ; Buginese # Lo [23] BUGINESE LETTER KA..BUGINESE LETTER HA -1A17..1A18 ; Buginese # Mn [2] BUGINESE VOWEL SIGN I..BUGINESE VOWEL SIGN U -1A19..1A1A ; Buginese # Mc [2] BUGINESE VOWEL SIGN E..BUGINESE VOWEL SIGN O -1A1B ; Buginese # Mn BUGINESE VOWEL SIGN AE -1A1E..1A1F ; Buginese # Po [2] BUGINESE PALLAWA..BUGINESE END OF SECTION - -# Total code points: 30 - -# ================================================ - -03E2..03EF ; Coptic # L& [14] COPTIC CAPITAL LETTER SHEI..COPTIC SMALL LETTER DEI -2C80..2CE4 ; Coptic # L& [101] COPTIC CAPITAL LETTER ALFA..COPTIC SYMBOL KAI -2CE5..2CEA ; Coptic # So [6] COPTIC SYMBOL MI RO..COPTIC SYMBOL SHIMA SIMA -2CEB..2CEE ; Coptic # L& [4] COPTIC CAPITAL LETTER CRYPTOGRAMMIC SHEI..COPTIC SMALL LETTER CRYPTOGRAMMIC GANGIA -2CEF..2CF1 ; Coptic # Mn [3] COPTIC COMBINING NI ABOVE..COPTIC COMBINING SPIRITUS LENIS -2CF2..2CF3 ; Coptic # L& [2] COPTIC CAPITAL LETTER BOHAIRIC KHEI..COPTIC SMALL LETTER BOHAIRIC KHEI -2CF9..2CFC ; Coptic # Po [4] COPTIC OLD NUBIAN FULL STOP..COPTIC OLD NUBIAN VERSE DIVIDER -2CFD ; Coptic # No COPTIC FRACTION ONE HALF -2CFE..2CFF ; Coptic # Po [2] COPTIC FULL STOP..COPTIC MORPHOLOGICAL DIVIDER - -# Total code points: 137 - -# ================================================ - -1980..19AB ; New_Tai_Lue # Lo [44] NEW TAI LUE LETTER HIGH QA..NEW TAI LUE LETTER LOW SUA -19B0..19C9 ; New_Tai_Lue # Lo [26] NEW TAI LUE VOWEL SIGN VOWEL SHORTENER..NEW TAI LUE TONE MARK-2 -19D0..19D9 ; New_Tai_Lue # Nd [10] NEW TAI LUE DIGIT ZERO..NEW TAI LUE DIGIT NINE -19DA ; New_Tai_Lue # No NEW TAI LUE THAM DIGIT ONE -19DE..19DF ; New_Tai_Lue # So [2] NEW TAI LUE SIGN LAE..NEW TAI LUE SIGN LAEV - -# Total code points: 83 - -# ================================================ - -2C00..2C2E ; Glagolitic # L& [47] GLAGOLITIC CAPITAL LETTER AZU..GLAGOLITIC CAPITAL LETTER LATINATE MYSLITE -2C30..2C5E ; Glagolitic # L& [47] GLAGOLITIC SMALL LETTER AZU..GLAGOLITIC SMALL LETTER LATINATE MYSLITE -1E000..1E006 ; Glagolitic # Mn [7] COMBINING GLAGOLITIC LETTER AZU..COMBINING GLAGOLITIC LETTER ZHIVETE -1E008..1E018 ; Glagolitic # Mn [17] COMBINING GLAGOLITIC LETTER ZEMLJA..COMBINING GLAGOLITIC LETTER HERU -1E01B..1E021 ; Glagolitic # Mn [7] COMBINING GLAGOLITIC LETTER SHTA..COMBINING GLAGOLITIC LETTER YATI -1E023..1E024 ; Glagolitic # Mn [2] COMBINING GLAGOLITIC LETTER YU..COMBINING GLAGOLITIC LETTER SMALL YUS -1E026..1E02A ; Glagolitic # Mn [5] COMBINING GLAGOLITIC LETTER YO..COMBINING GLAGOLITIC LETTER FITA - -# Total code points: 132 - -# ================================================ - -2D30..2D67 ; Tifinagh # Lo [56] TIFINAGH LETTER YA..TIFINAGH LETTER YO -2D6F ; Tifinagh # Lm TIFINAGH MODIFIER LETTER LABIALIZATION MARK -2D70 ; Tifinagh # Po TIFINAGH SEPARATOR MARK -2D7F ; Tifinagh # Mn TIFINAGH CONSONANT JOINER - -# Total code points: 59 - -# ================================================ - -A800..A801 ; Syloti_Nagri # Lo [2] SYLOTI NAGRI LETTER A..SYLOTI NAGRI LETTER I -A802 ; Syloti_Nagri # Mn SYLOTI NAGRI SIGN DVISVARA -A803..A805 ; Syloti_Nagri # Lo [3] SYLOTI NAGRI LETTER U..SYLOTI NAGRI LETTER O -A806 ; Syloti_Nagri # Mn SYLOTI NAGRI SIGN HASANTA -A807..A80A ; Syloti_Nagri # Lo [4] SYLOTI NAGRI LETTER KO..SYLOTI NAGRI LETTER GHO -A80B ; Syloti_Nagri # Mn SYLOTI NAGRI SIGN ANUSVARA -A80C..A822 ; Syloti_Nagri # Lo [23] SYLOTI NAGRI LETTER CO..SYLOTI NAGRI LETTER HO -A823..A824 ; Syloti_Nagri # Mc [2] SYLOTI NAGRI VOWEL SIGN A..SYLOTI NAGRI VOWEL SIGN I -A825..A826 ; Syloti_Nagri # Mn [2] SYLOTI NAGRI VOWEL SIGN U..SYLOTI NAGRI VOWEL SIGN E -A827 ; Syloti_Nagri # Mc SYLOTI NAGRI VOWEL SIGN OO -A828..A82B ; Syloti_Nagri # So [4] SYLOTI NAGRI POETRY MARK-1..SYLOTI NAGRI POETRY MARK-4 - -# Total code points: 44 - -# ================================================ - -103A0..103C3 ; Old_Persian # Lo [36] OLD PERSIAN SIGN A..OLD PERSIAN SIGN HA -103C8..103CF ; Old_Persian # Lo [8] OLD PERSIAN SIGN AURAMAZDAA..OLD PERSIAN SIGN BUUMISH -103D0 ; Old_Persian # Po OLD PERSIAN WORD DIVIDER -103D1..103D5 ; Old_Persian # Nl [5] OLD PERSIAN NUMBER ONE..OLD PERSIAN NUMBER HUNDRED - -# Total code points: 50 - -# ================================================ - -10A00 ; Kharoshthi # Lo KHAROSHTHI LETTER A -10A01..10A03 ; Kharoshthi # Mn [3] KHAROSHTHI VOWEL SIGN I..KHAROSHTHI VOWEL SIGN VOCALIC R -10A05..10A06 ; Kharoshthi # Mn [2] KHAROSHTHI VOWEL SIGN E..KHAROSHTHI VOWEL SIGN O -10A0C..10A0F ; Kharoshthi # Mn [4] KHAROSHTHI VOWEL LENGTH MARK..KHAROSHTHI SIGN VISARGA -10A10..10A13 ; Kharoshthi # Lo [4] KHAROSHTHI LETTER KA..KHAROSHTHI LETTER GHA -10A15..10A17 ; Kharoshthi # Lo [3] KHAROSHTHI LETTER CA..KHAROSHTHI LETTER JA -10A19..10A33 ; Kharoshthi # Lo [27] KHAROSHTHI LETTER NYA..KHAROSHTHI LETTER TTTHA -10A38..10A3A ; Kharoshthi # Mn [3] KHAROSHTHI SIGN BAR ABOVE..KHAROSHTHI SIGN DOT BELOW -10A3F ; Kharoshthi # Mn KHAROSHTHI VIRAMA -10A40..10A47 ; Kharoshthi # No [8] KHAROSHTHI DIGIT ONE..KHAROSHTHI NUMBER ONE THOUSAND -10A50..10A58 ; Kharoshthi # Po [9] KHAROSHTHI PUNCTUATION DOT..KHAROSHTHI PUNCTUATION LINES - -# Total code points: 65 - -# ================================================ - -1B00..1B03 ; Balinese # Mn [4] BALINESE SIGN ULU RICEM..BALINESE SIGN SURANG -1B04 ; Balinese # Mc BALINESE SIGN BISAH -1B05..1B33 ; Balinese # Lo [47] BALINESE LETTER AKARA..BALINESE LETTER HA -1B34 ; Balinese # Mn BALINESE SIGN REREKAN -1B35 ; Balinese # Mc BALINESE VOWEL SIGN TEDUNG -1B36..1B3A ; Balinese # Mn [5] BALINESE VOWEL SIGN ULU..BALINESE VOWEL SIGN RA REPA -1B3B ; Balinese # Mc BALINESE VOWEL SIGN RA REPA TEDUNG -1B3C ; Balinese # Mn BALINESE VOWEL SIGN LA LENGA -1B3D..1B41 ; Balinese # Mc [5] BALINESE VOWEL SIGN LA LENGA TEDUNG..BALINESE VOWEL SIGN TALING REPA TEDUNG -1B42 ; Balinese # Mn BALINESE VOWEL SIGN PEPET -1B43..1B44 ; Balinese # Mc [2] BALINESE VOWEL SIGN PEPET TEDUNG..BALINESE ADEG ADEG -1B45..1B4B ; Balinese # Lo [7] BALINESE LETTER KAF SASAK..BALINESE LETTER ASYURA SASAK -1B50..1B59 ; Balinese # Nd [10] BALINESE DIGIT ZERO..BALINESE DIGIT NINE -1B5A..1B60 ; Balinese # Po [7] BALINESE PANTI..BALINESE PAMENENG -1B61..1B6A ; Balinese # So [10] BALINESE MUSICAL SYMBOL DONG..BALINESE MUSICAL SYMBOL DANG GEDE -1B6B..1B73 ; Balinese # Mn [9] BALINESE MUSICAL SYMBOL COMBINING TEGEH..BALINESE MUSICAL SYMBOL COMBINING GONG -1B74..1B7C ; Balinese # So [9] BALINESE MUSICAL SYMBOL RIGHT-HAND OPEN DUG..BALINESE MUSICAL SYMBOL LEFT-HAND OPEN PING - -# Total code points: 121 - -# ================================================ - -12000..12399 ; Cuneiform # Lo [922] CUNEIFORM SIGN A..CUNEIFORM SIGN U U -12400..1246E ; Cuneiform # Nl [111] CUNEIFORM NUMERIC SIGN TWO ASH..CUNEIFORM NUMERIC SIGN NINE U VARIANT FORM -12470..12474 ; Cuneiform # Po [5] CUNEIFORM PUNCTUATION SIGN OLD ASSYRIAN WORD DIVIDER..CUNEIFORM PUNCTUATION SIGN DIAGONAL QUADCOLON -12480..12543 ; Cuneiform # Lo [196] CUNEIFORM SIGN AB TIMES NUN TENU..CUNEIFORM SIGN ZU5 TIMES THREE DISH TENU - -# Total code points: 1234 - -# ================================================ - -10900..10915 ; Phoenician # Lo [22] PHOENICIAN LETTER ALF..PHOENICIAN LETTER TAU -10916..1091B ; Phoenician # No [6] PHOENICIAN NUMBER ONE..PHOENICIAN NUMBER THREE -1091F ; Phoenician # Po PHOENICIAN WORD SEPARATOR - -# Total code points: 29 - -# ================================================ - -A840..A873 ; Phags_Pa # Lo [52] PHAGS-PA LETTER KA..PHAGS-PA LETTER CANDRABINDU -A874..A877 ; Phags_Pa # Po [4] PHAGS-PA SINGLE HEAD MARK..PHAGS-PA MARK DOUBLE SHAD - -# Total code points: 56 - -# ================================================ - -07C0..07C9 ; Nko # Nd [10] NKO DIGIT ZERO..NKO DIGIT NINE -07CA..07EA ; Nko # Lo [33] NKO LETTER A..NKO LETTER JONA RA -07EB..07F3 ; Nko # Mn [9] NKO COMBINING SHORT HIGH TONE..NKO COMBINING DOUBLE DOT ABOVE -07F4..07F5 ; Nko # Lm [2] NKO HIGH TONE APOSTROPHE..NKO LOW TONE APOSTROPHE -07F6 ; Nko # So NKO SYMBOL OO DENNEN -07F7..07F9 ; Nko # Po [3] NKO SYMBOL GBAKURUNEN..NKO EXCLAMATION MARK -07FA ; Nko # Lm NKO LAJANYALAN - -# Total code points: 59 - -# ================================================ - -1B80..1B81 ; Sundanese # Mn [2] SUNDANESE SIGN PANYECEK..SUNDANESE SIGN PANGLAYAR -1B82 ; Sundanese # Mc SUNDANESE SIGN PANGWISAD -1B83..1BA0 ; Sundanese # Lo [30] SUNDANESE LETTER A..SUNDANESE LETTER HA -1BA1 ; Sundanese # Mc SUNDANESE CONSONANT SIGN PAMINGKAL -1BA2..1BA5 ; Sundanese # Mn [4] SUNDANESE CONSONANT SIGN PANYAKRA..SUNDANESE VOWEL SIGN PANYUKU -1BA6..1BA7 ; Sundanese # Mc [2] SUNDANESE VOWEL SIGN PANAELAENG..SUNDANESE VOWEL SIGN PANOLONG -1BA8..1BA9 ; Sundanese # Mn [2] SUNDANESE VOWEL SIGN PAMEPET..SUNDANESE VOWEL SIGN PANEULEUNG -1BAA ; Sundanese # Mc SUNDANESE SIGN PAMAAEH -1BAB..1BAD ; Sundanese # Mn [3] SUNDANESE SIGN VIRAMA..SUNDANESE CONSONANT SIGN PASANGAN WA -1BAE..1BAF ; Sundanese # Lo [2] SUNDANESE LETTER KHA..SUNDANESE LETTER SYA -1BB0..1BB9 ; Sundanese # Nd [10] SUNDANESE DIGIT ZERO..SUNDANESE DIGIT NINE -1BBA..1BBF ; Sundanese # Lo [6] SUNDANESE AVAGRAHA..SUNDANESE LETTER FINAL M -1CC0..1CC7 ; Sundanese # Po [8] SUNDANESE PUNCTUATION BINDU SURYA..SUNDANESE PUNCTUATION BINDU BA SATANGA - -# Total code points: 72 - -# ================================================ - -1C00..1C23 ; Lepcha # Lo [36] LEPCHA LETTER KA..LEPCHA LETTER A -1C24..1C2B ; Lepcha # Mc [8] LEPCHA SUBJOINED LETTER YA..LEPCHA VOWEL SIGN UU -1C2C..1C33 ; Lepcha # Mn [8] LEPCHA VOWEL SIGN E..LEPCHA CONSONANT SIGN T -1C34..1C35 ; Lepcha # Mc [2] LEPCHA CONSONANT SIGN NYIN-DO..LEPCHA CONSONANT SIGN KANG -1C36..1C37 ; Lepcha # Mn [2] LEPCHA SIGN RAN..LEPCHA SIGN NUKTA -1C3B..1C3F ; Lepcha # Po [5] LEPCHA PUNCTUATION TA-ROL..LEPCHA PUNCTUATION TSHOOK -1C40..1C49 ; Lepcha # Nd [10] LEPCHA DIGIT ZERO..LEPCHA DIGIT NINE -1C4D..1C4F ; Lepcha # Lo [3] LEPCHA LETTER TTA..LEPCHA LETTER DDA - -# Total code points: 74 - -# ================================================ - -1C50..1C59 ; Ol_Chiki # Nd [10] OL CHIKI DIGIT ZERO..OL CHIKI DIGIT NINE -1C5A..1C77 ; Ol_Chiki # Lo [30] OL CHIKI LETTER LA..OL CHIKI LETTER OH -1C78..1C7D ; Ol_Chiki # Lm [6] OL CHIKI MU TTUDDAG..OL CHIKI AHAD -1C7E..1C7F ; Ol_Chiki # Po [2] OL CHIKI PUNCTUATION MUCAAD..OL CHIKI PUNCTUATION DOUBLE MUCAAD - -# Total code points: 48 - -# ================================================ - -A500..A60B ; Vai # Lo [268] VAI SYLLABLE EE..VAI SYLLABLE NG -A60C ; Vai # Lm VAI SYLLABLE LENGTHENER -A60D..A60F ; Vai # Po [3] VAI COMMA..VAI QUESTION MARK -A610..A61F ; Vai # Lo [16] VAI SYLLABLE NDOLE FA..VAI SYMBOL JONG -A620..A629 ; Vai # Nd [10] VAI DIGIT ZERO..VAI DIGIT NINE -A62A..A62B ; Vai # Lo [2] VAI SYLLABLE NDOLE MA..VAI SYLLABLE NDOLE DO - -# Total code points: 300 - -# ================================================ - -A880..A881 ; Saurashtra # Mc [2] SAURASHTRA SIGN ANUSVARA..SAURASHTRA SIGN VISARGA -A882..A8B3 ; Saurashtra # Lo [50] SAURASHTRA LETTER A..SAURASHTRA LETTER LLA -A8B4..A8C3 ; Saurashtra # Mc [16] SAURASHTRA CONSONANT SIGN HAARU..SAURASHTRA VOWEL SIGN AU -A8C4..A8C5 ; Saurashtra # Mn [2] SAURASHTRA SIGN VIRAMA..SAURASHTRA SIGN CANDRABINDU -A8CE..A8CF ; Saurashtra # Po [2] SAURASHTRA DANDA..SAURASHTRA DOUBLE DANDA -A8D0..A8D9 ; Saurashtra # Nd [10] SAURASHTRA DIGIT ZERO..SAURASHTRA DIGIT NINE - -# Total code points: 82 - -# ================================================ - -A900..A909 ; Kayah_Li # Nd [10] KAYAH LI DIGIT ZERO..KAYAH LI DIGIT NINE -A90A..A925 ; Kayah_Li # Lo [28] KAYAH LI LETTER KA..KAYAH LI LETTER OO -A926..A92D ; Kayah_Li # Mn [8] KAYAH LI VOWEL UE..KAYAH LI TONE CALYA PLOPHU -A92F ; Kayah_Li # Po KAYAH LI SIGN SHYA - -# Total code points: 47 - -# ================================================ - -A930..A946 ; Rejang # Lo [23] REJANG LETTER KA..REJANG LETTER A -A947..A951 ; Rejang # Mn [11] REJANG VOWEL SIGN I..REJANG CONSONANT SIGN R -A952..A953 ; Rejang # Mc [2] REJANG CONSONANT SIGN H..REJANG VIRAMA -A95F ; Rejang # Po REJANG SECTION MARK - -# Total code points: 37 - -# ================================================ - -10280..1029C ; Lycian # Lo [29] LYCIAN LETTER A..LYCIAN LETTER X - -# Total code points: 29 - -# ================================================ - -102A0..102D0 ; Carian # Lo [49] CARIAN LETTER A..CARIAN LETTER UUU3 - -# Total code points: 49 - -# ================================================ - -10920..10939 ; Lydian # Lo [26] LYDIAN LETTER A..LYDIAN LETTER C -1093F ; Lydian # Po LYDIAN TRIANGULAR MARK - -# Total code points: 27 - -# ================================================ - -AA00..AA28 ; Cham # Lo [41] CHAM LETTER A..CHAM LETTER HA -AA29..AA2E ; Cham # Mn [6] CHAM VOWEL SIGN AA..CHAM VOWEL SIGN OE -AA2F..AA30 ; Cham # Mc [2] CHAM VOWEL SIGN O..CHAM VOWEL SIGN AI -AA31..AA32 ; Cham # Mn [2] CHAM VOWEL SIGN AU..CHAM VOWEL SIGN UE -AA33..AA34 ; Cham # Mc [2] CHAM CONSONANT SIGN YA..CHAM CONSONANT SIGN RA -AA35..AA36 ; Cham # Mn [2] CHAM CONSONANT SIGN LA..CHAM CONSONANT SIGN WA -AA40..AA42 ; Cham # Lo [3] CHAM LETTER FINAL K..CHAM LETTER FINAL NG -AA43 ; Cham # Mn CHAM CONSONANT SIGN FINAL NG -AA44..AA4B ; Cham # Lo [8] CHAM LETTER FINAL CH..CHAM LETTER FINAL SS -AA4C ; Cham # Mn CHAM CONSONANT SIGN FINAL M -AA4D ; Cham # Mc CHAM CONSONANT SIGN FINAL H -AA50..AA59 ; Cham # Nd [10] CHAM DIGIT ZERO..CHAM DIGIT NINE -AA5C..AA5F ; Cham # Po [4] CHAM PUNCTUATION SPIRAL..CHAM PUNCTUATION TRIPLE DANDA - -# Total code points: 83 - -# ================================================ - -1A20..1A54 ; Tai_Tham # Lo [53] TAI THAM LETTER HIGH KA..TAI THAM LETTER GREAT SA -1A55 ; Tai_Tham # Mc TAI THAM CONSONANT SIGN MEDIAL RA -1A56 ; Tai_Tham # Mn TAI THAM CONSONANT SIGN MEDIAL LA -1A57 ; Tai_Tham # Mc TAI THAM CONSONANT SIGN LA TANG LAI -1A58..1A5E ; Tai_Tham # Mn [7] TAI THAM SIGN MAI KANG LAI..TAI THAM CONSONANT SIGN SA -1A60 ; Tai_Tham # Mn TAI THAM SIGN SAKOT -1A61 ; Tai_Tham # Mc TAI THAM VOWEL SIGN A -1A62 ; Tai_Tham # Mn TAI THAM VOWEL SIGN MAI SAT -1A63..1A64 ; Tai_Tham # Mc [2] TAI THAM VOWEL SIGN AA..TAI THAM VOWEL SIGN TALL AA -1A65..1A6C ; Tai_Tham # Mn [8] TAI THAM VOWEL SIGN I..TAI THAM VOWEL SIGN OA BELOW -1A6D..1A72 ; Tai_Tham # Mc [6] TAI THAM VOWEL SIGN OY..TAI THAM VOWEL SIGN THAM AI -1A73..1A7C ; Tai_Tham # Mn [10] TAI THAM VOWEL SIGN OA ABOVE..TAI THAM SIGN KHUEN-LUE KARAN -1A7F ; Tai_Tham # Mn TAI THAM COMBINING CRYPTOGRAMMIC DOT -1A80..1A89 ; Tai_Tham # Nd [10] TAI THAM HORA DIGIT ZERO..TAI THAM HORA DIGIT NINE -1A90..1A99 ; Tai_Tham # Nd [10] TAI THAM THAM DIGIT ZERO..TAI THAM THAM DIGIT NINE -1AA0..1AA6 ; Tai_Tham # Po [7] TAI THAM SIGN WIANG..TAI THAM SIGN REVERSED ROTATED RANA -1AA7 ; Tai_Tham # Lm TAI THAM SIGN MAI YAMOK -1AA8..1AAD ; Tai_Tham # Po [6] TAI THAM SIGN KAAN..TAI THAM SIGN CAANG - -# Total code points: 127 - -# ================================================ - -AA80..AAAF ; Tai_Viet # Lo [48] TAI VIET LETTER LOW KO..TAI VIET LETTER HIGH O -AAB0 ; Tai_Viet # Mn TAI VIET MAI KANG -AAB1 ; Tai_Viet # Lo TAI VIET VOWEL AA -AAB2..AAB4 ; Tai_Viet # Mn [3] TAI VIET VOWEL I..TAI VIET VOWEL U -AAB5..AAB6 ; Tai_Viet # Lo [2] TAI VIET VOWEL E..TAI VIET VOWEL O -AAB7..AAB8 ; Tai_Viet # Mn [2] TAI VIET MAI KHIT..TAI VIET VOWEL IA -AAB9..AABD ; Tai_Viet # Lo [5] TAI VIET VOWEL UEA..TAI VIET VOWEL AN -AABE..AABF ; Tai_Viet # Mn [2] TAI VIET VOWEL AM..TAI VIET TONE MAI EK -AAC0 ; Tai_Viet # Lo TAI VIET TONE MAI NUENG -AAC1 ; Tai_Viet # Mn TAI VIET TONE MAI THO -AAC2 ; Tai_Viet # Lo TAI VIET TONE MAI SONG -AADB..AADC ; Tai_Viet # Lo [2] TAI VIET SYMBOL KON..TAI VIET SYMBOL NUENG -AADD ; Tai_Viet # Lm TAI VIET SYMBOL SAM -AADE..AADF ; Tai_Viet # Po [2] TAI VIET SYMBOL HO HOI..TAI VIET SYMBOL KOI KOI - -# Total code points: 72 - -# ================================================ - -10B00..10B35 ; Avestan # Lo [54] AVESTAN LETTER A..AVESTAN LETTER HE -10B39..10B3F ; Avestan # Po [7] AVESTAN ABBREVIATION MARK..LARGE ONE RING OVER TWO RINGS PUNCTUATION - -# Total code points: 61 - -# ================================================ - -13000..1342E ; Egyptian_Hieroglyphs # Lo [1071] EGYPTIAN HIEROGLYPH A001..EGYPTIAN HIEROGLYPH AA032 - -# Total code points: 1071 - -# ================================================ - -0800..0815 ; Samaritan # Lo [22] SAMARITAN LETTER ALAF..SAMARITAN LETTER TAAF -0816..0819 ; Samaritan # Mn [4] SAMARITAN MARK IN..SAMARITAN MARK DAGESH -081A ; Samaritan # Lm SAMARITAN MODIFIER LETTER EPENTHETIC YUT -081B..0823 ; Samaritan # Mn [9] SAMARITAN MARK EPENTHETIC YUT..SAMARITAN VOWEL SIGN A -0824 ; Samaritan # Lm SAMARITAN MODIFIER LETTER SHORT A -0825..0827 ; Samaritan # Mn [3] SAMARITAN VOWEL SIGN SHORT A..SAMARITAN VOWEL SIGN U -0828 ; Samaritan # Lm SAMARITAN MODIFIER LETTER I -0829..082D ; Samaritan # Mn [5] SAMARITAN VOWEL SIGN LONG I..SAMARITAN MARK NEQUDAA -0830..083E ; Samaritan # Po [15] SAMARITAN PUNCTUATION NEQUDAA..SAMARITAN PUNCTUATION ANNAAU - -# Total code points: 61 - -# ================================================ - -A4D0..A4F7 ; Lisu # Lo [40] LISU LETTER BA..LISU LETTER OE -A4F8..A4FD ; Lisu # Lm [6] LISU LETTER TONE MYA TI..LISU LETTER TONE MYA JEU -A4FE..A4FF ; Lisu # Po [2] LISU PUNCTUATION COMMA..LISU PUNCTUATION FULL STOP - -# Total code points: 48 - -# ================================================ - -A6A0..A6E5 ; Bamum # Lo [70] BAMUM LETTER A..BAMUM LETTER KI -A6E6..A6EF ; Bamum # Nl [10] BAMUM LETTER MO..BAMUM LETTER KOGHOM -A6F0..A6F1 ; Bamum # Mn [2] BAMUM COMBINING MARK KOQNDON..BAMUM COMBINING MARK TUKWENTIS -A6F2..A6F7 ; Bamum # Po [6] BAMUM NJAEMLI..BAMUM QUESTION MARK -16800..16A38 ; Bamum # Lo [569] BAMUM LETTER PHASE-A NGKUE MFON..BAMUM LETTER PHASE-F VUEQ - -# Total code points: 657 - -# ================================================ - -A980..A982 ; Javanese # Mn [3] JAVANESE SIGN PANYANGGA..JAVANESE SIGN LAYAR -A983 ; Javanese # Mc JAVANESE SIGN WIGNYAN -A984..A9B2 ; Javanese # Lo [47] JAVANESE LETTER A..JAVANESE LETTER HA -A9B3 ; Javanese # Mn JAVANESE SIGN CECAK TELU -A9B4..A9B5 ; Javanese # Mc [2] JAVANESE VOWEL SIGN TARUNG..JAVANESE VOWEL SIGN TOLONG -A9B6..A9B9 ; Javanese # Mn [4] JAVANESE VOWEL SIGN WULU..JAVANESE VOWEL SIGN SUKU MENDUT -A9BA..A9BB ; Javanese # Mc [2] JAVANESE VOWEL SIGN TALING..JAVANESE VOWEL SIGN DIRGA MURE -A9BC ; Javanese # Mn JAVANESE VOWEL SIGN PEPET -A9BD..A9C0 ; Javanese # Mc [4] JAVANESE CONSONANT SIGN KERET..JAVANESE PANGKON -A9C1..A9CD ; Javanese # Po [13] JAVANESE LEFT RERENGGAN..JAVANESE TURNED PADA PISELEH -A9D0..A9D9 ; Javanese # Nd [10] JAVANESE DIGIT ZERO..JAVANESE DIGIT NINE -A9DE..A9DF ; Javanese # Po [2] JAVANESE PADA TIRTA TUMETES..JAVANESE PADA ISEN-ISEN - -# Total code points: 90 - -# ================================================ - -AAE0..AAEA ; Meetei_Mayek # Lo [11] MEETEI MAYEK LETTER E..MEETEI MAYEK LETTER SSA -AAEB ; Meetei_Mayek # Mc MEETEI MAYEK VOWEL SIGN II -AAEC..AAED ; Meetei_Mayek # Mn [2] MEETEI MAYEK VOWEL SIGN UU..MEETEI MAYEK VOWEL SIGN AAI -AAEE..AAEF ; Meetei_Mayek # Mc [2] MEETEI MAYEK VOWEL SIGN AU..MEETEI MAYEK VOWEL SIGN AAU -AAF0..AAF1 ; Meetei_Mayek # Po [2] MEETEI MAYEK CHEIKHAN..MEETEI MAYEK AHANG KHUDAM -AAF2 ; Meetei_Mayek # Lo MEETEI MAYEK ANJI -AAF3..AAF4 ; Meetei_Mayek # Lm [2] MEETEI MAYEK SYLLABLE REPETITION MARK..MEETEI MAYEK WORD REPETITION MARK -AAF5 ; Meetei_Mayek # Mc MEETEI MAYEK VOWEL SIGN VISARGA -AAF6 ; Meetei_Mayek # Mn MEETEI MAYEK VIRAMA -ABC0..ABE2 ; Meetei_Mayek # Lo [35] MEETEI MAYEK LETTER KOK..MEETEI MAYEK LETTER I LONSUM -ABE3..ABE4 ; Meetei_Mayek # Mc [2] MEETEI MAYEK VOWEL SIGN ONAP..MEETEI MAYEK VOWEL SIGN INAP -ABE5 ; Meetei_Mayek # Mn MEETEI MAYEK VOWEL SIGN ANAP -ABE6..ABE7 ; Meetei_Mayek # Mc [2] MEETEI MAYEK VOWEL SIGN YENAP..MEETEI MAYEK VOWEL SIGN SOUNAP -ABE8 ; Meetei_Mayek # Mn MEETEI MAYEK VOWEL SIGN UNAP -ABE9..ABEA ; Meetei_Mayek # Mc [2] MEETEI MAYEK VOWEL SIGN CHEINAP..MEETEI MAYEK VOWEL SIGN NUNG -ABEB ; Meetei_Mayek # Po MEETEI MAYEK CHEIKHEI -ABEC ; Meetei_Mayek # Mc MEETEI MAYEK LUM IYEK -ABED ; Meetei_Mayek # Mn MEETEI MAYEK APUN IYEK -ABF0..ABF9 ; Meetei_Mayek # Nd [10] MEETEI MAYEK DIGIT ZERO..MEETEI MAYEK DIGIT NINE - -# Total code points: 79 - -# ================================================ - -10840..10855 ; Imperial_Aramaic # Lo [22] IMPERIAL ARAMAIC LETTER ALEPH..IMPERIAL ARAMAIC LETTER TAW -10857 ; Imperial_Aramaic # Po IMPERIAL ARAMAIC SECTION SIGN -10858..1085F ; Imperial_Aramaic # No [8] IMPERIAL ARAMAIC NUMBER ONE..IMPERIAL ARAMAIC NUMBER TEN THOUSAND - -# Total code points: 31 - -# ================================================ - -10A60..10A7C ; Old_South_Arabian # Lo [29] OLD SOUTH ARABIAN LETTER HE..OLD SOUTH ARABIAN LETTER THETH -10A7D..10A7E ; Old_South_Arabian # No [2] OLD SOUTH ARABIAN NUMBER ONE..OLD SOUTH ARABIAN NUMBER FIFTY -10A7F ; Old_South_Arabian # Po OLD SOUTH ARABIAN NUMERIC INDICATOR - -# Total code points: 32 - -# ================================================ - -10B40..10B55 ; Inscriptional_Parthian # Lo [22] INSCRIPTIONAL PARTHIAN LETTER ALEPH..INSCRIPTIONAL PARTHIAN LETTER TAW -10B58..10B5F ; Inscriptional_Parthian # No [8] INSCRIPTIONAL PARTHIAN NUMBER ONE..INSCRIPTIONAL PARTHIAN NUMBER ONE THOUSAND - -# Total code points: 30 - -# ================================================ - -10B60..10B72 ; Inscriptional_Pahlavi # Lo [19] INSCRIPTIONAL PAHLAVI LETTER ALEPH..INSCRIPTIONAL PAHLAVI LETTER TAW -10B78..10B7F ; Inscriptional_Pahlavi # No [8] INSCRIPTIONAL PAHLAVI NUMBER ONE..INSCRIPTIONAL PAHLAVI NUMBER ONE THOUSAND - -# Total code points: 27 - -# ================================================ - -10C00..10C48 ; Old_Turkic # Lo [73] OLD TURKIC LETTER ORKHON A..OLD TURKIC LETTER ORKHON BASH - -# Total code points: 73 - -# ================================================ - -11080..11081 ; Kaithi # Mn [2] KAITHI SIGN CANDRABINDU..KAITHI SIGN ANUSVARA -11082 ; Kaithi # Mc KAITHI SIGN VISARGA -11083..110AF ; Kaithi # Lo [45] KAITHI LETTER A..KAITHI LETTER HA -110B0..110B2 ; Kaithi # Mc [3] KAITHI VOWEL SIGN AA..KAITHI VOWEL SIGN II -110B3..110B6 ; Kaithi # Mn [4] KAITHI VOWEL SIGN U..KAITHI VOWEL SIGN AI -110B7..110B8 ; Kaithi # Mc [2] KAITHI VOWEL SIGN O..KAITHI VOWEL SIGN AU -110B9..110BA ; Kaithi # Mn [2] KAITHI SIGN VIRAMA..KAITHI SIGN NUKTA -110BB..110BC ; Kaithi # Po [2] KAITHI ABBREVIATION SIGN..KAITHI ENUMERATION SIGN -110BD ; Kaithi # Cf KAITHI NUMBER SIGN -110BE..110C1 ; Kaithi # Po [4] KAITHI SECTION MARK..KAITHI DOUBLE DANDA - -# Total code points: 66 - -# ================================================ - -1BC0..1BE5 ; Batak # Lo [38] BATAK LETTER A..BATAK LETTER U -1BE6 ; Batak # Mn BATAK SIGN TOMPI -1BE7 ; Batak # Mc BATAK VOWEL SIGN E -1BE8..1BE9 ; Batak # Mn [2] BATAK VOWEL SIGN PAKPAK E..BATAK VOWEL SIGN EE -1BEA..1BEC ; Batak # Mc [3] BATAK VOWEL SIGN I..BATAK VOWEL SIGN O -1BED ; Batak # Mn BATAK VOWEL SIGN KARO O -1BEE ; Batak # Mc BATAK VOWEL SIGN U -1BEF..1BF1 ; Batak # Mn [3] BATAK VOWEL SIGN U FOR SIMALUNGUN SA..BATAK CONSONANT SIGN H -1BF2..1BF3 ; Batak # Mc [2] BATAK PANGOLAT..BATAK PANONGONAN -1BFC..1BFF ; Batak # Po [4] BATAK SYMBOL BINDU NA METEK..BATAK SYMBOL BINDU PANGOLAT - -# Total code points: 56 - -# ================================================ - -11000 ; Brahmi # Mc BRAHMI SIGN CANDRABINDU -11001 ; Brahmi # Mn BRAHMI SIGN ANUSVARA -11002 ; Brahmi # Mc BRAHMI SIGN VISARGA -11003..11037 ; Brahmi # Lo [53] BRAHMI SIGN JIHVAMULIYA..BRAHMI LETTER OLD TAMIL NNNA -11038..11046 ; Brahmi # Mn [15] BRAHMI VOWEL SIGN AA..BRAHMI VIRAMA -11047..1104D ; Brahmi # Po [7] BRAHMI DANDA..BRAHMI PUNCTUATION LOTUS -11052..11065 ; Brahmi # No [20] BRAHMI NUMBER ONE..BRAHMI NUMBER ONE THOUSAND -11066..1106F ; Brahmi # Nd [10] BRAHMI DIGIT ZERO..BRAHMI DIGIT NINE -1107F ; Brahmi # Mn BRAHMI NUMBER JOINER - -# Total code points: 109 - -# ================================================ - -0840..0858 ; Mandaic # Lo [25] MANDAIC LETTER HALQA..MANDAIC LETTER AIN -0859..085B ; Mandaic # Mn [3] MANDAIC AFFRICATION MARK..MANDAIC GEMINATION MARK -085E ; Mandaic # Po MANDAIC PUNCTUATION - -# Total code points: 29 - -# ================================================ - -11100..11102 ; Chakma # Mn [3] CHAKMA SIGN CANDRABINDU..CHAKMA SIGN VISARGA -11103..11126 ; Chakma # Lo [36] CHAKMA LETTER AA..CHAKMA LETTER HAA -11127..1112B ; Chakma # Mn [5] CHAKMA VOWEL SIGN A..CHAKMA VOWEL SIGN UU -1112C ; Chakma # Mc CHAKMA VOWEL SIGN E -1112D..11134 ; Chakma # Mn [8] CHAKMA VOWEL SIGN AI..CHAKMA MAAYYAA -11136..1113F ; Chakma # Nd [10] CHAKMA DIGIT ZERO..CHAKMA DIGIT NINE -11140..11143 ; Chakma # Po [4] CHAKMA SECTION MARK..CHAKMA QUESTION MARK - -# Total code points: 67 - -# ================================================ - -109A0..109B7 ; Meroitic_Cursive # Lo [24] MEROITIC CURSIVE LETTER A..MEROITIC CURSIVE LETTER DA -109BC..109BD ; Meroitic_Cursive # No [2] MEROITIC CURSIVE FRACTION ELEVEN TWELFTHS..MEROITIC CURSIVE FRACTION ONE HALF -109BE..109BF ; Meroitic_Cursive # Lo [2] MEROITIC CURSIVE LOGOGRAM RMT..MEROITIC CURSIVE LOGOGRAM IMN -109C0..109CF ; Meroitic_Cursive # No [16] MEROITIC CURSIVE NUMBER ONE..MEROITIC CURSIVE NUMBER SEVENTY -109D2..109FF ; Meroitic_Cursive # No [46] MEROITIC CURSIVE NUMBER ONE HUNDRED..MEROITIC CURSIVE FRACTION TEN TWELFTHS - -# Total code points: 90 - -# ================================================ - -10980..1099F ; Meroitic_Hieroglyphs # Lo [32] MEROITIC HIEROGLYPHIC LETTER A..MEROITIC HIEROGLYPHIC SYMBOL VIDJ-2 - -# Total code points: 32 - -# ================================================ - -16F00..16F44 ; Miao # Lo [69] MIAO LETTER PA..MIAO LETTER HHA -16F50 ; Miao # Lo MIAO LETTER NASALIZATION -16F51..16F7E ; Miao # Mc [46] MIAO SIGN ASPIRATION..MIAO VOWEL SIGN NG -16F8F..16F92 ; Miao # Mn [4] MIAO TONE RIGHT..MIAO TONE BELOW -16F93..16F9F ; Miao # Lm [13] MIAO LETTER TONE-2..MIAO LETTER REFORMED TONE-8 - -# Total code points: 133 - -# ================================================ - -11180..11181 ; Sharada # Mn [2] SHARADA SIGN CANDRABINDU..SHARADA SIGN ANUSVARA -11182 ; Sharada # Mc SHARADA SIGN VISARGA -11183..111B2 ; Sharada # Lo [48] SHARADA LETTER A..SHARADA LETTER HA -111B3..111B5 ; Sharada # Mc [3] SHARADA VOWEL SIGN AA..SHARADA VOWEL SIGN II -111B6..111BE ; Sharada # Mn [9] SHARADA VOWEL SIGN U..SHARADA VOWEL SIGN O -111BF..111C0 ; Sharada # Mc [2] SHARADA VOWEL SIGN AU..SHARADA SIGN VIRAMA -111C1..111C4 ; Sharada # Lo [4] SHARADA SIGN AVAGRAHA..SHARADA OM -111C5..111C9 ; Sharada # Po [5] SHARADA DANDA..SHARADA SANDHI MARK -111CA..111CC ; Sharada # Mn [3] SHARADA SIGN NUKTA..SHARADA EXTRA SHORT VOWEL MARK -111CD ; Sharada # Po SHARADA SUTRA MARK -111D0..111D9 ; Sharada # Nd [10] SHARADA DIGIT ZERO..SHARADA DIGIT NINE -111DA ; Sharada # Lo SHARADA EKAM -111DB ; Sharada # Po SHARADA SIGN SIDDHAM -111DC ; Sharada # Lo SHARADA HEADSTROKE -111DD..111DF ; Sharada # Po [3] SHARADA CONTINUATION SIGN..SHARADA SECTION MARK-2 - -# Total code points: 94 - -# ================================================ - -110D0..110E8 ; Sora_Sompeng # Lo [25] SORA SOMPENG LETTER SAH..SORA SOMPENG LETTER MAE -110F0..110F9 ; Sora_Sompeng # Nd [10] SORA SOMPENG DIGIT ZERO..SORA SOMPENG DIGIT NINE - -# Total code points: 35 - -# ================================================ - -11680..116AA ; Takri # Lo [43] TAKRI LETTER A..TAKRI LETTER RRA -116AB ; Takri # Mn TAKRI SIGN ANUSVARA -116AC ; Takri # Mc TAKRI SIGN VISARGA -116AD ; Takri # Mn TAKRI VOWEL SIGN AA -116AE..116AF ; Takri # Mc [2] TAKRI VOWEL SIGN I..TAKRI VOWEL SIGN II -116B0..116B5 ; Takri # Mn [6] TAKRI VOWEL SIGN U..TAKRI VOWEL SIGN AU -116B6 ; Takri # Mc TAKRI SIGN VIRAMA -116B7 ; Takri # Mn TAKRI SIGN NUKTA -116C0..116C9 ; Takri # Nd [10] TAKRI DIGIT ZERO..TAKRI DIGIT NINE - -# Total code points: 66 - -# ================================================ - -10530..10563 ; Caucasian_Albanian # Lo [52] CAUCASIAN ALBANIAN LETTER ALT..CAUCASIAN ALBANIAN LETTER KIW -1056F ; Caucasian_Albanian # Po CAUCASIAN ALBANIAN CITATION MARK - -# Total code points: 53 - -# ================================================ - -16AD0..16AED ; Bassa_Vah # Lo [30] BASSA VAH LETTER ENNI..BASSA VAH LETTER I -16AF0..16AF4 ; Bassa_Vah # Mn [5] BASSA VAH COMBINING HIGH TONE..BASSA VAH COMBINING HIGH-LOW TONE -16AF5 ; Bassa_Vah # Po BASSA VAH FULL STOP - -# Total code points: 36 - -# ================================================ - -1BC00..1BC6A ; Duployan # Lo [107] DUPLOYAN LETTER H..DUPLOYAN LETTER VOCALIC M -1BC70..1BC7C ; Duployan # Lo [13] DUPLOYAN AFFIX LEFT HORIZONTAL SECANT..DUPLOYAN AFFIX ATTACHED TANGENT HOOK -1BC80..1BC88 ; Duployan # Lo [9] DUPLOYAN AFFIX HIGH ACUTE..DUPLOYAN AFFIX HIGH VERTICAL -1BC90..1BC99 ; Duployan # Lo [10] DUPLOYAN AFFIX LOW ACUTE..DUPLOYAN AFFIX LOW ARROW -1BC9C ; Duployan # So DUPLOYAN SIGN O WITH CROSS -1BC9D..1BC9E ; Duployan # Mn [2] DUPLOYAN THICK LETTER SELECTOR..DUPLOYAN DOUBLE MARK -1BC9F ; Duployan # Po DUPLOYAN PUNCTUATION CHINOOK FULL STOP - -# Total code points: 143 - -# ================================================ - -10500..10527 ; Elbasan # Lo [40] ELBASAN LETTER A..ELBASAN LETTER KHE - -# Total code points: 40 - -# ================================================ - -11300..11301 ; Grantha # Mn [2] GRANTHA SIGN COMBINING ANUSVARA ABOVE..GRANTHA SIGN CANDRABINDU -11302..11303 ; Grantha # Mc [2] GRANTHA SIGN ANUSVARA..GRANTHA SIGN VISARGA -11305..1130C ; Grantha # Lo [8] GRANTHA LETTER A..GRANTHA LETTER VOCALIC L -1130F..11310 ; Grantha # Lo [2] GRANTHA LETTER EE..GRANTHA LETTER AI -11313..11328 ; Grantha # Lo [22] GRANTHA LETTER OO..GRANTHA LETTER NA -1132A..11330 ; Grantha # Lo [7] GRANTHA LETTER PA..GRANTHA LETTER RA -11332..11333 ; Grantha # Lo [2] GRANTHA LETTER LA..GRANTHA LETTER LLA -11335..11339 ; Grantha # Lo [5] GRANTHA LETTER VA..GRANTHA LETTER HA -1133C ; Grantha # Mn GRANTHA SIGN NUKTA -1133D ; Grantha # Lo GRANTHA SIGN AVAGRAHA -1133E..1133F ; Grantha # Mc [2] GRANTHA VOWEL SIGN AA..GRANTHA VOWEL SIGN I -11340 ; Grantha # Mn GRANTHA VOWEL SIGN II -11341..11344 ; Grantha # Mc [4] GRANTHA VOWEL SIGN U..GRANTHA VOWEL SIGN VOCALIC RR -11347..11348 ; Grantha # Mc [2] GRANTHA VOWEL SIGN EE..GRANTHA VOWEL SIGN AI -1134B..1134D ; Grantha # Mc [3] GRANTHA VOWEL SIGN OO..GRANTHA SIGN VIRAMA -11350 ; Grantha # Lo GRANTHA OM -11357 ; Grantha # Mc GRANTHA AU LENGTH MARK -1135D..11361 ; Grantha # Lo [5] GRANTHA SIGN PLUTA..GRANTHA LETTER VOCALIC LL -11362..11363 ; Grantha # Mc [2] GRANTHA VOWEL SIGN VOCALIC L..GRANTHA VOWEL SIGN VOCALIC LL -11366..1136C ; Grantha # Mn [7] COMBINING GRANTHA DIGIT ZERO..COMBINING GRANTHA DIGIT SIX -11370..11374 ; Grantha # Mn [5] COMBINING GRANTHA LETTER A..COMBINING GRANTHA LETTER PA - -# Total code points: 85 - -# ================================================ - -16B00..16B2F ; Pahawh_Hmong # Lo [48] PAHAWH HMONG VOWEL KEEB..PAHAWH HMONG CONSONANT CAU -16B30..16B36 ; Pahawh_Hmong # Mn [7] PAHAWH HMONG MARK CIM TUB..PAHAWH HMONG MARK CIM TAUM -16B37..16B3B ; Pahawh_Hmong # Po [5] PAHAWH HMONG SIGN VOS THOM..PAHAWH HMONG SIGN VOS FEEM -16B3C..16B3F ; Pahawh_Hmong # So [4] PAHAWH HMONG SIGN XYEEM NTXIV..PAHAWH HMONG SIGN XYEEM FAIB -16B40..16B43 ; Pahawh_Hmong # Lm [4] PAHAWH HMONG SIGN VOS SEEV..PAHAWH HMONG SIGN IB YAM -16B44 ; Pahawh_Hmong # Po PAHAWH HMONG SIGN XAUS -16B45 ; Pahawh_Hmong # So PAHAWH HMONG SIGN CIM TSOV ROG -16B50..16B59 ; Pahawh_Hmong # Nd [10] PAHAWH HMONG DIGIT ZERO..PAHAWH HMONG DIGIT NINE -16B5B..16B61 ; Pahawh_Hmong # No [7] PAHAWH HMONG NUMBER TENS..PAHAWH HMONG NUMBER TRILLIONS -16B63..16B77 ; Pahawh_Hmong # Lo [21] PAHAWH HMONG SIGN VOS LUB..PAHAWH HMONG SIGN CIM NRES TOS -16B7D..16B8F ; Pahawh_Hmong # Lo [19] PAHAWH HMONG CLAN SIGN TSHEEJ..PAHAWH HMONG CLAN SIGN VWJ - -# Total code points: 127 - -# ================================================ - -11200..11211 ; Khojki # Lo [18] KHOJKI LETTER A..KHOJKI LETTER JJA -11213..1122B ; Khojki # Lo [25] KHOJKI LETTER NYA..KHOJKI LETTER LLA -1122C..1122E ; Khojki # Mc [3] KHOJKI VOWEL SIGN AA..KHOJKI VOWEL SIGN II -1122F..11231 ; Khojki # Mn [3] KHOJKI VOWEL SIGN U..KHOJKI VOWEL SIGN AI -11232..11233 ; Khojki # Mc [2] KHOJKI VOWEL SIGN O..KHOJKI VOWEL SIGN AU -11234 ; Khojki # Mn KHOJKI SIGN ANUSVARA -11235 ; Khojki # Mc KHOJKI SIGN VIRAMA -11236..11237 ; Khojki # Mn [2] KHOJKI SIGN NUKTA..KHOJKI SIGN SHADDA -11238..1123D ; Khojki # Po [6] KHOJKI DANDA..KHOJKI ABBREVIATION SIGN -1123E ; Khojki # Mn KHOJKI SIGN SUKUN - -# Total code points: 62 - -# ================================================ - -10600..10736 ; Linear_A # Lo [311] LINEAR A SIGN AB001..LINEAR A SIGN A664 -10740..10755 ; Linear_A # Lo [22] LINEAR A SIGN A701 A..LINEAR A SIGN A732 JE -10760..10767 ; Linear_A # Lo [8] LINEAR A SIGN A800..LINEAR A SIGN A807 - -# Total code points: 341 - -# ================================================ - -11150..11172 ; Mahajani # Lo [35] MAHAJANI LETTER A..MAHAJANI LETTER RRA -11173 ; Mahajani # Mn MAHAJANI SIGN NUKTA -11174..11175 ; Mahajani # Po [2] MAHAJANI ABBREVIATION SIGN..MAHAJANI SECTION MARK -11176 ; Mahajani # Lo MAHAJANI LIGATURE SHRI - -# Total code points: 39 - -# ================================================ - -10AC0..10AC7 ; Manichaean # Lo [8] MANICHAEAN LETTER ALEPH..MANICHAEAN LETTER WAW -10AC8 ; Manichaean # So MANICHAEAN SIGN UD -10AC9..10AE4 ; Manichaean # Lo [28] MANICHAEAN LETTER ZAYIN..MANICHAEAN LETTER TAW -10AE5..10AE6 ; Manichaean # Mn [2] MANICHAEAN ABBREVIATION MARK ABOVE..MANICHAEAN ABBREVIATION MARK BELOW -10AEB..10AEF ; Manichaean # No [5] MANICHAEAN NUMBER ONE..MANICHAEAN NUMBER ONE HUNDRED -10AF0..10AF6 ; Manichaean # Po [7] MANICHAEAN PUNCTUATION STAR..MANICHAEAN PUNCTUATION LINE FILLER - -# Total code points: 51 - -# ================================================ - -1E800..1E8C4 ; Mende_Kikakui # Lo [197] MENDE KIKAKUI SYLLABLE M001 KI..MENDE KIKAKUI SYLLABLE M060 NYON -1E8C7..1E8CF ; Mende_Kikakui # No [9] MENDE KIKAKUI DIGIT ONE..MENDE KIKAKUI DIGIT NINE -1E8D0..1E8D6 ; Mende_Kikakui # Mn [7] MENDE KIKAKUI COMBINING NUMBER TEENS..MENDE KIKAKUI COMBINING NUMBER MILLIONS - -# Total code points: 213 - -# ================================================ - -11600..1162F ; Modi # Lo [48] MODI LETTER A..MODI LETTER LLA -11630..11632 ; Modi # Mc [3] MODI VOWEL SIGN AA..MODI VOWEL SIGN II -11633..1163A ; Modi # Mn [8] MODI VOWEL SIGN U..MODI VOWEL SIGN AI -1163B..1163C ; Modi # Mc [2] MODI VOWEL SIGN O..MODI VOWEL SIGN AU -1163D ; Modi # Mn MODI SIGN ANUSVARA -1163E ; Modi # Mc MODI SIGN VISARGA -1163F..11640 ; Modi # Mn [2] MODI SIGN VIRAMA..MODI SIGN ARDHACANDRA -11641..11643 ; Modi # Po [3] MODI DANDA..MODI ABBREVIATION SIGN -11644 ; Modi # Lo MODI SIGN HUVA -11650..11659 ; Modi # Nd [10] MODI DIGIT ZERO..MODI DIGIT NINE - -# Total code points: 79 - -# ================================================ - -16A40..16A5E ; Mro # Lo [31] MRO LETTER TA..MRO LETTER TEK -16A60..16A69 ; Mro # Nd [10] MRO DIGIT ZERO..MRO DIGIT NINE -16A6E..16A6F ; Mro # Po [2] MRO DANDA..MRO DOUBLE DANDA - -# Total code points: 43 - -# ================================================ - -10A80..10A9C ; Old_North_Arabian # Lo [29] OLD NORTH ARABIAN LETTER HEH..OLD NORTH ARABIAN LETTER ZAH -10A9D..10A9F ; Old_North_Arabian # No [3] OLD NORTH ARABIAN NUMBER ONE..OLD NORTH ARABIAN NUMBER TWENTY - -# Total code points: 32 - -# ================================================ - -10880..1089E ; Nabataean # Lo [31] NABATAEAN LETTER FINAL ALEPH..NABATAEAN LETTER TAW -108A7..108AF ; Nabataean # No [9] NABATAEAN NUMBER ONE..NABATAEAN NUMBER ONE HUNDRED - -# Total code points: 40 - -# ================================================ - -10860..10876 ; Palmyrene # Lo [23] PALMYRENE LETTER ALEPH..PALMYRENE LETTER TAW -10877..10878 ; Palmyrene # So [2] PALMYRENE LEFT-POINTING FLEURON..PALMYRENE RIGHT-POINTING FLEURON -10879..1087F ; Palmyrene # No [7] PALMYRENE NUMBER ONE..PALMYRENE NUMBER TWENTY - -# Total code points: 32 - -# ================================================ - -11AC0..11AF8 ; Pau_Cin_Hau # Lo [57] PAU CIN HAU LETTER PA..PAU CIN HAU GLOTTAL STOP FINAL - -# Total code points: 57 - -# ================================================ - -10350..10375 ; Old_Permic # Lo [38] OLD PERMIC LETTER AN..OLD PERMIC LETTER IA -10376..1037A ; Old_Permic # Mn [5] COMBINING OLD PERMIC LETTER AN..COMBINING OLD PERMIC LETTER SII - -# Total code points: 43 - -# ================================================ - -10B80..10B91 ; Psalter_Pahlavi # Lo [18] PSALTER PAHLAVI LETTER ALEPH..PSALTER PAHLAVI LETTER TAW -10B99..10B9C ; Psalter_Pahlavi # Po [4] PSALTER PAHLAVI SECTION MARK..PSALTER PAHLAVI FOUR DOTS WITH DOT -10BA9..10BAF ; Psalter_Pahlavi # No [7] PSALTER PAHLAVI NUMBER ONE..PSALTER PAHLAVI NUMBER ONE HUNDRED - -# Total code points: 29 - -# ================================================ - -11580..115AE ; Siddham # Lo [47] SIDDHAM LETTER A..SIDDHAM LETTER HA -115AF..115B1 ; Siddham # Mc [3] SIDDHAM VOWEL SIGN AA..SIDDHAM VOWEL SIGN II -115B2..115B5 ; Siddham # Mn [4] SIDDHAM VOWEL SIGN U..SIDDHAM VOWEL SIGN VOCALIC RR -115B8..115BB ; Siddham # Mc [4] SIDDHAM VOWEL SIGN E..SIDDHAM VOWEL SIGN AU -115BC..115BD ; Siddham # Mn [2] SIDDHAM SIGN CANDRABINDU..SIDDHAM SIGN ANUSVARA -115BE ; Siddham # Mc SIDDHAM SIGN VISARGA -115BF..115C0 ; Siddham # Mn [2] SIDDHAM SIGN VIRAMA..SIDDHAM SIGN NUKTA -115C1..115D7 ; Siddham # Po [23] SIDDHAM SIGN SIDDHAM..SIDDHAM SECTION MARK WITH CIRCLES AND FOUR ENCLOSURES -115D8..115DB ; Siddham # Lo [4] SIDDHAM LETTER THREE-CIRCLE ALTERNATE I..SIDDHAM LETTER ALTERNATE U -115DC..115DD ; Siddham # Mn [2] SIDDHAM VOWEL SIGN ALTERNATE U..SIDDHAM VOWEL SIGN ALTERNATE UU - -# Total code points: 92 - -# ================================================ - -112B0..112DE ; Khudawadi # Lo [47] KHUDAWADI LETTER A..KHUDAWADI LETTER HA -112DF ; Khudawadi # Mn KHUDAWADI SIGN ANUSVARA -112E0..112E2 ; Khudawadi # Mc [3] KHUDAWADI VOWEL SIGN AA..KHUDAWADI VOWEL SIGN II -112E3..112EA ; Khudawadi # Mn [8] KHUDAWADI VOWEL SIGN U..KHUDAWADI SIGN VIRAMA -112F0..112F9 ; Khudawadi # Nd [10] KHUDAWADI DIGIT ZERO..KHUDAWADI DIGIT NINE - -# Total code points: 69 - -# ================================================ - -11480..114AF ; Tirhuta # Lo [48] TIRHUTA ANJI..TIRHUTA LETTER HA -114B0..114B2 ; Tirhuta # Mc [3] TIRHUTA VOWEL SIGN AA..TIRHUTA VOWEL SIGN II -114B3..114B8 ; Tirhuta # Mn [6] TIRHUTA VOWEL SIGN U..TIRHUTA VOWEL SIGN VOCALIC LL -114B9 ; Tirhuta # Mc TIRHUTA VOWEL SIGN E -114BA ; Tirhuta # Mn TIRHUTA VOWEL SIGN SHORT E -114BB..114BE ; Tirhuta # Mc [4] TIRHUTA VOWEL SIGN AI..TIRHUTA VOWEL SIGN AU -114BF..114C0 ; Tirhuta # Mn [2] TIRHUTA SIGN CANDRABINDU..TIRHUTA SIGN ANUSVARA -114C1 ; Tirhuta # Mc TIRHUTA SIGN VISARGA -114C2..114C3 ; Tirhuta # Mn [2] TIRHUTA SIGN VIRAMA..TIRHUTA SIGN NUKTA -114C4..114C5 ; Tirhuta # Lo [2] TIRHUTA SIGN AVAGRAHA..TIRHUTA GVANG -114C6 ; Tirhuta # Po TIRHUTA ABBREVIATION SIGN -114C7 ; Tirhuta # Lo TIRHUTA OM -114D0..114D9 ; Tirhuta # Nd [10] TIRHUTA DIGIT ZERO..TIRHUTA DIGIT NINE - -# Total code points: 82 - -# ================================================ - -118A0..118DF ; Warang_Citi # L& [64] WARANG CITI CAPITAL LETTER NGAA..WARANG CITI SMALL LETTER VIYO -118E0..118E9 ; Warang_Citi # Nd [10] WARANG CITI DIGIT ZERO..WARANG CITI DIGIT NINE -118EA..118F2 ; Warang_Citi # No [9] WARANG CITI NUMBER TEN..WARANG CITI NUMBER NINETY -118FF ; Warang_Citi # Lo WARANG CITI OM - -# Total code points: 84 - -# ================================================ - -11700..11719 ; Ahom # Lo [26] AHOM LETTER KA..AHOM LETTER JHA -1171D..1171F ; Ahom # Mn [3] AHOM CONSONANT SIGN MEDIAL LA..AHOM CONSONANT SIGN MEDIAL LIGATING RA -11720..11721 ; Ahom # Mc [2] AHOM VOWEL SIGN A..AHOM VOWEL SIGN AA -11722..11725 ; Ahom # Mn [4] AHOM VOWEL SIGN I..AHOM VOWEL SIGN UU -11726 ; Ahom # Mc AHOM VOWEL SIGN E -11727..1172B ; Ahom # Mn [5] AHOM VOWEL SIGN AW..AHOM SIGN KILLER -11730..11739 ; Ahom # Nd [10] AHOM DIGIT ZERO..AHOM DIGIT NINE -1173A..1173B ; Ahom # No [2] AHOM NUMBER TEN..AHOM NUMBER TWENTY -1173C..1173E ; Ahom # Po [3] AHOM SIGN SMALL SECTION..AHOM SIGN RULAI -1173F ; Ahom # So AHOM SYMBOL VI - -# Total code points: 57 - -# ================================================ - -14400..14646 ; Anatolian_Hieroglyphs # Lo [583] ANATOLIAN HIEROGLYPH A001..ANATOLIAN HIEROGLYPH A530 - -# Total code points: 583 - -# ================================================ - -108E0..108F2 ; Hatran # Lo [19] HATRAN LETTER ALEPH..HATRAN LETTER QOPH -108F4..108F5 ; Hatran # Lo [2] HATRAN LETTER SHIN..HATRAN LETTER TAW -108FB..108FF ; Hatran # No [5] HATRAN NUMBER ONE..HATRAN NUMBER ONE HUNDRED - -# Total code points: 26 - -# ================================================ - -11280..11286 ; Multani # Lo [7] MULTANI LETTER A..MULTANI LETTER GA -11288 ; Multani # Lo MULTANI LETTER GHA -1128A..1128D ; Multani # Lo [4] MULTANI LETTER CA..MULTANI LETTER JJA -1128F..1129D ; Multani # Lo [15] MULTANI LETTER NYA..MULTANI LETTER BA -1129F..112A8 ; Multani # Lo [10] MULTANI LETTER BHA..MULTANI LETTER RHA -112A9 ; Multani # Po MULTANI SECTION MARK - -# Total code points: 38 - -# ================================================ - -10C80..10CB2 ; Old_Hungarian # L& [51] OLD HUNGARIAN CAPITAL LETTER A..OLD HUNGARIAN CAPITAL LETTER US -10CC0..10CF2 ; Old_Hungarian # L& [51] OLD HUNGARIAN SMALL LETTER A..OLD HUNGARIAN SMALL LETTER US -10CFA..10CFF ; Old_Hungarian # No [6] OLD HUNGARIAN NUMBER ONE..OLD HUNGARIAN NUMBER ONE THOUSAND - -# Total code points: 108 - -# ================================================ - -1D800..1D9FF ; SignWriting # So [512] SIGNWRITING HAND-FIST INDEX..SIGNWRITING HEAD -1DA00..1DA36 ; SignWriting # Mn [55] SIGNWRITING HEAD RIM..SIGNWRITING AIR SUCKING IN -1DA37..1DA3A ; SignWriting # So [4] SIGNWRITING AIR BLOW SMALL ROTATIONS..SIGNWRITING BREATH EXHALE -1DA3B..1DA6C ; SignWriting # Mn [50] SIGNWRITING MOUTH CLOSED NEUTRAL..SIGNWRITING EXCITEMENT -1DA6D..1DA74 ; SignWriting # So [8] SIGNWRITING SHOULDER HIP SPINE..SIGNWRITING TORSO-FLOORPLANE TWISTING -1DA75 ; SignWriting # Mn SIGNWRITING UPPER BODY TILTING FROM HIP JOINTS -1DA76..1DA83 ; SignWriting # So [14] SIGNWRITING LIMB COMBINATION..SIGNWRITING LOCATION DEPTH -1DA84 ; SignWriting # Mn SIGNWRITING LOCATION HEAD NECK -1DA85..1DA86 ; SignWriting # So [2] SIGNWRITING LOCATION TORSO..SIGNWRITING LOCATION LIMBS DIGITS -1DA87..1DA8B ; SignWriting # Po [5] SIGNWRITING COMMA..SIGNWRITING PARENTHESIS -1DA9B..1DA9F ; SignWriting # Mn [5] SIGNWRITING FILL MODIFIER-2..SIGNWRITING FILL MODIFIER-6 -1DAA1..1DAAF ; SignWriting # Mn [15] SIGNWRITING ROTATION MODIFIER-2..SIGNWRITING ROTATION MODIFIER-16 - -# Total code points: 672 - -# ================================================ - -1E900..1E943 ; Adlam # L& [68] ADLAM CAPITAL LETTER ALIF..ADLAM SMALL LETTER SHA -1E944..1E94A ; Adlam # Mn [7] ADLAM ALIF LENGTHENER..ADLAM NUKTA -1E950..1E959 ; Adlam # Nd [10] ADLAM DIGIT ZERO..ADLAM DIGIT NINE -1E95E..1E95F ; Adlam # Po [2] ADLAM INITIAL EXCLAMATION MARK..ADLAM INITIAL QUESTION MARK - -# Total code points: 87 - -# ================================================ - -11C00..11C08 ; Bhaiksuki # Lo [9] BHAIKSUKI LETTER A..BHAIKSUKI LETTER VOCALIC L -11C0A..11C2E ; Bhaiksuki # Lo [37] BHAIKSUKI LETTER E..BHAIKSUKI LETTER HA -11C2F ; Bhaiksuki # Mc BHAIKSUKI VOWEL SIGN AA -11C30..11C36 ; Bhaiksuki # Mn [7] BHAIKSUKI VOWEL SIGN I..BHAIKSUKI VOWEL SIGN VOCALIC L -11C38..11C3D ; Bhaiksuki # Mn [6] BHAIKSUKI VOWEL SIGN E..BHAIKSUKI SIGN ANUSVARA -11C3E ; Bhaiksuki # Mc BHAIKSUKI SIGN VISARGA -11C3F ; Bhaiksuki # Mn BHAIKSUKI SIGN VIRAMA -11C40 ; Bhaiksuki # Lo BHAIKSUKI SIGN AVAGRAHA -11C41..11C45 ; Bhaiksuki # Po [5] BHAIKSUKI DANDA..BHAIKSUKI GAP FILLER-2 -11C50..11C59 ; Bhaiksuki # Nd [10] BHAIKSUKI DIGIT ZERO..BHAIKSUKI DIGIT NINE -11C5A..11C6C ; Bhaiksuki # No [19] BHAIKSUKI NUMBER ONE..BHAIKSUKI HUNDREDS UNIT MARK - -# Total code points: 97 - -# ================================================ - -11C70..11C71 ; Marchen # Po [2] MARCHEN HEAD MARK..MARCHEN MARK SHAD -11C72..11C8F ; Marchen # Lo [30] MARCHEN LETTER KA..MARCHEN LETTER A -11C92..11CA7 ; Marchen # Mn [22] MARCHEN SUBJOINED LETTER KA..MARCHEN SUBJOINED LETTER ZA -11CA9 ; Marchen # Mc MARCHEN SUBJOINED LETTER YA -11CAA..11CB0 ; Marchen # Mn [7] MARCHEN SUBJOINED LETTER RA..MARCHEN VOWEL SIGN AA -11CB1 ; Marchen # Mc MARCHEN VOWEL SIGN I -11CB2..11CB3 ; Marchen # Mn [2] MARCHEN VOWEL SIGN U..MARCHEN VOWEL SIGN E -11CB4 ; Marchen # Mc MARCHEN VOWEL SIGN O -11CB5..11CB6 ; Marchen # Mn [2] MARCHEN SIGN ANUSVARA..MARCHEN SIGN CANDRABINDU - -# Total code points: 68 - -# ================================================ - -11400..11434 ; Newa # Lo [53] NEWA LETTER A..NEWA LETTER HA -11435..11437 ; Newa # Mc [3] NEWA VOWEL SIGN AA..NEWA VOWEL SIGN II -11438..1143F ; Newa # Mn [8] NEWA VOWEL SIGN U..NEWA VOWEL SIGN AI -11440..11441 ; Newa # Mc [2] NEWA VOWEL SIGN O..NEWA VOWEL SIGN AU -11442..11444 ; Newa # Mn [3] NEWA SIGN VIRAMA..NEWA SIGN ANUSVARA -11445 ; Newa # Mc NEWA SIGN VISARGA -11446 ; Newa # Mn NEWA SIGN NUKTA -11447..1144A ; Newa # Lo [4] NEWA SIGN AVAGRAHA..NEWA SIDDHI -1144B..1144F ; Newa # Po [5] NEWA DANDA..NEWA ABBREVIATION SIGN -11450..11459 ; Newa # Nd [10] NEWA DIGIT ZERO..NEWA DIGIT NINE -1145B ; Newa # Po NEWA PLACEHOLDER MARK -1145D ; Newa # Po NEWA INSERTION SIGN - -# Total code points: 92 - -# ================================================ - -104B0..104D3 ; Osage # L& [36] OSAGE CAPITAL LETTER A..OSAGE CAPITAL LETTER ZHA -104D8..104FB ; Osage # L& [36] OSAGE SMALL LETTER A..OSAGE SMALL LETTER ZHA - -# Total code points: 72 - -# ================================================ - -16FE0 ; Tangut # Lm TANGUT ITERATION MARK -17000..187EC ; Tangut # Lo [6125] TANGUT IDEOGRAPH-17000..TANGUT IDEOGRAPH-187EC -18800..18AF2 ; Tangut # Lo [755] TANGUT COMPONENT-001..TANGUT COMPONENT-755 - -# Total code points: 6881 - -# ================================================ - -11D00..11D06 ; Masaram_Gondi # Lo [7] MASARAM GONDI LETTER A..MASARAM GONDI LETTER E -11D08..11D09 ; Masaram_Gondi # Lo [2] MASARAM GONDI LETTER AI..MASARAM GONDI LETTER O -11D0B..11D30 ; Masaram_Gondi # Lo [38] MASARAM GONDI LETTER AU..MASARAM GONDI LETTER TRA -11D31..11D36 ; Masaram_Gondi # Mn [6] MASARAM GONDI VOWEL SIGN AA..MASARAM GONDI VOWEL SIGN VOCALIC R -11D3A ; Masaram_Gondi # Mn MASARAM GONDI VOWEL SIGN E -11D3C..11D3D ; Masaram_Gondi # Mn [2] MASARAM GONDI VOWEL SIGN AI..MASARAM GONDI VOWEL SIGN O -11D3F..11D45 ; Masaram_Gondi # Mn [7] MASARAM GONDI VOWEL SIGN AU..MASARAM GONDI VIRAMA -11D46 ; Masaram_Gondi # Lo MASARAM GONDI REPHA -11D47 ; Masaram_Gondi # Mn MASARAM GONDI RA-KARA -11D50..11D59 ; Masaram_Gondi # Nd [10] MASARAM GONDI DIGIT ZERO..MASARAM GONDI DIGIT NINE - -# Total code points: 75 - -# ================================================ - -16FE1 ; Nushu # Lm NUSHU ITERATION MARK -1B170..1B2FB ; Nushu # Lo [396] NUSHU CHARACTER-1B170..NUSHU CHARACTER-1B2FB - -# Total code points: 397 - -# ================================================ - -11A50 ; Soyombo # Lo SOYOMBO LETTER A -11A51..11A56 ; Soyombo # Mn [6] SOYOMBO VOWEL SIGN I..SOYOMBO VOWEL SIGN OE -11A57..11A58 ; Soyombo # Mc [2] SOYOMBO VOWEL SIGN AI..SOYOMBO VOWEL SIGN AU -11A59..11A5B ; Soyombo # Mn [3] SOYOMBO VOWEL SIGN VOCALIC R..SOYOMBO VOWEL LENGTH MARK -11A5C..11A83 ; Soyombo # Lo [40] SOYOMBO LETTER KA..SOYOMBO LETTER KSSA -11A86..11A89 ; Soyombo # Lo [4] SOYOMBO CLUSTER-INITIAL LETTER RA..SOYOMBO CLUSTER-INITIAL LETTER SA -11A8A..11A96 ; Soyombo # Mn [13] SOYOMBO FINAL CONSONANT SIGN G..SOYOMBO SIGN ANUSVARA -11A97 ; Soyombo # Mc SOYOMBO SIGN VISARGA -11A98..11A99 ; Soyombo # Mn [2] SOYOMBO GEMINATION MARK..SOYOMBO SUBJOINER -11A9A..11A9C ; Soyombo # Po [3] SOYOMBO MARK TSHEG..SOYOMBO MARK DOUBLE SHAD -11A9E..11AA2 ; Soyombo # Po [5] SOYOMBO HEAD MARK WITH MOON AND SUN AND TRIPLE FLAME..SOYOMBO TERMINAL MARK-2 - -# Total code points: 80 - -# ================================================ - -11A00 ; Zanabazar_Square # Lo ZANABAZAR SQUARE LETTER A -11A01..11A06 ; Zanabazar_Square # Mn [6] ZANABAZAR SQUARE VOWEL SIGN I..ZANABAZAR SQUARE VOWEL SIGN O -11A07..11A08 ; Zanabazar_Square # Mc [2] ZANABAZAR SQUARE VOWEL SIGN AI..ZANABAZAR SQUARE VOWEL SIGN AU -11A09..11A0A ; Zanabazar_Square # Mn [2] ZANABAZAR SQUARE VOWEL SIGN REVERSED I..ZANABAZAR SQUARE VOWEL LENGTH MARK -11A0B..11A32 ; Zanabazar_Square # Lo [40] ZANABAZAR SQUARE LETTER KA..ZANABAZAR SQUARE LETTER KSSA -11A33..11A38 ; Zanabazar_Square # Mn [6] ZANABAZAR SQUARE FINAL CONSONANT MARK..ZANABAZAR SQUARE SIGN ANUSVARA -11A39 ; Zanabazar_Square # Mc ZANABAZAR SQUARE SIGN VISARGA -11A3A ; Zanabazar_Square # Lo ZANABAZAR SQUARE CLUSTER-INITIAL LETTER RA -11A3B..11A3E ; Zanabazar_Square # Mn [4] ZANABAZAR SQUARE CLUSTER-FINAL LETTER YA..ZANABAZAR SQUARE CLUSTER-FINAL LETTER VA -11A3F..11A46 ; Zanabazar_Square # Po [8] ZANABAZAR SQUARE INITIAL HEAD MARK..ZANABAZAR SQUARE CLOSING DOUBLE-LINED HEAD MARK -11A47 ; Zanabazar_Square # Mn ZANABAZAR SQUARE SUBJOINER - -# Total code points: 72 - -# EOF +# Scripts-15.1.0.txt +# Date: 2023-07-28, 16:01:07 GMT +# © 2023 Unicode®, Inc. +# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries. +# For terms of use, see https://www.unicode.org/terms_of_use.html +# +# Unicode Character Database +# For documentation, see https://www.unicode.org/reports/tr44/ +# For more information, see: +# UAX #24, Unicode Script Property: https://www.unicode.org/reports/tr24/ +# Especially the sections: +# https://www.unicode.org/reports/tr24/#Assignment_Script_Values +# https://www.unicode.org/reports/tr24/#Assignment_ScriptX_Values +# + +# ================================================ + +# Property: Script + +# All code points not explicitly listed for Script +# have the value Unknown (Zzzz). + +# @missing: 0000..10FFFF; Unknown + +# ================================================ + +0000..001F ; Common # Cc [32] .. +0020 ; Common # Zs SPACE +0021..0023 ; Common # Po [3] EXCLAMATION MARK..NUMBER SIGN +0024 ; Common # Sc DOLLAR SIGN +0025..0027 ; Common # Po [3] PERCENT SIGN..APOSTROPHE +0028 ; Common # Ps LEFT PARENTHESIS +0029 ; Common # Pe RIGHT PARENTHESIS +002A ; Common # Po ASTERISK +002B ; Common # Sm PLUS SIGN +002C ; Common # Po COMMA +002D ; Common # Pd HYPHEN-MINUS +002E..002F ; Common # Po [2] FULL STOP..SOLIDUS +0030..0039 ; Common # Nd [10] DIGIT ZERO..DIGIT NINE +003A..003B ; Common # Po [2] COLON..SEMICOLON +003C..003E ; Common # Sm [3] LESS-THAN SIGN..GREATER-THAN SIGN +003F..0040 ; Common # Po [2] QUESTION MARK..COMMERCIAL AT +005B ; Common # Ps LEFT SQUARE BRACKET +005C ; Common # Po REVERSE SOLIDUS +005D ; Common # Pe RIGHT SQUARE BRACKET +005E ; Common # Sk CIRCUMFLEX ACCENT +005F ; Common # Pc LOW LINE +0060 ; Common # Sk GRAVE ACCENT +007B ; Common # Ps LEFT CURLY BRACKET +007C ; Common # Sm VERTICAL LINE +007D ; Common # Pe RIGHT CURLY BRACKET +007E ; Common # Sm TILDE +007F..009F ; Common # Cc [33] .. +00A0 ; Common # Zs NO-BREAK SPACE +00A1 ; Common # Po INVERTED EXCLAMATION MARK +00A2..00A5 ; Common # Sc [4] CENT SIGN..YEN SIGN +00A6 ; Common # So BROKEN BAR +00A7 ; Common # Po SECTION SIGN +00A8 ; Common # Sk DIAERESIS +00A9 ; Common # So COPYRIGHT SIGN +00AB ; Common # Pi LEFT-POINTING DOUBLE ANGLE QUOTATION MARK +00AC ; Common # Sm NOT SIGN +00AD ; Common # Cf SOFT HYPHEN +00AE ; Common # So REGISTERED SIGN +00AF ; Common # Sk MACRON +00B0 ; Common # So DEGREE SIGN +00B1 ; Common # Sm PLUS-MINUS SIGN +00B2..00B3 ; Common # No [2] SUPERSCRIPT TWO..SUPERSCRIPT THREE +00B4 ; Common # Sk ACUTE ACCENT +00B5 ; Common # L& MICRO SIGN +00B6..00B7 ; Common # Po [2] PILCROW SIGN..MIDDLE DOT +00B8 ; Common # Sk CEDILLA +00B9 ; Common # No SUPERSCRIPT ONE +00BB ; Common # Pf RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK +00BC..00BE ; Common # No [3] VULGAR FRACTION ONE QUARTER..VULGAR FRACTION THREE QUARTERS +00BF ; Common # Po INVERTED QUESTION MARK +00D7 ; Common # Sm MULTIPLICATION SIGN +00F7 ; Common # Sm DIVISION SIGN +02B9..02C1 ; Common # Lm [9] MODIFIER LETTER PRIME..MODIFIER LETTER REVERSED GLOTTAL STOP +02C2..02C5 ; Common # Sk [4] MODIFIER LETTER LEFT ARROWHEAD..MODIFIER LETTER DOWN ARROWHEAD +02C6..02D1 ; Common # Lm [12] MODIFIER LETTER CIRCUMFLEX ACCENT..MODIFIER LETTER HALF TRIANGULAR COLON +02D2..02DF ; Common # Sk [14] MODIFIER LETTER CENTRED RIGHT HALF RING..MODIFIER LETTER CROSS ACCENT +02E5..02E9 ; Common # Sk [5] MODIFIER LETTER EXTRA-HIGH TONE BAR..MODIFIER LETTER EXTRA-LOW TONE BAR +02EC ; Common # Lm MODIFIER LETTER VOICING +02ED ; Common # Sk MODIFIER LETTER UNASPIRATED +02EE ; Common # Lm MODIFIER LETTER DOUBLE APOSTROPHE +02EF..02FF ; Common # Sk [17] MODIFIER LETTER LOW DOWN ARROWHEAD..MODIFIER LETTER LOW LEFT ARROW +0374 ; Common # Lm GREEK NUMERAL SIGN +037E ; Common # Po GREEK QUESTION MARK +0385 ; Common # Sk GREEK DIALYTIKA TONOS +0387 ; Common # Po GREEK ANO TELEIA +0605 ; Common # Cf ARABIC NUMBER MARK ABOVE +060C ; Common # Po ARABIC COMMA +061B ; Common # Po ARABIC SEMICOLON +061F ; Common # Po ARABIC QUESTION MARK +0640 ; Common # Lm ARABIC TATWEEL +06DD ; Common # Cf ARABIC END OF AYAH +08E2 ; Common # Cf ARABIC DISPUTED END OF AYAH +0964..0965 ; Common # Po [2] DEVANAGARI DANDA..DEVANAGARI DOUBLE DANDA +0E3F ; Common # Sc THAI CURRENCY SYMBOL BAHT +0FD5..0FD8 ; Common # So [4] RIGHT-FACING SVASTI SIGN..LEFT-FACING SVASTI SIGN WITH DOTS +10FB ; Common # Po GEORGIAN PARAGRAPH SEPARATOR +16EB..16ED ; Common # Po [3] RUNIC SINGLE PUNCTUATION..RUNIC CROSS PUNCTUATION +1735..1736 ; Common # Po [2] PHILIPPINE SINGLE PUNCTUATION..PHILIPPINE DOUBLE PUNCTUATION +1802..1803 ; Common # Po [2] MONGOLIAN COMMA..MONGOLIAN FULL STOP +1805 ; Common # Po MONGOLIAN FOUR DOTS +1CD3 ; Common # Po VEDIC SIGN NIHSHVASA +1CE1 ; Common # Mc VEDIC TONE ATHARVAVEDIC INDEPENDENT SVARITA +1CE9..1CEC ; Common # Lo [4] VEDIC SIGN ANUSVARA ANTARGOMUKHA..VEDIC SIGN ANUSVARA VAMAGOMUKHA WITH TAIL +1CEE..1CF3 ; Common # Lo [6] VEDIC SIGN HEXIFORM LONG ANUSVARA..VEDIC SIGN ROTATED ARDHAVISARGA +1CF5..1CF6 ; Common # Lo [2] VEDIC SIGN JIHVAMULIYA..VEDIC SIGN UPADHMANIYA +1CF7 ; Common # Mc VEDIC SIGN ATIKRAMA +1CFA ; Common # Lo VEDIC SIGN DOUBLE ANUSVARA ANTARGOMUKHA +2000..200A ; Common # Zs [11] EN QUAD..HAIR SPACE +200B ; Common # Cf ZERO WIDTH SPACE +200E..200F ; Common # Cf [2] LEFT-TO-RIGHT MARK..RIGHT-TO-LEFT MARK +2010..2015 ; Common # Pd [6] HYPHEN..HORIZONTAL BAR +2016..2017 ; Common # Po [2] DOUBLE VERTICAL LINE..DOUBLE LOW LINE +2018 ; Common # Pi LEFT SINGLE QUOTATION MARK +2019 ; Common # Pf RIGHT SINGLE QUOTATION MARK +201A ; Common # Ps SINGLE LOW-9 QUOTATION MARK +201B..201C ; Common # Pi [2] SINGLE HIGH-REVERSED-9 QUOTATION MARK..LEFT DOUBLE QUOTATION MARK +201D ; Common # Pf RIGHT DOUBLE QUOTATION MARK +201E ; Common # Ps DOUBLE LOW-9 QUOTATION MARK +201F ; Common # Pi DOUBLE HIGH-REVERSED-9 QUOTATION MARK +2020..2027 ; Common # Po [8] DAGGER..HYPHENATION POINT +2028 ; Common # Zl LINE SEPARATOR +2029 ; Common # Zp PARAGRAPH SEPARATOR +202A..202E ; Common # Cf [5] LEFT-TO-RIGHT EMBEDDING..RIGHT-TO-LEFT OVERRIDE +202F ; Common # Zs NARROW NO-BREAK SPACE +2030..2038 ; Common # Po [9] PER MILLE SIGN..CARET +2039 ; Common # Pi SINGLE LEFT-POINTING ANGLE QUOTATION MARK +203A ; Common # Pf SINGLE RIGHT-POINTING ANGLE QUOTATION MARK +203B..203E ; Common # Po [4] REFERENCE MARK..OVERLINE +203F..2040 ; Common # Pc [2] UNDERTIE..CHARACTER TIE +2041..2043 ; Common # Po [3] CARET INSERTION POINT..HYPHEN BULLET +2044 ; Common # Sm FRACTION SLASH +2045 ; Common # Ps LEFT SQUARE BRACKET WITH QUILL +2046 ; Common # Pe RIGHT SQUARE BRACKET WITH QUILL +2047..2051 ; Common # Po [11] DOUBLE QUESTION MARK..TWO ASTERISKS ALIGNED VERTICALLY +2052 ; Common # Sm COMMERCIAL MINUS SIGN +2053 ; Common # Po SWUNG DASH +2054 ; Common # Pc INVERTED UNDERTIE +2055..205E ; Common # Po [10] FLOWER PUNCTUATION MARK..VERTICAL FOUR DOTS +205F ; Common # Zs MEDIUM MATHEMATICAL SPACE +2060..2064 ; Common # Cf [5] WORD JOINER..INVISIBLE PLUS +2066..206F ; Common # Cf [10] LEFT-TO-RIGHT ISOLATE..NOMINAL DIGIT SHAPES +2070 ; Common # No SUPERSCRIPT ZERO +2074..2079 ; Common # No [6] SUPERSCRIPT FOUR..SUPERSCRIPT NINE +207A..207C ; Common # Sm [3] SUPERSCRIPT PLUS SIGN..SUPERSCRIPT EQUALS SIGN +207D ; Common # Ps SUPERSCRIPT LEFT PARENTHESIS +207E ; Common # Pe SUPERSCRIPT RIGHT PARENTHESIS +2080..2089 ; Common # No [10] SUBSCRIPT ZERO..SUBSCRIPT NINE +208A..208C ; Common # Sm [3] SUBSCRIPT PLUS SIGN..SUBSCRIPT EQUALS SIGN +208D ; Common # Ps SUBSCRIPT LEFT PARENTHESIS +208E ; Common # Pe SUBSCRIPT RIGHT PARENTHESIS +20A0..20C0 ; Common # Sc [33] EURO-CURRENCY SIGN..SOM SIGN +2100..2101 ; Common # So [2] ACCOUNT OF..ADDRESSED TO THE SUBJECT +2102 ; Common # L& DOUBLE-STRUCK CAPITAL C +2103..2106 ; Common # So [4] DEGREE CELSIUS..CADA UNA +2107 ; Common # L& EULER CONSTANT +2108..2109 ; Common # So [2] SCRUPLE..DEGREE FAHRENHEIT +210A..2113 ; Common # L& [10] SCRIPT SMALL G..SCRIPT SMALL L +2114 ; Common # So L B BAR SYMBOL +2115 ; Common # L& DOUBLE-STRUCK CAPITAL N +2116..2117 ; Common # So [2] NUMERO SIGN..SOUND RECORDING COPYRIGHT +2118 ; Common # Sm SCRIPT CAPITAL P +2119..211D ; Common # L& [5] DOUBLE-STRUCK CAPITAL P..DOUBLE-STRUCK CAPITAL R +211E..2123 ; Common # So [6] PRESCRIPTION TAKE..VERSICLE +2124 ; Common # L& DOUBLE-STRUCK CAPITAL Z +2125 ; Common # So OUNCE SIGN +2127 ; Common # So INVERTED OHM SIGN +2128 ; Common # L& BLACK-LETTER CAPITAL Z +2129 ; Common # So TURNED GREEK SMALL LETTER IOTA +212C..212D ; Common # L& [2] SCRIPT CAPITAL B..BLACK-LETTER CAPITAL C +212E ; Common # So ESTIMATED SYMBOL +212F..2131 ; Common # L& [3] SCRIPT SMALL E..SCRIPT CAPITAL F +2133..2134 ; Common # L& [2] SCRIPT CAPITAL M..SCRIPT SMALL O +2135..2138 ; Common # Lo [4] ALEF SYMBOL..DALET SYMBOL +2139 ; Common # L& INFORMATION SOURCE +213A..213B ; Common # So [2] ROTATED CAPITAL Q..FACSIMILE SIGN +213C..213F ; Common # L& [4] DOUBLE-STRUCK SMALL PI..DOUBLE-STRUCK CAPITAL PI +2140..2144 ; Common # Sm [5] DOUBLE-STRUCK N-ARY SUMMATION..TURNED SANS-SERIF CAPITAL Y +2145..2149 ; Common # L& [5] DOUBLE-STRUCK ITALIC CAPITAL D..DOUBLE-STRUCK ITALIC SMALL J +214A ; Common # So PROPERTY LINE +214B ; Common # Sm TURNED AMPERSAND +214C..214D ; Common # So [2] PER SIGN..AKTIESELSKAB +214F ; Common # So SYMBOL FOR SAMARITAN SOURCE +2150..215F ; Common # No [16] VULGAR FRACTION ONE SEVENTH..FRACTION NUMERATOR ONE +2189 ; Common # No VULGAR FRACTION ZERO THIRDS +218A..218B ; Common # So [2] TURNED DIGIT TWO..TURNED DIGIT THREE +2190..2194 ; Common # Sm [5] LEFTWARDS ARROW..LEFT RIGHT ARROW +2195..2199 ; Common # So [5] UP DOWN ARROW..SOUTH WEST ARROW +219A..219B ; Common # Sm [2] LEFTWARDS ARROW WITH STROKE..RIGHTWARDS ARROW WITH STROKE +219C..219F ; Common # So [4] LEFTWARDS WAVE ARROW..UPWARDS TWO HEADED ARROW +21A0 ; Common # Sm RIGHTWARDS TWO HEADED ARROW +21A1..21A2 ; Common # So [2] DOWNWARDS TWO HEADED ARROW..LEFTWARDS ARROW WITH TAIL +21A3 ; Common # Sm RIGHTWARDS ARROW WITH TAIL +21A4..21A5 ; Common # So [2] LEFTWARDS ARROW FROM BAR..UPWARDS ARROW FROM BAR +21A6 ; Common # Sm RIGHTWARDS ARROW FROM BAR +21A7..21AD ; Common # So [7] DOWNWARDS ARROW FROM BAR..LEFT RIGHT WAVE ARROW +21AE ; Common # Sm LEFT RIGHT ARROW WITH STROKE +21AF..21CD ; Common # So [31] DOWNWARDS ZIGZAG ARROW..LEFTWARDS DOUBLE ARROW WITH STROKE +21CE..21CF ; Common # Sm [2] LEFT RIGHT DOUBLE ARROW WITH STROKE..RIGHTWARDS DOUBLE ARROW WITH STROKE +21D0..21D1 ; Common # So [2] LEFTWARDS DOUBLE ARROW..UPWARDS DOUBLE ARROW +21D2 ; Common # Sm RIGHTWARDS DOUBLE ARROW +21D3 ; Common # So DOWNWARDS DOUBLE ARROW +21D4 ; Common # Sm LEFT RIGHT DOUBLE ARROW +21D5..21F3 ; Common # So [31] UP DOWN DOUBLE ARROW..UP DOWN WHITE ARROW +21F4..22FF ; Common # Sm [268] RIGHT ARROW WITH SMALL CIRCLE..Z NOTATION BAG MEMBERSHIP +2300..2307 ; Common # So [8] DIAMETER SIGN..WAVY LINE +2308 ; Common # Ps LEFT CEILING +2309 ; Common # Pe RIGHT CEILING +230A ; Common # Ps LEFT FLOOR +230B ; Common # Pe RIGHT FLOOR +230C..231F ; Common # So [20] BOTTOM RIGHT CROP..BOTTOM RIGHT CORNER +2320..2321 ; Common # Sm [2] TOP HALF INTEGRAL..BOTTOM HALF INTEGRAL +2322..2328 ; Common # So [7] FROWN..KEYBOARD +2329 ; Common # Ps LEFT-POINTING ANGLE BRACKET +232A ; Common # Pe RIGHT-POINTING ANGLE BRACKET +232B..237B ; Common # So [81] ERASE TO THE LEFT..NOT CHECK MARK +237C ; Common # Sm RIGHT ANGLE WITH DOWNWARDS ZIGZAG ARROW +237D..239A ; Common # So [30] SHOULDERED OPEN BOX..CLEAR SCREEN SYMBOL +239B..23B3 ; Common # Sm [25] LEFT PARENTHESIS UPPER HOOK..SUMMATION BOTTOM +23B4..23DB ; Common # So [40] TOP SQUARE BRACKET..FUSE +23DC..23E1 ; Common # Sm [6] TOP PARENTHESIS..BOTTOM TORTOISE SHELL BRACKET +23E2..2426 ; Common # So [69] WHITE TRAPEZIUM..SYMBOL FOR SUBSTITUTE FORM TWO +2440..244A ; Common # So [11] OCR HOOK..OCR DOUBLE BACKSLASH +2460..249B ; Common # No [60] CIRCLED DIGIT ONE..NUMBER TWENTY FULL STOP +249C..24E9 ; Common # So [78] PARENTHESIZED LATIN SMALL LETTER A..CIRCLED LATIN SMALL LETTER Z +24EA..24FF ; Common # No [22] CIRCLED DIGIT ZERO..NEGATIVE CIRCLED DIGIT ZERO +2500..25B6 ; Common # So [183] BOX DRAWINGS LIGHT HORIZONTAL..BLACK RIGHT-POINTING TRIANGLE +25B7 ; Common # Sm WHITE RIGHT-POINTING TRIANGLE +25B8..25C0 ; Common # So [9] BLACK RIGHT-POINTING SMALL TRIANGLE..BLACK LEFT-POINTING TRIANGLE +25C1 ; Common # Sm WHITE LEFT-POINTING TRIANGLE +25C2..25F7 ; Common # So [54] BLACK LEFT-POINTING SMALL TRIANGLE..WHITE CIRCLE WITH UPPER RIGHT QUADRANT +25F8..25FF ; Common # Sm [8] UPPER LEFT TRIANGLE..LOWER RIGHT TRIANGLE +2600..266E ; Common # So [111] BLACK SUN WITH RAYS..MUSIC NATURAL SIGN +266F ; Common # Sm MUSIC SHARP SIGN +2670..2767 ; Common # So [248] WEST SYRIAC CROSS..ROTATED FLORAL HEART BULLET +2768 ; Common # Ps MEDIUM LEFT PARENTHESIS ORNAMENT +2769 ; Common # Pe MEDIUM RIGHT PARENTHESIS ORNAMENT +276A ; Common # Ps MEDIUM FLATTENED LEFT PARENTHESIS ORNAMENT +276B ; Common # Pe MEDIUM FLATTENED RIGHT PARENTHESIS ORNAMENT +276C ; Common # Ps MEDIUM LEFT-POINTING ANGLE BRACKET ORNAMENT +276D ; Common # Pe MEDIUM RIGHT-POINTING ANGLE BRACKET ORNAMENT +276E ; Common # Ps HEAVY LEFT-POINTING ANGLE QUOTATION MARK ORNAMENT +276F ; Common # Pe HEAVY RIGHT-POINTING ANGLE QUOTATION MARK ORNAMENT +2770 ; Common # Ps HEAVY LEFT-POINTING ANGLE BRACKET ORNAMENT +2771 ; Common # Pe HEAVY RIGHT-POINTING ANGLE BRACKET ORNAMENT +2772 ; Common # Ps LIGHT LEFT TORTOISE SHELL BRACKET ORNAMENT +2773 ; Common # Pe LIGHT RIGHT TORTOISE SHELL BRACKET ORNAMENT +2774 ; Common # Ps MEDIUM LEFT CURLY BRACKET ORNAMENT +2775 ; Common # Pe MEDIUM RIGHT CURLY BRACKET ORNAMENT +2776..2793 ; Common # No [30] DINGBAT NEGATIVE CIRCLED DIGIT ONE..DINGBAT NEGATIVE CIRCLED SANS-SERIF NUMBER TEN +2794..27BF ; Common # So [44] HEAVY WIDE-HEADED RIGHTWARDS ARROW..DOUBLE CURLY LOOP +27C0..27C4 ; Common # Sm [5] THREE DIMENSIONAL ANGLE..OPEN SUPERSET +27C5 ; Common # Ps LEFT S-SHAPED BAG DELIMITER +27C6 ; Common # Pe RIGHT S-SHAPED BAG DELIMITER +27C7..27E5 ; Common # Sm [31] OR WITH DOT INSIDE..WHITE SQUARE WITH RIGHTWARDS TICK +27E6 ; Common # Ps MATHEMATICAL LEFT WHITE SQUARE BRACKET +27E7 ; Common # Pe MATHEMATICAL RIGHT WHITE SQUARE BRACKET +27E8 ; Common # Ps MATHEMATICAL LEFT ANGLE BRACKET +27E9 ; Common # Pe MATHEMATICAL RIGHT ANGLE BRACKET +27EA ; Common # Ps MATHEMATICAL LEFT DOUBLE ANGLE BRACKET +27EB ; Common # Pe MATHEMATICAL RIGHT DOUBLE ANGLE BRACKET +27EC ; Common # Ps MATHEMATICAL LEFT WHITE TORTOISE SHELL BRACKET +27ED ; Common # Pe MATHEMATICAL RIGHT WHITE TORTOISE SHELL BRACKET +27EE ; Common # Ps MATHEMATICAL LEFT FLATTENED PARENTHESIS +27EF ; Common # Pe MATHEMATICAL RIGHT FLATTENED PARENTHESIS +27F0..27FF ; Common # Sm [16] UPWARDS QUADRUPLE ARROW..LONG RIGHTWARDS SQUIGGLE ARROW +2900..2982 ; Common # Sm [131] RIGHTWARDS TWO-HEADED ARROW WITH VERTICAL STROKE..Z NOTATION TYPE COLON +2983 ; Common # Ps LEFT WHITE CURLY BRACKET +2984 ; Common # Pe RIGHT WHITE CURLY BRACKET +2985 ; Common # Ps LEFT WHITE PARENTHESIS +2986 ; Common # Pe RIGHT WHITE PARENTHESIS +2987 ; Common # Ps Z NOTATION LEFT IMAGE BRACKET +2988 ; Common # Pe Z NOTATION RIGHT IMAGE BRACKET +2989 ; Common # Ps Z NOTATION LEFT BINDING BRACKET +298A ; Common # Pe Z NOTATION RIGHT BINDING BRACKET +298B ; Common # Ps LEFT SQUARE BRACKET WITH UNDERBAR +298C ; Common # Pe RIGHT SQUARE BRACKET WITH UNDERBAR +298D ; Common # Ps LEFT SQUARE BRACKET WITH TICK IN TOP CORNER +298E ; Common # Pe RIGHT SQUARE BRACKET WITH TICK IN BOTTOM CORNER +298F ; Common # Ps LEFT SQUARE BRACKET WITH TICK IN BOTTOM CORNER +2990 ; Common # Pe RIGHT SQUARE BRACKET WITH TICK IN TOP CORNER +2991 ; Common # Ps LEFT ANGLE BRACKET WITH DOT +2992 ; Common # Pe RIGHT ANGLE BRACKET WITH DOT +2993 ; Common # Ps LEFT ARC LESS-THAN BRACKET +2994 ; Common # Pe RIGHT ARC GREATER-THAN BRACKET +2995 ; Common # Ps DOUBLE LEFT ARC GREATER-THAN BRACKET +2996 ; Common # Pe DOUBLE RIGHT ARC LESS-THAN BRACKET +2997 ; Common # Ps LEFT BLACK TORTOISE SHELL BRACKET +2998 ; Common # Pe RIGHT BLACK TORTOISE SHELL BRACKET +2999..29D7 ; Common # Sm [63] DOTTED FENCE..BLACK HOURGLASS +29D8 ; Common # Ps LEFT WIGGLY FENCE +29D9 ; Common # Pe RIGHT WIGGLY FENCE +29DA ; Common # Ps LEFT DOUBLE WIGGLY FENCE +29DB ; Common # Pe RIGHT DOUBLE WIGGLY FENCE +29DC..29FB ; Common # Sm [32] INCOMPLETE INFINITY..TRIPLE PLUS +29FC ; Common # Ps LEFT-POINTING CURVED ANGLE BRACKET +29FD ; Common # Pe RIGHT-POINTING CURVED ANGLE BRACKET +29FE..2AFF ; Common # Sm [258] TINY..N-ARY WHITE VERTICAL BAR +2B00..2B2F ; Common # So [48] NORTH EAST WHITE ARROW..WHITE VERTICAL ELLIPSE +2B30..2B44 ; Common # Sm [21] LEFT ARROW WITH SMALL CIRCLE..RIGHTWARDS ARROW THROUGH SUPERSET +2B45..2B46 ; Common # So [2] LEFTWARDS QUADRUPLE ARROW..RIGHTWARDS QUADRUPLE ARROW +2B47..2B4C ; Common # Sm [6] REVERSE TILDE OPERATOR ABOVE RIGHTWARDS ARROW..RIGHTWARDS ARROW ABOVE REVERSE TILDE OPERATOR +2B4D..2B73 ; Common # So [39] DOWNWARDS TRIANGLE-HEADED ZIGZAG ARROW..DOWNWARDS TRIANGLE-HEADED ARROW TO BAR +2B76..2B95 ; Common # So [32] NORTH WEST TRIANGLE-HEADED ARROW TO BAR..RIGHTWARDS BLACK ARROW +2B97..2BFF ; Common # So [105] SYMBOL FOR TYPE A ELECTRONICS..HELLSCHREIBER PAUSE SYMBOL +2E00..2E01 ; Common # Po [2] RIGHT ANGLE SUBSTITUTION MARKER..RIGHT ANGLE DOTTED SUBSTITUTION MARKER +2E02 ; Common # Pi LEFT SUBSTITUTION BRACKET +2E03 ; Common # Pf RIGHT SUBSTITUTION BRACKET +2E04 ; Common # Pi LEFT DOTTED SUBSTITUTION BRACKET +2E05 ; Common # Pf RIGHT DOTTED SUBSTITUTION BRACKET +2E06..2E08 ; Common # Po [3] RAISED INTERPOLATION MARKER..DOTTED TRANSPOSITION MARKER +2E09 ; Common # Pi LEFT TRANSPOSITION BRACKET +2E0A ; Common # Pf RIGHT TRANSPOSITION BRACKET +2E0B ; Common # Po RAISED SQUARE +2E0C ; Common # Pi LEFT RAISED OMISSION BRACKET +2E0D ; Common # Pf RIGHT RAISED OMISSION BRACKET +2E0E..2E16 ; Common # Po [9] EDITORIAL CORONIS..DOTTED RIGHT-POINTING ANGLE +2E17 ; Common # Pd DOUBLE OBLIQUE HYPHEN +2E18..2E19 ; Common # Po [2] INVERTED INTERROBANG..PALM BRANCH +2E1A ; Common # Pd HYPHEN WITH DIAERESIS +2E1B ; Common # Po TILDE WITH RING ABOVE +2E1C ; Common # Pi LEFT LOW PARAPHRASE BRACKET +2E1D ; Common # Pf RIGHT LOW PARAPHRASE BRACKET +2E1E..2E1F ; Common # Po [2] TILDE WITH DOT ABOVE..TILDE WITH DOT BELOW +2E20 ; Common # Pi LEFT VERTICAL BAR WITH QUILL +2E21 ; Common # Pf RIGHT VERTICAL BAR WITH QUILL +2E22 ; Common # Ps TOP LEFT HALF BRACKET +2E23 ; Common # Pe TOP RIGHT HALF BRACKET +2E24 ; Common # Ps BOTTOM LEFT HALF BRACKET +2E25 ; Common # Pe BOTTOM RIGHT HALF BRACKET +2E26 ; Common # Ps LEFT SIDEWAYS U BRACKET +2E27 ; Common # Pe RIGHT SIDEWAYS U BRACKET +2E28 ; Common # Ps LEFT DOUBLE PARENTHESIS +2E29 ; Common # Pe RIGHT DOUBLE PARENTHESIS +2E2A..2E2E ; Common # Po [5] TWO DOTS OVER ONE DOT PUNCTUATION..REVERSED QUESTION MARK +2E2F ; Common # Lm VERTICAL TILDE +2E30..2E39 ; Common # Po [10] RING POINT..TOP HALF SECTION SIGN +2E3A..2E3B ; Common # Pd [2] TWO-EM DASH..THREE-EM DASH +2E3C..2E3F ; Common # Po [4] STENOGRAPHIC FULL STOP..CAPITULUM +2E40 ; Common # Pd DOUBLE HYPHEN +2E41 ; Common # Po REVERSED COMMA +2E42 ; Common # Ps DOUBLE LOW-REVERSED-9 QUOTATION MARK +2E43..2E4F ; Common # Po [13] DASH WITH LEFT UPTURN..CORNISH VERSE DIVIDER +2E50..2E51 ; Common # So [2] CROSS PATTY WITH RIGHT CROSSBAR..CROSS PATTY WITH LEFT CROSSBAR +2E52..2E54 ; Common # Po [3] TIRONIAN SIGN CAPITAL ET..MEDIEVAL QUESTION MARK +2E55 ; Common # Ps LEFT SQUARE BRACKET WITH STROKE +2E56 ; Common # Pe RIGHT SQUARE BRACKET WITH STROKE +2E57 ; Common # Ps LEFT SQUARE BRACKET WITH DOUBLE STROKE +2E58 ; Common # Pe RIGHT SQUARE BRACKET WITH DOUBLE STROKE +2E59 ; Common # Ps TOP HALF LEFT PARENTHESIS +2E5A ; Common # Pe TOP HALF RIGHT PARENTHESIS +2E5B ; Common # Ps BOTTOM HALF LEFT PARENTHESIS +2E5C ; Common # Pe BOTTOM HALF RIGHT PARENTHESIS +2E5D ; Common # Pd OBLIQUE HYPHEN +2FF0..2FFF ; Common # So [16] IDEOGRAPHIC DESCRIPTION CHARACTER LEFT TO RIGHT..IDEOGRAPHIC DESCRIPTION CHARACTER ROTATION +3000 ; Common # Zs IDEOGRAPHIC SPACE +3001..3003 ; Common # Po [3] IDEOGRAPHIC COMMA..DITTO MARK +3004 ; Common # So JAPANESE INDUSTRIAL STANDARD SYMBOL +3006 ; Common # Lo IDEOGRAPHIC CLOSING MARK +3008 ; Common # Ps LEFT ANGLE BRACKET +3009 ; Common # Pe RIGHT ANGLE BRACKET +300A ; Common # Ps LEFT DOUBLE ANGLE BRACKET +300B ; Common # Pe RIGHT DOUBLE ANGLE BRACKET +300C ; Common # Ps LEFT CORNER BRACKET +300D ; Common # Pe RIGHT CORNER BRACKET +300E ; Common # Ps LEFT WHITE CORNER BRACKET +300F ; Common # Pe RIGHT WHITE CORNER BRACKET +3010 ; Common # Ps LEFT BLACK LENTICULAR BRACKET +3011 ; Common # Pe RIGHT BLACK LENTICULAR BRACKET +3012..3013 ; Common # So [2] POSTAL MARK..GETA MARK +3014 ; Common # Ps LEFT TORTOISE SHELL BRACKET +3015 ; Common # Pe RIGHT TORTOISE SHELL BRACKET +3016 ; Common # Ps LEFT WHITE LENTICULAR BRACKET +3017 ; Common # Pe RIGHT WHITE LENTICULAR BRACKET +3018 ; Common # Ps LEFT WHITE TORTOISE SHELL BRACKET +3019 ; Common # Pe RIGHT WHITE TORTOISE SHELL BRACKET +301A ; Common # Ps LEFT WHITE SQUARE BRACKET +301B ; Common # Pe RIGHT WHITE SQUARE BRACKET +301C ; Common # Pd WAVE DASH +301D ; Common # Ps REVERSED DOUBLE PRIME QUOTATION MARK +301E..301F ; Common # Pe [2] DOUBLE PRIME QUOTATION MARK..LOW DOUBLE PRIME QUOTATION MARK +3020 ; Common # So POSTAL MARK FACE +3030 ; Common # Pd WAVY DASH +3031..3035 ; Common # Lm [5] VERTICAL KANA REPEAT MARK..VERTICAL KANA REPEAT MARK LOWER HALF +3036..3037 ; Common # So [2] CIRCLED POSTAL MARK..IDEOGRAPHIC TELEGRAPH LINE FEED SEPARATOR SYMBOL +303C ; Common # Lo MASU MARK +303D ; Common # Po PART ALTERNATION MARK +303E..303F ; Common # So [2] IDEOGRAPHIC VARIATION INDICATOR..IDEOGRAPHIC HALF FILL SPACE +309B..309C ; Common # Sk [2] KATAKANA-HIRAGANA VOICED SOUND MARK..KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK +30A0 ; Common # Pd KATAKANA-HIRAGANA DOUBLE HYPHEN +30FB ; Common # Po KATAKANA MIDDLE DOT +30FC ; Common # Lm KATAKANA-HIRAGANA PROLONGED SOUND MARK +3190..3191 ; Common # So [2] IDEOGRAPHIC ANNOTATION LINKING MARK..IDEOGRAPHIC ANNOTATION REVERSE MARK +3192..3195 ; Common # No [4] IDEOGRAPHIC ANNOTATION ONE MARK..IDEOGRAPHIC ANNOTATION FOUR MARK +3196..319F ; Common # So [10] IDEOGRAPHIC ANNOTATION TOP MARK..IDEOGRAPHIC ANNOTATION MAN MARK +31C0..31E3 ; Common # So [36] CJK STROKE T..CJK STROKE Q +31EF ; Common # So IDEOGRAPHIC DESCRIPTION CHARACTER SUBTRACTION +3220..3229 ; Common # No [10] PARENTHESIZED IDEOGRAPH ONE..PARENTHESIZED IDEOGRAPH TEN +322A..3247 ; Common # So [30] PARENTHESIZED IDEOGRAPH MOON..CIRCLED IDEOGRAPH KOTO +3248..324F ; Common # No [8] CIRCLED NUMBER TEN ON BLACK SQUARE..CIRCLED NUMBER EIGHTY ON BLACK SQUARE +3250 ; Common # So PARTNERSHIP SIGN +3251..325F ; Common # No [15] CIRCLED NUMBER TWENTY ONE..CIRCLED NUMBER THIRTY FIVE +327F ; Common # So KOREAN STANDARD SYMBOL +3280..3289 ; Common # No [10] CIRCLED IDEOGRAPH ONE..CIRCLED IDEOGRAPH TEN +328A..32B0 ; Common # So [39] CIRCLED IDEOGRAPH MOON..CIRCLED IDEOGRAPH NIGHT +32B1..32BF ; Common # No [15] CIRCLED NUMBER THIRTY SIX..CIRCLED NUMBER FIFTY +32C0..32CF ; Common # So [16] IDEOGRAPHIC TELEGRAPH SYMBOL FOR JANUARY..LIMITED LIABILITY SIGN +32FF ; Common # So SQUARE ERA NAME REIWA +3358..33FF ; Common # So [168] IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR ZERO..SQUARE GAL +4DC0..4DFF ; Common # So [64] HEXAGRAM FOR THE CREATIVE HEAVEN..HEXAGRAM FOR BEFORE COMPLETION +A700..A716 ; Common # Sk [23] MODIFIER LETTER CHINESE TONE YIN PING..MODIFIER LETTER EXTRA-LOW LEFT-STEM TONE BAR +A717..A71F ; Common # Lm [9] MODIFIER LETTER DOT VERTICAL BAR..MODIFIER LETTER LOW INVERTED EXCLAMATION MARK +A720..A721 ; Common # Sk [2] MODIFIER LETTER STRESS AND HIGH TONE..MODIFIER LETTER STRESS AND LOW TONE +A788 ; Common # Lm MODIFIER LETTER LOW CIRCUMFLEX ACCENT +A789..A78A ; Common # Sk [2] MODIFIER LETTER COLON..MODIFIER LETTER SHORT EQUALS SIGN +A830..A835 ; Common # No [6] NORTH INDIC FRACTION ONE QUARTER..NORTH INDIC FRACTION THREE SIXTEENTHS +A836..A837 ; Common # So [2] NORTH INDIC QUARTER MARK..NORTH INDIC PLACEHOLDER MARK +A838 ; Common # Sc NORTH INDIC RUPEE MARK +A839 ; Common # So NORTH INDIC QUANTITY MARK +A92E ; Common # Po KAYAH LI SIGN CWI +A9CF ; Common # Lm JAVANESE PANGRANGKEP +AB5B ; Common # Sk MODIFIER BREVE WITH INVERTED BREVE +AB6A..AB6B ; Common # Sk [2] MODIFIER LETTER LEFT TACK..MODIFIER LETTER RIGHT TACK +FD3E ; Common # Pe ORNATE LEFT PARENTHESIS +FD3F ; Common # Ps ORNATE RIGHT PARENTHESIS +FE10..FE16 ; Common # Po [7] PRESENTATION FORM FOR VERTICAL COMMA..PRESENTATION FORM FOR VERTICAL QUESTION MARK +FE17 ; Common # Ps PRESENTATION FORM FOR VERTICAL LEFT WHITE LENTICULAR BRACKET +FE18 ; Common # Pe PRESENTATION FORM FOR VERTICAL RIGHT WHITE LENTICULAR BRAKCET +FE19 ; Common # Po PRESENTATION FORM FOR VERTICAL HORIZONTAL ELLIPSIS +FE30 ; Common # Po PRESENTATION FORM FOR VERTICAL TWO DOT LEADER +FE31..FE32 ; Common # Pd [2] PRESENTATION FORM FOR VERTICAL EM DASH..PRESENTATION FORM FOR VERTICAL EN DASH +FE33..FE34 ; Common # Pc [2] PRESENTATION FORM FOR VERTICAL LOW LINE..PRESENTATION FORM FOR VERTICAL WAVY LOW LINE +FE35 ; Common # Ps PRESENTATION FORM FOR VERTICAL LEFT PARENTHESIS +FE36 ; Common # Pe PRESENTATION FORM FOR VERTICAL RIGHT PARENTHESIS +FE37 ; Common # Ps PRESENTATION FORM FOR VERTICAL LEFT CURLY BRACKET +FE38 ; Common # Pe PRESENTATION FORM FOR VERTICAL RIGHT CURLY BRACKET +FE39 ; Common # Ps PRESENTATION FORM FOR VERTICAL LEFT TORTOISE SHELL BRACKET +FE3A ; Common # Pe PRESENTATION FORM FOR VERTICAL RIGHT TORTOISE SHELL BRACKET +FE3B ; Common # Ps PRESENTATION FORM FOR VERTICAL LEFT BLACK LENTICULAR BRACKET +FE3C ; Common # Pe PRESENTATION FORM FOR VERTICAL RIGHT BLACK LENTICULAR BRACKET +FE3D ; Common # Ps PRESENTATION FORM FOR VERTICAL LEFT DOUBLE ANGLE BRACKET +FE3E ; Common # Pe PRESENTATION FORM FOR VERTICAL RIGHT DOUBLE ANGLE BRACKET +FE3F ; Common # Ps PRESENTATION FORM FOR VERTICAL LEFT ANGLE BRACKET +FE40 ; Common # Pe PRESENTATION FORM FOR VERTICAL RIGHT ANGLE BRACKET +FE41 ; Common # Ps PRESENTATION FORM FOR VERTICAL LEFT CORNER BRACKET +FE42 ; Common # Pe PRESENTATION FORM FOR VERTICAL RIGHT CORNER BRACKET +FE43 ; Common # Ps PRESENTATION FORM FOR VERTICAL LEFT WHITE CORNER BRACKET +FE44 ; Common # Pe PRESENTATION FORM FOR VERTICAL RIGHT WHITE CORNER BRACKET +FE45..FE46 ; Common # Po [2] SESAME DOT..WHITE SESAME DOT +FE47 ; Common # Ps PRESENTATION FORM FOR VERTICAL LEFT SQUARE BRACKET +FE48 ; Common # Pe PRESENTATION FORM FOR VERTICAL RIGHT SQUARE BRACKET +FE49..FE4C ; Common # Po [4] DASHED OVERLINE..DOUBLE WAVY OVERLINE +FE4D..FE4F ; Common # Pc [3] DASHED LOW LINE..WAVY LOW LINE +FE50..FE52 ; Common # Po [3] SMALL COMMA..SMALL FULL STOP +FE54..FE57 ; Common # Po [4] SMALL SEMICOLON..SMALL EXCLAMATION MARK +FE58 ; Common # Pd SMALL EM DASH +FE59 ; Common # Ps SMALL LEFT PARENTHESIS +FE5A ; Common # Pe SMALL RIGHT PARENTHESIS +FE5B ; Common # Ps SMALL LEFT CURLY BRACKET +FE5C ; Common # Pe SMALL RIGHT CURLY BRACKET +FE5D ; Common # Ps SMALL LEFT TORTOISE SHELL BRACKET +FE5E ; Common # Pe SMALL RIGHT TORTOISE SHELL BRACKET +FE5F..FE61 ; Common # Po [3] SMALL NUMBER SIGN..SMALL ASTERISK +FE62 ; Common # Sm SMALL PLUS SIGN +FE63 ; Common # Pd SMALL HYPHEN-MINUS +FE64..FE66 ; Common # Sm [3] SMALL LESS-THAN SIGN..SMALL EQUALS SIGN +FE68 ; Common # Po SMALL REVERSE SOLIDUS +FE69 ; Common # Sc SMALL DOLLAR SIGN +FE6A..FE6B ; Common # Po [2] SMALL PERCENT SIGN..SMALL COMMERCIAL AT +FEFF ; Common # Cf ZERO WIDTH NO-BREAK SPACE +FF01..FF03 ; Common # Po [3] FULLWIDTH EXCLAMATION MARK..FULLWIDTH NUMBER SIGN +FF04 ; Common # Sc FULLWIDTH DOLLAR SIGN +FF05..FF07 ; Common # Po [3] FULLWIDTH PERCENT SIGN..FULLWIDTH APOSTROPHE +FF08 ; Common # Ps FULLWIDTH LEFT PARENTHESIS +FF09 ; Common # Pe FULLWIDTH RIGHT PARENTHESIS +FF0A ; Common # Po FULLWIDTH ASTERISK +FF0B ; Common # Sm FULLWIDTH PLUS SIGN +FF0C ; Common # Po FULLWIDTH COMMA +FF0D ; Common # Pd FULLWIDTH HYPHEN-MINUS +FF0E..FF0F ; Common # Po [2] FULLWIDTH FULL STOP..FULLWIDTH SOLIDUS +FF10..FF19 ; Common # Nd [10] FULLWIDTH DIGIT ZERO..FULLWIDTH DIGIT NINE +FF1A..FF1B ; Common # Po [2] FULLWIDTH COLON..FULLWIDTH SEMICOLON +FF1C..FF1E ; Common # Sm [3] FULLWIDTH LESS-THAN SIGN..FULLWIDTH GREATER-THAN SIGN +FF1F..FF20 ; Common # Po [2] FULLWIDTH QUESTION MARK..FULLWIDTH COMMERCIAL AT +FF3B ; Common # Ps FULLWIDTH LEFT SQUARE BRACKET +FF3C ; Common # Po FULLWIDTH REVERSE SOLIDUS +FF3D ; Common # Pe FULLWIDTH RIGHT SQUARE BRACKET +FF3E ; Common # Sk FULLWIDTH CIRCUMFLEX ACCENT +FF3F ; Common # Pc FULLWIDTH LOW LINE +FF40 ; Common # Sk FULLWIDTH GRAVE ACCENT +FF5B ; Common # Ps FULLWIDTH LEFT CURLY BRACKET +FF5C ; Common # Sm FULLWIDTH VERTICAL LINE +FF5D ; Common # Pe FULLWIDTH RIGHT CURLY BRACKET +FF5E ; Common # Sm FULLWIDTH TILDE +FF5F ; Common # Ps FULLWIDTH LEFT WHITE PARENTHESIS +FF60 ; Common # Pe FULLWIDTH RIGHT WHITE PARENTHESIS +FF61 ; Common # Po HALFWIDTH IDEOGRAPHIC FULL STOP +FF62 ; Common # Ps HALFWIDTH LEFT CORNER BRACKET +FF63 ; Common # Pe HALFWIDTH RIGHT CORNER BRACKET +FF64..FF65 ; Common # Po [2] HALFWIDTH IDEOGRAPHIC COMMA..HALFWIDTH KATAKANA MIDDLE DOT +FF70 ; Common # Lm HALFWIDTH KATAKANA-HIRAGANA PROLONGED SOUND MARK +FF9E..FF9F ; Common # Lm [2] HALFWIDTH KATAKANA VOICED SOUND MARK..HALFWIDTH KATAKANA SEMI-VOICED SOUND MARK +FFE0..FFE1 ; Common # Sc [2] FULLWIDTH CENT SIGN..FULLWIDTH POUND SIGN +FFE2 ; Common # Sm FULLWIDTH NOT SIGN +FFE3 ; Common # Sk FULLWIDTH MACRON +FFE4 ; Common # So FULLWIDTH BROKEN BAR +FFE5..FFE6 ; Common # Sc [2] FULLWIDTH YEN SIGN..FULLWIDTH WON SIGN +FFE8 ; Common # So HALFWIDTH FORMS LIGHT VERTICAL +FFE9..FFEC ; Common # Sm [4] HALFWIDTH LEFTWARDS ARROW..HALFWIDTH DOWNWARDS ARROW +FFED..FFEE ; Common # So [2] HALFWIDTH BLACK SQUARE..HALFWIDTH WHITE CIRCLE +FFF9..FFFB ; Common # Cf [3] INTERLINEAR ANNOTATION ANCHOR..INTERLINEAR ANNOTATION TERMINATOR +FFFC..FFFD ; Common # So [2] OBJECT REPLACEMENT CHARACTER..REPLACEMENT CHARACTER +10100..10102 ; Common # Po [3] AEGEAN WORD SEPARATOR LINE..AEGEAN CHECK MARK +10107..10133 ; Common # No [45] AEGEAN NUMBER ONE..AEGEAN NUMBER NINETY THOUSAND +10137..1013F ; Common # So [9] AEGEAN WEIGHT BASE UNIT..AEGEAN MEASURE THIRD SUBUNIT +10190..1019C ; Common # So [13] ROMAN SEXTANS SIGN..ASCIA SYMBOL +101D0..101FC ; Common # So [45] PHAISTOS DISC SIGN PEDESTRIAN..PHAISTOS DISC SIGN WAVY BAND +102E1..102FB ; Common # No [27] COPTIC EPACT DIGIT ONE..COPTIC EPACT NUMBER NINE HUNDRED +1BCA0..1BCA3 ; Common # Cf [4] SHORTHAND FORMAT LETTER OVERLAP..SHORTHAND FORMAT UP STEP +1CF50..1CFC3 ; Common # So [116] ZNAMENNY NEUME KRYUK..ZNAMENNY NEUME PAUK +1D000..1D0F5 ; Common # So [246] BYZANTINE MUSICAL SYMBOL PSILI..BYZANTINE MUSICAL SYMBOL GORGON NEO KATO +1D100..1D126 ; Common # So [39] MUSICAL SYMBOL SINGLE BARLINE..MUSICAL SYMBOL DRUM CLEF-2 +1D129..1D164 ; Common # So [60] MUSICAL SYMBOL MULTIPLE MEASURE REST..MUSICAL SYMBOL ONE HUNDRED TWENTY-EIGHTH NOTE +1D165..1D166 ; Common # Mc [2] MUSICAL SYMBOL COMBINING STEM..MUSICAL SYMBOL COMBINING SPRECHGESANG STEM +1D16A..1D16C ; Common # So [3] MUSICAL SYMBOL FINGERED TREMOLO-1..MUSICAL SYMBOL FINGERED TREMOLO-3 +1D16D..1D172 ; Common # Mc [6] MUSICAL SYMBOL COMBINING AUGMENTATION DOT..MUSICAL SYMBOL COMBINING FLAG-5 +1D173..1D17A ; Common # Cf [8] MUSICAL SYMBOL BEGIN BEAM..MUSICAL SYMBOL END PHRASE +1D183..1D184 ; Common # So [2] MUSICAL SYMBOL ARPEGGIATO UP..MUSICAL SYMBOL ARPEGGIATO DOWN +1D18C..1D1A9 ; Common # So [30] MUSICAL SYMBOL RINFORZANDO..MUSICAL SYMBOL DEGREE SLASH +1D1AE..1D1EA ; Common # So [61] MUSICAL SYMBOL PEDAL MARK..MUSICAL SYMBOL KORON +1D2C0..1D2D3 ; Common # No [20] KAKTOVIK NUMERAL ZERO..KAKTOVIK NUMERAL NINETEEN +1D2E0..1D2F3 ; Common # No [20] MAYAN NUMERAL ZERO..MAYAN NUMERAL NINETEEN +1D300..1D356 ; Common # So [87] MONOGRAM FOR EARTH..TETRAGRAM FOR FOSTERING +1D360..1D378 ; Common # No [25] COUNTING ROD UNIT DIGIT ONE..TALLY MARK FIVE +1D400..1D454 ; Common # L& [85] MATHEMATICAL BOLD CAPITAL A..MATHEMATICAL ITALIC SMALL G +1D456..1D49C ; Common # L& [71] MATHEMATICAL ITALIC SMALL I..MATHEMATICAL SCRIPT CAPITAL A +1D49E..1D49F ; Common # L& [2] MATHEMATICAL SCRIPT CAPITAL C..MATHEMATICAL SCRIPT CAPITAL D +1D4A2 ; Common # L& MATHEMATICAL SCRIPT CAPITAL G +1D4A5..1D4A6 ; Common # L& [2] MATHEMATICAL SCRIPT CAPITAL J..MATHEMATICAL SCRIPT CAPITAL K +1D4A9..1D4AC ; Common # L& [4] MATHEMATICAL SCRIPT CAPITAL N..MATHEMATICAL SCRIPT CAPITAL Q +1D4AE..1D4B9 ; Common # L& [12] MATHEMATICAL SCRIPT CAPITAL S..MATHEMATICAL SCRIPT SMALL D +1D4BB ; Common # L& MATHEMATICAL SCRIPT SMALL F +1D4BD..1D4C3 ; Common # L& [7] MATHEMATICAL SCRIPT SMALL H..MATHEMATICAL SCRIPT SMALL N +1D4C5..1D505 ; Common # L& [65] MATHEMATICAL SCRIPT SMALL P..MATHEMATICAL FRAKTUR CAPITAL B +1D507..1D50A ; Common # L& [4] MATHEMATICAL FRAKTUR CAPITAL D..MATHEMATICAL FRAKTUR CAPITAL G +1D50D..1D514 ; Common # L& [8] MATHEMATICAL FRAKTUR CAPITAL J..MATHEMATICAL FRAKTUR CAPITAL Q +1D516..1D51C ; Common # L& [7] MATHEMATICAL FRAKTUR CAPITAL S..MATHEMATICAL FRAKTUR CAPITAL Y +1D51E..1D539 ; Common # L& [28] MATHEMATICAL FRAKTUR SMALL A..MATHEMATICAL DOUBLE-STRUCK CAPITAL B +1D53B..1D53E ; Common # L& [4] MATHEMATICAL DOUBLE-STRUCK CAPITAL D..MATHEMATICAL DOUBLE-STRUCK CAPITAL G +1D540..1D544 ; Common # L& [5] MATHEMATICAL DOUBLE-STRUCK CAPITAL I..MATHEMATICAL DOUBLE-STRUCK CAPITAL M +1D546 ; Common # L& MATHEMATICAL DOUBLE-STRUCK CAPITAL O +1D54A..1D550 ; Common # L& [7] MATHEMATICAL DOUBLE-STRUCK CAPITAL S..MATHEMATICAL DOUBLE-STRUCK CAPITAL Y +1D552..1D6A5 ; Common # L& [340] MATHEMATICAL DOUBLE-STRUCK SMALL A..MATHEMATICAL ITALIC SMALL DOTLESS J +1D6A8..1D6C0 ; Common # L& [25] MATHEMATICAL BOLD CAPITAL ALPHA..MATHEMATICAL BOLD CAPITAL OMEGA +1D6C1 ; Common # Sm MATHEMATICAL BOLD NABLA +1D6C2..1D6DA ; Common # L& [25] MATHEMATICAL BOLD SMALL ALPHA..MATHEMATICAL BOLD SMALL OMEGA +1D6DB ; Common # Sm MATHEMATICAL BOLD PARTIAL DIFFERENTIAL +1D6DC..1D6FA ; Common # L& [31] MATHEMATICAL BOLD EPSILON SYMBOL..MATHEMATICAL ITALIC CAPITAL OMEGA +1D6FB ; Common # Sm MATHEMATICAL ITALIC NABLA +1D6FC..1D714 ; Common # L& [25] MATHEMATICAL ITALIC SMALL ALPHA..MATHEMATICAL ITALIC SMALL OMEGA +1D715 ; Common # Sm MATHEMATICAL ITALIC PARTIAL DIFFERENTIAL +1D716..1D734 ; Common # L& [31] MATHEMATICAL ITALIC EPSILON SYMBOL..MATHEMATICAL BOLD ITALIC CAPITAL OMEGA +1D735 ; Common # Sm MATHEMATICAL BOLD ITALIC NABLA +1D736..1D74E ; Common # L& [25] MATHEMATICAL BOLD ITALIC SMALL ALPHA..MATHEMATICAL BOLD ITALIC SMALL OMEGA +1D74F ; Common # Sm MATHEMATICAL BOLD ITALIC PARTIAL DIFFERENTIAL +1D750..1D76E ; Common # L& [31] MATHEMATICAL BOLD ITALIC EPSILON SYMBOL..MATHEMATICAL SANS-SERIF BOLD CAPITAL OMEGA +1D76F ; Common # Sm MATHEMATICAL SANS-SERIF BOLD NABLA +1D770..1D788 ; Common # L& [25] MATHEMATICAL SANS-SERIF BOLD SMALL ALPHA..MATHEMATICAL SANS-SERIF BOLD SMALL OMEGA +1D789 ; Common # Sm MATHEMATICAL SANS-SERIF BOLD PARTIAL DIFFERENTIAL +1D78A..1D7A8 ; Common # L& [31] MATHEMATICAL SANS-SERIF BOLD EPSILON SYMBOL..MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL OMEGA +1D7A9 ; Common # Sm MATHEMATICAL SANS-SERIF BOLD ITALIC NABLA +1D7AA..1D7C2 ; Common # L& [25] MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL ALPHA..MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL OMEGA +1D7C3 ; Common # Sm MATHEMATICAL SANS-SERIF BOLD ITALIC PARTIAL DIFFERENTIAL +1D7C4..1D7CB ; Common # L& [8] MATHEMATICAL SANS-SERIF BOLD ITALIC EPSILON SYMBOL..MATHEMATICAL BOLD SMALL DIGAMMA +1D7CE..1D7FF ; Common # Nd [50] MATHEMATICAL BOLD DIGIT ZERO..MATHEMATICAL MONOSPACE DIGIT NINE +1EC71..1ECAB ; Common # No [59] INDIC SIYAQ NUMBER ONE..INDIC SIYAQ NUMBER PREFIXED NINE +1ECAC ; Common # So INDIC SIYAQ PLACEHOLDER +1ECAD..1ECAF ; Common # No [3] INDIC SIYAQ FRACTION ONE QUARTER..INDIC SIYAQ FRACTION THREE QUARTERS +1ECB0 ; Common # Sc INDIC SIYAQ RUPEE MARK +1ECB1..1ECB4 ; Common # No [4] INDIC SIYAQ NUMBER ALTERNATE ONE..INDIC SIYAQ ALTERNATE LAKH MARK +1ED01..1ED2D ; Common # No [45] OTTOMAN SIYAQ NUMBER ONE..OTTOMAN SIYAQ NUMBER NINETY THOUSAND +1ED2E ; Common # So OTTOMAN SIYAQ MARRATAN +1ED2F..1ED3D ; Common # No [15] OTTOMAN SIYAQ ALTERNATE NUMBER TWO..OTTOMAN SIYAQ FRACTION ONE SIXTH +1F000..1F02B ; Common # So [44] MAHJONG TILE EAST WIND..MAHJONG TILE BACK +1F030..1F093 ; Common # So [100] DOMINO TILE HORIZONTAL BACK..DOMINO TILE VERTICAL-06-06 +1F0A0..1F0AE ; Common # So [15] PLAYING CARD BACK..PLAYING CARD KING OF SPADES +1F0B1..1F0BF ; Common # So [15] PLAYING CARD ACE OF HEARTS..PLAYING CARD RED JOKER +1F0C1..1F0CF ; Common # So [15] PLAYING CARD ACE OF DIAMONDS..PLAYING CARD BLACK JOKER +1F0D1..1F0F5 ; Common # So [37] PLAYING CARD ACE OF CLUBS..PLAYING CARD TRUMP-21 +1F100..1F10C ; Common # No [13] DIGIT ZERO FULL STOP..DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT ZERO +1F10D..1F1AD ; Common # So [161] CIRCLED ZERO WITH SLASH..MASK WORK SYMBOL +1F1E6..1F1FF ; Common # So [26] REGIONAL INDICATOR SYMBOL LETTER A..REGIONAL INDICATOR SYMBOL LETTER Z +1F201..1F202 ; Common # So [2] SQUARED KATAKANA KOKO..SQUARED KATAKANA SA +1F210..1F23B ; Common # So [44] SQUARED CJK UNIFIED IDEOGRAPH-624B..SQUARED CJK UNIFIED IDEOGRAPH-914D +1F240..1F248 ; Common # So [9] TORTOISE SHELL BRACKETED CJK UNIFIED IDEOGRAPH-672C..TORTOISE SHELL BRACKETED CJK UNIFIED IDEOGRAPH-6557 +1F250..1F251 ; Common # So [2] CIRCLED IDEOGRAPH ADVANTAGE..CIRCLED IDEOGRAPH ACCEPT +1F260..1F265 ; Common # So [6] ROUNDED SYMBOL FOR FU..ROUNDED SYMBOL FOR CAI +1F300..1F3FA ; Common # So [251] CYCLONE..AMPHORA +1F3FB..1F3FF ; Common # Sk [5] EMOJI MODIFIER FITZPATRICK TYPE-1-2..EMOJI MODIFIER FITZPATRICK TYPE-6 +1F400..1F6D7 ; Common # So [728] RAT..ELEVATOR +1F6DC..1F6EC ; Common # So [17] WIRELESS..AIRPLANE ARRIVING +1F6F0..1F6FC ; Common # So [13] SATELLITE..ROLLER SKATE +1F700..1F776 ; Common # So [119] ALCHEMICAL SYMBOL FOR QUINTESSENCE..LUNAR ECLIPSE +1F77B..1F7D9 ; Common # So [95] HAUMEA..NINE POINTED WHITE STAR +1F7E0..1F7EB ; Common # So [12] LARGE ORANGE CIRCLE..LARGE BROWN SQUARE +1F7F0 ; Common # So HEAVY EQUALS SIGN +1F800..1F80B ; Common # So [12] LEFTWARDS ARROW WITH SMALL TRIANGLE ARROWHEAD..DOWNWARDS ARROW WITH LARGE TRIANGLE ARROWHEAD +1F810..1F847 ; Common # So [56] LEFTWARDS ARROW WITH SMALL EQUILATERAL ARROWHEAD..DOWNWARDS HEAVY ARROW +1F850..1F859 ; Common # So [10] LEFTWARDS SANS-SERIF ARROW..UP DOWN SANS-SERIF ARROW +1F860..1F887 ; Common # So [40] WIDE-HEADED LEFTWARDS LIGHT BARB ARROW..WIDE-HEADED SOUTH WEST VERY HEAVY BARB ARROW +1F890..1F8AD ; Common # So [30] LEFTWARDS TRIANGLE ARROWHEAD..WHITE ARROW SHAFT WIDTH TWO THIRDS +1F8B0..1F8B1 ; Common # So [2] ARROW POINTING UPWARDS THEN NORTH WEST..ARROW POINTING RIGHTWARDS THEN CURVING SOUTH WEST +1F900..1FA53 ; Common # So [340] CIRCLED CROSS FORMEE WITH FOUR DOTS..BLACK CHESS KNIGHT-BISHOP +1FA60..1FA6D ; Common # So [14] XIANGQI RED GENERAL..XIANGQI BLACK SOLDIER +1FA70..1FA7C ; Common # So [13] BALLET SHOES..CRUTCH +1FA80..1FA88 ; Common # So [9] YO-YO..FLUTE +1FA90..1FABD ; Common # So [46] RINGED PLANET..WING +1FABF..1FAC5 ; Common # So [7] GOOSE..PERSON WITH CROWN +1FACE..1FADB ; Common # So [14] MOOSE..PEA POD +1FAE0..1FAE8 ; Common # So [9] MELTING FACE..SHAKING FACE +1FAF0..1FAF8 ; Common # So [9] HAND WITH INDEX FINGER AND THUMB CROSSED..RIGHTWARDS PUSHING HAND +1FB00..1FB92 ; Common # So [147] BLOCK SEXTANT-1..UPPER HALF INVERSE MEDIUM SHADE AND LOWER HALF BLOCK +1FB94..1FBCA ; Common # So [55] LEFT HALF INVERSE MEDIUM SHADE AND RIGHT HALF BLOCK..WHITE UP-POINTING CHEVRON +1FBF0..1FBF9 ; Common # Nd [10] SEGMENTED DIGIT ZERO..SEGMENTED DIGIT NINE +E0001 ; Common # Cf LANGUAGE TAG +E0020..E007F ; Common # Cf [96] TAG SPACE..CANCEL TAG + +# Total code points: 8306 + +# ================================================ + +0041..005A ; Latin # L& [26] LATIN CAPITAL LETTER A..LATIN CAPITAL LETTER Z +0061..007A ; Latin # L& [26] LATIN SMALL LETTER A..LATIN SMALL LETTER Z +00AA ; Latin # Lo FEMININE ORDINAL INDICATOR +00BA ; Latin # Lo MASCULINE ORDINAL INDICATOR +00C0..00D6 ; Latin # L& [23] LATIN CAPITAL LETTER A WITH GRAVE..LATIN CAPITAL LETTER O WITH DIAERESIS +00D8..00F6 ; Latin # L& [31] LATIN CAPITAL LETTER O WITH STROKE..LATIN SMALL LETTER O WITH DIAERESIS +00F8..01BA ; Latin # L& [195] LATIN SMALL LETTER O WITH STROKE..LATIN SMALL LETTER EZH WITH TAIL +01BB ; Latin # Lo LATIN LETTER TWO WITH STROKE +01BC..01BF ; Latin # L& [4] LATIN CAPITAL LETTER TONE FIVE..LATIN LETTER WYNN +01C0..01C3 ; Latin # Lo [4] LATIN LETTER DENTAL CLICK..LATIN LETTER RETROFLEX CLICK +01C4..0293 ; Latin # L& [208] LATIN CAPITAL LETTER DZ WITH CARON..LATIN SMALL LETTER EZH WITH CURL +0294 ; Latin # Lo LATIN LETTER GLOTTAL STOP +0295..02AF ; Latin # L& [27] LATIN LETTER PHARYNGEAL VOICED FRICATIVE..LATIN SMALL LETTER TURNED H WITH FISHHOOK AND TAIL +02B0..02B8 ; Latin # Lm [9] MODIFIER LETTER SMALL H..MODIFIER LETTER SMALL Y +02E0..02E4 ; Latin # Lm [5] MODIFIER LETTER SMALL GAMMA..MODIFIER LETTER SMALL REVERSED GLOTTAL STOP +1D00..1D25 ; Latin # L& [38] LATIN LETTER SMALL CAPITAL A..LATIN LETTER AIN +1D2C..1D5C ; Latin # Lm [49] MODIFIER LETTER CAPITAL A..MODIFIER LETTER SMALL AIN +1D62..1D65 ; Latin # Lm [4] LATIN SUBSCRIPT SMALL LETTER I..LATIN SUBSCRIPT SMALL LETTER V +1D6B..1D77 ; Latin # L& [13] LATIN SMALL LETTER UE..LATIN SMALL LETTER TURNED G +1D79..1D9A ; Latin # L& [34] LATIN SMALL LETTER INSULAR G..LATIN SMALL LETTER EZH WITH RETROFLEX HOOK +1D9B..1DBE ; Latin # Lm [36] MODIFIER LETTER SMALL TURNED ALPHA..MODIFIER LETTER SMALL EZH +1E00..1EFF ; Latin # L& [256] LATIN CAPITAL LETTER A WITH RING BELOW..LATIN SMALL LETTER Y WITH LOOP +2071 ; Latin # Lm SUPERSCRIPT LATIN SMALL LETTER I +207F ; Latin # Lm SUPERSCRIPT LATIN SMALL LETTER N +2090..209C ; Latin # Lm [13] LATIN SUBSCRIPT SMALL LETTER A..LATIN SUBSCRIPT SMALL LETTER T +212A..212B ; Latin # L& [2] KELVIN SIGN..ANGSTROM SIGN +2132 ; Latin # L& TURNED CAPITAL F +214E ; Latin # L& TURNED SMALL F +2160..2182 ; Latin # Nl [35] ROMAN NUMERAL ONE..ROMAN NUMERAL TEN THOUSAND +2183..2184 ; Latin # L& [2] ROMAN NUMERAL REVERSED ONE HUNDRED..LATIN SMALL LETTER REVERSED C +2185..2188 ; Latin # Nl [4] ROMAN NUMERAL SIX LATE FORM..ROMAN NUMERAL ONE HUNDRED THOUSAND +2C60..2C7B ; Latin # L& [28] LATIN CAPITAL LETTER L WITH DOUBLE BAR..LATIN LETTER SMALL CAPITAL TURNED E +2C7C..2C7D ; Latin # Lm [2] LATIN SUBSCRIPT SMALL LETTER J..MODIFIER LETTER CAPITAL V +2C7E..2C7F ; Latin # L& [2] LATIN CAPITAL LETTER S WITH SWASH TAIL..LATIN CAPITAL LETTER Z WITH SWASH TAIL +A722..A76F ; Latin # L& [78] LATIN CAPITAL LETTER EGYPTOLOGICAL ALEF..LATIN SMALL LETTER CON +A770 ; Latin # Lm MODIFIER LETTER US +A771..A787 ; Latin # L& [23] LATIN SMALL LETTER DUM..LATIN SMALL LETTER INSULAR T +A78B..A78E ; Latin # L& [4] LATIN CAPITAL LETTER SALTILLO..LATIN SMALL LETTER L WITH RETROFLEX HOOK AND BELT +A78F ; Latin # Lo LATIN LETTER SINOLOGICAL DOT +A790..A7CA ; Latin # L& [59] LATIN CAPITAL LETTER N WITH DESCENDER..LATIN SMALL LETTER S WITH SHORT STROKE OVERLAY +A7D0..A7D1 ; Latin # L& [2] LATIN CAPITAL LETTER CLOSED INSULAR G..LATIN SMALL LETTER CLOSED INSULAR G +A7D3 ; Latin # L& LATIN SMALL LETTER DOUBLE THORN +A7D5..A7D9 ; Latin # L& [5] LATIN SMALL LETTER DOUBLE WYNN..LATIN SMALL LETTER SIGMOID S +A7F2..A7F4 ; Latin # Lm [3] MODIFIER LETTER CAPITAL C..MODIFIER LETTER CAPITAL Q +A7F5..A7F6 ; Latin # L& [2] LATIN CAPITAL LETTER REVERSED HALF H..LATIN SMALL LETTER REVERSED HALF H +A7F7 ; Latin # Lo LATIN EPIGRAPHIC LETTER SIDEWAYS I +A7F8..A7F9 ; Latin # Lm [2] MODIFIER LETTER CAPITAL H WITH STROKE..MODIFIER LETTER SMALL LIGATURE OE +A7FA ; Latin # L& LATIN LETTER SMALL CAPITAL TURNED M +A7FB..A7FF ; Latin # Lo [5] LATIN EPIGRAPHIC LETTER REVERSED F..LATIN EPIGRAPHIC LETTER ARCHAIC M +AB30..AB5A ; Latin # L& [43] LATIN SMALL LETTER BARRED ALPHA..LATIN SMALL LETTER Y WITH SHORT RIGHT LEG +AB5C..AB5F ; Latin # Lm [4] MODIFIER LETTER SMALL HENG..MODIFIER LETTER SMALL U WITH LEFT HOOK +AB60..AB64 ; Latin # L& [5] LATIN SMALL LETTER SAKHA YAT..LATIN SMALL LETTER INVERTED ALPHA +AB66..AB68 ; Latin # L& [3] LATIN SMALL LETTER DZ DIGRAPH WITH RETROFLEX HOOK..LATIN SMALL LETTER TURNED R WITH MIDDLE TILDE +AB69 ; Latin # Lm MODIFIER LETTER SMALL TURNED W +FB00..FB06 ; Latin # L& [7] LATIN SMALL LIGATURE FF..LATIN SMALL LIGATURE ST +FF21..FF3A ; Latin # L& [26] FULLWIDTH LATIN CAPITAL LETTER A..FULLWIDTH LATIN CAPITAL LETTER Z +FF41..FF5A ; Latin # L& [26] FULLWIDTH LATIN SMALL LETTER A..FULLWIDTH LATIN SMALL LETTER Z +10780..10785 ; Latin # Lm [6] MODIFIER LETTER SMALL CAPITAL AA..MODIFIER LETTER SMALL B WITH HOOK +10787..107B0 ; Latin # Lm [42] MODIFIER LETTER SMALL DZ DIGRAPH..MODIFIER LETTER SMALL V WITH RIGHT HOOK +107B2..107BA ; Latin # Lm [9] MODIFIER LETTER SMALL CAPITAL Y..MODIFIER LETTER SMALL S WITH CURL +1DF00..1DF09 ; Latin # L& [10] LATIN SMALL LETTER FENG DIGRAPH WITH TRILL..LATIN SMALL LETTER T WITH HOOK AND RETROFLEX HOOK +1DF0A ; Latin # Lo LATIN LETTER RETROFLEX CLICK WITH RETROFLEX HOOK +1DF0B..1DF1E ; Latin # L& [20] LATIN SMALL LETTER ESH WITH DOUBLE BAR..LATIN SMALL LETTER S WITH CURL +1DF25..1DF2A ; Latin # L& [6] LATIN SMALL LETTER D WITH MID-HEIGHT LEFT HOOK..LATIN SMALL LETTER T WITH MID-HEIGHT LEFT HOOK + +# Total code points: 1481 + +# ================================================ + +0370..0373 ; Greek # L& [4] GREEK CAPITAL LETTER HETA..GREEK SMALL LETTER ARCHAIC SAMPI +0375 ; Greek # Sk GREEK LOWER NUMERAL SIGN +0376..0377 ; Greek # L& [2] GREEK CAPITAL LETTER PAMPHYLIAN DIGAMMA..GREEK SMALL LETTER PAMPHYLIAN DIGAMMA +037A ; Greek # Lm GREEK YPOGEGRAMMENI +037B..037D ; Greek # L& [3] GREEK SMALL REVERSED LUNATE SIGMA SYMBOL..GREEK SMALL REVERSED DOTTED LUNATE SIGMA SYMBOL +037F ; Greek # L& GREEK CAPITAL LETTER YOT +0384 ; Greek # Sk GREEK TONOS +0386 ; Greek # L& GREEK CAPITAL LETTER ALPHA WITH TONOS +0388..038A ; Greek # L& [3] GREEK CAPITAL LETTER EPSILON WITH TONOS..GREEK CAPITAL LETTER IOTA WITH TONOS +038C ; Greek # L& GREEK CAPITAL LETTER OMICRON WITH TONOS +038E..03A1 ; Greek # L& [20] GREEK CAPITAL LETTER UPSILON WITH TONOS..GREEK CAPITAL LETTER RHO +03A3..03E1 ; Greek # L& [63] GREEK CAPITAL LETTER SIGMA..GREEK SMALL LETTER SAMPI +03F0..03F5 ; Greek # L& [6] GREEK KAPPA SYMBOL..GREEK LUNATE EPSILON SYMBOL +03F6 ; Greek # Sm GREEK REVERSED LUNATE EPSILON SYMBOL +03F7..03FF ; Greek # L& [9] GREEK CAPITAL LETTER SHO..GREEK CAPITAL REVERSED DOTTED LUNATE SIGMA SYMBOL +1D26..1D2A ; Greek # L& [5] GREEK LETTER SMALL CAPITAL GAMMA..GREEK LETTER SMALL CAPITAL PSI +1D5D..1D61 ; Greek # Lm [5] MODIFIER LETTER SMALL BETA..MODIFIER LETTER SMALL CHI +1D66..1D6A ; Greek # Lm [5] GREEK SUBSCRIPT SMALL LETTER BETA..GREEK SUBSCRIPT SMALL LETTER CHI +1DBF ; Greek # Lm MODIFIER LETTER SMALL THETA +1F00..1F15 ; Greek # L& [22] GREEK SMALL LETTER ALPHA WITH PSILI..GREEK SMALL LETTER EPSILON WITH DASIA AND OXIA +1F18..1F1D ; Greek # L& [6] GREEK CAPITAL LETTER EPSILON WITH PSILI..GREEK CAPITAL LETTER EPSILON WITH DASIA AND OXIA +1F20..1F45 ; Greek # L& [38] GREEK SMALL LETTER ETA WITH PSILI..GREEK SMALL LETTER OMICRON WITH DASIA AND OXIA +1F48..1F4D ; Greek # L& [6] GREEK CAPITAL LETTER OMICRON WITH PSILI..GREEK CAPITAL LETTER OMICRON WITH DASIA AND OXIA +1F50..1F57 ; Greek # L& [8] GREEK SMALL LETTER UPSILON WITH PSILI..GREEK SMALL LETTER UPSILON WITH DASIA AND PERISPOMENI +1F59 ; Greek # L& GREEK CAPITAL LETTER UPSILON WITH DASIA +1F5B ; Greek # L& GREEK CAPITAL LETTER UPSILON WITH DASIA AND VARIA +1F5D ; Greek # L& GREEK CAPITAL LETTER UPSILON WITH DASIA AND OXIA +1F5F..1F7D ; Greek # L& [31] GREEK CAPITAL LETTER UPSILON WITH DASIA AND PERISPOMENI..GREEK SMALL LETTER OMEGA WITH OXIA +1F80..1FB4 ; Greek # L& [53] GREEK SMALL LETTER ALPHA WITH PSILI AND YPOGEGRAMMENI..GREEK SMALL LETTER ALPHA WITH OXIA AND YPOGEGRAMMENI +1FB6..1FBC ; Greek # L& [7] GREEK SMALL LETTER ALPHA WITH PERISPOMENI..GREEK CAPITAL LETTER ALPHA WITH PROSGEGRAMMENI +1FBD ; Greek # Sk GREEK KORONIS +1FBE ; Greek # L& GREEK PROSGEGRAMMENI +1FBF..1FC1 ; Greek # Sk [3] GREEK PSILI..GREEK DIALYTIKA AND PERISPOMENI +1FC2..1FC4 ; Greek # L& [3] GREEK SMALL LETTER ETA WITH VARIA AND YPOGEGRAMMENI..GREEK SMALL LETTER ETA WITH OXIA AND YPOGEGRAMMENI +1FC6..1FCC ; Greek # L& [7] GREEK SMALL LETTER ETA WITH PERISPOMENI..GREEK CAPITAL LETTER ETA WITH PROSGEGRAMMENI +1FCD..1FCF ; Greek # Sk [3] GREEK PSILI AND VARIA..GREEK PSILI AND PERISPOMENI +1FD0..1FD3 ; Greek # L& [4] GREEK SMALL LETTER IOTA WITH VRACHY..GREEK SMALL LETTER IOTA WITH DIALYTIKA AND OXIA +1FD6..1FDB ; Greek # L& [6] GREEK SMALL LETTER IOTA WITH PERISPOMENI..GREEK CAPITAL LETTER IOTA WITH OXIA +1FDD..1FDF ; Greek # Sk [3] GREEK DASIA AND VARIA..GREEK DASIA AND PERISPOMENI +1FE0..1FEC ; Greek # L& [13] GREEK SMALL LETTER UPSILON WITH VRACHY..GREEK CAPITAL LETTER RHO WITH DASIA +1FED..1FEF ; Greek # Sk [3] GREEK DIALYTIKA AND VARIA..GREEK VARIA +1FF2..1FF4 ; Greek # L& [3] GREEK SMALL LETTER OMEGA WITH VARIA AND YPOGEGRAMMENI..GREEK SMALL LETTER OMEGA WITH OXIA AND YPOGEGRAMMENI +1FF6..1FFC ; Greek # L& [7] GREEK SMALL LETTER OMEGA WITH PERISPOMENI..GREEK CAPITAL LETTER OMEGA WITH PROSGEGRAMMENI +1FFD..1FFE ; Greek # Sk [2] GREEK OXIA..GREEK DASIA +2126 ; Greek # L& OHM SIGN +AB65 ; Greek # L& GREEK LETTER SMALL CAPITAL OMEGA +10140..10174 ; Greek # Nl [53] GREEK ACROPHONIC ATTIC ONE QUARTER..GREEK ACROPHONIC STRATIAN FIFTY MNAS +10175..10178 ; Greek # No [4] GREEK ONE HALF SIGN..GREEK THREE QUARTERS SIGN +10179..10189 ; Greek # So [17] GREEK YEAR SIGN..GREEK TRYBLION BASE SIGN +1018A..1018B ; Greek # No [2] GREEK ZERO SIGN..GREEK ONE QUARTER SIGN +1018C..1018E ; Greek # So [3] GREEK SINUSOID SIGN..NOMISMA SIGN +101A0 ; Greek # So GREEK SYMBOL TAU RHO +1D200..1D241 ; Greek # So [66] GREEK VOCAL NOTATION SYMBOL-1..GREEK INSTRUMENTAL NOTATION SYMBOL-54 +1D242..1D244 ; Greek # Mn [3] COMBINING GREEK MUSICAL TRISEME..COMBINING GREEK MUSICAL PENTASEME +1D245 ; Greek # So GREEK MUSICAL LEIMMA + +# Total code points: 518 + +# ================================================ + +0400..0481 ; Cyrillic # L& [130] CYRILLIC CAPITAL LETTER IE WITH GRAVE..CYRILLIC SMALL LETTER KOPPA +0482 ; Cyrillic # So CYRILLIC THOUSANDS SIGN +0483..0484 ; Cyrillic # Mn [2] COMBINING CYRILLIC TITLO..COMBINING CYRILLIC PALATALIZATION +0487 ; Cyrillic # Mn COMBINING CYRILLIC POKRYTIE +0488..0489 ; Cyrillic # Me [2] COMBINING CYRILLIC HUNDRED THOUSANDS SIGN..COMBINING CYRILLIC MILLIONS SIGN +048A..052F ; Cyrillic # L& [166] CYRILLIC CAPITAL LETTER SHORT I WITH TAIL..CYRILLIC SMALL LETTER EL WITH DESCENDER +1C80..1C88 ; Cyrillic # L& [9] CYRILLIC SMALL LETTER ROUNDED VE..CYRILLIC SMALL LETTER UNBLENDED UK +1D2B ; Cyrillic # L& CYRILLIC LETTER SMALL CAPITAL EL +1D78 ; Cyrillic # Lm MODIFIER LETTER CYRILLIC EN +2DE0..2DFF ; Cyrillic # Mn [32] COMBINING CYRILLIC LETTER BE..COMBINING CYRILLIC LETTER IOTIFIED BIG YUS +A640..A66D ; Cyrillic # L& [46] CYRILLIC CAPITAL LETTER ZEMLYA..CYRILLIC SMALL LETTER DOUBLE MONOCULAR O +A66E ; Cyrillic # Lo CYRILLIC LETTER MULTIOCULAR O +A66F ; Cyrillic # Mn COMBINING CYRILLIC VZMET +A670..A672 ; Cyrillic # Me [3] COMBINING CYRILLIC TEN MILLIONS SIGN..COMBINING CYRILLIC THOUSAND MILLIONS SIGN +A673 ; Cyrillic # Po SLAVONIC ASTERISK +A674..A67D ; Cyrillic # Mn [10] COMBINING CYRILLIC LETTER UKRAINIAN IE..COMBINING CYRILLIC PAYEROK +A67E ; Cyrillic # Po CYRILLIC KAVYKA +A67F ; Cyrillic # Lm CYRILLIC PAYEROK +A680..A69B ; Cyrillic # L& [28] CYRILLIC CAPITAL LETTER DWE..CYRILLIC SMALL LETTER CROSSED O +A69C..A69D ; Cyrillic # Lm [2] MODIFIER LETTER CYRILLIC HARD SIGN..MODIFIER LETTER CYRILLIC SOFT SIGN +A69E..A69F ; Cyrillic # Mn [2] COMBINING CYRILLIC LETTER EF..COMBINING CYRILLIC LETTER IOTIFIED E +FE2E..FE2F ; Cyrillic # Mn [2] COMBINING CYRILLIC TITLO LEFT HALF..COMBINING CYRILLIC TITLO RIGHT HALF +1E030..1E06D ; Cyrillic # Lm [62] MODIFIER LETTER CYRILLIC SMALL A..MODIFIER LETTER CYRILLIC SMALL STRAIGHT U WITH STROKE +1E08F ; Cyrillic # Mn COMBINING CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I + +# Total code points: 506 + +# ================================================ + +0531..0556 ; Armenian # L& [38] ARMENIAN CAPITAL LETTER AYB..ARMENIAN CAPITAL LETTER FEH +0559 ; Armenian # Lm ARMENIAN MODIFIER LETTER LEFT HALF RING +055A..055F ; Armenian # Po [6] ARMENIAN APOSTROPHE..ARMENIAN ABBREVIATION MARK +0560..0588 ; Armenian # L& [41] ARMENIAN SMALL LETTER TURNED AYB..ARMENIAN SMALL LETTER YI WITH STROKE +0589 ; Armenian # Po ARMENIAN FULL STOP +058A ; Armenian # Pd ARMENIAN HYPHEN +058D..058E ; Armenian # So [2] RIGHT-FACING ARMENIAN ETERNITY SIGN..LEFT-FACING ARMENIAN ETERNITY SIGN +058F ; Armenian # Sc ARMENIAN DRAM SIGN +FB13..FB17 ; Armenian # L& [5] ARMENIAN SMALL LIGATURE MEN NOW..ARMENIAN SMALL LIGATURE MEN XEH + +# Total code points: 96 + +# ================================================ + +0591..05BD ; Hebrew # Mn [45] HEBREW ACCENT ETNAHTA..HEBREW POINT METEG +05BE ; Hebrew # Pd HEBREW PUNCTUATION MAQAF +05BF ; Hebrew # Mn HEBREW POINT RAFE +05C0 ; Hebrew # Po HEBREW PUNCTUATION PASEQ +05C1..05C2 ; Hebrew # Mn [2] HEBREW POINT SHIN DOT..HEBREW POINT SIN DOT +05C3 ; Hebrew # Po HEBREW PUNCTUATION SOF PASUQ +05C4..05C5 ; Hebrew # Mn [2] HEBREW MARK UPPER DOT..HEBREW MARK LOWER DOT +05C6 ; Hebrew # Po HEBREW PUNCTUATION NUN HAFUKHA +05C7 ; Hebrew # Mn HEBREW POINT QAMATS QATAN +05D0..05EA ; Hebrew # Lo [27] HEBREW LETTER ALEF..HEBREW LETTER TAV +05EF..05F2 ; Hebrew # Lo [4] HEBREW YOD TRIANGLE..HEBREW LIGATURE YIDDISH DOUBLE YOD +05F3..05F4 ; Hebrew # Po [2] HEBREW PUNCTUATION GERESH..HEBREW PUNCTUATION GERSHAYIM +FB1D ; Hebrew # Lo HEBREW LETTER YOD WITH HIRIQ +FB1E ; Hebrew # Mn HEBREW POINT JUDEO-SPANISH VARIKA +FB1F..FB28 ; Hebrew # Lo [10] HEBREW LIGATURE YIDDISH YOD YOD PATAH..HEBREW LETTER WIDE TAV +FB29 ; Hebrew # Sm HEBREW LETTER ALTERNATIVE PLUS SIGN +FB2A..FB36 ; Hebrew # Lo [13] HEBREW LETTER SHIN WITH SHIN DOT..HEBREW LETTER ZAYIN WITH DAGESH +FB38..FB3C ; Hebrew # Lo [5] HEBREW LETTER TET WITH DAGESH..HEBREW LETTER LAMED WITH DAGESH +FB3E ; Hebrew # Lo HEBREW LETTER MEM WITH DAGESH +FB40..FB41 ; Hebrew # Lo [2] HEBREW LETTER NUN WITH DAGESH..HEBREW LETTER SAMEKH WITH DAGESH +FB43..FB44 ; Hebrew # Lo [2] HEBREW LETTER FINAL PE WITH DAGESH..HEBREW LETTER PE WITH DAGESH +FB46..FB4F ; Hebrew # Lo [10] HEBREW LETTER TSADI WITH DAGESH..HEBREW LIGATURE ALEF LAMED + +# Total code points: 134 + +# ================================================ + +0600..0604 ; Arabic # Cf [5] ARABIC NUMBER SIGN..ARABIC SIGN SAMVAT +0606..0608 ; Arabic # Sm [3] ARABIC-INDIC CUBE ROOT..ARABIC RAY +0609..060A ; Arabic # Po [2] ARABIC-INDIC PER MILLE SIGN..ARABIC-INDIC PER TEN THOUSAND SIGN +060B ; Arabic # Sc AFGHANI SIGN +060D ; Arabic # Po ARABIC DATE SEPARATOR +060E..060F ; Arabic # So [2] ARABIC POETIC VERSE SIGN..ARABIC SIGN MISRA +0610..061A ; Arabic # Mn [11] ARABIC SIGN SALLALLAHOU ALAYHE WASSALLAM..ARABIC SMALL KASRA +061C ; Arabic # Cf ARABIC LETTER MARK +061D..061E ; Arabic # Po [2] ARABIC END OF TEXT MARK..ARABIC TRIPLE DOT PUNCTUATION MARK +0620..063F ; Arabic # Lo [32] ARABIC LETTER KASHMIRI YEH..ARABIC LETTER FARSI YEH WITH THREE DOTS ABOVE +0641..064A ; Arabic # Lo [10] ARABIC LETTER FEH..ARABIC LETTER YEH +0656..065F ; Arabic # Mn [10] ARABIC SUBSCRIPT ALEF..ARABIC WAVY HAMZA BELOW +0660..0669 ; Arabic # Nd [10] ARABIC-INDIC DIGIT ZERO..ARABIC-INDIC DIGIT NINE +066A..066D ; Arabic # Po [4] ARABIC PERCENT SIGN..ARABIC FIVE POINTED STAR +066E..066F ; Arabic # Lo [2] ARABIC LETTER DOTLESS BEH..ARABIC LETTER DOTLESS QAF +0671..06D3 ; Arabic # Lo [99] ARABIC LETTER ALEF WASLA..ARABIC LETTER YEH BARREE WITH HAMZA ABOVE +06D4 ; Arabic # Po ARABIC FULL STOP +06D5 ; Arabic # Lo ARABIC LETTER AE +06D6..06DC ; Arabic # Mn [7] ARABIC SMALL HIGH LIGATURE SAD WITH LAM WITH ALEF MAKSURA..ARABIC SMALL HIGH SEEN +06DE ; Arabic # So ARABIC START OF RUB EL HIZB +06DF..06E4 ; Arabic # Mn [6] ARABIC SMALL HIGH ROUNDED ZERO..ARABIC SMALL HIGH MADDA +06E5..06E6 ; Arabic # Lm [2] ARABIC SMALL WAW..ARABIC SMALL YEH +06E7..06E8 ; Arabic # Mn [2] ARABIC SMALL HIGH YEH..ARABIC SMALL HIGH NOON +06E9 ; Arabic # So ARABIC PLACE OF SAJDAH +06EA..06ED ; Arabic # Mn [4] ARABIC EMPTY CENTRE LOW STOP..ARABIC SMALL LOW MEEM +06EE..06EF ; Arabic # Lo [2] ARABIC LETTER DAL WITH INVERTED V..ARABIC LETTER REH WITH INVERTED V +06F0..06F9 ; Arabic # Nd [10] EXTENDED ARABIC-INDIC DIGIT ZERO..EXTENDED ARABIC-INDIC DIGIT NINE +06FA..06FC ; Arabic # Lo [3] ARABIC LETTER SHEEN WITH DOT BELOW..ARABIC LETTER GHAIN WITH DOT BELOW +06FD..06FE ; Arabic # So [2] ARABIC SIGN SINDHI AMPERSAND..ARABIC SIGN SINDHI POSTPOSITION MEN +06FF ; Arabic # Lo ARABIC LETTER HEH WITH INVERTED V +0750..077F ; Arabic # Lo [48] ARABIC LETTER BEH WITH THREE DOTS HORIZONTALLY BELOW..ARABIC LETTER KAF WITH TWO DOTS ABOVE +0870..0887 ; Arabic # Lo [24] ARABIC LETTER ALEF WITH ATTACHED FATHA..ARABIC BASELINE ROUND DOT +0888 ; Arabic # Sk ARABIC RAISED ROUND DOT +0889..088E ; Arabic # Lo [6] ARABIC LETTER NOON WITH INVERTED SMALL V..ARABIC VERTICAL TAIL +0890..0891 ; Arabic # Cf [2] ARABIC POUND MARK ABOVE..ARABIC PIASTRE MARK ABOVE +0898..089F ; Arabic # Mn [8] ARABIC SMALL HIGH WORD AL-JUZ..ARABIC HALF MADDA OVER MADDA +08A0..08C8 ; Arabic # Lo [41] ARABIC LETTER BEH WITH SMALL V BELOW..ARABIC LETTER GRAF +08C9 ; Arabic # Lm ARABIC SMALL FARSI YEH +08CA..08E1 ; Arabic # Mn [24] ARABIC SMALL HIGH FARSI YEH..ARABIC SMALL HIGH SIGN SAFHA +08E3..08FF ; Arabic # Mn [29] ARABIC TURNED DAMMA BELOW..ARABIC MARK SIDEWAYS NOON GHUNNA +FB50..FBB1 ; Arabic # Lo [98] ARABIC LETTER ALEF WASLA ISOLATED FORM..ARABIC LETTER YEH BARREE WITH HAMZA ABOVE FINAL FORM +FBB2..FBC2 ; Arabic # Sk [17] ARABIC SYMBOL DOT ABOVE..ARABIC SYMBOL WASLA ABOVE +FBD3..FD3D ; Arabic # Lo [363] ARABIC LETTER NG ISOLATED FORM..ARABIC LIGATURE ALEF WITH FATHATAN ISOLATED FORM +FD40..FD4F ; Arabic # So [16] ARABIC LIGATURE RAHIMAHU ALLAAH..ARABIC LIGATURE RAHIMAHUM ALLAAH +FD50..FD8F ; Arabic # Lo [64] ARABIC LIGATURE TEH WITH JEEM WITH MEEM INITIAL FORM..ARABIC LIGATURE MEEM WITH KHAH WITH MEEM INITIAL FORM +FD92..FDC7 ; Arabic # Lo [54] ARABIC LIGATURE MEEM WITH JEEM WITH KHAH INITIAL FORM..ARABIC LIGATURE NOON WITH JEEM WITH YEH FINAL FORM +FDCF ; Arabic # So ARABIC LIGATURE SALAAMUHU ALAYNAA +FDF0..FDFB ; Arabic # Lo [12] ARABIC LIGATURE SALLA USED AS KORANIC STOP SIGN ISOLATED FORM..ARABIC LIGATURE JALLAJALALOUHOU +FDFC ; Arabic # Sc RIAL SIGN +FDFD..FDFF ; Arabic # So [3] ARABIC LIGATURE BISMILLAH AR-RAHMAN AR-RAHEEM..ARABIC LIGATURE AZZA WA JALL +FE70..FE74 ; Arabic # Lo [5] ARABIC FATHATAN ISOLATED FORM..ARABIC KASRATAN ISOLATED FORM +FE76..FEFC ; Arabic # Lo [135] ARABIC FATHA ISOLATED FORM..ARABIC LIGATURE LAM WITH ALEF FINAL FORM +10E60..10E7E ; Arabic # No [31] RUMI DIGIT ONE..RUMI FRACTION TWO THIRDS +10EFD..10EFF ; Arabic # Mn [3] ARABIC SMALL LOW WORD SAKTA..ARABIC SMALL LOW WORD MADDA +1EE00..1EE03 ; Arabic # Lo [4] ARABIC MATHEMATICAL ALEF..ARABIC MATHEMATICAL DAL +1EE05..1EE1F ; Arabic # Lo [27] ARABIC MATHEMATICAL WAW..ARABIC MATHEMATICAL DOTLESS QAF +1EE21..1EE22 ; Arabic # Lo [2] ARABIC MATHEMATICAL INITIAL BEH..ARABIC MATHEMATICAL INITIAL JEEM +1EE24 ; Arabic # Lo ARABIC MATHEMATICAL INITIAL HEH +1EE27 ; Arabic # Lo ARABIC MATHEMATICAL INITIAL HAH +1EE29..1EE32 ; Arabic # Lo [10] ARABIC MATHEMATICAL INITIAL YEH..ARABIC MATHEMATICAL INITIAL QAF +1EE34..1EE37 ; Arabic # Lo [4] ARABIC MATHEMATICAL INITIAL SHEEN..ARABIC MATHEMATICAL INITIAL KHAH +1EE39 ; Arabic # Lo ARABIC MATHEMATICAL INITIAL DAD +1EE3B ; Arabic # Lo ARABIC MATHEMATICAL INITIAL GHAIN +1EE42 ; Arabic # Lo ARABIC MATHEMATICAL TAILED JEEM +1EE47 ; Arabic # Lo ARABIC MATHEMATICAL TAILED HAH +1EE49 ; Arabic # Lo ARABIC MATHEMATICAL TAILED YEH +1EE4B ; Arabic # Lo ARABIC MATHEMATICAL TAILED LAM +1EE4D..1EE4F ; Arabic # Lo [3] ARABIC MATHEMATICAL TAILED NOON..ARABIC MATHEMATICAL TAILED AIN +1EE51..1EE52 ; Arabic # Lo [2] ARABIC MATHEMATICAL TAILED SAD..ARABIC MATHEMATICAL TAILED QAF +1EE54 ; Arabic # Lo ARABIC MATHEMATICAL TAILED SHEEN +1EE57 ; Arabic # Lo ARABIC MATHEMATICAL TAILED KHAH +1EE59 ; Arabic # Lo ARABIC MATHEMATICAL TAILED DAD +1EE5B ; Arabic # Lo ARABIC MATHEMATICAL TAILED GHAIN +1EE5D ; Arabic # Lo ARABIC MATHEMATICAL TAILED DOTLESS NOON +1EE5F ; Arabic # Lo ARABIC MATHEMATICAL TAILED DOTLESS QAF +1EE61..1EE62 ; Arabic # Lo [2] ARABIC MATHEMATICAL STRETCHED BEH..ARABIC MATHEMATICAL STRETCHED JEEM +1EE64 ; Arabic # Lo ARABIC MATHEMATICAL STRETCHED HEH +1EE67..1EE6A ; Arabic # Lo [4] ARABIC MATHEMATICAL STRETCHED HAH..ARABIC MATHEMATICAL STRETCHED KAF +1EE6C..1EE72 ; Arabic # Lo [7] ARABIC MATHEMATICAL STRETCHED MEEM..ARABIC MATHEMATICAL STRETCHED QAF +1EE74..1EE77 ; Arabic # Lo [4] ARABIC MATHEMATICAL STRETCHED SHEEN..ARABIC MATHEMATICAL STRETCHED KHAH +1EE79..1EE7C ; Arabic # Lo [4] ARABIC MATHEMATICAL STRETCHED DAD..ARABIC MATHEMATICAL STRETCHED DOTLESS BEH +1EE7E ; Arabic # Lo ARABIC MATHEMATICAL STRETCHED DOTLESS FEH +1EE80..1EE89 ; Arabic # Lo [10] ARABIC MATHEMATICAL LOOPED ALEF..ARABIC MATHEMATICAL LOOPED YEH +1EE8B..1EE9B ; Arabic # Lo [17] ARABIC MATHEMATICAL LOOPED LAM..ARABIC MATHEMATICAL LOOPED GHAIN +1EEA1..1EEA3 ; Arabic # Lo [3] ARABIC MATHEMATICAL DOUBLE-STRUCK BEH..ARABIC MATHEMATICAL DOUBLE-STRUCK DAL +1EEA5..1EEA9 ; Arabic # Lo [5] ARABIC MATHEMATICAL DOUBLE-STRUCK WAW..ARABIC MATHEMATICAL DOUBLE-STRUCK YEH +1EEAB..1EEBB ; Arabic # Lo [17] ARABIC MATHEMATICAL DOUBLE-STRUCK LAM..ARABIC MATHEMATICAL DOUBLE-STRUCK GHAIN +1EEF0..1EEF1 ; Arabic # Sm [2] ARABIC MATHEMATICAL OPERATOR MEEM WITH HAH WITH TATWEEL..ARABIC MATHEMATICAL OPERATOR HAH WITH DAL + +# Total code points: 1368 + +# ================================================ + +0700..070D ; Syriac # Po [14] SYRIAC END OF PARAGRAPH..SYRIAC HARKLEAN ASTERISCUS +070F ; Syriac # Cf SYRIAC ABBREVIATION MARK +0710 ; Syriac # Lo SYRIAC LETTER ALAPH +0711 ; Syriac # Mn SYRIAC LETTER SUPERSCRIPT ALAPH +0712..072F ; Syriac # Lo [30] SYRIAC LETTER BETH..SYRIAC LETTER PERSIAN DHALATH +0730..074A ; Syriac # Mn [27] SYRIAC PTHAHA ABOVE..SYRIAC BARREKH +074D..074F ; Syriac # Lo [3] SYRIAC LETTER SOGDIAN ZHAIN..SYRIAC LETTER SOGDIAN FE +0860..086A ; Syriac # Lo [11] SYRIAC LETTER MALAYALAM NGA..SYRIAC LETTER MALAYALAM SSA + +# Total code points: 88 + +# ================================================ + +0780..07A5 ; Thaana # Lo [38] THAANA LETTER HAA..THAANA LETTER WAAVU +07A6..07B0 ; Thaana # Mn [11] THAANA ABAFILI..THAANA SUKUN +07B1 ; Thaana # Lo THAANA LETTER NAA + +# Total code points: 50 + +# ================================================ + +0900..0902 ; Devanagari # Mn [3] DEVANAGARI SIGN INVERTED CANDRABINDU..DEVANAGARI SIGN ANUSVARA +0903 ; Devanagari # Mc DEVANAGARI SIGN VISARGA +0904..0939 ; Devanagari # Lo [54] DEVANAGARI LETTER SHORT A..DEVANAGARI LETTER HA +093A ; Devanagari # Mn DEVANAGARI VOWEL SIGN OE +093B ; Devanagari # Mc DEVANAGARI VOWEL SIGN OOE +093C ; Devanagari # Mn DEVANAGARI SIGN NUKTA +093D ; Devanagari # Lo DEVANAGARI SIGN AVAGRAHA +093E..0940 ; Devanagari # Mc [3] DEVANAGARI VOWEL SIGN AA..DEVANAGARI VOWEL SIGN II +0941..0948 ; Devanagari # Mn [8] DEVANAGARI VOWEL SIGN U..DEVANAGARI VOWEL SIGN AI +0949..094C ; Devanagari # Mc [4] DEVANAGARI VOWEL SIGN CANDRA O..DEVANAGARI VOWEL SIGN AU +094D ; Devanagari # Mn DEVANAGARI SIGN VIRAMA +094E..094F ; Devanagari # Mc [2] DEVANAGARI VOWEL SIGN PRISHTHAMATRA E..DEVANAGARI VOWEL SIGN AW +0950 ; Devanagari # Lo DEVANAGARI OM +0955..0957 ; Devanagari # Mn [3] DEVANAGARI VOWEL SIGN CANDRA LONG E..DEVANAGARI VOWEL SIGN UUE +0958..0961 ; Devanagari # Lo [10] DEVANAGARI LETTER QA..DEVANAGARI LETTER VOCALIC LL +0962..0963 ; Devanagari # Mn [2] DEVANAGARI VOWEL SIGN VOCALIC L..DEVANAGARI VOWEL SIGN VOCALIC LL +0966..096F ; Devanagari # Nd [10] DEVANAGARI DIGIT ZERO..DEVANAGARI DIGIT NINE +0970 ; Devanagari # Po DEVANAGARI ABBREVIATION SIGN +0971 ; Devanagari # Lm DEVANAGARI SIGN HIGH SPACING DOT +0972..097F ; Devanagari # Lo [14] DEVANAGARI LETTER CANDRA A..DEVANAGARI LETTER BBA +A8E0..A8F1 ; Devanagari # Mn [18] COMBINING DEVANAGARI DIGIT ZERO..COMBINING DEVANAGARI SIGN AVAGRAHA +A8F2..A8F7 ; Devanagari # Lo [6] DEVANAGARI SIGN SPACING CANDRABINDU..DEVANAGARI SIGN CANDRABINDU AVAGRAHA +A8F8..A8FA ; Devanagari # Po [3] DEVANAGARI SIGN PUSHPIKA..DEVANAGARI CARET +A8FB ; Devanagari # Lo DEVANAGARI HEADSTROKE +A8FC ; Devanagari # Po DEVANAGARI SIGN SIDDHAM +A8FD..A8FE ; Devanagari # Lo [2] DEVANAGARI JAIN OM..DEVANAGARI LETTER AY +A8FF ; Devanagari # Mn DEVANAGARI VOWEL SIGN AY +11B00..11B09 ; Devanagari # Po [10] DEVANAGARI HEAD MARK..DEVANAGARI SIGN MINDU + +# Total code points: 164 + +# ================================================ + +0980 ; Bengali # Lo BENGALI ANJI +0981 ; Bengali # Mn BENGALI SIGN CANDRABINDU +0982..0983 ; Bengali # Mc [2] BENGALI SIGN ANUSVARA..BENGALI SIGN VISARGA +0985..098C ; Bengali # Lo [8] BENGALI LETTER A..BENGALI LETTER VOCALIC L +098F..0990 ; Bengali # Lo [2] BENGALI LETTER E..BENGALI LETTER AI +0993..09A8 ; Bengali # Lo [22] BENGALI LETTER O..BENGALI LETTER NA +09AA..09B0 ; Bengali # Lo [7] BENGALI LETTER PA..BENGALI LETTER RA +09B2 ; Bengali # Lo BENGALI LETTER LA +09B6..09B9 ; Bengali # Lo [4] BENGALI LETTER SHA..BENGALI LETTER HA +09BC ; Bengali # Mn BENGALI SIGN NUKTA +09BD ; Bengali # Lo BENGALI SIGN AVAGRAHA +09BE..09C0 ; Bengali # Mc [3] BENGALI VOWEL SIGN AA..BENGALI VOWEL SIGN II +09C1..09C4 ; Bengali # Mn [4] BENGALI VOWEL SIGN U..BENGALI VOWEL SIGN VOCALIC RR +09C7..09C8 ; Bengali # Mc [2] BENGALI VOWEL SIGN E..BENGALI VOWEL SIGN AI +09CB..09CC ; Bengali # Mc [2] BENGALI VOWEL SIGN O..BENGALI VOWEL SIGN AU +09CD ; Bengali # Mn BENGALI SIGN VIRAMA +09CE ; Bengali # Lo BENGALI LETTER KHANDA TA +09D7 ; Bengali # Mc BENGALI AU LENGTH MARK +09DC..09DD ; Bengali # Lo [2] BENGALI LETTER RRA..BENGALI LETTER RHA +09DF..09E1 ; Bengali # Lo [3] BENGALI LETTER YYA..BENGALI LETTER VOCALIC LL +09E2..09E3 ; Bengali # Mn [2] BENGALI VOWEL SIGN VOCALIC L..BENGALI VOWEL SIGN VOCALIC LL +09E6..09EF ; Bengali # Nd [10] BENGALI DIGIT ZERO..BENGALI DIGIT NINE +09F0..09F1 ; Bengali # Lo [2] BENGALI LETTER RA WITH MIDDLE DIAGONAL..BENGALI LETTER RA WITH LOWER DIAGONAL +09F2..09F3 ; Bengali # Sc [2] BENGALI RUPEE MARK..BENGALI RUPEE SIGN +09F4..09F9 ; Bengali # No [6] BENGALI CURRENCY NUMERATOR ONE..BENGALI CURRENCY DENOMINATOR SIXTEEN +09FA ; Bengali # So BENGALI ISSHAR +09FB ; Bengali # Sc BENGALI GANDA MARK +09FC ; Bengali # Lo BENGALI LETTER VEDIC ANUSVARA +09FD ; Bengali # Po BENGALI ABBREVIATION SIGN +09FE ; Bengali # Mn BENGALI SANDHI MARK + +# Total code points: 96 + +# ================================================ + +0A01..0A02 ; Gurmukhi # Mn [2] GURMUKHI SIGN ADAK BINDI..GURMUKHI SIGN BINDI +0A03 ; Gurmukhi # Mc GURMUKHI SIGN VISARGA +0A05..0A0A ; Gurmukhi # Lo [6] GURMUKHI LETTER A..GURMUKHI LETTER UU +0A0F..0A10 ; Gurmukhi # Lo [2] GURMUKHI LETTER EE..GURMUKHI LETTER AI +0A13..0A28 ; Gurmukhi # Lo [22] GURMUKHI LETTER OO..GURMUKHI LETTER NA +0A2A..0A30 ; Gurmukhi # Lo [7] GURMUKHI LETTER PA..GURMUKHI LETTER RA +0A32..0A33 ; Gurmukhi # Lo [2] GURMUKHI LETTER LA..GURMUKHI LETTER LLA +0A35..0A36 ; Gurmukhi # Lo [2] GURMUKHI LETTER VA..GURMUKHI LETTER SHA +0A38..0A39 ; Gurmukhi # Lo [2] GURMUKHI LETTER SA..GURMUKHI LETTER HA +0A3C ; Gurmukhi # Mn GURMUKHI SIGN NUKTA +0A3E..0A40 ; Gurmukhi # Mc [3] GURMUKHI VOWEL SIGN AA..GURMUKHI VOWEL SIGN II +0A41..0A42 ; Gurmukhi # Mn [2] GURMUKHI VOWEL SIGN U..GURMUKHI VOWEL SIGN UU +0A47..0A48 ; Gurmukhi # Mn [2] GURMUKHI VOWEL SIGN EE..GURMUKHI VOWEL SIGN AI +0A4B..0A4D ; Gurmukhi # Mn [3] GURMUKHI VOWEL SIGN OO..GURMUKHI SIGN VIRAMA +0A51 ; Gurmukhi # Mn GURMUKHI SIGN UDAAT +0A59..0A5C ; Gurmukhi # Lo [4] GURMUKHI LETTER KHHA..GURMUKHI LETTER RRA +0A5E ; Gurmukhi # Lo GURMUKHI LETTER FA +0A66..0A6F ; Gurmukhi # Nd [10] GURMUKHI DIGIT ZERO..GURMUKHI DIGIT NINE +0A70..0A71 ; Gurmukhi # Mn [2] GURMUKHI TIPPI..GURMUKHI ADDAK +0A72..0A74 ; Gurmukhi # Lo [3] GURMUKHI IRI..GURMUKHI EK ONKAR +0A75 ; Gurmukhi # Mn GURMUKHI SIGN YAKASH +0A76 ; Gurmukhi # Po GURMUKHI ABBREVIATION SIGN + +# Total code points: 80 + +# ================================================ + +0A81..0A82 ; Gujarati # Mn [2] GUJARATI SIGN CANDRABINDU..GUJARATI SIGN ANUSVARA +0A83 ; Gujarati # Mc GUJARATI SIGN VISARGA +0A85..0A8D ; Gujarati # Lo [9] GUJARATI LETTER A..GUJARATI VOWEL CANDRA E +0A8F..0A91 ; Gujarati # Lo [3] GUJARATI LETTER E..GUJARATI VOWEL CANDRA O +0A93..0AA8 ; Gujarati # Lo [22] GUJARATI LETTER O..GUJARATI LETTER NA +0AAA..0AB0 ; Gujarati # Lo [7] GUJARATI LETTER PA..GUJARATI LETTER RA +0AB2..0AB3 ; Gujarati # Lo [2] GUJARATI LETTER LA..GUJARATI LETTER LLA +0AB5..0AB9 ; Gujarati # Lo [5] GUJARATI LETTER VA..GUJARATI LETTER HA +0ABC ; Gujarati # Mn GUJARATI SIGN NUKTA +0ABD ; Gujarati # Lo GUJARATI SIGN AVAGRAHA +0ABE..0AC0 ; Gujarati # Mc [3] GUJARATI VOWEL SIGN AA..GUJARATI VOWEL SIGN II +0AC1..0AC5 ; Gujarati # Mn [5] GUJARATI VOWEL SIGN U..GUJARATI VOWEL SIGN CANDRA E +0AC7..0AC8 ; Gujarati # Mn [2] GUJARATI VOWEL SIGN E..GUJARATI VOWEL SIGN AI +0AC9 ; Gujarati # Mc GUJARATI VOWEL SIGN CANDRA O +0ACB..0ACC ; Gujarati # Mc [2] GUJARATI VOWEL SIGN O..GUJARATI VOWEL SIGN AU +0ACD ; Gujarati # Mn GUJARATI SIGN VIRAMA +0AD0 ; Gujarati # Lo GUJARATI OM +0AE0..0AE1 ; Gujarati # Lo [2] GUJARATI LETTER VOCALIC RR..GUJARATI LETTER VOCALIC LL +0AE2..0AE3 ; Gujarati # Mn [2] GUJARATI VOWEL SIGN VOCALIC L..GUJARATI VOWEL SIGN VOCALIC LL +0AE6..0AEF ; Gujarati # Nd [10] GUJARATI DIGIT ZERO..GUJARATI DIGIT NINE +0AF0 ; Gujarati # Po GUJARATI ABBREVIATION SIGN +0AF1 ; Gujarati # Sc GUJARATI RUPEE SIGN +0AF9 ; Gujarati # Lo GUJARATI LETTER ZHA +0AFA..0AFF ; Gujarati # Mn [6] GUJARATI SIGN SUKUN..GUJARATI SIGN TWO-CIRCLE NUKTA ABOVE + +# Total code points: 91 + +# ================================================ + +0B01 ; Oriya # Mn ORIYA SIGN CANDRABINDU +0B02..0B03 ; Oriya # Mc [2] ORIYA SIGN ANUSVARA..ORIYA SIGN VISARGA +0B05..0B0C ; Oriya # Lo [8] ORIYA LETTER A..ORIYA LETTER VOCALIC L +0B0F..0B10 ; Oriya # Lo [2] ORIYA LETTER E..ORIYA LETTER AI +0B13..0B28 ; Oriya # Lo [22] ORIYA LETTER O..ORIYA LETTER NA +0B2A..0B30 ; Oriya # Lo [7] ORIYA LETTER PA..ORIYA LETTER RA +0B32..0B33 ; Oriya # Lo [2] ORIYA LETTER LA..ORIYA LETTER LLA +0B35..0B39 ; Oriya # Lo [5] ORIYA LETTER VA..ORIYA LETTER HA +0B3C ; Oriya # Mn ORIYA SIGN NUKTA +0B3D ; Oriya # Lo ORIYA SIGN AVAGRAHA +0B3E ; Oriya # Mc ORIYA VOWEL SIGN AA +0B3F ; Oriya # Mn ORIYA VOWEL SIGN I +0B40 ; Oriya # Mc ORIYA VOWEL SIGN II +0B41..0B44 ; Oriya # Mn [4] ORIYA VOWEL SIGN U..ORIYA VOWEL SIGN VOCALIC RR +0B47..0B48 ; Oriya # Mc [2] ORIYA VOWEL SIGN E..ORIYA VOWEL SIGN AI +0B4B..0B4C ; Oriya # Mc [2] ORIYA VOWEL SIGN O..ORIYA VOWEL SIGN AU +0B4D ; Oriya # Mn ORIYA SIGN VIRAMA +0B55..0B56 ; Oriya # Mn [2] ORIYA SIGN OVERLINE..ORIYA AI LENGTH MARK +0B57 ; Oriya # Mc ORIYA AU LENGTH MARK +0B5C..0B5D ; Oriya # Lo [2] ORIYA LETTER RRA..ORIYA LETTER RHA +0B5F..0B61 ; Oriya # Lo [3] ORIYA LETTER YYA..ORIYA LETTER VOCALIC LL +0B62..0B63 ; Oriya # Mn [2] ORIYA VOWEL SIGN VOCALIC L..ORIYA VOWEL SIGN VOCALIC LL +0B66..0B6F ; Oriya # Nd [10] ORIYA DIGIT ZERO..ORIYA DIGIT NINE +0B70 ; Oriya # So ORIYA ISSHAR +0B71 ; Oriya # Lo ORIYA LETTER WA +0B72..0B77 ; Oriya # No [6] ORIYA FRACTION ONE QUARTER..ORIYA FRACTION THREE SIXTEENTHS + +# Total code points: 91 + +# ================================================ + +0B82 ; Tamil # Mn TAMIL SIGN ANUSVARA +0B83 ; Tamil # Lo TAMIL SIGN VISARGA +0B85..0B8A ; Tamil # Lo [6] TAMIL LETTER A..TAMIL LETTER UU +0B8E..0B90 ; Tamil # Lo [3] TAMIL LETTER E..TAMIL LETTER AI +0B92..0B95 ; Tamil # Lo [4] TAMIL LETTER O..TAMIL LETTER KA +0B99..0B9A ; Tamil # Lo [2] TAMIL LETTER NGA..TAMIL LETTER CA +0B9C ; Tamil # Lo TAMIL LETTER JA +0B9E..0B9F ; Tamil # Lo [2] TAMIL LETTER NYA..TAMIL LETTER TTA +0BA3..0BA4 ; Tamil # Lo [2] TAMIL LETTER NNA..TAMIL LETTER TA +0BA8..0BAA ; Tamil # Lo [3] TAMIL LETTER NA..TAMIL LETTER PA +0BAE..0BB9 ; Tamil # Lo [12] TAMIL LETTER MA..TAMIL LETTER HA +0BBE..0BBF ; Tamil # Mc [2] TAMIL VOWEL SIGN AA..TAMIL VOWEL SIGN I +0BC0 ; Tamil # Mn TAMIL VOWEL SIGN II +0BC1..0BC2 ; Tamil # Mc [2] TAMIL VOWEL SIGN U..TAMIL VOWEL SIGN UU +0BC6..0BC8 ; Tamil # Mc [3] TAMIL VOWEL SIGN E..TAMIL VOWEL SIGN AI +0BCA..0BCC ; Tamil # Mc [3] TAMIL VOWEL SIGN O..TAMIL VOWEL SIGN AU +0BCD ; Tamil # Mn TAMIL SIGN VIRAMA +0BD0 ; Tamil # Lo TAMIL OM +0BD7 ; Tamil # Mc TAMIL AU LENGTH MARK +0BE6..0BEF ; Tamil # Nd [10] TAMIL DIGIT ZERO..TAMIL DIGIT NINE +0BF0..0BF2 ; Tamil # No [3] TAMIL NUMBER TEN..TAMIL NUMBER ONE THOUSAND +0BF3..0BF8 ; Tamil # So [6] TAMIL DAY SIGN..TAMIL AS ABOVE SIGN +0BF9 ; Tamil # Sc TAMIL RUPEE SIGN +0BFA ; Tamil # So TAMIL NUMBER SIGN +11FC0..11FD4 ; Tamil # No [21] TAMIL FRACTION ONE THREE-HUNDRED-AND-TWENTIETH..TAMIL FRACTION DOWNSCALING FACTOR KIIZH +11FD5..11FDC ; Tamil # So [8] TAMIL SIGN NEL..TAMIL SIGN MUKKURUNI +11FDD..11FE0 ; Tamil # Sc [4] TAMIL SIGN KAACU..TAMIL SIGN VARAAKAN +11FE1..11FF1 ; Tamil # So [17] TAMIL SIGN PAARAM..TAMIL SIGN VAKAIYARAA +11FFF ; Tamil # Po TAMIL PUNCTUATION END OF TEXT + +# Total code points: 123 + +# ================================================ + +0C00 ; Telugu # Mn TELUGU SIGN COMBINING CANDRABINDU ABOVE +0C01..0C03 ; Telugu # Mc [3] TELUGU SIGN CANDRABINDU..TELUGU SIGN VISARGA +0C04 ; Telugu # Mn TELUGU SIGN COMBINING ANUSVARA ABOVE +0C05..0C0C ; Telugu # Lo [8] TELUGU LETTER A..TELUGU LETTER VOCALIC L +0C0E..0C10 ; Telugu # Lo [3] TELUGU LETTER E..TELUGU LETTER AI +0C12..0C28 ; Telugu # Lo [23] TELUGU LETTER O..TELUGU LETTER NA +0C2A..0C39 ; Telugu # Lo [16] TELUGU LETTER PA..TELUGU LETTER HA +0C3C ; Telugu # Mn TELUGU SIGN NUKTA +0C3D ; Telugu # Lo TELUGU SIGN AVAGRAHA +0C3E..0C40 ; Telugu # Mn [3] TELUGU VOWEL SIGN AA..TELUGU VOWEL SIGN II +0C41..0C44 ; Telugu # Mc [4] TELUGU VOWEL SIGN U..TELUGU VOWEL SIGN VOCALIC RR +0C46..0C48 ; Telugu # Mn [3] TELUGU VOWEL SIGN E..TELUGU VOWEL SIGN AI +0C4A..0C4D ; Telugu # Mn [4] TELUGU VOWEL SIGN O..TELUGU SIGN VIRAMA +0C55..0C56 ; Telugu # Mn [2] TELUGU LENGTH MARK..TELUGU AI LENGTH MARK +0C58..0C5A ; Telugu # Lo [3] TELUGU LETTER TSA..TELUGU LETTER RRRA +0C5D ; Telugu # Lo TELUGU LETTER NAKAARA POLLU +0C60..0C61 ; Telugu # Lo [2] TELUGU LETTER VOCALIC RR..TELUGU LETTER VOCALIC LL +0C62..0C63 ; Telugu # Mn [2] TELUGU VOWEL SIGN VOCALIC L..TELUGU VOWEL SIGN VOCALIC LL +0C66..0C6F ; Telugu # Nd [10] TELUGU DIGIT ZERO..TELUGU DIGIT NINE +0C77 ; Telugu # Po TELUGU SIGN SIDDHAM +0C78..0C7E ; Telugu # No [7] TELUGU FRACTION DIGIT ZERO FOR ODD POWERS OF FOUR..TELUGU FRACTION DIGIT THREE FOR EVEN POWERS OF FOUR +0C7F ; Telugu # So TELUGU SIGN TUUMU + +# Total code points: 100 + +# ================================================ + +0C80 ; Kannada # Lo KANNADA SIGN SPACING CANDRABINDU +0C81 ; Kannada # Mn KANNADA SIGN CANDRABINDU +0C82..0C83 ; Kannada # Mc [2] KANNADA SIGN ANUSVARA..KANNADA SIGN VISARGA +0C84 ; Kannada # Po KANNADA SIGN SIDDHAM +0C85..0C8C ; Kannada # Lo [8] KANNADA LETTER A..KANNADA LETTER VOCALIC L +0C8E..0C90 ; Kannada # Lo [3] KANNADA LETTER E..KANNADA LETTER AI +0C92..0CA8 ; Kannada # Lo [23] KANNADA LETTER O..KANNADA LETTER NA +0CAA..0CB3 ; Kannada # Lo [10] KANNADA LETTER PA..KANNADA LETTER LLA +0CB5..0CB9 ; Kannada # Lo [5] KANNADA LETTER VA..KANNADA LETTER HA +0CBC ; Kannada # Mn KANNADA SIGN NUKTA +0CBD ; Kannada # Lo KANNADA SIGN AVAGRAHA +0CBE ; Kannada # Mc KANNADA VOWEL SIGN AA +0CBF ; Kannada # Mn KANNADA VOWEL SIGN I +0CC0..0CC4 ; Kannada # Mc [5] KANNADA VOWEL SIGN II..KANNADA VOWEL SIGN VOCALIC RR +0CC6 ; Kannada # Mn KANNADA VOWEL SIGN E +0CC7..0CC8 ; Kannada # Mc [2] KANNADA VOWEL SIGN EE..KANNADA VOWEL SIGN AI +0CCA..0CCB ; Kannada # Mc [2] KANNADA VOWEL SIGN O..KANNADA VOWEL SIGN OO +0CCC..0CCD ; Kannada # Mn [2] KANNADA VOWEL SIGN AU..KANNADA SIGN VIRAMA +0CD5..0CD6 ; Kannada # Mc [2] KANNADA LENGTH MARK..KANNADA AI LENGTH MARK +0CDD..0CDE ; Kannada # Lo [2] KANNADA LETTER NAKAARA POLLU..KANNADA LETTER FA +0CE0..0CE1 ; Kannada # Lo [2] KANNADA LETTER VOCALIC RR..KANNADA LETTER VOCALIC LL +0CE2..0CE3 ; Kannada # Mn [2] KANNADA VOWEL SIGN VOCALIC L..KANNADA VOWEL SIGN VOCALIC LL +0CE6..0CEF ; Kannada # Nd [10] KANNADA DIGIT ZERO..KANNADA DIGIT NINE +0CF1..0CF2 ; Kannada # Lo [2] KANNADA SIGN JIHVAMULIYA..KANNADA SIGN UPADHMANIYA +0CF3 ; Kannada # Mc KANNADA SIGN COMBINING ANUSVARA ABOVE RIGHT + +# Total code points: 91 + +# ================================================ + +0D00..0D01 ; Malayalam # Mn [2] MALAYALAM SIGN COMBINING ANUSVARA ABOVE..MALAYALAM SIGN CANDRABINDU +0D02..0D03 ; Malayalam # Mc [2] MALAYALAM SIGN ANUSVARA..MALAYALAM SIGN VISARGA +0D04..0D0C ; Malayalam # Lo [9] MALAYALAM LETTER VEDIC ANUSVARA..MALAYALAM LETTER VOCALIC L +0D0E..0D10 ; Malayalam # Lo [3] MALAYALAM LETTER E..MALAYALAM LETTER AI +0D12..0D3A ; Malayalam # Lo [41] MALAYALAM LETTER O..MALAYALAM LETTER TTTA +0D3B..0D3C ; Malayalam # Mn [2] MALAYALAM SIGN VERTICAL BAR VIRAMA..MALAYALAM SIGN CIRCULAR VIRAMA +0D3D ; Malayalam # Lo MALAYALAM SIGN AVAGRAHA +0D3E..0D40 ; Malayalam # Mc [3] MALAYALAM VOWEL SIGN AA..MALAYALAM VOWEL SIGN II +0D41..0D44 ; Malayalam # Mn [4] MALAYALAM VOWEL SIGN U..MALAYALAM VOWEL SIGN VOCALIC RR +0D46..0D48 ; Malayalam # Mc [3] MALAYALAM VOWEL SIGN E..MALAYALAM VOWEL SIGN AI +0D4A..0D4C ; Malayalam # Mc [3] MALAYALAM VOWEL SIGN O..MALAYALAM VOWEL SIGN AU +0D4D ; Malayalam # Mn MALAYALAM SIGN VIRAMA +0D4E ; Malayalam # Lo MALAYALAM LETTER DOT REPH +0D4F ; Malayalam # So MALAYALAM SIGN PARA +0D54..0D56 ; Malayalam # Lo [3] MALAYALAM LETTER CHILLU M..MALAYALAM LETTER CHILLU LLL +0D57 ; Malayalam # Mc MALAYALAM AU LENGTH MARK +0D58..0D5E ; Malayalam # No [7] MALAYALAM FRACTION ONE ONE-HUNDRED-AND-SIXTIETH..MALAYALAM FRACTION ONE FIFTH +0D5F..0D61 ; Malayalam # Lo [3] MALAYALAM LETTER ARCHAIC II..MALAYALAM LETTER VOCALIC LL +0D62..0D63 ; Malayalam # Mn [2] MALAYALAM VOWEL SIGN VOCALIC L..MALAYALAM VOWEL SIGN VOCALIC LL +0D66..0D6F ; Malayalam # Nd [10] MALAYALAM DIGIT ZERO..MALAYALAM DIGIT NINE +0D70..0D78 ; Malayalam # No [9] MALAYALAM NUMBER TEN..MALAYALAM FRACTION THREE SIXTEENTHS +0D79 ; Malayalam # So MALAYALAM DATE MARK +0D7A..0D7F ; Malayalam # Lo [6] MALAYALAM LETTER CHILLU NN..MALAYALAM LETTER CHILLU K + +# Total code points: 118 + +# ================================================ + +0D81 ; Sinhala # Mn SINHALA SIGN CANDRABINDU +0D82..0D83 ; Sinhala # Mc [2] SINHALA SIGN ANUSVARAYA..SINHALA SIGN VISARGAYA +0D85..0D96 ; Sinhala # Lo [18] SINHALA LETTER AYANNA..SINHALA LETTER AUYANNA +0D9A..0DB1 ; Sinhala # Lo [24] SINHALA LETTER ALPAPRAANA KAYANNA..SINHALA LETTER DANTAJA NAYANNA +0DB3..0DBB ; Sinhala # Lo [9] SINHALA LETTER SANYAKA DAYANNA..SINHALA LETTER RAYANNA +0DBD ; Sinhala # Lo SINHALA LETTER DANTAJA LAYANNA +0DC0..0DC6 ; Sinhala # Lo [7] SINHALA LETTER VAYANNA..SINHALA LETTER FAYANNA +0DCA ; Sinhala # Mn SINHALA SIGN AL-LAKUNA +0DCF..0DD1 ; Sinhala # Mc [3] SINHALA VOWEL SIGN AELA-PILLA..SINHALA VOWEL SIGN DIGA AEDA-PILLA +0DD2..0DD4 ; Sinhala # Mn [3] SINHALA VOWEL SIGN KETTI IS-PILLA..SINHALA VOWEL SIGN KETTI PAA-PILLA +0DD6 ; Sinhala # Mn SINHALA VOWEL SIGN DIGA PAA-PILLA +0DD8..0DDF ; Sinhala # Mc [8] SINHALA VOWEL SIGN GAETTA-PILLA..SINHALA VOWEL SIGN GAYANUKITTA +0DE6..0DEF ; Sinhala # Nd [10] SINHALA LITH DIGIT ZERO..SINHALA LITH DIGIT NINE +0DF2..0DF3 ; Sinhala # Mc [2] SINHALA VOWEL SIGN DIGA GAETTA-PILLA..SINHALA VOWEL SIGN DIGA GAYANUKITTA +0DF4 ; Sinhala # Po SINHALA PUNCTUATION KUNDDALIYA +111E1..111F4 ; Sinhala # No [20] SINHALA ARCHAIC DIGIT ONE..SINHALA ARCHAIC NUMBER ONE THOUSAND + +# Total code points: 111 + +# ================================================ + +0E01..0E30 ; Thai # Lo [48] THAI CHARACTER KO KAI..THAI CHARACTER SARA A +0E31 ; Thai # Mn THAI CHARACTER MAI HAN-AKAT +0E32..0E33 ; Thai # Lo [2] THAI CHARACTER SARA AA..THAI CHARACTER SARA AM +0E34..0E3A ; Thai # Mn [7] THAI CHARACTER SARA I..THAI CHARACTER PHINTHU +0E40..0E45 ; Thai # Lo [6] THAI CHARACTER SARA E..THAI CHARACTER LAKKHANGYAO +0E46 ; Thai # Lm THAI CHARACTER MAIYAMOK +0E47..0E4E ; Thai # Mn [8] THAI CHARACTER MAITAIKHU..THAI CHARACTER YAMAKKAN +0E4F ; Thai # Po THAI CHARACTER FONGMAN +0E50..0E59 ; Thai # Nd [10] THAI DIGIT ZERO..THAI DIGIT NINE +0E5A..0E5B ; Thai # Po [2] THAI CHARACTER ANGKHANKHU..THAI CHARACTER KHOMUT + +# Total code points: 86 + +# ================================================ + +0E81..0E82 ; Lao # Lo [2] LAO LETTER KO..LAO LETTER KHO SUNG +0E84 ; Lao # Lo LAO LETTER KHO TAM +0E86..0E8A ; Lao # Lo [5] LAO LETTER PALI GHA..LAO LETTER SO TAM +0E8C..0EA3 ; Lao # Lo [24] LAO LETTER PALI JHA..LAO LETTER LO LING +0EA5 ; Lao # Lo LAO LETTER LO LOOT +0EA7..0EB0 ; Lao # Lo [10] LAO LETTER WO..LAO VOWEL SIGN A +0EB1 ; Lao # Mn LAO VOWEL SIGN MAI KAN +0EB2..0EB3 ; Lao # Lo [2] LAO VOWEL SIGN AA..LAO VOWEL SIGN AM +0EB4..0EBC ; Lao # Mn [9] LAO VOWEL SIGN I..LAO SEMIVOWEL SIGN LO +0EBD ; Lao # Lo LAO SEMIVOWEL SIGN NYO +0EC0..0EC4 ; Lao # Lo [5] LAO VOWEL SIGN E..LAO VOWEL SIGN AI +0EC6 ; Lao # Lm LAO KO LA +0EC8..0ECE ; Lao # Mn [7] LAO TONE MAI EK..LAO YAMAKKAN +0ED0..0ED9 ; Lao # Nd [10] LAO DIGIT ZERO..LAO DIGIT NINE +0EDC..0EDF ; Lao # Lo [4] LAO HO NO..LAO LETTER KHMU NYO + +# Total code points: 83 + +# ================================================ + +0F00 ; Tibetan # Lo TIBETAN SYLLABLE OM +0F01..0F03 ; Tibetan # So [3] TIBETAN MARK GTER YIG MGO TRUNCATED A..TIBETAN MARK GTER YIG MGO -UM GTER TSHEG MA +0F04..0F12 ; Tibetan # Po [15] TIBETAN MARK INITIAL YIG MGO MDUN MA..TIBETAN MARK RGYA GRAM SHAD +0F13 ; Tibetan # So TIBETAN MARK CARET -DZUD RTAGS ME LONG CAN +0F14 ; Tibetan # Po TIBETAN MARK GTER TSHEG +0F15..0F17 ; Tibetan # So [3] TIBETAN LOGOTYPE SIGN CHAD RTAGS..TIBETAN ASTROLOGICAL SIGN SGRA GCAN -CHAR RTAGS +0F18..0F19 ; Tibetan # Mn [2] TIBETAN ASTROLOGICAL SIGN -KHYUD PA..TIBETAN ASTROLOGICAL SIGN SDONG TSHUGS +0F1A..0F1F ; Tibetan # So [6] TIBETAN SIGN RDEL DKAR GCIG..TIBETAN SIGN RDEL DKAR RDEL NAG +0F20..0F29 ; Tibetan # Nd [10] TIBETAN DIGIT ZERO..TIBETAN DIGIT NINE +0F2A..0F33 ; Tibetan # No [10] TIBETAN DIGIT HALF ONE..TIBETAN DIGIT HALF ZERO +0F34 ; Tibetan # So TIBETAN MARK BSDUS RTAGS +0F35 ; Tibetan # Mn TIBETAN MARK NGAS BZUNG NYI ZLA +0F36 ; Tibetan # So TIBETAN MARK CARET -DZUD RTAGS BZHI MIG CAN +0F37 ; Tibetan # Mn TIBETAN MARK NGAS BZUNG SGOR RTAGS +0F38 ; Tibetan # So TIBETAN MARK CHE MGO +0F39 ; Tibetan # Mn TIBETAN MARK TSA -PHRU +0F3A ; Tibetan # Ps TIBETAN MARK GUG RTAGS GYON +0F3B ; Tibetan # Pe TIBETAN MARK GUG RTAGS GYAS +0F3C ; Tibetan # Ps TIBETAN MARK ANG KHANG GYON +0F3D ; Tibetan # Pe TIBETAN MARK ANG KHANG GYAS +0F3E..0F3F ; Tibetan # Mc [2] TIBETAN SIGN YAR TSHES..TIBETAN SIGN MAR TSHES +0F40..0F47 ; Tibetan # Lo [8] TIBETAN LETTER KA..TIBETAN LETTER JA +0F49..0F6C ; Tibetan # Lo [36] TIBETAN LETTER NYA..TIBETAN LETTER RRA +0F71..0F7E ; Tibetan # Mn [14] TIBETAN VOWEL SIGN AA..TIBETAN SIGN RJES SU NGA RO +0F7F ; Tibetan # Mc TIBETAN SIGN RNAM BCAD +0F80..0F84 ; Tibetan # Mn [5] TIBETAN VOWEL SIGN REVERSED I..TIBETAN MARK HALANTA +0F85 ; Tibetan # Po TIBETAN MARK PALUTA +0F86..0F87 ; Tibetan # Mn [2] TIBETAN SIGN LCI RTAGS..TIBETAN SIGN YANG RTAGS +0F88..0F8C ; Tibetan # Lo [5] TIBETAN SIGN LCE TSA CAN..TIBETAN SIGN INVERTED MCHU CAN +0F8D..0F97 ; Tibetan # Mn [11] TIBETAN SUBJOINED SIGN LCE TSA CAN..TIBETAN SUBJOINED LETTER JA +0F99..0FBC ; Tibetan # Mn [36] TIBETAN SUBJOINED LETTER NYA..TIBETAN SUBJOINED LETTER FIXED-FORM RA +0FBE..0FC5 ; Tibetan # So [8] TIBETAN KU RU KHA..TIBETAN SYMBOL RDO RJE +0FC6 ; Tibetan # Mn TIBETAN SYMBOL PADMA GDAN +0FC7..0FCC ; Tibetan # So [6] TIBETAN SYMBOL RDO RJE RGYA GRAM..TIBETAN SYMBOL NOR BU BZHI -KHYIL +0FCE..0FCF ; Tibetan # So [2] TIBETAN SIGN RDEL NAG RDEL DKAR..TIBETAN SIGN RDEL NAG GSUM +0FD0..0FD4 ; Tibetan # Po [5] TIBETAN MARK BSKA- SHOG GI MGO RGYAN..TIBETAN MARK CLOSING BRDA RNYING YIG MGO SGAB MA +0FD9..0FDA ; Tibetan # Po [2] TIBETAN MARK LEADING MCHAN RTAGS..TIBETAN MARK TRAILING MCHAN RTAGS + +# Total code points: 207 + +# ================================================ + +1000..102A ; Myanmar # Lo [43] MYANMAR LETTER KA..MYANMAR LETTER AU +102B..102C ; Myanmar # Mc [2] MYANMAR VOWEL SIGN TALL AA..MYANMAR VOWEL SIGN AA +102D..1030 ; Myanmar # Mn [4] MYANMAR VOWEL SIGN I..MYANMAR VOWEL SIGN UU +1031 ; Myanmar # Mc MYANMAR VOWEL SIGN E +1032..1037 ; Myanmar # Mn [6] MYANMAR VOWEL SIGN AI..MYANMAR SIGN DOT BELOW +1038 ; Myanmar # Mc MYANMAR SIGN VISARGA +1039..103A ; Myanmar # Mn [2] MYANMAR SIGN VIRAMA..MYANMAR SIGN ASAT +103B..103C ; Myanmar # Mc [2] MYANMAR CONSONANT SIGN MEDIAL YA..MYANMAR CONSONANT SIGN MEDIAL RA +103D..103E ; Myanmar # Mn [2] MYANMAR CONSONANT SIGN MEDIAL WA..MYANMAR CONSONANT SIGN MEDIAL HA +103F ; Myanmar # Lo MYANMAR LETTER GREAT SA +1040..1049 ; Myanmar # Nd [10] MYANMAR DIGIT ZERO..MYANMAR DIGIT NINE +104A..104F ; Myanmar # Po [6] MYANMAR SIGN LITTLE SECTION..MYANMAR SYMBOL GENITIVE +1050..1055 ; Myanmar # Lo [6] MYANMAR LETTER SHA..MYANMAR LETTER VOCALIC LL +1056..1057 ; Myanmar # Mc [2] MYANMAR VOWEL SIGN VOCALIC R..MYANMAR VOWEL SIGN VOCALIC RR +1058..1059 ; Myanmar # Mn [2] MYANMAR VOWEL SIGN VOCALIC L..MYANMAR VOWEL SIGN VOCALIC LL +105A..105D ; Myanmar # Lo [4] MYANMAR LETTER MON NGA..MYANMAR LETTER MON BBE +105E..1060 ; Myanmar # Mn [3] MYANMAR CONSONANT SIGN MON MEDIAL NA..MYANMAR CONSONANT SIGN MON MEDIAL LA +1061 ; Myanmar # Lo MYANMAR LETTER SGAW KAREN SHA +1062..1064 ; Myanmar # Mc [3] MYANMAR VOWEL SIGN SGAW KAREN EU..MYANMAR TONE MARK SGAW KAREN KE PHO +1065..1066 ; Myanmar # Lo [2] MYANMAR LETTER WESTERN PWO KAREN THA..MYANMAR LETTER WESTERN PWO KAREN PWA +1067..106D ; Myanmar # Mc [7] MYANMAR VOWEL SIGN WESTERN PWO KAREN EU..MYANMAR SIGN WESTERN PWO KAREN TONE-5 +106E..1070 ; Myanmar # Lo [3] MYANMAR LETTER EASTERN PWO KAREN NNA..MYANMAR LETTER EASTERN PWO KAREN GHWA +1071..1074 ; Myanmar # Mn [4] MYANMAR VOWEL SIGN GEBA KAREN I..MYANMAR VOWEL SIGN KAYAH EE +1075..1081 ; Myanmar # Lo [13] MYANMAR LETTER SHAN KA..MYANMAR LETTER SHAN HA +1082 ; Myanmar # Mn MYANMAR CONSONANT SIGN SHAN MEDIAL WA +1083..1084 ; Myanmar # Mc [2] MYANMAR VOWEL SIGN SHAN AA..MYANMAR VOWEL SIGN SHAN E +1085..1086 ; Myanmar # Mn [2] MYANMAR VOWEL SIGN SHAN E ABOVE..MYANMAR VOWEL SIGN SHAN FINAL Y +1087..108C ; Myanmar # Mc [6] MYANMAR SIGN SHAN TONE-2..MYANMAR SIGN SHAN COUNCIL TONE-3 +108D ; Myanmar # Mn MYANMAR SIGN SHAN COUNCIL EMPHATIC TONE +108E ; Myanmar # Lo MYANMAR LETTER RUMAI PALAUNG FA +108F ; Myanmar # Mc MYANMAR SIGN RUMAI PALAUNG TONE-5 +1090..1099 ; Myanmar # Nd [10] MYANMAR SHAN DIGIT ZERO..MYANMAR SHAN DIGIT NINE +109A..109C ; Myanmar # Mc [3] MYANMAR SIGN KHAMTI TONE-1..MYANMAR VOWEL SIGN AITON A +109D ; Myanmar # Mn MYANMAR VOWEL SIGN AITON AI +109E..109F ; Myanmar # So [2] MYANMAR SYMBOL SHAN ONE..MYANMAR SYMBOL SHAN EXCLAMATION +A9E0..A9E4 ; Myanmar # Lo [5] MYANMAR LETTER SHAN GHA..MYANMAR LETTER SHAN BHA +A9E5 ; Myanmar # Mn MYANMAR SIGN SHAN SAW +A9E6 ; Myanmar # Lm MYANMAR MODIFIER LETTER SHAN REDUPLICATION +A9E7..A9EF ; Myanmar # Lo [9] MYANMAR LETTER TAI LAING NYA..MYANMAR LETTER TAI LAING NNA +A9F0..A9F9 ; Myanmar # Nd [10] MYANMAR TAI LAING DIGIT ZERO..MYANMAR TAI LAING DIGIT NINE +A9FA..A9FE ; Myanmar # Lo [5] MYANMAR LETTER TAI LAING LLA..MYANMAR LETTER TAI LAING BHA +AA60..AA6F ; Myanmar # Lo [16] MYANMAR LETTER KHAMTI GA..MYANMAR LETTER KHAMTI FA +AA70 ; Myanmar # Lm MYANMAR MODIFIER LETTER KHAMTI REDUPLICATION +AA71..AA76 ; Myanmar # Lo [6] MYANMAR LETTER KHAMTI XA..MYANMAR LOGOGRAM KHAMTI HM +AA77..AA79 ; Myanmar # So [3] MYANMAR SYMBOL AITON EXCLAMATION..MYANMAR SYMBOL AITON TWO +AA7A ; Myanmar # Lo MYANMAR LETTER AITON RA +AA7B ; Myanmar # Mc MYANMAR SIGN PAO KAREN TONE +AA7C ; Myanmar # Mn MYANMAR SIGN TAI LAING TONE-2 +AA7D ; Myanmar # Mc MYANMAR SIGN TAI LAING TONE-5 +AA7E..AA7F ; Myanmar # Lo [2] MYANMAR LETTER SHWE PALAUNG CHA..MYANMAR LETTER SHWE PALAUNG SHA + +# Total code points: 223 + +# ================================================ + +10A0..10C5 ; Georgian # L& [38] GEORGIAN CAPITAL LETTER AN..GEORGIAN CAPITAL LETTER HOE +10C7 ; Georgian # L& GEORGIAN CAPITAL LETTER YN +10CD ; Georgian # L& GEORGIAN CAPITAL LETTER AEN +10D0..10FA ; Georgian # L& [43] GEORGIAN LETTER AN..GEORGIAN LETTER AIN +10FC ; Georgian # Lm MODIFIER LETTER GEORGIAN NAR +10FD..10FF ; Georgian # L& [3] GEORGIAN LETTER AEN..GEORGIAN LETTER LABIAL SIGN +1C90..1CBA ; Georgian # L& [43] GEORGIAN MTAVRULI CAPITAL LETTER AN..GEORGIAN MTAVRULI CAPITAL LETTER AIN +1CBD..1CBF ; Georgian # L& [3] GEORGIAN MTAVRULI CAPITAL LETTER AEN..GEORGIAN MTAVRULI CAPITAL LETTER LABIAL SIGN +2D00..2D25 ; Georgian # L& [38] GEORGIAN SMALL LETTER AN..GEORGIAN SMALL LETTER HOE +2D27 ; Georgian # L& GEORGIAN SMALL LETTER YN +2D2D ; Georgian # L& GEORGIAN SMALL LETTER AEN + +# Total code points: 173 + +# ================================================ + +1100..11FF ; Hangul # Lo [256] HANGUL CHOSEONG KIYEOK..HANGUL JONGSEONG SSANGNIEUN +302E..302F ; Hangul # Mc [2] HANGUL SINGLE DOT TONE MARK..HANGUL DOUBLE DOT TONE MARK +3131..318E ; Hangul # Lo [94] HANGUL LETTER KIYEOK..HANGUL LETTER ARAEAE +3200..321E ; Hangul # So [31] PARENTHESIZED HANGUL KIYEOK..PARENTHESIZED KOREAN CHARACTER O HU +3260..327E ; Hangul # So [31] CIRCLED HANGUL KIYEOK..CIRCLED HANGUL IEUNG U +A960..A97C ; Hangul # Lo [29] HANGUL CHOSEONG TIKEUT-MIEUM..HANGUL CHOSEONG SSANGYEORINHIEUH +AC00..D7A3 ; Hangul # Lo [11172] HANGUL SYLLABLE GA..HANGUL SYLLABLE HIH +D7B0..D7C6 ; Hangul # Lo [23] HANGUL JUNGSEONG O-YEO..HANGUL JUNGSEONG ARAEA-E +D7CB..D7FB ; Hangul # Lo [49] HANGUL JONGSEONG NIEUN-RIEUL..HANGUL JONGSEONG PHIEUPH-THIEUTH +FFA0..FFBE ; Hangul # Lo [31] HALFWIDTH HANGUL FILLER..HALFWIDTH HANGUL LETTER HIEUH +FFC2..FFC7 ; Hangul # Lo [6] HALFWIDTH HANGUL LETTER A..HALFWIDTH HANGUL LETTER E +FFCA..FFCF ; Hangul # Lo [6] HALFWIDTH HANGUL LETTER YEO..HALFWIDTH HANGUL LETTER OE +FFD2..FFD7 ; Hangul # Lo [6] HALFWIDTH HANGUL LETTER YO..HALFWIDTH HANGUL LETTER YU +FFDA..FFDC ; Hangul # Lo [3] HALFWIDTH HANGUL LETTER EU..HALFWIDTH HANGUL LETTER I + +# Total code points: 11739 + +# ================================================ + +1200..1248 ; Ethiopic # Lo [73] ETHIOPIC SYLLABLE HA..ETHIOPIC SYLLABLE QWA +124A..124D ; Ethiopic # Lo [4] ETHIOPIC SYLLABLE QWI..ETHIOPIC SYLLABLE QWE +1250..1256 ; Ethiopic # Lo [7] ETHIOPIC SYLLABLE QHA..ETHIOPIC SYLLABLE QHO +1258 ; Ethiopic # Lo ETHIOPIC SYLLABLE QHWA +125A..125D ; Ethiopic # Lo [4] ETHIOPIC SYLLABLE QHWI..ETHIOPIC SYLLABLE QHWE +1260..1288 ; Ethiopic # Lo [41] ETHIOPIC SYLLABLE BA..ETHIOPIC SYLLABLE XWA +128A..128D ; Ethiopic # Lo [4] ETHIOPIC SYLLABLE XWI..ETHIOPIC SYLLABLE XWE +1290..12B0 ; Ethiopic # Lo [33] ETHIOPIC SYLLABLE NA..ETHIOPIC SYLLABLE KWA +12B2..12B5 ; Ethiopic # Lo [4] ETHIOPIC SYLLABLE KWI..ETHIOPIC SYLLABLE KWE +12B8..12BE ; Ethiopic # Lo [7] ETHIOPIC SYLLABLE KXA..ETHIOPIC SYLLABLE KXO +12C0 ; Ethiopic # Lo ETHIOPIC SYLLABLE KXWA +12C2..12C5 ; Ethiopic # Lo [4] ETHIOPIC SYLLABLE KXWI..ETHIOPIC SYLLABLE KXWE +12C8..12D6 ; Ethiopic # Lo [15] ETHIOPIC SYLLABLE WA..ETHIOPIC SYLLABLE PHARYNGEAL O +12D8..1310 ; Ethiopic # Lo [57] ETHIOPIC SYLLABLE ZA..ETHIOPIC SYLLABLE GWA +1312..1315 ; Ethiopic # Lo [4] ETHIOPIC SYLLABLE GWI..ETHIOPIC SYLLABLE GWE +1318..135A ; Ethiopic # Lo [67] ETHIOPIC SYLLABLE GGA..ETHIOPIC SYLLABLE FYA +135D..135F ; Ethiopic # Mn [3] ETHIOPIC COMBINING GEMINATION AND VOWEL LENGTH MARK..ETHIOPIC COMBINING GEMINATION MARK +1360..1368 ; Ethiopic # Po [9] ETHIOPIC SECTION MARK..ETHIOPIC PARAGRAPH SEPARATOR +1369..137C ; Ethiopic # No [20] ETHIOPIC DIGIT ONE..ETHIOPIC NUMBER TEN THOUSAND +1380..138F ; Ethiopic # Lo [16] ETHIOPIC SYLLABLE SEBATBEIT MWA..ETHIOPIC SYLLABLE PWE +1390..1399 ; Ethiopic # So [10] ETHIOPIC TONAL MARK YIZET..ETHIOPIC TONAL MARK KURT +2D80..2D96 ; Ethiopic # Lo [23] ETHIOPIC SYLLABLE LOA..ETHIOPIC SYLLABLE GGWE +2DA0..2DA6 ; Ethiopic # Lo [7] ETHIOPIC SYLLABLE SSA..ETHIOPIC SYLLABLE SSO +2DA8..2DAE ; Ethiopic # Lo [7] ETHIOPIC SYLLABLE CCA..ETHIOPIC SYLLABLE CCO +2DB0..2DB6 ; Ethiopic # Lo [7] ETHIOPIC SYLLABLE ZZA..ETHIOPIC SYLLABLE ZZO +2DB8..2DBE ; Ethiopic # Lo [7] ETHIOPIC SYLLABLE CCHA..ETHIOPIC SYLLABLE CCHO +2DC0..2DC6 ; Ethiopic # Lo [7] ETHIOPIC SYLLABLE QYA..ETHIOPIC SYLLABLE QYO +2DC8..2DCE ; Ethiopic # Lo [7] ETHIOPIC SYLLABLE KYA..ETHIOPIC SYLLABLE KYO +2DD0..2DD6 ; Ethiopic # Lo [7] ETHIOPIC SYLLABLE XYA..ETHIOPIC SYLLABLE XYO +2DD8..2DDE ; Ethiopic # Lo [7] ETHIOPIC SYLLABLE GYA..ETHIOPIC SYLLABLE GYO +AB01..AB06 ; Ethiopic # Lo [6] ETHIOPIC SYLLABLE TTHU..ETHIOPIC SYLLABLE TTHO +AB09..AB0E ; Ethiopic # Lo [6] ETHIOPIC SYLLABLE DDHU..ETHIOPIC SYLLABLE DDHO +AB11..AB16 ; Ethiopic # Lo [6] ETHIOPIC SYLLABLE DZU..ETHIOPIC SYLLABLE DZO +AB20..AB26 ; Ethiopic # Lo [7] ETHIOPIC SYLLABLE CCHHA..ETHIOPIC SYLLABLE CCHHO +AB28..AB2E ; Ethiopic # Lo [7] ETHIOPIC SYLLABLE BBA..ETHIOPIC SYLLABLE BBO +1E7E0..1E7E6 ; Ethiopic # Lo [7] ETHIOPIC SYLLABLE HHYA..ETHIOPIC SYLLABLE HHYO +1E7E8..1E7EB ; Ethiopic # Lo [4] ETHIOPIC SYLLABLE GURAGE HHWA..ETHIOPIC SYLLABLE HHWE +1E7ED..1E7EE ; Ethiopic # Lo [2] ETHIOPIC SYLLABLE GURAGE MWI..ETHIOPIC SYLLABLE GURAGE MWEE +1E7F0..1E7FE ; Ethiopic # Lo [15] ETHIOPIC SYLLABLE GURAGE QWI..ETHIOPIC SYLLABLE GURAGE PWEE + +# Total code points: 523 + +# ================================================ + +13A0..13F5 ; Cherokee # L& [86] CHEROKEE LETTER A..CHEROKEE LETTER MV +13F8..13FD ; Cherokee # L& [6] CHEROKEE SMALL LETTER YE..CHEROKEE SMALL LETTER MV +AB70..ABBF ; Cherokee # L& [80] CHEROKEE SMALL LETTER A..CHEROKEE SMALL LETTER YA + +# Total code points: 172 + +# ================================================ + +1400 ; Canadian_Aboriginal # Pd CANADIAN SYLLABICS HYPHEN +1401..166C ; Canadian_Aboriginal # Lo [620] CANADIAN SYLLABICS E..CANADIAN SYLLABICS CARRIER TTSA +166D ; Canadian_Aboriginal # So CANADIAN SYLLABICS CHI SIGN +166E ; Canadian_Aboriginal # Po CANADIAN SYLLABICS FULL STOP +166F..167F ; Canadian_Aboriginal # Lo [17] CANADIAN SYLLABICS QAI..CANADIAN SYLLABICS BLACKFOOT W +18B0..18F5 ; Canadian_Aboriginal # Lo [70] CANADIAN SYLLABICS OY..CANADIAN SYLLABICS CARRIER DENTAL S +11AB0..11ABF ; Canadian_Aboriginal # Lo [16] CANADIAN SYLLABICS NATTILIK HI..CANADIAN SYLLABICS SPA + +# Total code points: 726 + +# ================================================ + +1680 ; Ogham # Zs OGHAM SPACE MARK +1681..169A ; Ogham # Lo [26] OGHAM LETTER BEITH..OGHAM LETTER PEITH +169B ; Ogham # Ps OGHAM FEATHER MARK +169C ; Ogham # Pe OGHAM REVERSED FEATHER MARK + +# Total code points: 29 + +# ================================================ + +16A0..16EA ; Runic # Lo [75] RUNIC LETTER FEHU FEOH FE F..RUNIC LETTER X +16EE..16F0 ; Runic # Nl [3] RUNIC ARLAUG SYMBOL..RUNIC BELGTHOR SYMBOL +16F1..16F8 ; Runic # Lo [8] RUNIC LETTER K..RUNIC LETTER FRANKS CASKET AESC + +# Total code points: 86 + +# ================================================ + +1780..17B3 ; Khmer # Lo [52] KHMER LETTER KA..KHMER INDEPENDENT VOWEL QAU +17B4..17B5 ; Khmer # Mn [2] KHMER VOWEL INHERENT AQ..KHMER VOWEL INHERENT AA +17B6 ; Khmer # Mc KHMER VOWEL SIGN AA +17B7..17BD ; Khmer # Mn [7] KHMER VOWEL SIGN I..KHMER VOWEL SIGN UA +17BE..17C5 ; Khmer # Mc [8] KHMER VOWEL SIGN OE..KHMER VOWEL SIGN AU +17C6 ; Khmer # Mn KHMER SIGN NIKAHIT +17C7..17C8 ; Khmer # Mc [2] KHMER SIGN REAHMUK..KHMER SIGN YUUKALEAPINTU +17C9..17D3 ; Khmer # Mn [11] KHMER SIGN MUUSIKATOAN..KHMER SIGN BATHAMASAT +17D4..17D6 ; Khmer # Po [3] KHMER SIGN KHAN..KHMER SIGN CAMNUC PII KUUH +17D7 ; Khmer # Lm KHMER SIGN LEK TOO +17D8..17DA ; Khmer # Po [3] KHMER SIGN BEYYAL..KHMER SIGN KOOMUUT +17DB ; Khmer # Sc KHMER CURRENCY SYMBOL RIEL +17DC ; Khmer # Lo KHMER SIGN AVAKRAHASANYA +17DD ; Khmer # Mn KHMER SIGN ATTHACAN +17E0..17E9 ; Khmer # Nd [10] KHMER DIGIT ZERO..KHMER DIGIT NINE +17F0..17F9 ; Khmer # No [10] KHMER SYMBOL LEK ATTAK SON..KHMER SYMBOL LEK ATTAK PRAM-BUON +19E0..19FF ; Khmer # So [32] KHMER SYMBOL PATHAMASAT..KHMER SYMBOL DAP-PRAM ROC + +# Total code points: 146 + +# ================================================ + +1800..1801 ; Mongolian # Po [2] MONGOLIAN BIRGA..MONGOLIAN ELLIPSIS +1804 ; Mongolian # Po MONGOLIAN COLON +1806 ; Mongolian # Pd MONGOLIAN TODO SOFT HYPHEN +1807..180A ; Mongolian # Po [4] MONGOLIAN SIBE SYLLABLE BOUNDARY MARKER..MONGOLIAN NIRUGU +180B..180D ; Mongolian # Mn [3] MONGOLIAN FREE VARIATION SELECTOR ONE..MONGOLIAN FREE VARIATION SELECTOR THREE +180E ; Mongolian # Cf MONGOLIAN VOWEL SEPARATOR +180F ; Mongolian # Mn MONGOLIAN FREE VARIATION SELECTOR FOUR +1810..1819 ; Mongolian # Nd [10] MONGOLIAN DIGIT ZERO..MONGOLIAN DIGIT NINE +1820..1842 ; Mongolian # Lo [35] MONGOLIAN LETTER A..MONGOLIAN LETTER CHI +1843 ; Mongolian # Lm MONGOLIAN LETTER TODO LONG VOWEL SIGN +1844..1878 ; Mongolian # Lo [53] MONGOLIAN LETTER TODO E..MONGOLIAN LETTER CHA WITH TWO DOTS +1880..1884 ; Mongolian # Lo [5] MONGOLIAN LETTER ALI GALI ANUSVARA ONE..MONGOLIAN LETTER ALI GALI INVERTED UBADAMA +1885..1886 ; Mongolian # Mn [2] MONGOLIAN LETTER ALI GALI BALUDA..MONGOLIAN LETTER ALI GALI THREE BALUDA +1887..18A8 ; Mongolian # Lo [34] MONGOLIAN LETTER ALI GALI A..MONGOLIAN LETTER MANCHU ALI GALI BHA +18A9 ; Mongolian # Mn MONGOLIAN LETTER ALI GALI DAGALGA +18AA ; Mongolian # Lo MONGOLIAN LETTER MANCHU ALI GALI LHA +11660..1166C ; Mongolian # Po [13] MONGOLIAN BIRGA WITH ORNAMENT..MONGOLIAN TURNED SWIRL BIRGA WITH DOUBLE ORNAMENT + +# Total code points: 168 + +# ================================================ + +3041..3096 ; Hiragana # Lo [86] HIRAGANA LETTER SMALL A..HIRAGANA LETTER SMALL KE +309D..309E ; Hiragana # Lm [2] HIRAGANA ITERATION MARK..HIRAGANA VOICED ITERATION MARK +309F ; Hiragana # Lo HIRAGANA DIGRAPH YORI +1B001..1B11F ; Hiragana # Lo [287] HIRAGANA LETTER ARCHAIC YE..HIRAGANA LETTER ARCHAIC WU +1B132 ; Hiragana # Lo HIRAGANA LETTER SMALL KO +1B150..1B152 ; Hiragana # Lo [3] HIRAGANA LETTER SMALL WI..HIRAGANA LETTER SMALL WO +1F200 ; Hiragana # So SQUARE HIRAGANA HOKA + +# Total code points: 381 + +# ================================================ + +30A1..30FA ; Katakana # Lo [90] KATAKANA LETTER SMALL A..KATAKANA LETTER VO +30FD..30FE ; Katakana # Lm [2] KATAKANA ITERATION MARK..KATAKANA VOICED ITERATION MARK +30FF ; Katakana # Lo KATAKANA DIGRAPH KOTO +31F0..31FF ; Katakana # Lo [16] KATAKANA LETTER SMALL KU..KATAKANA LETTER SMALL RO +32D0..32FE ; Katakana # So [47] CIRCLED KATAKANA A..CIRCLED KATAKANA WO +3300..3357 ; Katakana # So [88] SQUARE APAATO..SQUARE WATTO +FF66..FF6F ; Katakana # Lo [10] HALFWIDTH KATAKANA LETTER WO..HALFWIDTH KATAKANA LETTER SMALL TU +FF71..FF9D ; Katakana # Lo [45] HALFWIDTH KATAKANA LETTER A..HALFWIDTH KATAKANA LETTER N +1AFF0..1AFF3 ; Katakana # Lm [4] KATAKANA LETTER MINNAN TONE-2..KATAKANA LETTER MINNAN TONE-5 +1AFF5..1AFFB ; Katakana # Lm [7] KATAKANA LETTER MINNAN TONE-7..KATAKANA LETTER MINNAN NASALIZED TONE-5 +1AFFD..1AFFE ; Katakana # Lm [2] KATAKANA LETTER MINNAN NASALIZED TONE-7..KATAKANA LETTER MINNAN NASALIZED TONE-8 +1B000 ; Katakana # Lo KATAKANA LETTER ARCHAIC E +1B120..1B122 ; Katakana # Lo [3] KATAKANA LETTER ARCHAIC YI..KATAKANA LETTER ARCHAIC WU +1B155 ; Katakana # Lo KATAKANA LETTER SMALL KO +1B164..1B167 ; Katakana # Lo [4] KATAKANA LETTER SMALL WI..KATAKANA LETTER SMALL N + +# Total code points: 321 + +# ================================================ + +02EA..02EB ; Bopomofo # Sk [2] MODIFIER LETTER YIN DEPARTING TONE MARK..MODIFIER LETTER YANG DEPARTING TONE MARK +3105..312F ; Bopomofo # Lo [43] BOPOMOFO LETTER B..BOPOMOFO LETTER NN +31A0..31BF ; Bopomofo # Lo [32] BOPOMOFO LETTER BU..BOPOMOFO LETTER AH + +# Total code points: 77 + +# ================================================ + +2E80..2E99 ; Han # So [26] CJK RADICAL REPEAT..CJK RADICAL RAP +2E9B..2EF3 ; Han # So [89] CJK RADICAL CHOKE..CJK RADICAL C-SIMPLIFIED TURTLE +2F00..2FD5 ; Han # So [214] KANGXI RADICAL ONE..KANGXI RADICAL FLUTE +3005 ; Han # Lm IDEOGRAPHIC ITERATION MARK +3007 ; Han # Nl IDEOGRAPHIC NUMBER ZERO +3021..3029 ; Han # Nl [9] HANGZHOU NUMERAL ONE..HANGZHOU NUMERAL NINE +3038..303A ; Han # Nl [3] HANGZHOU NUMERAL TEN..HANGZHOU NUMERAL THIRTY +303B ; Han # Lm VERTICAL IDEOGRAPHIC ITERATION MARK +3400..4DBF ; Han # Lo [6592] CJK UNIFIED IDEOGRAPH-3400..CJK UNIFIED IDEOGRAPH-4DBF +4E00..9FFF ; Han # Lo [20992] CJK UNIFIED IDEOGRAPH-4E00..CJK UNIFIED IDEOGRAPH-9FFF +F900..FA6D ; Han # Lo [366] CJK COMPATIBILITY IDEOGRAPH-F900..CJK COMPATIBILITY IDEOGRAPH-FA6D +FA70..FAD9 ; Han # Lo [106] CJK COMPATIBILITY IDEOGRAPH-FA70..CJK COMPATIBILITY IDEOGRAPH-FAD9 +16FE2 ; Han # Po OLD CHINESE HOOK MARK +16FE3 ; Han # Lm OLD CHINESE ITERATION MARK +16FF0..16FF1 ; Han # Mc [2] VIETNAMESE ALTERNATE READING MARK CA..VIETNAMESE ALTERNATE READING MARK NHAY +20000..2A6DF ; Han # Lo [42720] CJK UNIFIED IDEOGRAPH-20000..CJK UNIFIED IDEOGRAPH-2A6DF +2A700..2B739 ; Han # Lo [4154] CJK UNIFIED IDEOGRAPH-2A700..CJK UNIFIED IDEOGRAPH-2B739 +2B740..2B81D ; Han # Lo [222] CJK UNIFIED IDEOGRAPH-2B740..CJK UNIFIED IDEOGRAPH-2B81D +2B820..2CEA1 ; Han # Lo [5762] CJK UNIFIED IDEOGRAPH-2B820..CJK UNIFIED IDEOGRAPH-2CEA1 +2CEB0..2EBE0 ; Han # Lo [7473] CJK UNIFIED IDEOGRAPH-2CEB0..CJK UNIFIED IDEOGRAPH-2EBE0 +2EBF0..2EE5D ; Han # Lo [622] CJK UNIFIED IDEOGRAPH-2EBF0..CJK UNIFIED IDEOGRAPH-2EE5D +2F800..2FA1D ; Han # Lo [542] CJK COMPATIBILITY IDEOGRAPH-2F800..CJK COMPATIBILITY IDEOGRAPH-2FA1D +30000..3134A ; Han # Lo [4939] CJK UNIFIED IDEOGRAPH-30000..CJK UNIFIED IDEOGRAPH-3134A +31350..323AF ; Han # Lo [4192] CJK UNIFIED IDEOGRAPH-31350..CJK UNIFIED IDEOGRAPH-323AF + +# Total code points: 99030 + +# ================================================ + +A000..A014 ; Yi # Lo [21] YI SYLLABLE IT..YI SYLLABLE E +A015 ; Yi # Lm YI SYLLABLE WU +A016..A48C ; Yi # Lo [1143] YI SYLLABLE BIT..YI SYLLABLE YYR +A490..A4C6 ; Yi # So [55] YI RADICAL QOT..YI RADICAL KE + +# Total code points: 1220 + +# ================================================ + +10300..1031F ; Old_Italic # Lo [32] OLD ITALIC LETTER A..OLD ITALIC LETTER ESS +10320..10323 ; Old_Italic # No [4] OLD ITALIC NUMERAL ONE..OLD ITALIC NUMERAL FIFTY +1032D..1032F ; Old_Italic # Lo [3] OLD ITALIC LETTER YE..OLD ITALIC LETTER SOUTHERN TSE + +# Total code points: 39 + +# ================================================ + +10330..10340 ; Gothic # Lo [17] GOTHIC LETTER AHSA..GOTHIC LETTER PAIRTHRA +10341 ; Gothic # Nl GOTHIC LETTER NINETY +10342..10349 ; Gothic # Lo [8] GOTHIC LETTER RAIDA..GOTHIC LETTER OTHAL +1034A ; Gothic # Nl GOTHIC LETTER NINE HUNDRED + +# Total code points: 27 + +# ================================================ + +10400..1044F ; Deseret # L& [80] DESERET CAPITAL LETTER LONG I..DESERET SMALL LETTER EW + +# Total code points: 80 + +# ================================================ + +0300..036F ; Inherited # Mn [112] COMBINING GRAVE ACCENT..COMBINING LATIN SMALL LETTER X +0485..0486 ; Inherited # Mn [2] COMBINING CYRILLIC DASIA PNEUMATA..COMBINING CYRILLIC PSILI PNEUMATA +064B..0655 ; Inherited # Mn [11] ARABIC FATHATAN..ARABIC HAMZA BELOW +0670 ; Inherited # Mn ARABIC LETTER SUPERSCRIPT ALEF +0951..0954 ; Inherited # Mn [4] DEVANAGARI STRESS SIGN UDATTA..DEVANAGARI ACUTE ACCENT +1AB0..1ABD ; Inherited # Mn [14] COMBINING DOUBLED CIRCUMFLEX ACCENT..COMBINING PARENTHESES BELOW +1ABE ; Inherited # Me COMBINING PARENTHESES OVERLAY +1ABF..1ACE ; Inherited # Mn [16] COMBINING LATIN SMALL LETTER W BELOW..COMBINING LATIN SMALL LETTER INSULAR T +1CD0..1CD2 ; Inherited # Mn [3] VEDIC TONE KARSHANA..VEDIC TONE PRENKHA +1CD4..1CE0 ; Inherited # Mn [13] VEDIC SIGN YAJURVEDIC MIDLINE SVARITA..VEDIC TONE RIGVEDIC KASHMIRI INDEPENDENT SVARITA +1CE2..1CE8 ; Inherited # Mn [7] VEDIC SIGN VISARGA SVARITA..VEDIC SIGN VISARGA ANUDATTA WITH TAIL +1CED ; Inherited # Mn VEDIC SIGN TIRYAK +1CF4 ; Inherited # Mn VEDIC TONE CANDRA ABOVE +1CF8..1CF9 ; Inherited # Mn [2] VEDIC TONE RING ABOVE..VEDIC TONE DOUBLE RING ABOVE +1DC0..1DFF ; Inherited # Mn [64] COMBINING DOTTED GRAVE ACCENT..COMBINING RIGHT ARROWHEAD AND DOWN ARROWHEAD BELOW +200C..200D ; Inherited # Cf [2] ZERO WIDTH NON-JOINER..ZERO WIDTH JOINER +20D0..20DC ; Inherited # Mn [13] COMBINING LEFT HARPOON ABOVE..COMBINING FOUR DOTS ABOVE +20DD..20E0 ; Inherited # Me [4] COMBINING ENCLOSING CIRCLE..COMBINING ENCLOSING CIRCLE BACKSLASH +20E1 ; Inherited # Mn COMBINING LEFT RIGHT ARROW ABOVE +20E2..20E4 ; Inherited # Me [3] COMBINING ENCLOSING SCREEN..COMBINING ENCLOSING UPWARD POINTING TRIANGLE +20E5..20F0 ; Inherited # Mn [12] COMBINING REVERSE SOLIDUS OVERLAY..COMBINING ASTERISK ABOVE +302A..302D ; Inherited # Mn [4] IDEOGRAPHIC LEVEL TONE MARK..IDEOGRAPHIC ENTERING TONE MARK +3099..309A ; Inherited # Mn [2] COMBINING KATAKANA-HIRAGANA VOICED SOUND MARK..COMBINING KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK +FE00..FE0F ; Inherited # Mn [16] VARIATION SELECTOR-1..VARIATION SELECTOR-16 +FE20..FE2D ; Inherited # Mn [14] COMBINING LIGATURE LEFT HALF..COMBINING CONJOINING MACRON BELOW +101FD ; Inherited # Mn PHAISTOS DISC SIGN COMBINING OBLIQUE STROKE +102E0 ; Inherited # Mn COPTIC EPACT THOUSANDS MARK +1133B ; Inherited # Mn COMBINING BINDU BELOW +1CF00..1CF2D ; Inherited # Mn [46] ZNAMENNY COMBINING MARK GORAZDO NIZKO S KRYZHEM ON LEFT..ZNAMENNY COMBINING MARK KRYZH ON LEFT +1CF30..1CF46 ; Inherited # Mn [23] ZNAMENNY COMBINING TONAL RANGE MARK MRACHNO..ZNAMENNY PRIZNAK MODIFIER ROG +1D167..1D169 ; Inherited # Mn [3] MUSICAL SYMBOL COMBINING TREMOLO-1..MUSICAL SYMBOL COMBINING TREMOLO-3 +1D17B..1D182 ; Inherited # Mn [8] MUSICAL SYMBOL COMBINING ACCENT..MUSICAL SYMBOL COMBINING LOURE +1D185..1D18B ; Inherited # Mn [7] MUSICAL SYMBOL COMBINING DOIT..MUSICAL SYMBOL COMBINING TRIPLE TONGUE +1D1AA..1D1AD ; Inherited # Mn [4] MUSICAL SYMBOL COMBINING DOWN BOW..MUSICAL SYMBOL COMBINING SNAP PIZZICATO +E0100..E01EF ; Inherited # Mn [240] VARIATION SELECTOR-17..VARIATION SELECTOR-256 + +# Total code points: 657 + +# ================================================ + +1700..1711 ; Tagalog # Lo [18] TAGALOG LETTER A..TAGALOG LETTER HA +1712..1714 ; Tagalog # Mn [3] TAGALOG VOWEL SIGN I..TAGALOG SIGN VIRAMA +1715 ; Tagalog # Mc TAGALOG SIGN PAMUDPOD +171F ; Tagalog # Lo TAGALOG LETTER ARCHAIC RA + +# Total code points: 23 + +# ================================================ + +1720..1731 ; Hanunoo # Lo [18] HANUNOO LETTER A..HANUNOO LETTER HA +1732..1733 ; Hanunoo # Mn [2] HANUNOO VOWEL SIGN I..HANUNOO VOWEL SIGN U +1734 ; Hanunoo # Mc HANUNOO SIGN PAMUDPOD + +# Total code points: 21 + +# ================================================ + +1740..1751 ; Buhid # Lo [18] BUHID LETTER A..BUHID LETTER HA +1752..1753 ; Buhid # Mn [2] BUHID VOWEL SIGN I..BUHID VOWEL SIGN U + +# Total code points: 20 + +# ================================================ + +1760..176C ; Tagbanwa # Lo [13] TAGBANWA LETTER A..TAGBANWA LETTER YA +176E..1770 ; Tagbanwa # Lo [3] TAGBANWA LETTER LA..TAGBANWA LETTER SA +1772..1773 ; Tagbanwa # Mn [2] TAGBANWA VOWEL SIGN I..TAGBANWA VOWEL SIGN U + +# Total code points: 18 + +# ================================================ + +1900..191E ; Limbu # Lo [31] LIMBU VOWEL-CARRIER LETTER..LIMBU LETTER TRA +1920..1922 ; Limbu # Mn [3] LIMBU VOWEL SIGN A..LIMBU VOWEL SIGN U +1923..1926 ; Limbu # Mc [4] LIMBU VOWEL SIGN EE..LIMBU VOWEL SIGN AU +1927..1928 ; Limbu # Mn [2] LIMBU VOWEL SIGN E..LIMBU VOWEL SIGN O +1929..192B ; Limbu # Mc [3] LIMBU SUBJOINED LETTER YA..LIMBU SUBJOINED LETTER WA +1930..1931 ; Limbu # Mc [2] LIMBU SMALL LETTER KA..LIMBU SMALL LETTER NGA +1932 ; Limbu # Mn LIMBU SMALL LETTER ANUSVARA +1933..1938 ; Limbu # Mc [6] LIMBU SMALL LETTER TA..LIMBU SMALL LETTER LA +1939..193B ; Limbu # Mn [3] LIMBU SIGN MUKPHRENG..LIMBU SIGN SA-I +1940 ; Limbu # So LIMBU SIGN LOO +1944..1945 ; Limbu # Po [2] LIMBU EXCLAMATION MARK..LIMBU QUESTION MARK +1946..194F ; Limbu # Nd [10] LIMBU DIGIT ZERO..LIMBU DIGIT NINE + +# Total code points: 68 + +# ================================================ + +1950..196D ; Tai_Le # Lo [30] TAI LE LETTER KA..TAI LE LETTER AI +1970..1974 ; Tai_Le # Lo [5] TAI LE LETTER TONE-2..TAI LE LETTER TONE-6 + +# Total code points: 35 + +# ================================================ + +10000..1000B ; Linear_B # Lo [12] LINEAR B SYLLABLE B008 A..LINEAR B SYLLABLE B046 JE +1000D..10026 ; Linear_B # Lo [26] LINEAR B SYLLABLE B036 JO..LINEAR B SYLLABLE B032 QO +10028..1003A ; Linear_B # Lo [19] LINEAR B SYLLABLE B060 RA..LINEAR B SYLLABLE B042 WO +1003C..1003D ; Linear_B # Lo [2] LINEAR B SYLLABLE B017 ZA..LINEAR B SYLLABLE B074 ZE +1003F..1004D ; Linear_B # Lo [15] LINEAR B SYLLABLE B020 ZO..LINEAR B SYLLABLE B091 TWO +10050..1005D ; Linear_B # Lo [14] LINEAR B SYMBOL B018..LINEAR B SYMBOL B089 +10080..100FA ; Linear_B # Lo [123] LINEAR B IDEOGRAM B100 MAN..LINEAR B IDEOGRAM VESSEL B305 + +# Total code points: 211 + +# ================================================ + +10380..1039D ; Ugaritic # Lo [30] UGARITIC LETTER ALPA..UGARITIC LETTER SSU +1039F ; Ugaritic # Po UGARITIC WORD DIVIDER + +# Total code points: 31 + +# ================================================ + +10450..1047F ; Shavian # Lo [48] SHAVIAN LETTER PEEP..SHAVIAN LETTER YEW + +# Total code points: 48 + +# ================================================ + +10480..1049D ; Osmanya # Lo [30] OSMANYA LETTER ALEF..OSMANYA LETTER OO +104A0..104A9 ; Osmanya # Nd [10] OSMANYA DIGIT ZERO..OSMANYA DIGIT NINE + +# Total code points: 40 + +# ================================================ + +10800..10805 ; Cypriot # Lo [6] CYPRIOT SYLLABLE A..CYPRIOT SYLLABLE JA +10808 ; Cypriot # Lo CYPRIOT SYLLABLE JO +1080A..10835 ; Cypriot # Lo [44] CYPRIOT SYLLABLE KA..CYPRIOT SYLLABLE WO +10837..10838 ; Cypriot # Lo [2] CYPRIOT SYLLABLE XA..CYPRIOT SYLLABLE XE +1083C ; Cypriot # Lo CYPRIOT SYLLABLE ZA +1083F ; Cypriot # Lo CYPRIOT SYLLABLE ZO + +# Total code points: 55 + +# ================================================ + +2800..28FF ; Braille # So [256] BRAILLE PATTERN BLANK..BRAILLE PATTERN DOTS-12345678 + +# Total code points: 256 + +# ================================================ + +1A00..1A16 ; Buginese # Lo [23] BUGINESE LETTER KA..BUGINESE LETTER HA +1A17..1A18 ; Buginese # Mn [2] BUGINESE VOWEL SIGN I..BUGINESE VOWEL SIGN U +1A19..1A1A ; Buginese # Mc [2] BUGINESE VOWEL SIGN E..BUGINESE VOWEL SIGN O +1A1B ; Buginese # Mn BUGINESE VOWEL SIGN AE +1A1E..1A1F ; Buginese # Po [2] BUGINESE PALLAWA..BUGINESE END OF SECTION + +# Total code points: 30 + +# ================================================ + +03E2..03EF ; Coptic # L& [14] COPTIC CAPITAL LETTER SHEI..COPTIC SMALL LETTER DEI +2C80..2CE4 ; Coptic # L& [101] COPTIC CAPITAL LETTER ALFA..COPTIC SYMBOL KAI +2CE5..2CEA ; Coptic # So [6] COPTIC SYMBOL MI RO..COPTIC SYMBOL SHIMA SIMA +2CEB..2CEE ; Coptic # L& [4] COPTIC CAPITAL LETTER CRYPTOGRAMMIC SHEI..COPTIC SMALL LETTER CRYPTOGRAMMIC GANGIA +2CEF..2CF1 ; Coptic # Mn [3] COPTIC COMBINING NI ABOVE..COPTIC COMBINING SPIRITUS LENIS +2CF2..2CF3 ; Coptic # L& [2] COPTIC CAPITAL LETTER BOHAIRIC KHEI..COPTIC SMALL LETTER BOHAIRIC KHEI +2CF9..2CFC ; Coptic # Po [4] COPTIC OLD NUBIAN FULL STOP..COPTIC OLD NUBIAN VERSE DIVIDER +2CFD ; Coptic # No COPTIC FRACTION ONE HALF +2CFE..2CFF ; Coptic # Po [2] COPTIC FULL STOP..COPTIC MORPHOLOGICAL DIVIDER + +# Total code points: 137 + +# ================================================ + +1980..19AB ; New_Tai_Lue # Lo [44] NEW TAI LUE LETTER HIGH QA..NEW TAI LUE LETTER LOW SUA +19B0..19C9 ; New_Tai_Lue # Lo [26] NEW TAI LUE VOWEL SIGN VOWEL SHORTENER..NEW TAI LUE TONE MARK-2 +19D0..19D9 ; New_Tai_Lue # Nd [10] NEW TAI LUE DIGIT ZERO..NEW TAI LUE DIGIT NINE +19DA ; New_Tai_Lue # No NEW TAI LUE THAM DIGIT ONE +19DE..19DF ; New_Tai_Lue # So [2] NEW TAI LUE SIGN LAE..NEW TAI LUE SIGN LAEV + +# Total code points: 83 + +# ================================================ + +2C00..2C5F ; Glagolitic # L& [96] GLAGOLITIC CAPITAL LETTER AZU..GLAGOLITIC SMALL LETTER CAUDATE CHRIVI +1E000..1E006 ; Glagolitic # Mn [7] COMBINING GLAGOLITIC LETTER AZU..COMBINING GLAGOLITIC LETTER ZHIVETE +1E008..1E018 ; Glagolitic # Mn [17] COMBINING GLAGOLITIC LETTER ZEMLJA..COMBINING GLAGOLITIC LETTER HERU +1E01B..1E021 ; Glagolitic # Mn [7] COMBINING GLAGOLITIC LETTER SHTA..COMBINING GLAGOLITIC LETTER YATI +1E023..1E024 ; Glagolitic # Mn [2] COMBINING GLAGOLITIC LETTER YU..COMBINING GLAGOLITIC LETTER SMALL YUS +1E026..1E02A ; Glagolitic # Mn [5] COMBINING GLAGOLITIC LETTER YO..COMBINING GLAGOLITIC LETTER FITA + +# Total code points: 134 + +# ================================================ + +2D30..2D67 ; Tifinagh # Lo [56] TIFINAGH LETTER YA..TIFINAGH LETTER YO +2D6F ; Tifinagh # Lm TIFINAGH MODIFIER LETTER LABIALIZATION MARK +2D70 ; Tifinagh # Po TIFINAGH SEPARATOR MARK +2D7F ; Tifinagh # Mn TIFINAGH CONSONANT JOINER + +# Total code points: 59 + +# ================================================ + +A800..A801 ; Syloti_Nagri # Lo [2] SYLOTI NAGRI LETTER A..SYLOTI NAGRI LETTER I +A802 ; Syloti_Nagri # Mn SYLOTI NAGRI SIGN DVISVARA +A803..A805 ; Syloti_Nagri # Lo [3] SYLOTI NAGRI LETTER U..SYLOTI NAGRI LETTER O +A806 ; Syloti_Nagri # Mn SYLOTI NAGRI SIGN HASANTA +A807..A80A ; Syloti_Nagri # Lo [4] SYLOTI NAGRI LETTER KO..SYLOTI NAGRI LETTER GHO +A80B ; Syloti_Nagri # Mn SYLOTI NAGRI SIGN ANUSVARA +A80C..A822 ; Syloti_Nagri # Lo [23] SYLOTI NAGRI LETTER CO..SYLOTI NAGRI LETTER HO +A823..A824 ; Syloti_Nagri # Mc [2] SYLOTI NAGRI VOWEL SIGN A..SYLOTI NAGRI VOWEL SIGN I +A825..A826 ; Syloti_Nagri # Mn [2] SYLOTI NAGRI VOWEL SIGN U..SYLOTI NAGRI VOWEL SIGN E +A827 ; Syloti_Nagri # Mc SYLOTI NAGRI VOWEL SIGN OO +A828..A82B ; Syloti_Nagri # So [4] SYLOTI NAGRI POETRY MARK-1..SYLOTI NAGRI POETRY MARK-4 +A82C ; Syloti_Nagri # Mn SYLOTI NAGRI SIGN ALTERNATE HASANTA + +# Total code points: 45 + +# ================================================ + +103A0..103C3 ; Old_Persian # Lo [36] OLD PERSIAN SIGN A..OLD PERSIAN SIGN HA +103C8..103CF ; Old_Persian # Lo [8] OLD PERSIAN SIGN AURAMAZDAA..OLD PERSIAN SIGN BUUMISH +103D0 ; Old_Persian # Po OLD PERSIAN WORD DIVIDER +103D1..103D5 ; Old_Persian # Nl [5] OLD PERSIAN NUMBER ONE..OLD PERSIAN NUMBER HUNDRED + +# Total code points: 50 + +# ================================================ + +10A00 ; Kharoshthi # Lo KHAROSHTHI LETTER A +10A01..10A03 ; Kharoshthi # Mn [3] KHAROSHTHI VOWEL SIGN I..KHAROSHTHI VOWEL SIGN VOCALIC R +10A05..10A06 ; Kharoshthi # Mn [2] KHAROSHTHI VOWEL SIGN E..KHAROSHTHI VOWEL SIGN O +10A0C..10A0F ; Kharoshthi # Mn [4] KHAROSHTHI VOWEL LENGTH MARK..KHAROSHTHI SIGN VISARGA +10A10..10A13 ; Kharoshthi # Lo [4] KHAROSHTHI LETTER KA..KHAROSHTHI LETTER GHA +10A15..10A17 ; Kharoshthi # Lo [3] KHAROSHTHI LETTER CA..KHAROSHTHI LETTER JA +10A19..10A35 ; Kharoshthi # Lo [29] KHAROSHTHI LETTER NYA..KHAROSHTHI LETTER VHA +10A38..10A3A ; Kharoshthi # Mn [3] KHAROSHTHI SIGN BAR ABOVE..KHAROSHTHI SIGN DOT BELOW +10A3F ; Kharoshthi # Mn KHAROSHTHI VIRAMA +10A40..10A48 ; Kharoshthi # No [9] KHAROSHTHI DIGIT ONE..KHAROSHTHI FRACTION ONE HALF +10A50..10A58 ; Kharoshthi # Po [9] KHAROSHTHI PUNCTUATION DOT..KHAROSHTHI PUNCTUATION LINES + +# Total code points: 68 + +# ================================================ + +1B00..1B03 ; Balinese # Mn [4] BALINESE SIGN ULU RICEM..BALINESE SIGN SURANG +1B04 ; Balinese # Mc BALINESE SIGN BISAH +1B05..1B33 ; Balinese # Lo [47] BALINESE LETTER AKARA..BALINESE LETTER HA +1B34 ; Balinese # Mn BALINESE SIGN REREKAN +1B35 ; Balinese # Mc BALINESE VOWEL SIGN TEDUNG +1B36..1B3A ; Balinese # Mn [5] BALINESE VOWEL SIGN ULU..BALINESE VOWEL SIGN RA REPA +1B3B ; Balinese # Mc BALINESE VOWEL SIGN RA REPA TEDUNG +1B3C ; Balinese # Mn BALINESE VOWEL SIGN LA LENGA +1B3D..1B41 ; Balinese # Mc [5] BALINESE VOWEL SIGN LA LENGA TEDUNG..BALINESE VOWEL SIGN TALING REPA TEDUNG +1B42 ; Balinese # Mn BALINESE VOWEL SIGN PEPET +1B43..1B44 ; Balinese # Mc [2] BALINESE VOWEL SIGN PEPET TEDUNG..BALINESE ADEG ADEG +1B45..1B4C ; Balinese # Lo [8] BALINESE LETTER KAF SASAK..BALINESE LETTER ARCHAIC JNYA +1B50..1B59 ; Balinese # Nd [10] BALINESE DIGIT ZERO..BALINESE DIGIT NINE +1B5A..1B60 ; Balinese # Po [7] BALINESE PANTI..BALINESE PAMENENG +1B61..1B6A ; Balinese # So [10] BALINESE MUSICAL SYMBOL DONG..BALINESE MUSICAL SYMBOL DANG GEDE +1B6B..1B73 ; Balinese # Mn [9] BALINESE MUSICAL SYMBOL COMBINING TEGEH..BALINESE MUSICAL SYMBOL COMBINING GONG +1B74..1B7C ; Balinese # So [9] BALINESE MUSICAL SYMBOL RIGHT-HAND OPEN DUG..BALINESE MUSICAL SYMBOL LEFT-HAND OPEN PING +1B7D..1B7E ; Balinese # Po [2] BALINESE PANTI LANTANG..BALINESE PAMADA LANTANG + +# Total code points: 124 + +# ================================================ + +12000..12399 ; Cuneiform # Lo [922] CUNEIFORM SIGN A..CUNEIFORM SIGN U U +12400..1246E ; Cuneiform # Nl [111] CUNEIFORM NUMERIC SIGN TWO ASH..CUNEIFORM NUMERIC SIGN NINE U VARIANT FORM +12470..12474 ; Cuneiform # Po [5] CUNEIFORM PUNCTUATION SIGN OLD ASSYRIAN WORD DIVIDER..CUNEIFORM PUNCTUATION SIGN DIAGONAL QUADCOLON +12480..12543 ; Cuneiform # Lo [196] CUNEIFORM SIGN AB TIMES NUN TENU..CUNEIFORM SIGN ZU5 TIMES THREE DISH TENU + +# Total code points: 1234 + +# ================================================ + +10900..10915 ; Phoenician # Lo [22] PHOENICIAN LETTER ALF..PHOENICIAN LETTER TAU +10916..1091B ; Phoenician # No [6] PHOENICIAN NUMBER ONE..PHOENICIAN NUMBER THREE +1091F ; Phoenician # Po PHOENICIAN WORD SEPARATOR + +# Total code points: 29 + +# ================================================ + +A840..A873 ; Phags_Pa # Lo [52] PHAGS-PA LETTER KA..PHAGS-PA LETTER CANDRABINDU +A874..A877 ; Phags_Pa # Po [4] PHAGS-PA SINGLE HEAD MARK..PHAGS-PA MARK DOUBLE SHAD + +# Total code points: 56 + +# ================================================ + +07C0..07C9 ; Nko # Nd [10] NKO DIGIT ZERO..NKO DIGIT NINE +07CA..07EA ; Nko # Lo [33] NKO LETTER A..NKO LETTER JONA RA +07EB..07F3 ; Nko # Mn [9] NKO COMBINING SHORT HIGH TONE..NKO COMBINING DOUBLE DOT ABOVE +07F4..07F5 ; Nko # Lm [2] NKO HIGH TONE APOSTROPHE..NKO LOW TONE APOSTROPHE +07F6 ; Nko # So NKO SYMBOL OO DENNEN +07F7..07F9 ; Nko # Po [3] NKO SYMBOL GBAKURUNEN..NKO EXCLAMATION MARK +07FA ; Nko # Lm NKO LAJANYALAN +07FD ; Nko # Mn NKO DANTAYALAN +07FE..07FF ; Nko # Sc [2] NKO DOROME SIGN..NKO TAMAN SIGN + +# Total code points: 62 + +# ================================================ + +1B80..1B81 ; Sundanese # Mn [2] SUNDANESE SIGN PANYECEK..SUNDANESE SIGN PANGLAYAR +1B82 ; Sundanese # Mc SUNDANESE SIGN PANGWISAD +1B83..1BA0 ; Sundanese # Lo [30] SUNDANESE LETTER A..SUNDANESE LETTER HA +1BA1 ; Sundanese # Mc SUNDANESE CONSONANT SIGN PAMINGKAL +1BA2..1BA5 ; Sundanese # Mn [4] SUNDANESE CONSONANT SIGN PANYAKRA..SUNDANESE VOWEL SIGN PANYUKU +1BA6..1BA7 ; Sundanese # Mc [2] SUNDANESE VOWEL SIGN PANAELAENG..SUNDANESE VOWEL SIGN PANOLONG +1BA8..1BA9 ; Sundanese # Mn [2] SUNDANESE VOWEL SIGN PAMEPET..SUNDANESE VOWEL SIGN PANEULEUNG +1BAA ; Sundanese # Mc SUNDANESE SIGN PAMAAEH +1BAB..1BAD ; Sundanese # Mn [3] SUNDANESE SIGN VIRAMA..SUNDANESE CONSONANT SIGN PASANGAN WA +1BAE..1BAF ; Sundanese # Lo [2] SUNDANESE LETTER KHA..SUNDANESE LETTER SYA +1BB0..1BB9 ; Sundanese # Nd [10] SUNDANESE DIGIT ZERO..SUNDANESE DIGIT NINE +1BBA..1BBF ; Sundanese # Lo [6] SUNDANESE AVAGRAHA..SUNDANESE LETTER FINAL M +1CC0..1CC7 ; Sundanese # Po [8] SUNDANESE PUNCTUATION BINDU SURYA..SUNDANESE PUNCTUATION BINDU BA SATANGA + +# Total code points: 72 + +# ================================================ + +1C00..1C23 ; Lepcha # Lo [36] LEPCHA LETTER KA..LEPCHA LETTER A +1C24..1C2B ; Lepcha # Mc [8] LEPCHA SUBJOINED LETTER YA..LEPCHA VOWEL SIGN UU +1C2C..1C33 ; Lepcha # Mn [8] LEPCHA VOWEL SIGN E..LEPCHA CONSONANT SIGN T +1C34..1C35 ; Lepcha # Mc [2] LEPCHA CONSONANT SIGN NYIN-DO..LEPCHA CONSONANT SIGN KANG +1C36..1C37 ; Lepcha # Mn [2] LEPCHA SIGN RAN..LEPCHA SIGN NUKTA +1C3B..1C3F ; Lepcha # Po [5] LEPCHA PUNCTUATION TA-ROL..LEPCHA PUNCTUATION TSHOOK +1C40..1C49 ; Lepcha # Nd [10] LEPCHA DIGIT ZERO..LEPCHA DIGIT NINE +1C4D..1C4F ; Lepcha # Lo [3] LEPCHA LETTER TTA..LEPCHA LETTER DDA + +# Total code points: 74 + +# ================================================ + +1C50..1C59 ; Ol_Chiki # Nd [10] OL CHIKI DIGIT ZERO..OL CHIKI DIGIT NINE +1C5A..1C77 ; Ol_Chiki # Lo [30] OL CHIKI LETTER LA..OL CHIKI LETTER OH +1C78..1C7D ; Ol_Chiki # Lm [6] OL CHIKI MU TTUDDAG..OL CHIKI AHAD +1C7E..1C7F ; Ol_Chiki # Po [2] OL CHIKI PUNCTUATION MUCAAD..OL CHIKI PUNCTUATION DOUBLE MUCAAD + +# Total code points: 48 + +# ================================================ + +A500..A60B ; Vai # Lo [268] VAI SYLLABLE EE..VAI SYLLABLE NG +A60C ; Vai # Lm VAI SYLLABLE LENGTHENER +A60D..A60F ; Vai # Po [3] VAI COMMA..VAI QUESTION MARK +A610..A61F ; Vai # Lo [16] VAI SYLLABLE NDOLE FA..VAI SYMBOL JONG +A620..A629 ; Vai # Nd [10] VAI DIGIT ZERO..VAI DIGIT NINE +A62A..A62B ; Vai # Lo [2] VAI SYLLABLE NDOLE MA..VAI SYLLABLE NDOLE DO + +# Total code points: 300 + +# ================================================ + +A880..A881 ; Saurashtra # Mc [2] SAURASHTRA SIGN ANUSVARA..SAURASHTRA SIGN VISARGA +A882..A8B3 ; Saurashtra # Lo [50] SAURASHTRA LETTER A..SAURASHTRA LETTER LLA +A8B4..A8C3 ; Saurashtra # Mc [16] SAURASHTRA CONSONANT SIGN HAARU..SAURASHTRA VOWEL SIGN AU +A8C4..A8C5 ; Saurashtra # Mn [2] SAURASHTRA SIGN VIRAMA..SAURASHTRA SIGN CANDRABINDU +A8CE..A8CF ; Saurashtra # Po [2] SAURASHTRA DANDA..SAURASHTRA DOUBLE DANDA +A8D0..A8D9 ; Saurashtra # Nd [10] SAURASHTRA DIGIT ZERO..SAURASHTRA DIGIT NINE + +# Total code points: 82 + +# ================================================ + +A900..A909 ; Kayah_Li # Nd [10] KAYAH LI DIGIT ZERO..KAYAH LI DIGIT NINE +A90A..A925 ; Kayah_Li # Lo [28] KAYAH LI LETTER KA..KAYAH LI LETTER OO +A926..A92D ; Kayah_Li # Mn [8] KAYAH LI VOWEL UE..KAYAH LI TONE CALYA PLOPHU +A92F ; Kayah_Li # Po KAYAH LI SIGN SHYA + +# Total code points: 47 + +# ================================================ + +A930..A946 ; Rejang # Lo [23] REJANG LETTER KA..REJANG LETTER A +A947..A951 ; Rejang # Mn [11] REJANG VOWEL SIGN I..REJANG CONSONANT SIGN R +A952..A953 ; Rejang # Mc [2] REJANG CONSONANT SIGN H..REJANG VIRAMA +A95F ; Rejang # Po REJANG SECTION MARK + +# Total code points: 37 + +# ================================================ + +10280..1029C ; Lycian # Lo [29] LYCIAN LETTER A..LYCIAN LETTER X + +# Total code points: 29 + +# ================================================ + +102A0..102D0 ; Carian # Lo [49] CARIAN LETTER A..CARIAN LETTER UUU3 + +# Total code points: 49 + +# ================================================ + +10920..10939 ; Lydian # Lo [26] LYDIAN LETTER A..LYDIAN LETTER C +1093F ; Lydian # Po LYDIAN TRIANGULAR MARK + +# Total code points: 27 + +# ================================================ + +AA00..AA28 ; Cham # Lo [41] CHAM LETTER A..CHAM LETTER HA +AA29..AA2E ; Cham # Mn [6] CHAM VOWEL SIGN AA..CHAM VOWEL SIGN OE +AA2F..AA30 ; Cham # Mc [2] CHAM VOWEL SIGN O..CHAM VOWEL SIGN AI +AA31..AA32 ; Cham # Mn [2] CHAM VOWEL SIGN AU..CHAM VOWEL SIGN UE +AA33..AA34 ; Cham # Mc [2] CHAM CONSONANT SIGN YA..CHAM CONSONANT SIGN RA +AA35..AA36 ; Cham # Mn [2] CHAM CONSONANT SIGN LA..CHAM CONSONANT SIGN WA +AA40..AA42 ; Cham # Lo [3] CHAM LETTER FINAL K..CHAM LETTER FINAL NG +AA43 ; Cham # Mn CHAM CONSONANT SIGN FINAL NG +AA44..AA4B ; Cham # Lo [8] CHAM LETTER FINAL CH..CHAM LETTER FINAL SS +AA4C ; Cham # Mn CHAM CONSONANT SIGN FINAL M +AA4D ; Cham # Mc CHAM CONSONANT SIGN FINAL H +AA50..AA59 ; Cham # Nd [10] CHAM DIGIT ZERO..CHAM DIGIT NINE +AA5C..AA5F ; Cham # Po [4] CHAM PUNCTUATION SPIRAL..CHAM PUNCTUATION TRIPLE DANDA + +# Total code points: 83 + +# ================================================ + +1A20..1A54 ; Tai_Tham # Lo [53] TAI THAM LETTER HIGH KA..TAI THAM LETTER GREAT SA +1A55 ; Tai_Tham # Mc TAI THAM CONSONANT SIGN MEDIAL RA +1A56 ; Tai_Tham # Mn TAI THAM CONSONANT SIGN MEDIAL LA +1A57 ; Tai_Tham # Mc TAI THAM CONSONANT SIGN LA TANG LAI +1A58..1A5E ; Tai_Tham # Mn [7] TAI THAM SIGN MAI KANG LAI..TAI THAM CONSONANT SIGN SA +1A60 ; Tai_Tham # Mn TAI THAM SIGN SAKOT +1A61 ; Tai_Tham # Mc TAI THAM VOWEL SIGN A +1A62 ; Tai_Tham # Mn TAI THAM VOWEL SIGN MAI SAT +1A63..1A64 ; Tai_Tham # Mc [2] TAI THAM VOWEL SIGN AA..TAI THAM VOWEL SIGN TALL AA +1A65..1A6C ; Tai_Tham # Mn [8] TAI THAM VOWEL SIGN I..TAI THAM VOWEL SIGN OA BELOW +1A6D..1A72 ; Tai_Tham # Mc [6] TAI THAM VOWEL SIGN OY..TAI THAM VOWEL SIGN THAM AI +1A73..1A7C ; Tai_Tham # Mn [10] TAI THAM VOWEL SIGN OA ABOVE..TAI THAM SIGN KHUEN-LUE KARAN +1A7F ; Tai_Tham # Mn TAI THAM COMBINING CRYPTOGRAMMIC DOT +1A80..1A89 ; Tai_Tham # Nd [10] TAI THAM HORA DIGIT ZERO..TAI THAM HORA DIGIT NINE +1A90..1A99 ; Tai_Tham # Nd [10] TAI THAM THAM DIGIT ZERO..TAI THAM THAM DIGIT NINE +1AA0..1AA6 ; Tai_Tham # Po [7] TAI THAM SIGN WIANG..TAI THAM SIGN REVERSED ROTATED RANA +1AA7 ; Tai_Tham # Lm TAI THAM SIGN MAI YAMOK +1AA8..1AAD ; Tai_Tham # Po [6] TAI THAM SIGN KAAN..TAI THAM SIGN CAANG + +# Total code points: 127 + +# ================================================ + +AA80..AAAF ; Tai_Viet # Lo [48] TAI VIET LETTER LOW KO..TAI VIET LETTER HIGH O +AAB0 ; Tai_Viet # Mn TAI VIET MAI KANG +AAB1 ; Tai_Viet # Lo TAI VIET VOWEL AA +AAB2..AAB4 ; Tai_Viet # Mn [3] TAI VIET VOWEL I..TAI VIET VOWEL U +AAB5..AAB6 ; Tai_Viet # Lo [2] TAI VIET VOWEL E..TAI VIET VOWEL O +AAB7..AAB8 ; Tai_Viet # Mn [2] TAI VIET MAI KHIT..TAI VIET VOWEL IA +AAB9..AABD ; Tai_Viet # Lo [5] TAI VIET VOWEL UEA..TAI VIET VOWEL AN +AABE..AABF ; Tai_Viet # Mn [2] TAI VIET VOWEL AM..TAI VIET TONE MAI EK +AAC0 ; Tai_Viet # Lo TAI VIET TONE MAI NUENG +AAC1 ; Tai_Viet # Mn TAI VIET TONE MAI THO +AAC2 ; Tai_Viet # Lo TAI VIET TONE MAI SONG +AADB..AADC ; Tai_Viet # Lo [2] TAI VIET SYMBOL KON..TAI VIET SYMBOL NUENG +AADD ; Tai_Viet # Lm TAI VIET SYMBOL SAM +AADE..AADF ; Tai_Viet # Po [2] TAI VIET SYMBOL HO HOI..TAI VIET SYMBOL KOI KOI + +# Total code points: 72 + +# ================================================ + +10B00..10B35 ; Avestan # Lo [54] AVESTAN LETTER A..AVESTAN LETTER HE +10B39..10B3F ; Avestan # Po [7] AVESTAN ABBREVIATION MARK..LARGE ONE RING OVER TWO RINGS PUNCTUATION + +# Total code points: 61 + +# ================================================ + +13000..1342F ; Egyptian_Hieroglyphs # Lo [1072] EGYPTIAN HIEROGLYPH A001..EGYPTIAN HIEROGLYPH V011D +13430..1343F ; Egyptian_Hieroglyphs # Cf [16] EGYPTIAN HIEROGLYPH VERTICAL JOINER..EGYPTIAN HIEROGLYPH END WALLED ENCLOSURE +13440 ; Egyptian_Hieroglyphs # Mn EGYPTIAN HIEROGLYPH MIRROR HORIZONTALLY +13441..13446 ; Egyptian_Hieroglyphs # Lo [6] EGYPTIAN HIEROGLYPH FULL BLANK..EGYPTIAN HIEROGLYPH WIDE LOST SIGN +13447..13455 ; Egyptian_Hieroglyphs # Mn [15] EGYPTIAN HIEROGLYPH MODIFIER DAMAGED AT TOP START..EGYPTIAN HIEROGLYPH MODIFIER DAMAGED + +# Total code points: 1110 + +# ================================================ + +0800..0815 ; Samaritan # Lo [22] SAMARITAN LETTER ALAF..SAMARITAN LETTER TAAF +0816..0819 ; Samaritan # Mn [4] SAMARITAN MARK IN..SAMARITAN MARK DAGESH +081A ; Samaritan # Lm SAMARITAN MODIFIER LETTER EPENTHETIC YUT +081B..0823 ; Samaritan # Mn [9] SAMARITAN MARK EPENTHETIC YUT..SAMARITAN VOWEL SIGN A +0824 ; Samaritan # Lm SAMARITAN MODIFIER LETTER SHORT A +0825..0827 ; Samaritan # Mn [3] SAMARITAN VOWEL SIGN SHORT A..SAMARITAN VOWEL SIGN U +0828 ; Samaritan # Lm SAMARITAN MODIFIER LETTER I +0829..082D ; Samaritan # Mn [5] SAMARITAN VOWEL SIGN LONG I..SAMARITAN MARK NEQUDAA +0830..083E ; Samaritan # Po [15] SAMARITAN PUNCTUATION NEQUDAA..SAMARITAN PUNCTUATION ANNAAU + +# Total code points: 61 + +# ================================================ + +A4D0..A4F7 ; Lisu # Lo [40] LISU LETTER BA..LISU LETTER OE +A4F8..A4FD ; Lisu # Lm [6] LISU LETTER TONE MYA TI..LISU LETTER TONE MYA JEU +A4FE..A4FF ; Lisu # Po [2] LISU PUNCTUATION COMMA..LISU PUNCTUATION FULL STOP +11FB0 ; Lisu # Lo LISU LETTER YHA + +# Total code points: 49 + +# ================================================ + +A6A0..A6E5 ; Bamum # Lo [70] BAMUM LETTER A..BAMUM LETTER KI +A6E6..A6EF ; Bamum # Nl [10] BAMUM LETTER MO..BAMUM LETTER KOGHOM +A6F0..A6F1 ; Bamum # Mn [2] BAMUM COMBINING MARK KOQNDON..BAMUM COMBINING MARK TUKWENTIS +A6F2..A6F7 ; Bamum # Po [6] BAMUM NJAEMLI..BAMUM QUESTION MARK +16800..16A38 ; Bamum # Lo [569] BAMUM LETTER PHASE-A NGKUE MFON..BAMUM LETTER PHASE-F VUEQ + +# Total code points: 657 + +# ================================================ + +A980..A982 ; Javanese # Mn [3] JAVANESE SIGN PANYANGGA..JAVANESE SIGN LAYAR +A983 ; Javanese # Mc JAVANESE SIGN WIGNYAN +A984..A9B2 ; Javanese # Lo [47] JAVANESE LETTER A..JAVANESE LETTER HA +A9B3 ; Javanese # Mn JAVANESE SIGN CECAK TELU +A9B4..A9B5 ; Javanese # Mc [2] JAVANESE VOWEL SIGN TARUNG..JAVANESE VOWEL SIGN TOLONG +A9B6..A9B9 ; Javanese # Mn [4] JAVANESE VOWEL SIGN WULU..JAVANESE VOWEL SIGN SUKU MENDUT +A9BA..A9BB ; Javanese # Mc [2] JAVANESE VOWEL SIGN TALING..JAVANESE VOWEL SIGN DIRGA MURE +A9BC..A9BD ; Javanese # Mn [2] JAVANESE VOWEL SIGN PEPET..JAVANESE CONSONANT SIGN KERET +A9BE..A9C0 ; Javanese # Mc [3] JAVANESE CONSONANT SIGN PENGKAL..JAVANESE PANGKON +A9C1..A9CD ; Javanese # Po [13] JAVANESE LEFT RERENGGAN..JAVANESE TURNED PADA PISELEH +A9D0..A9D9 ; Javanese # Nd [10] JAVANESE DIGIT ZERO..JAVANESE DIGIT NINE +A9DE..A9DF ; Javanese # Po [2] JAVANESE PADA TIRTA TUMETES..JAVANESE PADA ISEN-ISEN + +# Total code points: 90 + +# ================================================ + +AAE0..AAEA ; Meetei_Mayek # Lo [11] MEETEI MAYEK LETTER E..MEETEI MAYEK LETTER SSA +AAEB ; Meetei_Mayek # Mc MEETEI MAYEK VOWEL SIGN II +AAEC..AAED ; Meetei_Mayek # Mn [2] MEETEI MAYEK VOWEL SIGN UU..MEETEI MAYEK VOWEL SIGN AAI +AAEE..AAEF ; Meetei_Mayek # Mc [2] MEETEI MAYEK VOWEL SIGN AU..MEETEI MAYEK VOWEL SIGN AAU +AAF0..AAF1 ; Meetei_Mayek # Po [2] MEETEI MAYEK CHEIKHAN..MEETEI MAYEK AHANG KHUDAM +AAF2 ; Meetei_Mayek # Lo MEETEI MAYEK ANJI +AAF3..AAF4 ; Meetei_Mayek # Lm [2] MEETEI MAYEK SYLLABLE REPETITION MARK..MEETEI MAYEK WORD REPETITION MARK +AAF5 ; Meetei_Mayek # Mc MEETEI MAYEK VOWEL SIGN VISARGA +AAF6 ; Meetei_Mayek # Mn MEETEI MAYEK VIRAMA +ABC0..ABE2 ; Meetei_Mayek # Lo [35] MEETEI MAYEK LETTER KOK..MEETEI MAYEK LETTER I LONSUM +ABE3..ABE4 ; Meetei_Mayek # Mc [2] MEETEI MAYEK VOWEL SIGN ONAP..MEETEI MAYEK VOWEL SIGN INAP +ABE5 ; Meetei_Mayek # Mn MEETEI MAYEK VOWEL SIGN ANAP +ABE6..ABE7 ; Meetei_Mayek # Mc [2] MEETEI MAYEK VOWEL SIGN YENAP..MEETEI MAYEK VOWEL SIGN SOUNAP +ABE8 ; Meetei_Mayek # Mn MEETEI MAYEK VOWEL SIGN UNAP +ABE9..ABEA ; Meetei_Mayek # Mc [2] MEETEI MAYEK VOWEL SIGN CHEINAP..MEETEI MAYEK VOWEL SIGN NUNG +ABEB ; Meetei_Mayek # Po MEETEI MAYEK CHEIKHEI +ABEC ; Meetei_Mayek # Mc MEETEI MAYEK LUM IYEK +ABED ; Meetei_Mayek # Mn MEETEI MAYEK APUN IYEK +ABF0..ABF9 ; Meetei_Mayek # Nd [10] MEETEI MAYEK DIGIT ZERO..MEETEI MAYEK DIGIT NINE + +# Total code points: 79 + +# ================================================ + +10840..10855 ; Imperial_Aramaic # Lo [22] IMPERIAL ARAMAIC LETTER ALEPH..IMPERIAL ARAMAIC LETTER TAW +10857 ; Imperial_Aramaic # Po IMPERIAL ARAMAIC SECTION SIGN +10858..1085F ; Imperial_Aramaic # No [8] IMPERIAL ARAMAIC NUMBER ONE..IMPERIAL ARAMAIC NUMBER TEN THOUSAND + +# Total code points: 31 + +# ================================================ + +10A60..10A7C ; Old_South_Arabian # Lo [29] OLD SOUTH ARABIAN LETTER HE..OLD SOUTH ARABIAN LETTER THETH +10A7D..10A7E ; Old_South_Arabian # No [2] OLD SOUTH ARABIAN NUMBER ONE..OLD SOUTH ARABIAN NUMBER FIFTY +10A7F ; Old_South_Arabian # Po OLD SOUTH ARABIAN NUMERIC INDICATOR + +# Total code points: 32 + +# ================================================ + +10B40..10B55 ; Inscriptional_Parthian # Lo [22] INSCRIPTIONAL PARTHIAN LETTER ALEPH..INSCRIPTIONAL PARTHIAN LETTER TAW +10B58..10B5F ; Inscriptional_Parthian # No [8] INSCRIPTIONAL PARTHIAN NUMBER ONE..INSCRIPTIONAL PARTHIAN NUMBER ONE THOUSAND + +# Total code points: 30 + +# ================================================ + +10B60..10B72 ; Inscriptional_Pahlavi # Lo [19] INSCRIPTIONAL PAHLAVI LETTER ALEPH..INSCRIPTIONAL PAHLAVI LETTER TAW +10B78..10B7F ; Inscriptional_Pahlavi # No [8] INSCRIPTIONAL PAHLAVI NUMBER ONE..INSCRIPTIONAL PAHLAVI NUMBER ONE THOUSAND + +# Total code points: 27 + +# ================================================ + +10C00..10C48 ; Old_Turkic # Lo [73] OLD TURKIC LETTER ORKHON A..OLD TURKIC LETTER ORKHON BASH + +# Total code points: 73 + +# ================================================ + +11080..11081 ; Kaithi # Mn [2] KAITHI SIGN CANDRABINDU..KAITHI SIGN ANUSVARA +11082 ; Kaithi # Mc KAITHI SIGN VISARGA +11083..110AF ; Kaithi # Lo [45] KAITHI LETTER A..KAITHI LETTER HA +110B0..110B2 ; Kaithi # Mc [3] KAITHI VOWEL SIGN AA..KAITHI VOWEL SIGN II +110B3..110B6 ; Kaithi # Mn [4] KAITHI VOWEL SIGN U..KAITHI VOWEL SIGN AI +110B7..110B8 ; Kaithi # Mc [2] KAITHI VOWEL SIGN O..KAITHI VOWEL SIGN AU +110B9..110BA ; Kaithi # Mn [2] KAITHI SIGN VIRAMA..KAITHI SIGN NUKTA +110BB..110BC ; Kaithi # Po [2] KAITHI ABBREVIATION SIGN..KAITHI ENUMERATION SIGN +110BD ; Kaithi # Cf KAITHI NUMBER SIGN +110BE..110C1 ; Kaithi # Po [4] KAITHI SECTION MARK..KAITHI DOUBLE DANDA +110C2 ; Kaithi # Mn KAITHI VOWEL SIGN VOCALIC R +110CD ; Kaithi # Cf KAITHI NUMBER SIGN ABOVE + +# Total code points: 68 + +# ================================================ + +1BC0..1BE5 ; Batak # Lo [38] BATAK LETTER A..BATAK LETTER U +1BE6 ; Batak # Mn BATAK SIGN TOMPI +1BE7 ; Batak # Mc BATAK VOWEL SIGN E +1BE8..1BE9 ; Batak # Mn [2] BATAK VOWEL SIGN PAKPAK E..BATAK VOWEL SIGN EE +1BEA..1BEC ; Batak # Mc [3] BATAK VOWEL SIGN I..BATAK VOWEL SIGN O +1BED ; Batak # Mn BATAK VOWEL SIGN KARO O +1BEE ; Batak # Mc BATAK VOWEL SIGN U +1BEF..1BF1 ; Batak # Mn [3] BATAK VOWEL SIGN U FOR SIMALUNGUN SA..BATAK CONSONANT SIGN H +1BF2..1BF3 ; Batak # Mc [2] BATAK PANGOLAT..BATAK PANONGONAN +1BFC..1BFF ; Batak # Po [4] BATAK SYMBOL BINDU NA METEK..BATAK SYMBOL BINDU PANGOLAT + +# Total code points: 56 + +# ================================================ + +11000 ; Brahmi # Mc BRAHMI SIGN CANDRABINDU +11001 ; Brahmi # Mn BRAHMI SIGN ANUSVARA +11002 ; Brahmi # Mc BRAHMI SIGN VISARGA +11003..11037 ; Brahmi # Lo [53] BRAHMI SIGN JIHVAMULIYA..BRAHMI LETTER OLD TAMIL NNNA +11038..11046 ; Brahmi # Mn [15] BRAHMI VOWEL SIGN AA..BRAHMI VIRAMA +11047..1104D ; Brahmi # Po [7] BRAHMI DANDA..BRAHMI PUNCTUATION LOTUS +11052..11065 ; Brahmi # No [20] BRAHMI NUMBER ONE..BRAHMI NUMBER ONE THOUSAND +11066..1106F ; Brahmi # Nd [10] BRAHMI DIGIT ZERO..BRAHMI DIGIT NINE +11070 ; Brahmi # Mn BRAHMI SIGN OLD TAMIL VIRAMA +11071..11072 ; Brahmi # Lo [2] BRAHMI LETTER OLD TAMIL SHORT E..BRAHMI LETTER OLD TAMIL SHORT O +11073..11074 ; Brahmi # Mn [2] BRAHMI VOWEL SIGN OLD TAMIL SHORT E..BRAHMI VOWEL SIGN OLD TAMIL SHORT O +11075 ; Brahmi # Lo BRAHMI LETTER OLD TAMIL LLA +1107F ; Brahmi # Mn BRAHMI NUMBER JOINER + +# Total code points: 115 + +# ================================================ + +0840..0858 ; Mandaic # Lo [25] MANDAIC LETTER HALQA..MANDAIC LETTER AIN +0859..085B ; Mandaic # Mn [3] MANDAIC AFFRICATION MARK..MANDAIC GEMINATION MARK +085E ; Mandaic # Po MANDAIC PUNCTUATION + +# Total code points: 29 + +# ================================================ + +11100..11102 ; Chakma # Mn [3] CHAKMA SIGN CANDRABINDU..CHAKMA SIGN VISARGA +11103..11126 ; Chakma # Lo [36] CHAKMA LETTER AA..CHAKMA LETTER HAA +11127..1112B ; Chakma # Mn [5] CHAKMA VOWEL SIGN A..CHAKMA VOWEL SIGN UU +1112C ; Chakma # Mc CHAKMA VOWEL SIGN E +1112D..11134 ; Chakma # Mn [8] CHAKMA VOWEL SIGN AI..CHAKMA MAAYYAA +11136..1113F ; Chakma # Nd [10] CHAKMA DIGIT ZERO..CHAKMA DIGIT NINE +11140..11143 ; Chakma # Po [4] CHAKMA SECTION MARK..CHAKMA QUESTION MARK +11144 ; Chakma # Lo CHAKMA LETTER LHAA +11145..11146 ; Chakma # Mc [2] CHAKMA VOWEL SIGN AA..CHAKMA VOWEL SIGN EI +11147 ; Chakma # Lo CHAKMA LETTER VAA + +# Total code points: 71 + +# ================================================ + +109A0..109B7 ; Meroitic_Cursive # Lo [24] MEROITIC CURSIVE LETTER A..MEROITIC CURSIVE LETTER DA +109BC..109BD ; Meroitic_Cursive # No [2] MEROITIC CURSIVE FRACTION ELEVEN TWELFTHS..MEROITIC CURSIVE FRACTION ONE HALF +109BE..109BF ; Meroitic_Cursive # Lo [2] MEROITIC CURSIVE LOGOGRAM RMT..MEROITIC CURSIVE LOGOGRAM IMN +109C0..109CF ; Meroitic_Cursive # No [16] MEROITIC CURSIVE NUMBER ONE..MEROITIC CURSIVE NUMBER SEVENTY +109D2..109FF ; Meroitic_Cursive # No [46] MEROITIC CURSIVE NUMBER ONE HUNDRED..MEROITIC CURSIVE FRACTION TEN TWELFTHS + +# Total code points: 90 + +# ================================================ + +10980..1099F ; Meroitic_Hieroglyphs # Lo [32] MEROITIC HIEROGLYPHIC LETTER A..MEROITIC HIEROGLYPHIC SYMBOL VIDJ-2 + +# Total code points: 32 + +# ================================================ + +16F00..16F4A ; Miao # Lo [75] MIAO LETTER PA..MIAO LETTER RTE +16F4F ; Miao # Mn MIAO SIGN CONSONANT MODIFIER BAR +16F50 ; Miao # Lo MIAO LETTER NASALIZATION +16F51..16F87 ; Miao # Mc [55] MIAO SIGN ASPIRATION..MIAO VOWEL SIGN UI +16F8F..16F92 ; Miao # Mn [4] MIAO TONE RIGHT..MIAO TONE BELOW +16F93..16F9F ; Miao # Lm [13] MIAO LETTER TONE-2..MIAO LETTER REFORMED TONE-8 + +# Total code points: 149 + +# ================================================ + +11180..11181 ; Sharada # Mn [2] SHARADA SIGN CANDRABINDU..SHARADA SIGN ANUSVARA +11182 ; Sharada # Mc SHARADA SIGN VISARGA +11183..111B2 ; Sharada # Lo [48] SHARADA LETTER A..SHARADA LETTER HA +111B3..111B5 ; Sharada # Mc [3] SHARADA VOWEL SIGN AA..SHARADA VOWEL SIGN II +111B6..111BE ; Sharada # Mn [9] SHARADA VOWEL SIGN U..SHARADA VOWEL SIGN O +111BF..111C0 ; Sharada # Mc [2] SHARADA VOWEL SIGN AU..SHARADA SIGN VIRAMA +111C1..111C4 ; Sharada # Lo [4] SHARADA SIGN AVAGRAHA..SHARADA OM +111C5..111C8 ; Sharada # Po [4] SHARADA DANDA..SHARADA SEPARATOR +111C9..111CC ; Sharada # Mn [4] SHARADA SANDHI MARK..SHARADA EXTRA SHORT VOWEL MARK +111CD ; Sharada # Po SHARADA SUTRA MARK +111CE ; Sharada # Mc SHARADA VOWEL SIGN PRISHTHAMATRA E +111CF ; Sharada # Mn SHARADA SIGN INVERTED CANDRABINDU +111D0..111D9 ; Sharada # Nd [10] SHARADA DIGIT ZERO..SHARADA DIGIT NINE +111DA ; Sharada # Lo SHARADA EKAM +111DB ; Sharada # Po SHARADA SIGN SIDDHAM +111DC ; Sharada # Lo SHARADA HEADSTROKE +111DD..111DF ; Sharada # Po [3] SHARADA CONTINUATION SIGN..SHARADA SECTION MARK-2 + +# Total code points: 96 + +# ================================================ + +110D0..110E8 ; Sora_Sompeng # Lo [25] SORA SOMPENG LETTER SAH..SORA SOMPENG LETTER MAE +110F0..110F9 ; Sora_Sompeng # Nd [10] SORA SOMPENG DIGIT ZERO..SORA SOMPENG DIGIT NINE + +# Total code points: 35 + +# ================================================ + +11680..116AA ; Takri # Lo [43] TAKRI LETTER A..TAKRI LETTER RRA +116AB ; Takri # Mn TAKRI SIGN ANUSVARA +116AC ; Takri # Mc TAKRI SIGN VISARGA +116AD ; Takri # Mn TAKRI VOWEL SIGN AA +116AE..116AF ; Takri # Mc [2] TAKRI VOWEL SIGN I..TAKRI VOWEL SIGN II +116B0..116B5 ; Takri # Mn [6] TAKRI VOWEL SIGN U..TAKRI VOWEL SIGN AU +116B6 ; Takri # Mc TAKRI SIGN VIRAMA +116B7 ; Takri # Mn TAKRI SIGN NUKTA +116B8 ; Takri # Lo TAKRI LETTER ARCHAIC KHA +116B9 ; Takri # Po TAKRI ABBREVIATION SIGN +116C0..116C9 ; Takri # Nd [10] TAKRI DIGIT ZERO..TAKRI DIGIT NINE + +# Total code points: 68 + +# ================================================ + +10530..10563 ; Caucasian_Albanian # Lo [52] CAUCASIAN ALBANIAN LETTER ALT..CAUCASIAN ALBANIAN LETTER KIW +1056F ; Caucasian_Albanian # Po CAUCASIAN ALBANIAN CITATION MARK + +# Total code points: 53 + +# ================================================ + +16AD0..16AED ; Bassa_Vah # Lo [30] BASSA VAH LETTER ENNI..BASSA VAH LETTER I +16AF0..16AF4 ; Bassa_Vah # Mn [5] BASSA VAH COMBINING HIGH TONE..BASSA VAH COMBINING HIGH-LOW TONE +16AF5 ; Bassa_Vah # Po BASSA VAH FULL STOP + +# Total code points: 36 + +# ================================================ + +1BC00..1BC6A ; Duployan # Lo [107] DUPLOYAN LETTER H..DUPLOYAN LETTER VOCALIC M +1BC70..1BC7C ; Duployan # Lo [13] DUPLOYAN AFFIX LEFT HORIZONTAL SECANT..DUPLOYAN AFFIX ATTACHED TANGENT HOOK +1BC80..1BC88 ; Duployan # Lo [9] DUPLOYAN AFFIX HIGH ACUTE..DUPLOYAN AFFIX HIGH VERTICAL +1BC90..1BC99 ; Duployan # Lo [10] DUPLOYAN AFFIX LOW ACUTE..DUPLOYAN AFFIX LOW ARROW +1BC9C ; Duployan # So DUPLOYAN SIGN O WITH CROSS +1BC9D..1BC9E ; Duployan # Mn [2] DUPLOYAN THICK LETTER SELECTOR..DUPLOYAN DOUBLE MARK +1BC9F ; Duployan # Po DUPLOYAN PUNCTUATION CHINOOK FULL STOP + +# Total code points: 143 + +# ================================================ + +10500..10527 ; Elbasan # Lo [40] ELBASAN LETTER A..ELBASAN LETTER KHE + +# Total code points: 40 + +# ================================================ + +11300..11301 ; Grantha # Mn [2] GRANTHA SIGN COMBINING ANUSVARA ABOVE..GRANTHA SIGN CANDRABINDU +11302..11303 ; Grantha # Mc [2] GRANTHA SIGN ANUSVARA..GRANTHA SIGN VISARGA +11305..1130C ; Grantha # Lo [8] GRANTHA LETTER A..GRANTHA LETTER VOCALIC L +1130F..11310 ; Grantha # Lo [2] GRANTHA LETTER EE..GRANTHA LETTER AI +11313..11328 ; Grantha # Lo [22] GRANTHA LETTER OO..GRANTHA LETTER NA +1132A..11330 ; Grantha # Lo [7] GRANTHA LETTER PA..GRANTHA LETTER RA +11332..11333 ; Grantha # Lo [2] GRANTHA LETTER LA..GRANTHA LETTER LLA +11335..11339 ; Grantha # Lo [5] GRANTHA LETTER VA..GRANTHA LETTER HA +1133C ; Grantha # Mn GRANTHA SIGN NUKTA +1133D ; Grantha # Lo GRANTHA SIGN AVAGRAHA +1133E..1133F ; Grantha # Mc [2] GRANTHA VOWEL SIGN AA..GRANTHA VOWEL SIGN I +11340 ; Grantha # Mn GRANTHA VOWEL SIGN II +11341..11344 ; Grantha # Mc [4] GRANTHA VOWEL SIGN U..GRANTHA VOWEL SIGN VOCALIC RR +11347..11348 ; Grantha # Mc [2] GRANTHA VOWEL SIGN EE..GRANTHA VOWEL SIGN AI +1134B..1134D ; Grantha # Mc [3] GRANTHA VOWEL SIGN OO..GRANTHA SIGN VIRAMA +11350 ; Grantha # Lo GRANTHA OM +11357 ; Grantha # Mc GRANTHA AU LENGTH MARK +1135D..11361 ; Grantha # Lo [5] GRANTHA SIGN PLUTA..GRANTHA LETTER VOCALIC LL +11362..11363 ; Grantha # Mc [2] GRANTHA VOWEL SIGN VOCALIC L..GRANTHA VOWEL SIGN VOCALIC LL +11366..1136C ; Grantha # Mn [7] COMBINING GRANTHA DIGIT ZERO..COMBINING GRANTHA DIGIT SIX +11370..11374 ; Grantha # Mn [5] COMBINING GRANTHA LETTER A..COMBINING GRANTHA LETTER PA + +# Total code points: 85 + +# ================================================ + +16B00..16B2F ; Pahawh_Hmong # Lo [48] PAHAWH HMONG VOWEL KEEB..PAHAWH HMONG CONSONANT CAU +16B30..16B36 ; Pahawh_Hmong # Mn [7] PAHAWH HMONG MARK CIM TUB..PAHAWH HMONG MARK CIM TAUM +16B37..16B3B ; Pahawh_Hmong # Po [5] PAHAWH HMONG SIGN VOS THOM..PAHAWH HMONG SIGN VOS FEEM +16B3C..16B3F ; Pahawh_Hmong # So [4] PAHAWH HMONG SIGN XYEEM NTXIV..PAHAWH HMONG SIGN XYEEM FAIB +16B40..16B43 ; Pahawh_Hmong # Lm [4] PAHAWH HMONG SIGN VOS SEEV..PAHAWH HMONG SIGN IB YAM +16B44 ; Pahawh_Hmong # Po PAHAWH HMONG SIGN XAUS +16B45 ; Pahawh_Hmong # So PAHAWH HMONG SIGN CIM TSOV ROG +16B50..16B59 ; Pahawh_Hmong # Nd [10] PAHAWH HMONG DIGIT ZERO..PAHAWH HMONG DIGIT NINE +16B5B..16B61 ; Pahawh_Hmong # No [7] PAHAWH HMONG NUMBER TENS..PAHAWH HMONG NUMBER TRILLIONS +16B63..16B77 ; Pahawh_Hmong # Lo [21] PAHAWH HMONG SIGN VOS LUB..PAHAWH HMONG SIGN CIM NRES TOS +16B7D..16B8F ; Pahawh_Hmong # Lo [19] PAHAWH HMONG CLAN SIGN TSHEEJ..PAHAWH HMONG CLAN SIGN VWJ + +# Total code points: 127 + +# ================================================ + +11200..11211 ; Khojki # Lo [18] KHOJKI LETTER A..KHOJKI LETTER JJA +11213..1122B ; Khojki # Lo [25] KHOJKI LETTER NYA..KHOJKI LETTER LLA +1122C..1122E ; Khojki # Mc [3] KHOJKI VOWEL SIGN AA..KHOJKI VOWEL SIGN II +1122F..11231 ; Khojki # Mn [3] KHOJKI VOWEL SIGN U..KHOJKI VOWEL SIGN AI +11232..11233 ; Khojki # Mc [2] KHOJKI VOWEL SIGN O..KHOJKI VOWEL SIGN AU +11234 ; Khojki # Mn KHOJKI SIGN ANUSVARA +11235 ; Khojki # Mc KHOJKI SIGN VIRAMA +11236..11237 ; Khojki # Mn [2] KHOJKI SIGN NUKTA..KHOJKI SIGN SHADDA +11238..1123D ; Khojki # Po [6] KHOJKI DANDA..KHOJKI ABBREVIATION SIGN +1123E ; Khojki # Mn KHOJKI SIGN SUKUN +1123F..11240 ; Khojki # Lo [2] KHOJKI LETTER QA..KHOJKI LETTER SHORT I +11241 ; Khojki # Mn KHOJKI VOWEL SIGN VOCALIC R + +# Total code points: 65 + +# ================================================ + +10600..10736 ; Linear_A # Lo [311] LINEAR A SIGN AB001..LINEAR A SIGN A664 +10740..10755 ; Linear_A # Lo [22] LINEAR A SIGN A701 A..LINEAR A SIGN A732 JE +10760..10767 ; Linear_A # Lo [8] LINEAR A SIGN A800..LINEAR A SIGN A807 + +# Total code points: 341 + +# ================================================ + +11150..11172 ; Mahajani # Lo [35] MAHAJANI LETTER A..MAHAJANI LETTER RRA +11173 ; Mahajani # Mn MAHAJANI SIGN NUKTA +11174..11175 ; Mahajani # Po [2] MAHAJANI ABBREVIATION SIGN..MAHAJANI SECTION MARK +11176 ; Mahajani # Lo MAHAJANI LIGATURE SHRI + +# Total code points: 39 + +# ================================================ + +10AC0..10AC7 ; Manichaean # Lo [8] MANICHAEAN LETTER ALEPH..MANICHAEAN LETTER WAW +10AC8 ; Manichaean # So MANICHAEAN SIGN UD +10AC9..10AE4 ; Manichaean # Lo [28] MANICHAEAN LETTER ZAYIN..MANICHAEAN LETTER TAW +10AE5..10AE6 ; Manichaean # Mn [2] MANICHAEAN ABBREVIATION MARK ABOVE..MANICHAEAN ABBREVIATION MARK BELOW +10AEB..10AEF ; Manichaean # No [5] MANICHAEAN NUMBER ONE..MANICHAEAN NUMBER ONE HUNDRED +10AF0..10AF6 ; Manichaean # Po [7] MANICHAEAN PUNCTUATION STAR..MANICHAEAN PUNCTUATION LINE FILLER + +# Total code points: 51 + +# ================================================ + +1E800..1E8C4 ; Mende_Kikakui # Lo [197] MENDE KIKAKUI SYLLABLE M001 KI..MENDE KIKAKUI SYLLABLE M060 NYON +1E8C7..1E8CF ; Mende_Kikakui # No [9] MENDE KIKAKUI DIGIT ONE..MENDE KIKAKUI DIGIT NINE +1E8D0..1E8D6 ; Mende_Kikakui # Mn [7] MENDE KIKAKUI COMBINING NUMBER TEENS..MENDE KIKAKUI COMBINING NUMBER MILLIONS + +# Total code points: 213 + +# ================================================ + +11600..1162F ; Modi # Lo [48] MODI LETTER A..MODI LETTER LLA +11630..11632 ; Modi # Mc [3] MODI VOWEL SIGN AA..MODI VOWEL SIGN II +11633..1163A ; Modi # Mn [8] MODI VOWEL SIGN U..MODI VOWEL SIGN AI +1163B..1163C ; Modi # Mc [2] MODI VOWEL SIGN O..MODI VOWEL SIGN AU +1163D ; Modi # Mn MODI SIGN ANUSVARA +1163E ; Modi # Mc MODI SIGN VISARGA +1163F..11640 ; Modi # Mn [2] MODI SIGN VIRAMA..MODI SIGN ARDHACANDRA +11641..11643 ; Modi # Po [3] MODI DANDA..MODI ABBREVIATION SIGN +11644 ; Modi # Lo MODI SIGN HUVA +11650..11659 ; Modi # Nd [10] MODI DIGIT ZERO..MODI DIGIT NINE + +# Total code points: 79 + +# ================================================ + +16A40..16A5E ; Mro # Lo [31] MRO LETTER TA..MRO LETTER TEK +16A60..16A69 ; Mro # Nd [10] MRO DIGIT ZERO..MRO DIGIT NINE +16A6E..16A6F ; Mro # Po [2] MRO DANDA..MRO DOUBLE DANDA + +# Total code points: 43 + +# ================================================ + +10A80..10A9C ; Old_North_Arabian # Lo [29] OLD NORTH ARABIAN LETTER HEH..OLD NORTH ARABIAN LETTER ZAH +10A9D..10A9F ; Old_North_Arabian # No [3] OLD NORTH ARABIAN NUMBER ONE..OLD NORTH ARABIAN NUMBER TWENTY + +# Total code points: 32 + +# ================================================ + +10880..1089E ; Nabataean # Lo [31] NABATAEAN LETTER FINAL ALEPH..NABATAEAN LETTER TAW +108A7..108AF ; Nabataean # No [9] NABATAEAN NUMBER ONE..NABATAEAN NUMBER ONE HUNDRED + +# Total code points: 40 + +# ================================================ + +10860..10876 ; Palmyrene # Lo [23] PALMYRENE LETTER ALEPH..PALMYRENE LETTER TAW +10877..10878 ; Palmyrene # So [2] PALMYRENE LEFT-POINTING FLEURON..PALMYRENE RIGHT-POINTING FLEURON +10879..1087F ; Palmyrene # No [7] PALMYRENE NUMBER ONE..PALMYRENE NUMBER TWENTY + +# Total code points: 32 + +# ================================================ + +11AC0..11AF8 ; Pau_Cin_Hau # Lo [57] PAU CIN HAU LETTER PA..PAU CIN HAU GLOTTAL STOP FINAL + +# Total code points: 57 + +# ================================================ + +10350..10375 ; Old_Permic # Lo [38] OLD PERMIC LETTER AN..OLD PERMIC LETTER IA +10376..1037A ; Old_Permic # Mn [5] COMBINING OLD PERMIC LETTER AN..COMBINING OLD PERMIC LETTER SII + +# Total code points: 43 + +# ================================================ + +10B80..10B91 ; Psalter_Pahlavi # Lo [18] PSALTER PAHLAVI LETTER ALEPH..PSALTER PAHLAVI LETTER TAW +10B99..10B9C ; Psalter_Pahlavi # Po [4] PSALTER PAHLAVI SECTION MARK..PSALTER PAHLAVI FOUR DOTS WITH DOT +10BA9..10BAF ; Psalter_Pahlavi # No [7] PSALTER PAHLAVI NUMBER ONE..PSALTER PAHLAVI NUMBER ONE HUNDRED + +# Total code points: 29 + +# ================================================ + +11580..115AE ; Siddham # Lo [47] SIDDHAM LETTER A..SIDDHAM LETTER HA +115AF..115B1 ; Siddham # Mc [3] SIDDHAM VOWEL SIGN AA..SIDDHAM VOWEL SIGN II +115B2..115B5 ; Siddham # Mn [4] SIDDHAM VOWEL SIGN U..SIDDHAM VOWEL SIGN VOCALIC RR +115B8..115BB ; Siddham # Mc [4] SIDDHAM VOWEL SIGN E..SIDDHAM VOWEL SIGN AU +115BC..115BD ; Siddham # Mn [2] SIDDHAM SIGN CANDRABINDU..SIDDHAM SIGN ANUSVARA +115BE ; Siddham # Mc SIDDHAM SIGN VISARGA +115BF..115C0 ; Siddham # Mn [2] SIDDHAM SIGN VIRAMA..SIDDHAM SIGN NUKTA +115C1..115D7 ; Siddham # Po [23] SIDDHAM SIGN SIDDHAM..SIDDHAM SECTION MARK WITH CIRCLES AND FOUR ENCLOSURES +115D8..115DB ; Siddham # Lo [4] SIDDHAM LETTER THREE-CIRCLE ALTERNATE I..SIDDHAM LETTER ALTERNATE U +115DC..115DD ; Siddham # Mn [2] SIDDHAM VOWEL SIGN ALTERNATE U..SIDDHAM VOWEL SIGN ALTERNATE UU + +# Total code points: 92 + +# ================================================ + +112B0..112DE ; Khudawadi # Lo [47] KHUDAWADI LETTER A..KHUDAWADI LETTER HA +112DF ; Khudawadi # Mn KHUDAWADI SIGN ANUSVARA +112E0..112E2 ; Khudawadi # Mc [3] KHUDAWADI VOWEL SIGN AA..KHUDAWADI VOWEL SIGN II +112E3..112EA ; Khudawadi # Mn [8] KHUDAWADI VOWEL SIGN U..KHUDAWADI SIGN VIRAMA +112F0..112F9 ; Khudawadi # Nd [10] KHUDAWADI DIGIT ZERO..KHUDAWADI DIGIT NINE + +# Total code points: 69 + +# ================================================ + +11480..114AF ; Tirhuta # Lo [48] TIRHUTA ANJI..TIRHUTA LETTER HA +114B0..114B2 ; Tirhuta # Mc [3] TIRHUTA VOWEL SIGN AA..TIRHUTA VOWEL SIGN II +114B3..114B8 ; Tirhuta # Mn [6] TIRHUTA VOWEL SIGN U..TIRHUTA VOWEL SIGN VOCALIC LL +114B9 ; Tirhuta # Mc TIRHUTA VOWEL SIGN E +114BA ; Tirhuta # Mn TIRHUTA VOWEL SIGN SHORT E +114BB..114BE ; Tirhuta # Mc [4] TIRHUTA VOWEL SIGN AI..TIRHUTA VOWEL SIGN AU +114BF..114C0 ; Tirhuta # Mn [2] TIRHUTA SIGN CANDRABINDU..TIRHUTA SIGN ANUSVARA +114C1 ; Tirhuta # Mc TIRHUTA SIGN VISARGA +114C2..114C3 ; Tirhuta # Mn [2] TIRHUTA SIGN VIRAMA..TIRHUTA SIGN NUKTA +114C4..114C5 ; Tirhuta # Lo [2] TIRHUTA SIGN AVAGRAHA..TIRHUTA GVANG +114C6 ; Tirhuta # Po TIRHUTA ABBREVIATION SIGN +114C7 ; Tirhuta # Lo TIRHUTA OM +114D0..114D9 ; Tirhuta # Nd [10] TIRHUTA DIGIT ZERO..TIRHUTA DIGIT NINE + +# Total code points: 82 + +# ================================================ + +118A0..118DF ; Warang_Citi # L& [64] WARANG CITI CAPITAL LETTER NGAA..WARANG CITI SMALL LETTER VIYO +118E0..118E9 ; Warang_Citi # Nd [10] WARANG CITI DIGIT ZERO..WARANG CITI DIGIT NINE +118EA..118F2 ; Warang_Citi # No [9] WARANG CITI NUMBER TEN..WARANG CITI NUMBER NINETY +118FF ; Warang_Citi # Lo WARANG CITI OM + +# Total code points: 84 + +# ================================================ + +11700..1171A ; Ahom # Lo [27] AHOM LETTER KA..AHOM LETTER ALTERNATE BA +1171D..1171F ; Ahom # Mn [3] AHOM CONSONANT SIGN MEDIAL LA..AHOM CONSONANT SIGN MEDIAL LIGATING RA +11720..11721 ; Ahom # Mc [2] AHOM VOWEL SIGN A..AHOM VOWEL SIGN AA +11722..11725 ; Ahom # Mn [4] AHOM VOWEL SIGN I..AHOM VOWEL SIGN UU +11726 ; Ahom # Mc AHOM VOWEL SIGN E +11727..1172B ; Ahom # Mn [5] AHOM VOWEL SIGN AW..AHOM SIGN KILLER +11730..11739 ; Ahom # Nd [10] AHOM DIGIT ZERO..AHOM DIGIT NINE +1173A..1173B ; Ahom # No [2] AHOM NUMBER TEN..AHOM NUMBER TWENTY +1173C..1173E ; Ahom # Po [3] AHOM SIGN SMALL SECTION..AHOM SIGN RULAI +1173F ; Ahom # So AHOM SYMBOL VI +11740..11746 ; Ahom # Lo [7] AHOM LETTER CA..AHOM LETTER LLA + +# Total code points: 65 + +# ================================================ + +14400..14646 ; Anatolian_Hieroglyphs # Lo [583] ANATOLIAN HIEROGLYPH A001..ANATOLIAN HIEROGLYPH A530 + +# Total code points: 583 + +# ================================================ + +108E0..108F2 ; Hatran # Lo [19] HATRAN LETTER ALEPH..HATRAN LETTER QOPH +108F4..108F5 ; Hatran # Lo [2] HATRAN LETTER SHIN..HATRAN LETTER TAW +108FB..108FF ; Hatran # No [5] HATRAN NUMBER ONE..HATRAN NUMBER ONE HUNDRED + +# Total code points: 26 + +# ================================================ + +11280..11286 ; Multani # Lo [7] MULTANI LETTER A..MULTANI LETTER GA +11288 ; Multani # Lo MULTANI LETTER GHA +1128A..1128D ; Multani # Lo [4] MULTANI LETTER CA..MULTANI LETTER JJA +1128F..1129D ; Multani # Lo [15] MULTANI LETTER NYA..MULTANI LETTER BA +1129F..112A8 ; Multani # Lo [10] MULTANI LETTER BHA..MULTANI LETTER RHA +112A9 ; Multani # Po MULTANI SECTION MARK + +# Total code points: 38 + +# ================================================ + +10C80..10CB2 ; Old_Hungarian # L& [51] OLD HUNGARIAN CAPITAL LETTER A..OLD HUNGARIAN CAPITAL LETTER US +10CC0..10CF2 ; Old_Hungarian # L& [51] OLD HUNGARIAN SMALL LETTER A..OLD HUNGARIAN SMALL LETTER US +10CFA..10CFF ; Old_Hungarian # No [6] OLD HUNGARIAN NUMBER ONE..OLD HUNGARIAN NUMBER ONE THOUSAND + +# Total code points: 108 + +# ================================================ + +1D800..1D9FF ; SignWriting # So [512] SIGNWRITING HAND-FIST INDEX..SIGNWRITING HEAD +1DA00..1DA36 ; SignWriting # Mn [55] SIGNWRITING HEAD RIM..SIGNWRITING AIR SUCKING IN +1DA37..1DA3A ; SignWriting # So [4] SIGNWRITING AIR BLOW SMALL ROTATIONS..SIGNWRITING BREATH EXHALE +1DA3B..1DA6C ; SignWriting # Mn [50] SIGNWRITING MOUTH CLOSED NEUTRAL..SIGNWRITING EXCITEMENT +1DA6D..1DA74 ; SignWriting # So [8] SIGNWRITING SHOULDER HIP SPINE..SIGNWRITING TORSO-FLOORPLANE TWISTING +1DA75 ; SignWriting # Mn SIGNWRITING UPPER BODY TILTING FROM HIP JOINTS +1DA76..1DA83 ; SignWriting # So [14] SIGNWRITING LIMB COMBINATION..SIGNWRITING LOCATION DEPTH +1DA84 ; SignWriting # Mn SIGNWRITING LOCATION HEAD NECK +1DA85..1DA86 ; SignWriting # So [2] SIGNWRITING LOCATION TORSO..SIGNWRITING LOCATION LIMBS DIGITS +1DA87..1DA8B ; SignWriting # Po [5] SIGNWRITING COMMA..SIGNWRITING PARENTHESIS +1DA9B..1DA9F ; SignWriting # Mn [5] SIGNWRITING FILL MODIFIER-2..SIGNWRITING FILL MODIFIER-6 +1DAA1..1DAAF ; SignWriting # Mn [15] SIGNWRITING ROTATION MODIFIER-2..SIGNWRITING ROTATION MODIFIER-16 + +# Total code points: 672 + +# ================================================ + +1E900..1E943 ; Adlam # L& [68] ADLAM CAPITAL LETTER ALIF..ADLAM SMALL LETTER SHA +1E944..1E94A ; Adlam # Mn [7] ADLAM ALIF LENGTHENER..ADLAM NUKTA +1E94B ; Adlam # Lm ADLAM NASALIZATION MARK +1E950..1E959 ; Adlam # Nd [10] ADLAM DIGIT ZERO..ADLAM DIGIT NINE +1E95E..1E95F ; Adlam # Po [2] ADLAM INITIAL EXCLAMATION MARK..ADLAM INITIAL QUESTION MARK + +# Total code points: 88 + +# ================================================ + +11C00..11C08 ; Bhaiksuki # Lo [9] BHAIKSUKI LETTER A..BHAIKSUKI LETTER VOCALIC L +11C0A..11C2E ; Bhaiksuki # Lo [37] BHAIKSUKI LETTER E..BHAIKSUKI LETTER HA +11C2F ; Bhaiksuki # Mc BHAIKSUKI VOWEL SIGN AA +11C30..11C36 ; Bhaiksuki # Mn [7] BHAIKSUKI VOWEL SIGN I..BHAIKSUKI VOWEL SIGN VOCALIC L +11C38..11C3D ; Bhaiksuki # Mn [6] BHAIKSUKI VOWEL SIGN E..BHAIKSUKI SIGN ANUSVARA +11C3E ; Bhaiksuki # Mc BHAIKSUKI SIGN VISARGA +11C3F ; Bhaiksuki # Mn BHAIKSUKI SIGN VIRAMA +11C40 ; Bhaiksuki # Lo BHAIKSUKI SIGN AVAGRAHA +11C41..11C45 ; Bhaiksuki # Po [5] BHAIKSUKI DANDA..BHAIKSUKI GAP FILLER-2 +11C50..11C59 ; Bhaiksuki # Nd [10] BHAIKSUKI DIGIT ZERO..BHAIKSUKI DIGIT NINE +11C5A..11C6C ; Bhaiksuki # No [19] BHAIKSUKI NUMBER ONE..BHAIKSUKI HUNDREDS UNIT MARK + +# Total code points: 97 + +# ================================================ + +11C70..11C71 ; Marchen # Po [2] MARCHEN HEAD MARK..MARCHEN MARK SHAD +11C72..11C8F ; Marchen # Lo [30] MARCHEN LETTER KA..MARCHEN LETTER A +11C92..11CA7 ; Marchen # Mn [22] MARCHEN SUBJOINED LETTER KA..MARCHEN SUBJOINED LETTER ZA +11CA9 ; Marchen # Mc MARCHEN SUBJOINED LETTER YA +11CAA..11CB0 ; Marchen # Mn [7] MARCHEN SUBJOINED LETTER RA..MARCHEN VOWEL SIGN AA +11CB1 ; Marchen # Mc MARCHEN VOWEL SIGN I +11CB2..11CB3 ; Marchen # Mn [2] MARCHEN VOWEL SIGN U..MARCHEN VOWEL SIGN E +11CB4 ; Marchen # Mc MARCHEN VOWEL SIGN O +11CB5..11CB6 ; Marchen # Mn [2] MARCHEN SIGN ANUSVARA..MARCHEN SIGN CANDRABINDU + +# Total code points: 68 + +# ================================================ + +11400..11434 ; Newa # Lo [53] NEWA LETTER A..NEWA LETTER HA +11435..11437 ; Newa # Mc [3] NEWA VOWEL SIGN AA..NEWA VOWEL SIGN II +11438..1143F ; Newa # Mn [8] NEWA VOWEL SIGN U..NEWA VOWEL SIGN AI +11440..11441 ; Newa # Mc [2] NEWA VOWEL SIGN O..NEWA VOWEL SIGN AU +11442..11444 ; Newa # Mn [3] NEWA SIGN VIRAMA..NEWA SIGN ANUSVARA +11445 ; Newa # Mc NEWA SIGN VISARGA +11446 ; Newa # Mn NEWA SIGN NUKTA +11447..1144A ; Newa # Lo [4] NEWA SIGN AVAGRAHA..NEWA SIDDHI +1144B..1144F ; Newa # Po [5] NEWA DANDA..NEWA ABBREVIATION SIGN +11450..11459 ; Newa # Nd [10] NEWA DIGIT ZERO..NEWA DIGIT NINE +1145A..1145B ; Newa # Po [2] NEWA DOUBLE COMMA..NEWA PLACEHOLDER MARK +1145D ; Newa # Po NEWA INSERTION SIGN +1145E ; Newa # Mn NEWA SANDHI MARK +1145F..11461 ; Newa # Lo [3] NEWA LETTER VEDIC ANUSVARA..NEWA SIGN UPADHMANIYA + +# Total code points: 97 + +# ================================================ + +104B0..104D3 ; Osage # L& [36] OSAGE CAPITAL LETTER A..OSAGE CAPITAL LETTER ZHA +104D8..104FB ; Osage # L& [36] OSAGE SMALL LETTER A..OSAGE SMALL LETTER ZHA + +# Total code points: 72 + +# ================================================ + +16FE0 ; Tangut # Lm TANGUT ITERATION MARK +17000..187F7 ; Tangut # Lo [6136] TANGUT IDEOGRAPH-17000..TANGUT IDEOGRAPH-187F7 +18800..18AFF ; Tangut # Lo [768] TANGUT COMPONENT-001..TANGUT COMPONENT-768 +18D00..18D08 ; Tangut # Lo [9] TANGUT IDEOGRAPH-18D00..TANGUT IDEOGRAPH-18D08 + +# Total code points: 6914 + +# ================================================ + +11D00..11D06 ; Masaram_Gondi # Lo [7] MASARAM GONDI LETTER A..MASARAM GONDI LETTER E +11D08..11D09 ; Masaram_Gondi # Lo [2] MASARAM GONDI LETTER AI..MASARAM GONDI LETTER O +11D0B..11D30 ; Masaram_Gondi # Lo [38] MASARAM GONDI LETTER AU..MASARAM GONDI LETTER TRA +11D31..11D36 ; Masaram_Gondi # Mn [6] MASARAM GONDI VOWEL SIGN AA..MASARAM GONDI VOWEL SIGN VOCALIC R +11D3A ; Masaram_Gondi # Mn MASARAM GONDI VOWEL SIGN E +11D3C..11D3D ; Masaram_Gondi # Mn [2] MASARAM GONDI VOWEL SIGN AI..MASARAM GONDI VOWEL SIGN O +11D3F..11D45 ; Masaram_Gondi # Mn [7] MASARAM GONDI VOWEL SIGN AU..MASARAM GONDI VIRAMA +11D46 ; Masaram_Gondi # Lo MASARAM GONDI REPHA +11D47 ; Masaram_Gondi # Mn MASARAM GONDI RA-KARA +11D50..11D59 ; Masaram_Gondi # Nd [10] MASARAM GONDI DIGIT ZERO..MASARAM GONDI DIGIT NINE + +# Total code points: 75 + +# ================================================ + +16FE1 ; Nushu # Lm NUSHU ITERATION MARK +1B170..1B2FB ; Nushu # Lo [396] NUSHU CHARACTER-1B170..NUSHU CHARACTER-1B2FB + +# Total code points: 397 + +# ================================================ + +11A50 ; Soyombo # Lo SOYOMBO LETTER A +11A51..11A56 ; Soyombo # Mn [6] SOYOMBO VOWEL SIGN I..SOYOMBO VOWEL SIGN OE +11A57..11A58 ; Soyombo # Mc [2] SOYOMBO VOWEL SIGN AI..SOYOMBO VOWEL SIGN AU +11A59..11A5B ; Soyombo # Mn [3] SOYOMBO VOWEL SIGN VOCALIC R..SOYOMBO VOWEL LENGTH MARK +11A5C..11A89 ; Soyombo # Lo [46] SOYOMBO LETTER KA..SOYOMBO CLUSTER-INITIAL LETTER SA +11A8A..11A96 ; Soyombo # Mn [13] SOYOMBO FINAL CONSONANT SIGN G..SOYOMBO SIGN ANUSVARA +11A97 ; Soyombo # Mc SOYOMBO SIGN VISARGA +11A98..11A99 ; Soyombo # Mn [2] SOYOMBO GEMINATION MARK..SOYOMBO SUBJOINER +11A9A..11A9C ; Soyombo # Po [3] SOYOMBO MARK TSHEG..SOYOMBO MARK DOUBLE SHAD +11A9D ; Soyombo # Lo SOYOMBO MARK PLUTA +11A9E..11AA2 ; Soyombo # Po [5] SOYOMBO HEAD MARK WITH MOON AND SUN AND TRIPLE FLAME..SOYOMBO TERMINAL MARK-2 + +# Total code points: 83 + +# ================================================ + +11A00 ; Zanabazar_Square # Lo ZANABAZAR SQUARE LETTER A +11A01..11A0A ; Zanabazar_Square # Mn [10] ZANABAZAR SQUARE VOWEL SIGN I..ZANABAZAR SQUARE VOWEL LENGTH MARK +11A0B..11A32 ; Zanabazar_Square # Lo [40] ZANABAZAR SQUARE LETTER KA..ZANABAZAR SQUARE LETTER KSSA +11A33..11A38 ; Zanabazar_Square # Mn [6] ZANABAZAR SQUARE FINAL CONSONANT MARK..ZANABAZAR SQUARE SIGN ANUSVARA +11A39 ; Zanabazar_Square # Mc ZANABAZAR SQUARE SIGN VISARGA +11A3A ; Zanabazar_Square # Lo ZANABAZAR SQUARE CLUSTER-INITIAL LETTER RA +11A3B..11A3E ; Zanabazar_Square # Mn [4] ZANABAZAR SQUARE CLUSTER-FINAL LETTER YA..ZANABAZAR SQUARE CLUSTER-FINAL LETTER VA +11A3F..11A46 ; Zanabazar_Square # Po [8] ZANABAZAR SQUARE INITIAL HEAD MARK..ZANABAZAR SQUARE CLOSING DOUBLE-LINED HEAD MARK +11A47 ; Zanabazar_Square # Mn ZANABAZAR SQUARE SUBJOINER + +# Total code points: 72 + +# ================================================ + +11800..1182B ; Dogra # Lo [44] DOGRA LETTER A..DOGRA LETTER RRA +1182C..1182E ; Dogra # Mc [3] DOGRA VOWEL SIGN AA..DOGRA VOWEL SIGN II +1182F..11837 ; Dogra # Mn [9] DOGRA VOWEL SIGN U..DOGRA SIGN ANUSVARA +11838 ; Dogra # Mc DOGRA SIGN VISARGA +11839..1183A ; Dogra # Mn [2] DOGRA SIGN VIRAMA..DOGRA SIGN NUKTA +1183B ; Dogra # Po DOGRA ABBREVIATION SIGN + +# Total code points: 60 + +# ================================================ + +11D60..11D65 ; Gunjala_Gondi # Lo [6] GUNJALA GONDI LETTER A..GUNJALA GONDI LETTER UU +11D67..11D68 ; Gunjala_Gondi # Lo [2] GUNJALA GONDI LETTER EE..GUNJALA GONDI LETTER AI +11D6A..11D89 ; Gunjala_Gondi # Lo [32] GUNJALA GONDI LETTER OO..GUNJALA GONDI LETTER SA +11D8A..11D8E ; Gunjala_Gondi # Mc [5] GUNJALA GONDI VOWEL SIGN AA..GUNJALA GONDI VOWEL SIGN UU +11D90..11D91 ; Gunjala_Gondi # Mn [2] GUNJALA GONDI VOWEL SIGN EE..GUNJALA GONDI VOWEL SIGN AI +11D93..11D94 ; Gunjala_Gondi # Mc [2] GUNJALA GONDI VOWEL SIGN OO..GUNJALA GONDI VOWEL SIGN AU +11D95 ; Gunjala_Gondi # Mn GUNJALA GONDI SIGN ANUSVARA +11D96 ; Gunjala_Gondi # Mc GUNJALA GONDI SIGN VISARGA +11D97 ; Gunjala_Gondi # Mn GUNJALA GONDI VIRAMA +11D98 ; Gunjala_Gondi # Lo GUNJALA GONDI OM +11DA0..11DA9 ; Gunjala_Gondi # Nd [10] GUNJALA GONDI DIGIT ZERO..GUNJALA GONDI DIGIT NINE + +# Total code points: 63 + +# ================================================ + +11EE0..11EF2 ; Makasar # Lo [19] MAKASAR LETTER KA..MAKASAR ANGKA +11EF3..11EF4 ; Makasar # Mn [2] MAKASAR VOWEL SIGN I..MAKASAR VOWEL SIGN U +11EF5..11EF6 ; Makasar # Mc [2] MAKASAR VOWEL SIGN E..MAKASAR VOWEL SIGN O +11EF7..11EF8 ; Makasar # Po [2] MAKASAR PASSIMBANG..MAKASAR END OF SECTION + +# Total code points: 25 + +# ================================================ + +16E40..16E7F ; Medefaidrin # L& [64] MEDEFAIDRIN CAPITAL LETTER M..MEDEFAIDRIN SMALL LETTER Y +16E80..16E96 ; Medefaidrin # No [23] MEDEFAIDRIN DIGIT ZERO..MEDEFAIDRIN DIGIT THREE ALTERNATE FORM +16E97..16E9A ; Medefaidrin # Po [4] MEDEFAIDRIN COMMA..MEDEFAIDRIN EXCLAMATION OH + +# Total code points: 91 + +# ================================================ + +10D00..10D23 ; Hanifi_Rohingya # Lo [36] HANIFI ROHINGYA LETTER A..HANIFI ROHINGYA MARK NA KHONNA +10D24..10D27 ; Hanifi_Rohingya # Mn [4] HANIFI ROHINGYA SIGN HARBAHAY..HANIFI ROHINGYA SIGN TASSI +10D30..10D39 ; Hanifi_Rohingya # Nd [10] HANIFI ROHINGYA DIGIT ZERO..HANIFI ROHINGYA DIGIT NINE + +# Total code points: 50 + +# ================================================ + +10F30..10F45 ; Sogdian # Lo [22] SOGDIAN LETTER ALEPH..SOGDIAN INDEPENDENT SHIN +10F46..10F50 ; Sogdian # Mn [11] SOGDIAN COMBINING DOT BELOW..SOGDIAN COMBINING STROKE BELOW +10F51..10F54 ; Sogdian # No [4] SOGDIAN NUMBER ONE..SOGDIAN NUMBER ONE HUNDRED +10F55..10F59 ; Sogdian # Po [5] SOGDIAN PUNCTUATION TWO VERTICAL BARS..SOGDIAN PUNCTUATION HALF CIRCLE WITH DOT + +# Total code points: 42 + +# ================================================ + +10F00..10F1C ; Old_Sogdian # Lo [29] OLD SOGDIAN LETTER ALEPH..OLD SOGDIAN LETTER FINAL TAW WITH VERTICAL TAIL +10F1D..10F26 ; Old_Sogdian # No [10] OLD SOGDIAN NUMBER ONE..OLD SOGDIAN FRACTION ONE HALF +10F27 ; Old_Sogdian # Lo OLD SOGDIAN LIGATURE AYIN-DALETH + +# Total code points: 40 + +# ================================================ + +10FE0..10FF6 ; Elymaic # Lo [23] ELYMAIC LETTER ALEPH..ELYMAIC LIGATURE ZAYIN-YODH + +# Total code points: 23 + +# ================================================ + +119A0..119A7 ; Nandinagari # Lo [8] NANDINAGARI LETTER A..NANDINAGARI LETTER VOCALIC RR +119AA..119D0 ; Nandinagari # Lo [39] NANDINAGARI LETTER E..NANDINAGARI LETTER RRA +119D1..119D3 ; Nandinagari # Mc [3] NANDINAGARI VOWEL SIGN AA..NANDINAGARI VOWEL SIGN II +119D4..119D7 ; Nandinagari # Mn [4] NANDINAGARI VOWEL SIGN U..NANDINAGARI VOWEL SIGN VOCALIC RR +119DA..119DB ; Nandinagari # Mn [2] NANDINAGARI VOWEL SIGN E..NANDINAGARI VOWEL SIGN AI +119DC..119DF ; Nandinagari # Mc [4] NANDINAGARI VOWEL SIGN O..NANDINAGARI SIGN VISARGA +119E0 ; Nandinagari # Mn NANDINAGARI SIGN VIRAMA +119E1 ; Nandinagari # Lo NANDINAGARI SIGN AVAGRAHA +119E2 ; Nandinagari # Po NANDINAGARI SIGN SIDDHAM +119E3 ; Nandinagari # Lo NANDINAGARI HEADSTROKE +119E4 ; Nandinagari # Mc NANDINAGARI VOWEL SIGN PRISHTHAMATRA E + +# Total code points: 65 + +# ================================================ + +1E100..1E12C ; Nyiakeng_Puachue_Hmong # Lo [45] NYIAKENG PUACHUE HMONG LETTER MA..NYIAKENG PUACHUE HMONG LETTER W +1E130..1E136 ; Nyiakeng_Puachue_Hmong # Mn [7] NYIAKENG PUACHUE HMONG TONE-B..NYIAKENG PUACHUE HMONG TONE-D +1E137..1E13D ; Nyiakeng_Puachue_Hmong # Lm [7] NYIAKENG PUACHUE HMONG SIGN FOR PERSON..NYIAKENG PUACHUE HMONG SYLLABLE LENGTHENER +1E140..1E149 ; Nyiakeng_Puachue_Hmong # Nd [10] NYIAKENG PUACHUE HMONG DIGIT ZERO..NYIAKENG PUACHUE HMONG DIGIT NINE +1E14E ; Nyiakeng_Puachue_Hmong # Lo NYIAKENG PUACHUE HMONG LOGOGRAM NYAJ +1E14F ; Nyiakeng_Puachue_Hmong # So NYIAKENG PUACHUE HMONG CIRCLED CA + +# Total code points: 71 + +# ================================================ + +1E2C0..1E2EB ; Wancho # Lo [44] WANCHO LETTER AA..WANCHO LETTER YIH +1E2EC..1E2EF ; Wancho # Mn [4] WANCHO TONE TUP..WANCHO TONE KOINI +1E2F0..1E2F9 ; Wancho # Nd [10] WANCHO DIGIT ZERO..WANCHO DIGIT NINE +1E2FF ; Wancho # Sc WANCHO NGUN SIGN + +# Total code points: 59 + +# ================================================ + +10FB0..10FC4 ; Chorasmian # Lo [21] CHORASMIAN LETTER ALEPH..CHORASMIAN LETTER TAW +10FC5..10FCB ; Chorasmian # No [7] CHORASMIAN NUMBER ONE..CHORASMIAN NUMBER ONE HUNDRED + +# Total code points: 28 + +# ================================================ + +11900..11906 ; Dives_Akuru # Lo [7] DIVES AKURU LETTER A..DIVES AKURU LETTER E +11909 ; Dives_Akuru # Lo DIVES AKURU LETTER O +1190C..11913 ; Dives_Akuru # Lo [8] DIVES AKURU LETTER KA..DIVES AKURU LETTER JA +11915..11916 ; Dives_Akuru # Lo [2] DIVES AKURU LETTER NYA..DIVES AKURU LETTER TTA +11918..1192F ; Dives_Akuru # Lo [24] DIVES AKURU LETTER DDA..DIVES AKURU LETTER ZA +11930..11935 ; Dives_Akuru # Mc [6] DIVES AKURU VOWEL SIGN AA..DIVES AKURU VOWEL SIGN E +11937..11938 ; Dives_Akuru # Mc [2] DIVES AKURU VOWEL SIGN AI..DIVES AKURU VOWEL SIGN O +1193B..1193C ; Dives_Akuru # Mn [2] DIVES AKURU SIGN ANUSVARA..DIVES AKURU SIGN CANDRABINDU +1193D ; Dives_Akuru # Mc DIVES AKURU SIGN HALANTA +1193E ; Dives_Akuru # Mn DIVES AKURU VIRAMA +1193F ; Dives_Akuru # Lo DIVES AKURU PREFIXED NASAL SIGN +11940 ; Dives_Akuru # Mc DIVES AKURU MEDIAL YA +11941 ; Dives_Akuru # Lo DIVES AKURU INITIAL RA +11942 ; Dives_Akuru # Mc DIVES AKURU MEDIAL RA +11943 ; Dives_Akuru # Mn DIVES AKURU SIGN NUKTA +11944..11946 ; Dives_Akuru # Po [3] DIVES AKURU DOUBLE DANDA..DIVES AKURU END OF TEXT MARK +11950..11959 ; Dives_Akuru # Nd [10] DIVES AKURU DIGIT ZERO..DIVES AKURU DIGIT NINE + +# Total code points: 72 + +# ================================================ + +16FE4 ; Khitan_Small_Script # Mn KHITAN SMALL SCRIPT FILLER +18B00..18CD5 ; Khitan_Small_Script # Lo [470] KHITAN SMALL SCRIPT CHARACTER-18B00..KHITAN SMALL SCRIPT CHARACTER-18CD5 + +# Total code points: 471 + +# ================================================ + +10E80..10EA9 ; Yezidi # Lo [42] YEZIDI LETTER ELIF..YEZIDI LETTER ET +10EAB..10EAC ; Yezidi # Mn [2] YEZIDI COMBINING HAMZA MARK..YEZIDI COMBINING MADDA MARK +10EAD ; Yezidi # Pd YEZIDI HYPHENATION MARK +10EB0..10EB1 ; Yezidi # Lo [2] YEZIDI LETTER LAM WITH DOT ABOVE..YEZIDI LETTER YOT WITH CIRCUMFLEX ABOVE + +# Total code points: 47 + +# ================================================ + +12F90..12FF0 ; Cypro_Minoan # Lo [97] CYPRO-MINOAN SIGN CM001..CYPRO-MINOAN SIGN CM114 +12FF1..12FF2 ; Cypro_Minoan # Po [2] CYPRO-MINOAN SIGN CM301..CYPRO-MINOAN SIGN CM302 + +# Total code points: 99 + +# ================================================ + +10F70..10F81 ; Old_Uyghur # Lo [18] OLD UYGHUR LETTER ALEPH..OLD UYGHUR LETTER LESH +10F82..10F85 ; Old_Uyghur # Mn [4] OLD UYGHUR COMBINING DOT ABOVE..OLD UYGHUR COMBINING TWO DOTS BELOW +10F86..10F89 ; Old_Uyghur # Po [4] OLD UYGHUR PUNCTUATION BAR..OLD UYGHUR PUNCTUATION FOUR DOTS + +# Total code points: 26 + +# ================================================ + +16A70..16ABE ; Tangsa # Lo [79] TANGSA LETTER OZ..TANGSA LETTER ZA +16AC0..16AC9 ; Tangsa # Nd [10] TANGSA DIGIT ZERO..TANGSA DIGIT NINE + +# Total code points: 89 + +# ================================================ + +1E290..1E2AD ; Toto # Lo [30] TOTO LETTER PA..TOTO LETTER A +1E2AE ; Toto # Mn TOTO SIGN RISING TONE + +# Total code points: 31 + +# ================================================ + +10570..1057A ; Vithkuqi # L& [11] VITHKUQI CAPITAL LETTER A..VITHKUQI CAPITAL LETTER GA +1057C..1058A ; Vithkuqi # L& [15] VITHKUQI CAPITAL LETTER HA..VITHKUQI CAPITAL LETTER RE +1058C..10592 ; Vithkuqi # L& [7] VITHKUQI CAPITAL LETTER SE..VITHKUQI CAPITAL LETTER XE +10594..10595 ; Vithkuqi # L& [2] VITHKUQI CAPITAL LETTER Y..VITHKUQI CAPITAL LETTER ZE +10597..105A1 ; Vithkuqi # L& [11] VITHKUQI SMALL LETTER A..VITHKUQI SMALL LETTER GA +105A3..105B1 ; Vithkuqi # L& [15] VITHKUQI SMALL LETTER HA..VITHKUQI SMALL LETTER RE +105B3..105B9 ; Vithkuqi # L& [7] VITHKUQI SMALL LETTER SE..VITHKUQI SMALL LETTER XE +105BB..105BC ; Vithkuqi # L& [2] VITHKUQI SMALL LETTER Y..VITHKUQI SMALL LETTER ZE + +# Total code points: 70 + +# ================================================ + +11F00..11F01 ; Kawi # Mn [2] KAWI SIGN CANDRABINDU..KAWI SIGN ANUSVARA +11F02 ; Kawi # Lo KAWI SIGN REPHA +11F03 ; Kawi # Mc KAWI SIGN VISARGA +11F04..11F10 ; Kawi # Lo [13] KAWI LETTER A..KAWI LETTER O +11F12..11F33 ; Kawi # Lo [34] KAWI LETTER KA..KAWI LETTER JNYA +11F34..11F35 ; Kawi # Mc [2] KAWI VOWEL SIGN AA..KAWI VOWEL SIGN ALTERNATE AA +11F36..11F3A ; Kawi # Mn [5] KAWI VOWEL SIGN I..KAWI VOWEL SIGN VOCALIC R +11F3E..11F3F ; Kawi # Mc [2] KAWI VOWEL SIGN E..KAWI VOWEL SIGN AI +11F40 ; Kawi # Mn KAWI VOWEL SIGN EU +11F41 ; Kawi # Mc KAWI SIGN KILLER +11F42 ; Kawi # Mn KAWI CONJOINER +11F43..11F4F ; Kawi # Po [13] KAWI DANDA..KAWI PUNCTUATION CLOSING SPIRAL +11F50..11F59 ; Kawi # Nd [10] KAWI DIGIT ZERO..KAWI DIGIT NINE + +# Total code points: 86 + +# ================================================ + +1E4D0..1E4EA ; Nag_Mundari # Lo [27] NAG MUNDARI LETTER O..NAG MUNDARI LETTER ELL +1E4EB ; Nag_Mundari # Lm NAG MUNDARI SIGN OJOD +1E4EC..1E4EF ; Nag_Mundari # Mn [4] NAG MUNDARI SIGN MUHOR..NAG MUNDARI SIGN SUTUH +1E4F0..1E4F9 ; Nag_Mundari # Nd [10] NAG MUNDARI DIGIT ZERO..NAG MUNDARI DIGIT NINE + +# Total code points: 42 + +# EOF diff --git a/library/src/main/java/com/tom_roush/fontbox/afm/AFMParser.java b/library/src/main/java/com/tom_roush/fontbox/afm/AFMParser.java index c4795e4e5..625de5a0c 100644 --- a/library/src/main/java/com/tom_roush/fontbox/afm/AFMParser.java +++ b/library/src/main/java/com/tom_roush/fontbox/afm/AFMParser.java @@ -20,11 +20,11 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.List; +import java.util.NoSuchElementException; import java.util.StringTokenizer; import com.tom_roush.fontbox.util.BoundingBox; import com.tom_roush.fontbox.util.Charsets; - /** * This class is used to parse AFM(Adobe Font Metrics) documents. * @@ -310,7 +310,7 @@ public AFMParser( InputStream in ) */ public FontMetrics parse() throws IOException { - return parseFontMetric(false); + return parse(false); } /** @@ -324,8 +324,16 @@ public FontMetrics parse() throws IOException */ public FontMetrics parse(boolean reducedDataset) throws IOException { - return parseFontMetric(reducedDataset); + try + { + return parseFontMetric(reducedDataset); + } + catch (NoSuchElementException ex) + { + throw new IOException(ex); + } } + /** * This will parse a font metrics item. * @@ -340,7 +348,7 @@ private FontMetrics parseFontMetric(boolean reducedDataset) throws IOException if( !START_FONT_METRICS.equals( startFontMetrics ) ) { throw new IOException( "Error: The AFM file should start with " + START_FONT_METRICS + - " and not '" + startFontMetrics + "'" ); + " and not '" + startFontMetrics + "'" ); } fontMetrics.setAFMVersion( readFloat() ); String nextCommand; @@ -479,7 +487,7 @@ else if( START_CHAR_METRICS.equals( nextCommand ) ) if( !end.equals( END_CHAR_METRICS ) ) { throw new IOException( "Error: Expected '" + END_CHAR_METRICS + "' actual '" + - end + "'" ); + end + "'" ); } charMetricsRead = true; fontMetrics.setCharMetrics(charMetrics); @@ -496,7 +504,7 @@ else if( !reducedDataset && START_COMPOSITES.equals( nextCommand ) ) if( !end.equals( END_COMPOSITES ) ) { throw new IOException( "Error: Expected '" + END_COMPOSITES + "' actual '" + - end + "'" ); + end + "'" ); } } else if( !reducedDataset && START_KERN_DATA.equals( nextCommand ) ) @@ -544,7 +552,7 @@ private void parseKernData( FontMetrics fontMetrics ) throws IOException if( !end.equals( END_TRACK_KERN ) ) { throw new IOException( "Error: Expected '" + END_TRACK_KERN + "' actual '" + - end + "'" ); + end + "'" ); } } else if( START_KERN_PAIRS.equals( nextCommand ) ) @@ -559,7 +567,7 @@ else if( START_KERN_PAIRS.equals( nextCommand ) ) if( !end.equals( END_KERN_PAIRS ) ) { throw new IOException( "Error: Expected '" + END_KERN_PAIRS + "' actual '" + - end + "'" ); + end + "'" ); } } else if( START_KERN_PAIRS0.equals( nextCommand ) ) @@ -574,7 +582,7 @@ else if( START_KERN_PAIRS0.equals( nextCommand ) ) if( !end.equals( END_KERN_PAIRS ) ) { throw new IOException( "Error: Expected '" + END_KERN_PAIRS + "' actual '" + - end + "'" ); + end + "'" ); } } else if( START_KERN_PAIRS1.equals( nextCommand ) ) @@ -589,7 +597,7 @@ else if( START_KERN_PAIRS1.equals( nextCommand ) ) if( !end.equals( END_KERN_PAIRS ) ) { throw new IOException( "Error: Expected '" + END_KERN_PAIRS + "' actual '" + - end + "'" ); + end + "'" ); } } else @@ -757,12 +765,41 @@ private CharMetric parseCharMetric() throws IOException while( metricsTokenizer.hasMoreTokens() ) { String nextCommand = metricsTokenizer.nextToken(); + // top 5 most used first if( nextCommand.equals( CHARMETRICS_C ) ) { String charCode = metricsTokenizer.nextToken(); charMetric.setCharacterCode( Integer.parseInt( charCode ) ); verifySemicolon( metricsTokenizer ); } + else if( nextCommand.equals( CHARMETRICS_WX ) ) + { + charMetric.setWx(Float.parseFloat(metricsTokenizer.nextToken())); + verifySemicolon( metricsTokenizer ); + } + else if( nextCommand.equals( CHARMETRICS_N ) ) + { + charMetric.setName(metricsTokenizer.nextToken()); + verifySemicolon( metricsTokenizer ); + } + else if( nextCommand.equals( CHARMETRICS_B ) ) + { + BoundingBox box = new BoundingBox(); + box.setLowerLeftX(Float.parseFloat(metricsTokenizer.nextToken())); + box.setLowerLeftY(Float.parseFloat(metricsTokenizer.nextToken())); + box.setUpperRightX(Float.parseFloat(metricsTokenizer.nextToken())); + box.setUpperRightY(Float.parseFloat(metricsTokenizer.nextToken())); + charMetric.setBoundingBox( box ); + verifySemicolon( metricsTokenizer ); + } + else if( nextCommand.equals( CHARMETRICS_L ) ) + { + Ligature lig = new Ligature(); + lig.setSuccessor(metricsTokenizer.nextToken()); + lig.setLigature(metricsTokenizer.nextToken()); + charMetric.addLigature( lig ); + verifySemicolon( metricsTokenizer ); + } else if( nextCommand.equals( CHARMETRICS_CH ) ) { //Is the hex string or FF, the spec is a little @@ -771,11 +808,6 @@ else if( nextCommand.equals( CHARMETRICS_CH ) ) charMetric.setCharacterCode( Integer.parseInt( charCode, BITS_IN_HEX ) ); verifySemicolon( metricsTokenizer ); } - else if( nextCommand.equals( CHARMETRICS_WX ) ) - { - charMetric.setWx(Float.parseFloat(metricsTokenizer.nextToken())); - verifySemicolon( metricsTokenizer ); - } else if( nextCommand.equals( CHARMETRICS_W0X ) ) { charMetric.setW0x(Float.parseFloat(metricsTokenizer.nextToken())); @@ -833,29 +865,6 @@ else if( nextCommand.equals( CHARMETRICS_VV ) ) charMetric.setVv( vv ); verifySemicolon( metricsTokenizer ); } - else if( nextCommand.equals( CHARMETRICS_N ) ) - { - charMetric.setName(metricsTokenizer.nextToken()); - verifySemicolon( metricsTokenizer ); - } - else if( nextCommand.equals( CHARMETRICS_B ) ) - { - BoundingBox box = new BoundingBox(); - box.setLowerLeftX(Float.parseFloat(metricsTokenizer.nextToken())); - box.setLowerLeftY(Float.parseFloat(metricsTokenizer.nextToken())); - box.setUpperRightX(Float.parseFloat(metricsTokenizer.nextToken())); - box.setUpperRightY(Float.parseFloat(metricsTokenizer.nextToken())); - charMetric.setBoundingBox( box ); - verifySemicolon( metricsTokenizer ); - } - else if( nextCommand.equals( CHARMETRICS_L ) ) - { - Ligature lig = new Ligature(); - lig.setSuccessor(metricsTokenizer.nextToken()); - lig.setLigature(metricsTokenizer.nextToken()); - charMetric.addLigature( lig ); - verifySemicolon( metricsTokenizer ); - } else { throw new IOException( "Unknown CharMetrics command '" + nextCommand + "'" ); @@ -884,7 +893,7 @@ private void verifySemicolon( StringTokenizer tokenizer ) throws IOException if (!";".equals(semicolon)) { throw new IOException( "Error: Expected semicolon in stream actual='" + - semicolon + "'" ); + semicolon + "'" ); } } else @@ -996,10 +1005,9 @@ private String readString() throws IOException * * @return true If the character is whitespace as defined by the AFM spec. */ - private boolean isEOL( int character ) + private static boolean isEOL( int character ) { - return character == 0x0D || - character == 0x0A; + return character == 0x0D || character == 0x0A; } /** @@ -1009,11 +1017,17 @@ private boolean isEOL( int character ) * * @return true If the character is whitespace as defined by the AFM spec. */ - private boolean isWhitespace( int character ) + private static boolean isWhitespace( int character ) { - return character == ' ' || - character == '\t' || - character == 0x0D || - character == 0x0A; + switch (character) + { + case ' ': + case '\t': + case 0x0D: + case 0x0A: + return true; + default: + return false; + } } } diff --git a/library/src/main/java/com/tom_roush/fontbox/cff/CFFCIDFont.java b/library/src/main/java/com/tom_roush/fontbox/cff/CFFCIDFont.java index 86baa78b7..5661d19f1 100644 --- a/library/src/main/java/com/tom_roush/fontbox/cff/CFFCIDFont.java +++ b/library/src/main/java/com/tom_roush/fontbox/cff/CFFCIDFont.java @@ -20,13 +20,12 @@ import android.graphics.Path; import java.io.IOException; -import java.util.LinkedList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import com.tom_roush.fontbox.type1.Type1CharStringReader; - /** * A Type 0 CIDFont represented in a CFF file. Thread safe. * @@ -39,12 +38,12 @@ public class CFFCIDFont extends CFFFont private String ordering; private int supplement; - private List> fontDictionaries = new LinkedList>(); - private List> privateDictionaries = new LinkedList>(); + private List> fontDictionaries = Collections.emptyList(); + private List> privateDictionaries = Collections.emptyList(); private FDSelect fdSelect; private final Map charStringCache = - new ConcurrentHashMap(); + new ConcurrentHashMap(); private final PrivateType1CharStringReader reader = new PrivateType1CharStringReader(); @@ -175,12 +174,12 @@ void setFdSelect(FDSelect fdSelect) private int getDefaultWidthX(int gid) { int fdArrayIndex = this.fdSelect.getFDIndex(gid); - if (fdArrayIndex == -1) + if (fdArrayIndex == -1 || fdArrayIndex >= this.privateDictionaries.size()) { return 1000; } - Map privDict = this.privateDictionaries.get(fdArrayIndex); - return privDict.containsKey("defaultWidthX") ? ((Number)privDict.get("defaultWidthX")).intValue() : 1000; + Object privDictValue = this.privateDictionaries.get(fdArrayIndex).get("defaultWidthX"); + return privDictValue instanceof Number ? ((Number) privDictValue).intValue() : 1000; } /** @@ -191,12 +190,12 @@ private int getDefaultWidthX(int gid) private int getNominalWidthX(int gid) { int fdArrayIndex = this.fdSelect.getFDIndex(gid); - if (fdArrayIndex == -1) + if (fdArrayIndex == -1 || fdArrayIndex >= this.privateDictionaries.size()) { return 0; } - Map privDict = this.privateDictionaries.get(fdArrayIndex); - return privDict.containsKey("nominalWidthX") ? ((Number)privDict.get("nominalWidthX")).intValue() : 0; + Object privDictValue = this.privateDictionaries.get(fdArrayIndex).get("nominalWidthX"); + return privDictValue instanceof Number ? ((Number) privDictValue).intValue() : 0; } /** @@ -207,7 +206,7 @@ private int getNominalWidthX(int gid) private byte[][] getLocalSubrIndex(int gid) { int fdArrayIndex = this.fdSelect.getFDIndex(gid); - if (fdArrayIndex == -1) + if (fdArrayIndex == -1 || fdArrayIndex >= this.privateDictionaries.size()) { return null; } @@ -237,7 +236,7 @@ public CIDKeyedType2CharString getType2CharString(int cid) throws IOException Type2CharStringParser parser = new Type2CharStringParser(fontName, cid); List type2seq = parser.parse(bytes, globalSubrIndex, getLocalSubrIndex(gid)); type2 = new CIDKeyedType2CharString(reader, fontName, cid, gid, type2seq, - getDefaultWidthX(gid), getNominalWidthX(gid)); + getDefaultWidthX(gid), getNominalWidthX(gid)); charStringCache.put(cid, type2); } return type2; diff --git a/library/src/main/java/com/tom_roush/fontbox/cff/CFFParser.java b/library/src/main/java/com/tom_roush/fontbox/cff/CFFParser.java index c4bf61012..7a0db5dae 100644 --- a/library/src/main/java/com/tom_roush/fontbox/cff/CFFParser.java +++ b/library/src/main/java/com/tom_roush/fontbox/cff/CFFParser.java @@ -28,9 +28,8 @@ import java.util.Map; import com.tom_roush.fontbox.util.Charsets; - /** - * This class represents a parser for a CFF font. + * This class represents a parser for a CFF font. * @author Villu Ruusmann */ public class CFFParser @@ -223,8 +222,8 @@ private static String[] readStringIndexData(CFFDataInput input) throws IOExcepti if (length < 0) { throw new IOException("Negative index data length + " + length + " at " + - i + ": offsets[" + (i + 1) + "]=" + offsets[i + 1] + - ", offsets[" + i + "]=" + offsets[i]); + i + ": offsets[" + (i + 1) + "]=" + offsets[i + 1] + + ", offsets[" + i + "]=" + offsets[i]); } indexDataValues[i] = new String(input.readBytes(length), Charsets.ISO_8859_1); } @@ -346,53 +345,53 @@ private static Double readRealNumber(CFFDataInput input) throws IOException { switch (nibble) { - case 0x0: - case 0x1: - case 0x2: - case 0x3: - case 0x4: - case 0x5: - case 0x6: - case 0x7: - case 0x8: - case 0x9: - sb.append(nibble); - exponentMissing = false; - break; - case 0xa: - sb.append("."); - break; - case 0xb: - if (hasExponent) - { - Log.w("PdfBox-Android", "duplicate 'E' ignored after " + sb); - break; - } - sb.append("E"); - exponentMissing = true; - hasExponent = true; - break; - case 0xc: - if (hasExponent) - { - Log.w("PdfBox-Android", "duplicate 'E-' ignored after " + sb); - break; - } - sb.append("E-"); - exponentMissing = true; - hasExponent = true; - break; - case 0xd: - break; - case 0xe: - sb.append("-"); + case 0x0: + case 0x1: + case 0x2: + case 0x3: + case 0x4: + case 0x5: + case 0x6: + case 0x7: + case 0x8: + case 0x9: + sb.append(nibble); + exponentMissing = false; + break; + case 0xa: + sb.append('.'); + break; + case 0xb: + if (hasExponent) + { + Log.w("PdfBox-Android", "duplicate 'E' ignored after " + sb); break; - case 0xf: - done = true; + } + sb.append('E'); + exponentMissing = true; + hasExponent = true; + break; + case 0xc: + if (hasExponent) + { + Log.w("PdfBox-Android", "duplicate 'E-' ignored after " + sb); break; - default: - // can only be a programming error because a nibble is between 0 and F - throw new IllegalArgumentException("illegal nibble " + nibble); + } + sb.append("E-"); + exponentMissing = true; + hasExponent = true; + break; + case 0xd: + break; + case 0xe: + sb.append('-'); + break; + case 0xf: + done = true; + break; + default: + // can only be a programming error because a nibble is between 0 and F + throw new IllegalArgumentException("illegal nibble " + nibble); } } } @@ -401,7 +400,7 @@ private static Double readRealNumber(CFFDataInput input) throws IOException // the exponent is missing, just append "0" to avoid an exception // not sure if 0 is the correct value, but it seems to fit // see PDFBOX-1522 - sb.append("0"); + sb.append('0'); } if (sb.length() == 0) { @@ -470,11 +469,11 @@ private CFFFont parseFont(CFFDataInput input, String name, byte[] topDictIndex) font.addValueToTopDict("PaintType", topDict.getNumber("PaintType", 0)); font.addValueToTopDict("CharstringType", topDict.getNumber("CharstringType", 2)); font.addValueToTopDict("FontMatrix", topDict.getArray("FontMatrix", Arrays.asList( - 0.001, (double) 0, (double) 0, 0.001, - (double) 0, (double) 0))); + 0.001, (double) 0, (double) 0, 0.001, + (double) 0, (double) 0))); font.addValueToTopDict("UniqueID", topDict.getNumber("UniqueID", null)); font.addValueToTopDict("FontBBox", topDict.getArray("FontBBox", - Arrays. asList(0, 0, 0, 0))); + Arrays. asList(0, 0, 0, 0))); font.addValueToTopDict("StrokeWidth", topDict.getNumber("StrokeWidth", 0)); font.addValueToTopDict("XUID", topDict.getArray("XUID", null)); @@ -556,8 +555,8 @@ else if (!isCIDFont && charsetId == 2) { // default font.addValueToTopDict("FontMatrix", topDict.getArray("FontMatrix", - Arrays.asList(0.001, (double) 0, (double) 0, 0.001, - (double) 0, (double) 0))); + Arrays.asList(0.001, (double) 0, (double) 0, 0.001, + (double) 0, (double) 0))); } } else if (privMatrix != null) @@ -609,7 +608,7 @@ private void concatenateMatrix(List matrixDest, List matrixConca * Parse dictionaries specific to a CIDFont. */ private void parseCIDFontDicts(CFFDataInput input, DictData topDict, CFFCIDFont font, int nrOfcharStrings) - throws IOException + throws IOException { // In a CIDKeyed Font, the Private dictionary isn't in the Top Dict but in the Font dict // which can be accessed by a lookup using FDArray and FDSelect @@ -631,18 +630,12 @@ private void parseCIDFontDicts(CFFDataInput input, DictData topDict, CFFCIDFont List> privateDictionaries = new LinkedList>(); List> fontDictionaries = new LinkedList>(); + boolean privateDictPopulated = false; for (byte[] bytes : fdIndex) { CFFDataInput fontDictInput = new CFFDataInput(bytes); DictData fontDict = readDictData(fontDictInput); - // read private dict - DictData.Entry privateEntry = fontDict.getEntry("Private"); - if (privateEntry == null || privateEntry.size() < 2) - { - throw new IOException("Font DICT invalid without \"Private\" entry"); - } - // font dict Map fontDictMap = new LinkedHashMap(4); fontDictMap.put("FontName", getString(fontDict, "FontName")); @@ -652,12 +645,23 @@ private void parseCIDFontDicts(CFFDataInput input, DictData topDict, CFFCIDFont // TODO OD-4 : Add here other keys fontDictionaries.add(fontDictMap); + // read private dict + DictData.Entry privateEntry = fontDict.getEntry("Private"); + if (privateEntry == null || privateEntry.size() < 2) + { + // PDFBOX-5843 don't abort here, and don't skip empty bytes entries, because + // getLocalSubrIndex() expects subr at a specific index + privateDictionaries.add(new HashMap()); + continue; + } + int privateOffset = privateEntry.getNumber(1).intValue(); input.setPosition(privateOffset); int privateSize = privateEntry.getNumber(0).intValue(); DictData privateDict = readDictData(input, privateSize); // populate private dict + privateDictPopulated = true; Map privDict = readPrivateDict(privateDict); privateDictionaries.add(privDict); @@ -670,6 +674,11 @@ private void parseCIDFontDicts(CFFDataInput input, DictData topDict, CFFCIDFont } } + if (!privateDictPopulated) + { + throw new IOException("Font DICT invalid without \"Private\" entry"); + } + // font-dict (FD) select DictData.Entry fdSelectEntry = topDict.getEntry("FDSelect"); if (fdSelectEntry == null || !fdSelectEntry.hasOperands()) @@ -716,13 +725,13 @@ private Map readPrivateDict(DictData privateDict) * Parse dictionaries specific to a Type 1-equivalent font. */ private void parseType1Dicts(CFFDataInput input, DictData topDict, CFFType1Font font, CFFCharset charset) - throws IOException + throws IOException { // encoding DictData.Entry encodingEntry = topDict.getEntry("Encoding"); CFFEncoding encoding; int encodingId = encodingEntry != null && encodingEntry.hasOperands() ? - encodingEntry.getNumber(0).intValue() : 0; + encodingEntry.getNumber(0).intValue() : 0; switch (encodingId) { case 0: @@ -806,7 +815,7 @@ private CFFEncoding readEncoding(CFFDataInput dataInput, CFFCharset charset) thr } private Format0Encoding readFormat0Encoding(CFFDataInput dataInput, CFFCharset charset, - int format) throws IOException + int format) throws IOException { Format0Encoding encoding = new Format0Encoding(); encoding.format = format; @@ -826,7 +835,7 @@ private Format0Encoding readFormat0Encoding(CFFDataInput dataInput, CFFCharset c } private Format1Encoding readFormat1Encoding(CFFDataInput dataInput, CFFCharset charset, - int format) throws IOException + int format) throws IOException { Format1Encoding encoding = new Format1Encoding(); encoding.format = format; @@ -899,7 +908,7 @@ private static FDSelect readFDSelect(CFFDataInput dataInput, int nGlyphs, CFFCID * @throws IOException */ private static Format0FDSelect readFormat0FDSelect(CFFDataInput dataInput, int format, int nGlyphs, CFFCIDFont ros) - throws IOException + throws IOException { Format0FDSelect fdselect = new Format0FDSelect(ros); fdselect.format = format; @@ -922,7 +931,7 @@ private static Format0FDSelect readFormat0FDSelect(CFFDataInput dataInput, int f * @throws IOException */ private static Format3FDSelect readFormat3FDSelect(CFFDataInput dataInput, int format, int nGlyphs, CFFCIDFont ros) - throws IOException + throws IOException { Format3FDSelect fdselect = new Format3FDSelect(ros); fdselect.format = format; @@ -990,7 +999,7 @@ public int getFDIndex(int gid) public String toString() { return getClass().getName() + "[format=" + format + " nbRanges=" + nbRanges + ", range3=" - + Arrays.toString(range3) + " sentinel=" + sentinel + "]"; + + Arrays.toString(range3) + " sentinel=" + sentinel + "]"; } } @@ -1041,7 +1050,7 @@ public String toString() } private CFFCharset readCharset(CFFDataInput dataInput, int nGlyphs, boolean isCIDFont) - throws IOException + throws IOException { int format = dataInput.readCard8(); switch (format) @@ -1058,7 +1067,7 @@ private CFFCharset readCharset(CFFDataInput dataInput, int nGlyphs, boolean isCI } private Format0Charset readFormat0Charset(CFFDataInput dataInput, int format, int nGlyphs, - boolean isCIDFont) throws IOException + boolean isCIDFont) throws IOException { Format0Charset charset = new Format0Charset(isCIDFont); charset.format = format; @@ -1087,7 +1096,7 @@ private Format0Charset readFormat0Charset(CFFDataInput dataInput, int format, in } private Format1Charset readFormat1Charset(CFFDataInput dataInput, int format, int nGlyphs, - boolean isCIDFont) throws IOException + boolean isCIDFont) throws IOException { Format1Charset charset = new Format1Charset(isCIDFont); charset.format = format; @@ -1123,7 +1132,7 @@ private Format1Charset readFormat1Charset(CFFDataInput dataInput, int format, in } private Format2Charset readFormat2Charset(CFFDataInput dataInput, int format, int nGlyphs, - boolean isCIDFont) throws IOException + boolean isCIDFont) throws IOException { Format2Charset charset = new Format2Charset(isCIDFont); charset.format = format; @@ -1159,7 +1168,7 @@ private Format2Charset readFormat2Charset(CFFDataInput dataInput, int format, in } /** - * Inner class holding the header of a CFF font. + * Inner class holding the header of a CFF font. */ private static class Header { @@ -1172,12 +1181,12 @@ private static class Header public String toString() { return getClass().getName() + "[major=" + major + ", minor=" + minor + ", hdrSize=" + hdrSize - + ", offSize=" + offSize + "]"; + + ", offSize=" + offSize + "]"; } } /** - * Inner class holding the DictData of a CFF font. + * Inner class holding the DictData of a CFF font. */ private static class DictData { @@ -1230,7 +1239,7 @@ public String toString() } /** - * Inner class holding an operand of a CFF font. + * Inner class holding an operand of a CFF font. */ private static class Entry { @@ -1304,7 +1313,7 @@ public String toString() } /** - * Inner class representing a font's built-in CFF encoding. + * Inner class representing a font's built-in CFF encoding. */ abstract static class CFFBuiltInEncoding extends CFFEncoding { @@ -1312,7 +1321,7 @@ abstract static class CFFBuiltInEncoding extends CFFEncoding private Supplement[] supplement; /** - * Inner class representing a supplement for an encoding. + * Inner class representing a supplement for an encoding. */ static class Supplement { @@ -1344,7 +1353,7 @@ public String toString() } /** - * Inner class representing a Format0 encoding. + * Inner class representing a Format0 encoding. */ private static class Format0Encoding extends CFFBuiltInEncoding { @@ -1355,12 +1364,12 @@ private static class Format0Encoding extends CFFBuiltInEncoding public String toString() { return getClass().getName() + "[format=" + format + ", nCodes=" + nCodes - + ", supplement=" + Arrays.toString(super.supplement) + "]"; + + ", supplement=" + Arrays.toString(super.supplement) + "]"; } } /** - * Inner class representing a Format1 encoding. + * Inner class representing a Format1 encoding. */ private static class Format1Encoding extends CFFBuiltInEncoding { @@ -1371,7 +1380,7 @@ private static class Format1Encoding extends CFFBuiltInEncoding public String toString() { return getClass().getName() + "[format=" + format + ", nRanges=" + nRanges - + ", supplement=" + Arrays.toString(super.supplement) + "]"; + + ", supplement=" + Arrays.toString(super.supplement) + "]"; } } @@ -1411,7 +1420,7 @@ public String toString() } /** - * Inner class representing a Format0 charset. + * Inner class representing a Format0 charset. */ private static class Format0Charset extends EmbeddedCharset { @@ -1430,7 +1439,7 @@ public String toString() } /** - * Inner class representing a Format1 charset. + * Inner class representing a Format1 charset. */ private static class Format1Charset extends EmbeddedCharset { @@ -1482,7 +1491,7 @@ public String toString() } /** - * Inner class representing a Format2 charset. + * Inner class representing a Format2 charset. */ private static class Format2Charset extends EmbeddedCharset { @@ -1529,7 +1538,7 @@ public String toString() } /** - * Inner class representing a rang mapping for a CID charset. + * Inner class representing a rang mapping for a CID charset. */ private static final class RangeMapping { diff --git a/library/src/main/java/com/tom_roush/fontbox/cff/CFFType1Font.java b/library/src/main/java/com/tom_roush/fontbox/cff/CFFType1Font.java index 672904ebf..d1dce336f 100644 --- a/library/src/main/java/com/tom_roush/fontbox/cff/CFFType1Font.java +++ b/library/src/main/java/com/tom_roush/fontbox/cff/CFFType1Font.java @@ -26,7 +26,6 @@ import com.tom_roush.fontbox.EncodedFont; import com.tom_roush.fontbox.type1.Type1CharStringReader; - /** * A Type 1-equivalent font program represented in a CFF file. Thread safe. * @@ -39,7 +38,7 @@ public class CFFType1Font extends CFFFont implements EncodedFont private CFFEncoding encoding; private final Map charStringCache = - new ConcurrentHashMap(); + new ConcurrentHashMap(); private final PrivateType1CharStringReader reader = new PrivateType1CharStringReader(); @@ -71,9 +70,7 @@ public float getWidth(String name) throws IOException @Override public boolean hasGlyph(String name) { - int sid = charset.getSID(name); - int gid = charset.getGIDForSID(sid); - return gid != 0; + return nameToGID(name) != 0; } @Override @@ -142,7 +139,7 @@ private Type2CharString getType2CharString(int gid, String name) throws IOExcept Type2CharStringParser parser = new Type2CharStringParser(fontName, name); List type2seq = parser.parse(bytes, globalSubrIndex, getLocalSubrIndex()); type2 = new Type2CharString(reader, fontName, name, gid, type2seq, getDefaultWidthX(), - getNominalWidthX()); + getNominalWidthX()); charStringCache.put(gid, type2); } return type2; diff --git a/library/src/main/java/com/tom_roush/fontbox/cff/Type1CharString.java b/library/src/main/java/com/tom_roush/fontbox/cff/Type1CharString.java index 566442e19..6acf09167 100644 --- a/library/src/main/java/com/tom_roush/fontbox/cff/Type1CharString.java +++ b/library/src/main/java/com/tom_roush/fontbox/cff/Type1CharString.java @@ -280,14 +280,14 @@ else if ("setcurrentpoint".equals(name)) { if (numbers.size() >= 2) { - setcurrentpoint(numbers.get(0), numbers.get(1)); + setCurrentPoint(numbers.get(0), numbers.get(1)); } } else if ("callothersubr".equals(name)) { if (!numbers.isEmpty()) { - callothersubr(numbers.get(0).intValue()); + callOtherSubr(numbers.get(0).intValue()); } } else if ("div".equals(name)) @@ -339,7 +339,7 @@ else if (name != null) * Sets the current absolute point without performing a moveto. * Used only with results from callothersubr */ - private void setcurrentpoint(Number x, Number y) + private void setCurrentPoint(Number x, Number y) { current.set(x.floatValue(), y.floatValue()); } @@ -348,7 +348,7 @@ private void setcurrentpoint(Number x, Number y) * Flex (via OtherSubrs) * @param num OtherSubrs entry number */ - private void callothersubr(int num) + private void callOtherSubr(int num) { if (num == 0) { @@ -491,9 +491,10 @@ private void seac(Number asb, Number adx, Number ady, Number bchar, Number achar try { Type1CharString accent = font.getType1CharString(accentName); - if (path == accent.getPath()) + Path accentPath = accent.getPath(); + if (path == accentPath) { - // PDFBOX-5339: avoid ArrayIndexOutOfBoundsException + // PDFBOX-5339: avoid ArrayIndexOutOfBoundsException // reproducable with poc file crash-4698e0dc7833a3f959d06707e01d03cda52a83f4 Log.w("PdfBox-Android", "Path for " + baseName + " and for accent " + accentName + " are same, ignored"); return; @@ -501,7 +502,7 @@ private void seac(Number asb, Number adx, Number ady, Number bchar, Number achar AffineTransform at = AffineTransform.getTranslateInstance( leftSideBearing.x + adx.floatValue() - asb.floatValue(), leftSideBearing.y + ady.floatValue()); - path.op(accent.getPath(), Path.Op.UNION); // TODO: PdfBox-Android + path.op(accentPath, Path.Op.UNION); // TODO: PdfBox-Android } catch (IOException e) { diff --git a/library/src/main/java/com/tom_roush/fontbox/cff/Type1CharStringParser.java b/library/src/main/java/com/tom_roush/fontbox/cff/Type1CharStringParser.java index 3db4f8161..c07644fa8 100644 --- a/library/src/main/java/com/tom_roush/fontbox/cff/Type1CharStringParser.java +++ b/library/src/main/java/com/tom_roush/fontbox/cff/Type1CharStringParser.java @@ -23,7 +23,6 @@ import java.util.ArrayList; import java.util.Deque; import java.util.List; - /** * This class represents a converter for a mapping into a Type 1 sequence. * @@ -34,6 +33,7 @@ */ public class Type1CharStringParser { + // 1-byte commands static final int RETURN = 11; static final int CALLSUBR = 10; @@ -84,10 +84,10 @@ private List parse(byte[] bytes, List subrs, List sequen if (!(obj instanceof Integer)) { Log.w("PdfBox-Android", "Parameter " + obj + " for CALLSUBR is ignored, integer expected in glyph '" - + glyphName + "' of font " + fontName); + + glyphName + "' of font " + fontName); continue; } - Integer operand = (Integer) obj; + int operand = (int) (Integer) obj; if (operand >= 0 && operand < subrs.size()) { @@ -95,7 +95,7 @@ private List parse(byte[] bytes, List subrs, List sequen parse(subrBytes, subrs, sequence); Object lastItem = sequence.get(sequence.size()-1); if (lastItem instanceof CharStringCommand && - ((CharStringCommand)lastItem).getKey().getValue()[0] == RETURN) + ((CharStringCommand)lastItem).getKey().getValue()[0] == RETURN) { sequence.remove(sequence.size()-1); // remove "return" command } @@ -103,10 +103,10 @@ private List parse(byte[] bytes, List subrs, List sequen else { Log.w("PdfBox-Android", "CALLSUBR is ignored, operand: " + operand - + ", subrs.size(): " + subrs.size() + " in glyph '" - + glyphName + "' of font " + fontName); + + ", subrs.size(): " + subrs.size() + " in glyph '" + + glyphName + "' of font " + fontName); // remove all parameters (there can be more than one) - while (sequence.get(sequence.size() - 1) instanceof Integer) + while (!sequence.isEmpty() && sequence.get(sequence.size() - 1) instanceof Integer) { sequence.remove(sequence.size() - 1); } diff --git a/library/src/main/java/com/tom_roush/fontbox/cff/Type1FontUtil.java b/library/src/main/java/com/tom_roush/fontbox/cff/Type1FontUtil.java index beb913d07..c2b5fc973 100644 --- a/library/src/main/java/com/tom_roush/fontbox/cff/Type1FontUtil.java +++ b/library/src/main/java/com/tom_roush/fontbox/cff/Type1FontUtil.java @@ -17,12 +17,13 @@ package com.tom_roush.fontbox.cff; import java.util.Locale; - /** * This class contains some helper methods handling Type1-Fonts. * * @author Villu Ruusmann + * @deprecated This class isn't used and will be removed in 4.0.0. */ +@Deprecated public final class Type1FontUtil { @@ -31,7 +32,7 @@ private Type1FontUtil() } /** - * Converts a byte-array into a string with the corresponding hex value. + * Converts a byte-array into a string with the corresponding hex value. * @param bytes the byte array * @return the string with the hex value */ @@ -43,7 +44,7 @@ public static String hexEncode(byte[] bytes) String string = Integer.toHexString(aByte & 0xff); if (string.length() == 1) { - sb.append("0"); + sb.append('0'); } sb.append(string.toUpperCase(Locale.US)); } diff --git a/library/src/main/java/com/tom_roush/fontbox/cff/Type2CharString.java b/library/src/main/java/com/tom_roush/fontbox/cff/Type2CharString.java index 2787cc003..496e81791 100644 --- a/library/src/main/java/com/tom_roush/fontbox/cff/Type2CharString.java +++ b/library/src/main/java/com/tom_roush/fontbox/cff/Type2CharString.java @@ -22,7 +22,6 @@ import java.util.List; import com.tom_roush.fontbox.type1.Type1CharStringReader; - /** * Represents a Type 2 CharString by converting it into an equivalent Type 1 CharString. * @@ -48,7 +47,7 @@ public class Type2CharString extends Type1CharString * @param nomWidthX nominal width */ public Type2CharString(Type1CharStringReader font, String fontName, String glyphName, int gid, List sequence, - int defaultWidthX, int nomWidthX) + int defaultWidthX, int nomWidthX) { super(font, fontName, glyphName); this.gid = gid; @@ -82,6 +81,43 @@ private void convertType1ToType2(List sequence) { type1Sequence = new ArrayList(); pathCount = 0; + + // PDFBOX-5987: the sequence contains several "num denom DIV" sequences whose results are used + // for further operations. However the converter only handles direct arguments properly, + // not arguments that are created at runtime on the stack. It's not possible to fix this + // by just copying the command codes because addAlternatingCurve / addCurve require + // switching the sequence of arguments. + // The solution below just replaces all "num denom DIV" sequences with its result. + // If more files with even more complex sequences appear we will have to get rid of the + // converter and implement a complete renderer like with type1 charstrings. + List newSequence = new ArrayList(sequence.size()); + for (int i = 0; i < sequence.size(); ++i) + { + Object obj = sequence.get(i); + if (obj instanceof CharStringCommand && + "div".equals(CharStringCommand.TYPE2_VOCABULARY.get(((CharStringCommand) obj).getKey())) && + i >= 2) + { + Object num = sequence.get(i - 2); + Object den = sequence.get(i - 1); + if (num instanceof Number && den instanceof Number) + { + float f = ((Number) num).floatValue() / ((Number) den).floatValue(); + newSequence.remove(newSequence.size() - 1); + newSequence.remove(newSequence.size() - 1); + newSequence.add(f); + } + else + { + newSequence.add(sequence.get(i)); // GIGO + } + } + else + { + newSequence.add(sequence.get(i)); + } + } + CharStringHandler handler = new CharStringHandler() { @Override public List handleCommand(List numbers, CharStringCommand command) @@ -89,7 +125,7 @@ public List handleCommand(List numbers, CharStringCommand comman return Type2CharString.this.handleCommand(numbers, command); } }; - handler.handleSequence(sequence); + handler.handleSequence(newSequence); } @SuppressWarnings(value = { "unchecked" }) @@ -170,10 +206,10 @@ else if ("hflex".equals(name)) if (numbers.size() >= 7) { List first = Arrays.asList(numbers.get(0), 0, - numbers.get(1), numbers.get(2), numbers.get(3), 0); + numbers.get(1), numbers.get(2), numbers.get(3), 0); List second = Arrays.asList(numbers.get(4), 0, - numbers.get(5), -(numbers.get(2).floatValue()), - numbers.get(6), 0); + numbers.get(5), -(numbers.get(2).floatValue()), + numbers.get(6), 0); addCommandList(Arrays.asList(first, second), new CharStringCommand(8)); } } @@ -188,9 +224,9 @@ else if ("hflex1".equals(name)) if (numbers.size() >= 9) { List first = Arrays.asList(numbers.get(0), numbers.get(1), - numbers.get(2), numbers.get(3), numbers.get(4), 0); + numbers.get(2), numbers.get(3), numbers.get(4), 0); List second = Arrays.asList(numbers.get(5), 0, - numbers.get(6), numbers.get(7), numbers.get(8), 0); + numbers.get(6), numbers.get(7), numbers.get(8), 0); addCommandList(Arrays.asList(first, second), new CharStringCommand(8)); } } @@ -204,9 +240,14 @@ else if ("flex1".equals(name)) dy += numbers.get(i * 2 + 1).intValue(); } List first = numbers.subList(0, 6); - List second = Arrays.asList(numbers.get(6), numbers.get(7), numbers.get(8), - numbers.get(9), (Math.abs(dx) > Math.abs(dy) ? numbers.get(10) : -dx), - (Math.abs(dx) > Math.abs(dy) ? -dy : numbers.get(10))); + boolean dxIsBigger = Math.abs(dx) > Math.abs(dy); + List second = Arrays.asList( + numbers.get(6), + numbers.get(7), + numbers.get(8), + numbers.get(9), + (dxIsBigger ? numbers.get(10) : -dx), + (dxIsBigger ? -dy : numbers.get(10))); addCommandList(Arrays.asList(first, second), new CharStringCommand(8)); } else if ("hstemhm".equals(name)) @@ -232,9 +273,9 @@ else if ("rcurveline".equals(name)) if (numbers.size() >= 2) { addCommandList(split(numbers.subList(0, numbers.size() - 2), 6), - new CharStringCommand(8)); + new CharStringCommand(8)); addCommand(numbers.subList(numbers.size() - 2, numbers.size()), - new CharStringCommand(5)); + new CharStringCommand(5)); } } else if ("rlinecurve".equals(name)) @@ -242,9 +283,9 @@ else if ("rlinecurve".equals(name)) if (numbers.size() >= 6) { addCommandList(split(numbers.subList(0, numbers.size() - 6), 2), - new CharStringCommand(5)); + new CharStringCommand(5)); addCommand(numbers.subList(numbers.size() - 6, numbers.size()), - new CharStringCommand(8)); + new CharStringCommand(8)); } } else if ("vvcurveto".equals(name)) @@ -269,13 +310,13 @@ private List clearStack(List numbers, boolean flag) if (flag) { addCommand(Arrays.asList((Number) 0f, numbers.get(0).floatValue() + nominalWidthX), - new CharStringCommand(13)); + new CharStringCommand(13)); numbers = numbers.subList(1, numbers.size()); } else { addCommand(Arrays.asList((Number) 0f, defWidthX), - new CharStringCommand(13)); + new CharStringCommand(13)); } } return numbers; @@ -302,8 +343,8 @@ private void markPath() private void closeCharString2Path() { CharStringCommand command = pathCount > 0 ? (CharStringCommand) type1Sequence - .get(type1Sequence.size() - 1) - : null; + .get(type1Sequence.size() - 1) + : null; CharStringCommand closepathCommand = new CharStringCommand(9); if (command != null && !closepathCommand.equals(command)) @@ -317,7 +358,7 @@ private void drawAlternatingLine(List numbers, boolean horizontal) while (!numbers.isEmpty()) { addCommand(numbers.subList(0, 1), new CharStringCommand( - horizontal ? 6 : 7)); + horizontal ? 6 : 7)); numbers = numbers.subList(1, numbers.size()); horizontal = !horizontal; } @@ -332,15 +373,15 @@ private void drawAlternatingCurve(List numbers, boolean horizontal) { addCommand(Arrays.asList(numbers.get(0), 0, numbers.get(1), numbers.get(2), last ? numbers.get(4) - : 0, numbers.get(3)), - new CharStringCommand(8)); + : 0, numbers.get(3)), + new CharStringCommand(8)); } else { addCommand(Arrays.asList(0, numbers.get(0), numbers.get(1), numbers.get(2), numbers.get(3), last ? numbers.get(4) : 0), - new CharStringCommand(8)); + new CharStringCommand(8)); } numbers = numbers.subList(last ? 5 : 4, numbers.size()); horizontal = !horizontal; @@ -356,17 +397,17 @@ private void drawCurve(List numbers, boolean horizontal) if (horizontal) { addCommand(Arrays.asList(numbers.get(first ? 1 : 0), - first ? numbers.get(0) : 0, numbers - .get(first ? 2 : 1), - numbers.get(first ? 3 : 2), numbers.get(first ? 4 : 3), - 0), new CharStringCommand(8)); + first ? numbers.get(0) : 0, numbers + .get(first ? 2 : 1), + numbers.get(first ? 3 : 2), numbers.get(first ? 4 : 3), + 0), new CharStringCommand(8)); } else { addCommand(Arrays.asList(first ? numbers.get(0) : 0, numbers.get(first ? 1 : 0), numbers - .get(first ? 2 : 1), numbers.get(first ? 3 : 2), + .get(first ? 2 : 1), numbers.get(first ? 3 : 2), 0, numbers.get(first ? 4 : 3)), - new CharStringCommand(8)); + new CharStringCommand(8)); } numbers = numbers.subList(first ? 5 : 4, numbers.size()); } diff --git a/library/src/main/java/com/tom_roush/fontbox/cmap/CMap.java b/library/src/main/java/com/tom_roush/fontbox/cmap/CMap.java index fcc652c51..6fd815759 100644 --- a/library/src/main/java/com/tom_roush/fontbox/cmap/CMap.java +++ b/library/src/main/java/com/tom_roush/fontbox/cmap/CMap.java @@ -24,7 +24,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; - /** * This class represents a CMap file. * @@ -32,6 +31,7 @@ */ public class CMap { + private int wmode = 0; private String cmapName = null; private String cmapVersion = null; @@ -140,7 +140,7 @@ public int readCode(InputStream in) throws IOException else { Log.w("PdfBox-Android", "mark() and reset() not supported, " + (maxCodeLength - 1) + - " bytes have been skipped"); + " bytes have been skipped"); } return toInt(bytes, minCodeLength); // Adobe Reader behavior } @@ -183,24 +183,6 @@ public int toCID(int code) return 0; } - /** - * Convert the given part of a byte array to an integer. - * @param data the byte array - * @param offset The offset into the byte array. - * @param length The length of the data we are getting. - * @return the resulting integer - */ - private int getCodeFromArray( byte[] data, int offset, int length ) - { - int code = 0; - for( int i=0; i tok } private void addMappingFrombfrange(CMap cmap, byte[] startCode, int values, - byte[] tokenBytes) + byte[] tokenBytes) { for (int i = 0; i < values; i++) { @@ -527,217 +526,218 @@ private Object parseNextToken(PushbackInputStream is) throws IOException } switch (nextByte) { - case '%': + case '%': + { + // header operations, for now return the entire line + // may need to smarter in the future + StringBuilder buffer = new StringBuilder(); + buffer.append((char) nextByte); + readUntilEndOfLine(is, buffer); + retval = buffer.toString(); + break; + } + case '(': + { + StringBuilder buffer = new StringBuilder(); + int stringByte = is.read(); + + while (stringByte != -1 && stringByte != ')') { - // header operations, for now return the entire line - // may need to smarter in the future - StringBuilder buffer = new StringBuilder(); - buffer.append((char) nextByte); - readUntilEndOfLine(is, buffer); - retval = buffer.toString(); - break; + buffer.append((char) stringByte); + stringByte = is.read(); } - case '(': + retval = buffer.toString(); + break; + } + case '>': + { + int secondCloseBrace = is.read(); + if (secondCloseBrace == '>') { - StringBuilder buffer = new StringBuilder(); - int stringByte = is.read(); - - while (stringByte != -1 && stringByte != ')') - { - buffer.append((char) stringByte); - stringByte = is.read(); - } - retval = buffer.toString(); - break; + retval = MARK_END_OF_DICTIONARY; } - case '>': + else { - int secondCloseBrace = is.read(); - if (secondCloseBrace == '>') - { - retval = MARK_END_OF_DICTIONARY; - } - else - { - throw new IOException("Error: expected the end of a dictionary."); - } - break; + throw new IOException("Error: expected the end of a dictionary."); } - case ']': + break; + } + case ']': + { + retval = MARK_END_OF_ARRAY; + break; + } + case '[': + { + List list = new ArrayList(); + + Object nextToken = parseNextToken(is); + while (nextToken != null && !MARK_END_OF_ARRAY.equals(nextToken)) { - retval = MARK_END_OF_ARRAY; - break; + list.add(nextToken); + nextToken = parseNextToken(is); } - case '[': - { - List list = new ArrayList(); - - Object nextToken = parseNextToken(is); - while (nextToken != null && !MARK_END_OF_ARRAY.equals(nextToken)) + retval = list; + break; + } + case '<': + { + int theNextByte = is.read(); + if (theNextByte == '<') + { + Map result = new HashMap(); + // we are reading a dictionary + Object key = parseNextToken(is); + while (key instanceof LiteralName && + !MARK_END_OF_DICTIONARY.equals(((LiteralName) key).name)) { - list.add(nextToken); - nextToken = parseNextToken(is); + Object value = parseNextToken(is); + result.put(((LiteralName) key).name, value); + key = parseNextToken(is); } - retval = list; - break; + retval = result; } - case '<': + else { - int theNextByte = is.read(); - if (theNextByte == '<') + // won't read more than 512 bytes + + int multiplyer = 16; + int bufferIndex = -1; + while (theNextByte != -1 && theNextByte != '>') { - Map result = new HashMap(); - // we are reading a dictionary - Object key = parseNextToken(is); - while (key instanceof LiteralName && !MARK_END_OF_DICTIONARY.equals(key)) + int intValue = 0; + if (theNextByte >= '0' && theNextByte <= '9') { - Object value = parseNextToken(is); - result.put(((LiteralName) key).name, value); - key = parseNextToken(is); + intValue = theNextByte - '0'; } - retval = result; - } - else - { - // won't read more than 512 bytes - - int multiplyer = 16; - int bufferIndex = -1; - while (theNextByte != -1 && theNextByte != '>') + else if (theNextByte >= 'A' && theNextByte <= 'F') { - int intValue = 0; - if (theNextByte >= '0' && theNextByte <= '9') - { - intValue = theNextByte - '0'; - } - else if (theNextByte >= 'A' && theNextByte <= 'F') - { - intValue = 10 + theNextByte - 'A'; - } - else if (theNextByte >= 'a' && theNextByte <= 'f') - { - intValue = 10 + theNextByte - 'a'; - } - // all kind of whitespaces may occur in malformed CMap files - // see PDFBOX-2035 - else if (isWhitespaceOrEOF(theNextByte)) - { - // skipping whitespaces - theNextByte = is.read(); - continue; - } - else - { - throw new IOException("Error: expected hex character and not " + (char) theNextByte + ":" + intValue = 10 + theNextByte - 'A'; + } + else if (theNextByte >= 'a' && theNextByte <= 'f') + { + intValue = 10 + theNextByte - 'a'; + } + // all kind of whitespaces may occur in malformed CMap files + // see PDFBOX-2035 + else if (isWhitespaceOrEOF(theNextByte)) + { + // skipping whitespaces + theNextByte = is.read(); + continue; + } + else + { + throw new IOException("Error: expected hex character and not " + (char) theNextByte + ":" + theNextByte); - } - intValue *= multiplyer; - if (multiplyer == 16) + } + intValue *= multiplyer; + if (multiplyer == 16) + { + bufferIndex++; + if (bufferIndex >= tokenParserByteBuffer.length) { - bufferIndex++; - if (bufferIndex >= tokenParserByteBuffer.length) - { - throw new IOException("cmap token ist larger than buffer size " + + throw new IOException("cmap token ist larger than buffer size " + tokenParserByteBuffer.length); - } - tokenParserByteBuffer[bufferIndex] = 0; - multiplyer = 1; } - else - { - multiplyer = 16; - } - tokenParserByteBuffer[bufferIndex] += intValue; - theNextByte = is.read(); + tokenParserByteBuffer[bufferIndex] = 0; + multiplyer = 1; + } + else + { + multiplyer = 16; } - byte[] finalResult = new byte[bufferIndex + 1]; - System.arraycopy(tokenParserByteBuffer, 0, finalResult, 0, bufferIndex + 1); - retval = finalResult; + tokenParserByteBuffer[bufferIndex] += intValue; + theNextByte = is.read(); } - break; + byte[] finalResult = new byte[bufferIndex + 1]; + System.arraycopy(tokenParserByteBuffer, 0, finalResult, 0, bufferIndex + 1); + retval = finalResult; } - case '/': - { - StringBuilder buffer = new StringBuilder(); - int stringByte = is.read(); + break; + } + case '/': + { + StringBuilder buffer = new StringBuilder(); + int stringByte = is.read(); - while (!isWhitespaceOrEOF(stringByte) && !isDelimiter(stringByte)) - { - buffer.append((char) stringByte); - stringByte = is.read(); - } - if (isDelimiter( stringByte)) - { - is.unread(stringByte); - } - retval = new LiteralName(buffer.toString()); - break; + while (!isWhitespaceOrEOF(stringByte) && !isDelimiter(stringByte)) + { + buffer.append((char) stringByte); + stringByte = is.read(); } - case -1: + if (isDelimiter( stringByte)) { - // EOF returning null - break; + is.unread(stringByte); } - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - { - StringBuilder buffer = new StringBuilder(); + retval = new LiteralName(buffer.toString()); + break; + } + case -1: + { + // EOF returning null + break; + } + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + { + StringBuilder buffer = new StringBuilder(); + buffer.append((char) nextByte); + nextByte = is.read(); + + while (!isWhitespaceOrEOF(nextByte) && (Character.isDigit((char) nextByte) || nextByte == '.')) + { buffer.append((char) nextByte); nextByte = is.read(); - - while (!isWhitespaceOrEOF(nextByte) && (Character.isDigit((char) nextByte) || nextByte == '.')) - { - buffer.append((char) nextByte); - nextByte = is.read(); - } - is.unread(nextByte); - String value = buffer.toString(); - try + } + is.unread(nextByte); + String value = buffer.toString(); + try + { + if (value.indexOf('.') >= 0) { - if (value.indexOf('.') >= 0) - { - retval = Double.valueOf(value); - } - else - { - retval = Integer.valueOf(value); - } + retval = Double.valueOf(value); } - catch (NumberFormatException ex) + else { - throw new IOException("Invalid number '" + value + "'", ex); + retval = Integer.valueOf(value); } - break; } - default: + catch (NumberFormatException ex) + { + throw new IOException("Invalid number '" + value + "'", ex); + } + break; + } + default: + { + StringBuilder buffer = new StringBuilder(); + buffer.append((char) nextByte); + nextByte = is.read(); + + // newline separator may be missing in malformed CMap files + // see PDFBOX-2035 + while (!isWhitespaceOrEOF(nextByte) && !isDelimiter(nextByte) && !Character.isDigit(nextByte)) { - StringBuilder buffer = new StringBuilder(); buffer.append((char) nextByte); nextByte = is.read(); - - // newline separator may be missing in malformed CMap files - // see PDFBOX-2035 - while (!isWhitespaceOrEOF(nextByte) && !isDelimiter(nextByte) && !Character.isDigit(nextByte)) - { - buffer.append((char) nextByte); - nextByte = is.read(); - } - if (isDelimiter(nextByte) || Character.isDigit(nextByte)) - { - is.unread(nextByte); - } - retval = new Operator(buffer.toString()); - - break; } + if (isDelimiter(nextByte) || Character.isDigit(nextByte)) + { + is.unread(nextByte); + } + retval = new Operator(buffer.toString()); + + break; + } } return retval; } @@ -780,6 +780,10 @@ private boolean isDelimiter(int aByte) private boolean increment(byte[] data, int position, boolean useStrictMode) { + if (position < 0) + { + return false; + } if (position > 0 && (data[position] & 0xFF) == 255) { // PDFBOX-4661: avoid overflow of the last byte, all following values are undefined @@ -811,7 +815,11 @@ private int createIntFromBytes(byte[] bytes) private String createStringFromBytes(byte[] bytes) { - return new String(bytes, bytes.length == 1 ? Charsets.ISO_8859_1 : Charsets.UTF_16BE); + if (bytes.length <= 2) + { + return CMapStrings.getMapping(bytes); + } + return new String(bytes, Charsets.UTF_16BE); } /** diff --git a/library/src/main/java/com/tom_roush/fontbox/cmap/CMapStrings.java b/library/src/main/java/com/tom_roush/fontbox/cmap/CMapStrings.java new file mode 100644 index 000000000..28003b5e5 --- /dev/null +++ b/library/src/main/java/com/tom_roush/fontbox/cmap/CMapStrings.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.tom_roush.fontbox.cmap; + +import java.util.ArrayList; +import java.util.List; + +import com.tom_roush.fontbox.util.Charsets; +/** + * Many CMaps are using the same values for the mapped strings. This class provides all common one- and two-byte + * mappings to avoid duplicate strings. + */ +public class CMapStrings +{ + private static final List twoByteMappings = new ArrayList(256 * 256); + private static final List oneByteMappings = new ArrayList(256); + + private static final List indexValues = new ArrayList(256 * 256); + private static final List oneByteValues = new ArrayList(256); + private static final List twoByteValues = new ArrayList(256 * 256); + + static + { + // create all mappings when loading the class to avoid concurrency issues + fillMappings(); + } + + private CMapStrings() + { + } + + private static void fillMappings() + { + for (int i = 0; i < 256; i++) + { + for (int j = 0; j < 256; j++) + { + byte[] bytes = { (byte) i, (byte) j }; + twoByteMappings.add(new String(bytes, Charsets.UTF_16BE)); + twoByteValues.add(bytes); + indexValues.add((i * 256) + j); + } + } + for (int i = 0; i < 256; i++) + { + byte[] bytes = { (byte) i }; + oneByteMappings.add(new String(bytes, Charsets.ISO_8859_1)); + oneByteValues.add(bytes); + } + } + + /** + * Get the mapped string value for the given combination of bytes. The mapping is limited to one and two-byte + * mappings. Any longer byte sequence produces null as return value. + * + * @param bytes the given combination of bytes + * @return the string representation for the given combination of bytes + */ + public static String getMapping(byte[] bytes) + { + if (bytes.length > 2) + { + return null; + } + return bytes.length == 1 ? oneByteMappings.get(CMap.toInt(bytes, 1)) + : twoByteMappings.get(CMap.toInt(bytes, bytes.length)); + } + + /** + * Get an Integer instance of the given combination of bytes. Each value is a singleton to avoid multiple instances + * for same value. The values are limited to one and two-byte sequences. Any longer byte sequence produces null as + * return value. + * + * @param bytes the given combination of bytes + * @return the Integer representation for the given combination of bytes + */ + public static Integer getIndexValue(byte[] bytes) + { + if (bytes.length > 2) + { + return null; + } + return indexValues.get(CMap.toInt(bytes, bytes.length)); + } + + /** + * Get a singleton instance of the given combination of bytes to avoid multiple instances for same value. The values + * are limited to one and two-byte sequences. Any longer byte sequence produces null as return value. + * + * @param bytes the given combination of bytes + * @return a singleton instance for the given combination of bytes + */ + public static byte[] getByteValue(byte[] bytes) + { + if (bytes.length > 2) + { + return null; + } + return bytes.length == 1 ? oneByteValues.get(CMap.toInt(bytes, 1)) + : twoByteValues.get(CMap.toInt(bytes, 2)); + } +} diff --git a/library/src/main/java/com/tom_roush/fontbox/pfb/PfbParser.java b/library/src/main/java/com/tom_roush/fontbox/pfb/PfbParser.java index b36016901..aeed65613 100644 --- a/library/src/main/java/com/tom_roush/fontbox/pfb/PfbParser.java +++ b/library/src/main/java/com/tom_roush/fontbox/pfb/PfbParser.java @@ -16,6 +16,8 @@ */ package com.tom_roush.fontbox.pfb; +import android.util.Log; + import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -23,8 +25,11 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; +import com.tom_roush.pdfbox.android.PDFBoxConfig; /** * Parser for a pfb-file. * @@ -33,6 +38,7 @@ */ public class PfbParser { + /** * the pfb header length. * (start-marker (1 byte), ascii-/binary-marker (1 byte), size (4 byte)) @@ -56,10 +62,9 @@ public class PfbParser private static final int BINARY_MARKER = 0x02; /** - * The record types in the pfb-file. + * the EOF marker. */ - private static final int[] PFB_RECORDS = {ASCII_MARKER, BINARY_MARKER, - ASCII_MARKER}; + private static final int EOF_MARKER = 0x03; /** * buffersize. @@ -72,12 +77,12 @@ public class PfbParser private byte[] pfbdata; /** - * the lengths of the records. + * the lengths of the records (ASCII, BINARY, ASCII) */ - private int[] lengths; + private final int[] lengths = new int[3]; // sample (pfb-file) - // 00000000 80 01 8b 15 00 00 25 21 50 53 2d 41 64 6f 62 65 + // 00000000 80 01 8b 15 00 00 25 21 50 53 2d 41 64 6f 62 65 // ......%!PS-Adobe @@ -136,47 +141,110 @@ private void parsePfb(final byte[] pfb) throws IOException { throw new IOException("PFB header missing"); } + // read into segments and keep them + List typeList = new ArrayList(3); + List barrList = new ArrayList(3); ByteArrayInputStream in = new ByteArrayInputStream(pfb); - pfbdata = new byte[pfb.length - PFB_HEADER_LENGTH]; - lengths = new int[PFB_RECORDS.length]; - int pointer = 0; - for (int records = 0; records < PFB_RECORDS.length; records++) + long total = 0; + do { - if (in.read() != START_MARKER) + int r = in.read(); + if (r == -1 && total > 0) + { + break; // EOF + } + if (r != START_MARKER) { throw new IOException("Start marker missing"); } - - if (in.read() != PFB_RECORDS[records]) + int recordType = in.read(); + if (recordType == EOF_MARKER) + { + break; + } + if (recordType != ASCII_MARKER && recordType != BINARY_MARKER) { - throw new IOException("Incorrect record type"); + throw new IOException("Incorrect record type: " + recordType); } int size = in.read(); size += in.read() << 8; size += in.read() << 16; size += in.read() << 24; + if (PDFBoxConfig.isDebugEnabled()) + { + Log.d("PdfBox-Android", "record type: " + recordType + ", segment size: " + size); + } if (size < 0) { - throw new IOException("PFB record size is negative: " + size); + throw new IOException("record size " + size + " is negative"); + } + if (size > pfb.length) + { + // PDFBOX-6044: avoid potential OOM + throw new IOException("record size " + size + " would be larger than the input"); + } + byte[] ar = new byte[size]; + int got = in.read(ar); + if (got != size) + { + throw new EOFException("EOF while reading PFB font"); } - lengths[records] = size; - if (pointer >= pfbdata.length) + total += size; + typeList.add(recordType); + barrList.add(ar); + } + while (true); + + // We now have ASCII and binary segments. Lets arrange these so that the ASCII segments + // come first, then the binary segments, then the last ASCII segment if it is + // 0000... cleartomark + + if (total > pfb.length) + { + // PDFBOX-6044: avoid potential OOM + throw new IOException("total record size " + total + " would be larger than the input"); + } + pfbdata = new byte[(int) total]; + byte[] cleartomarkSegment = null; + int dstPos = 0; + + // copy the ASCII segments + for (int i = 0; i < typeList.size(); ++i) + { + if (typeList.get(i) != ASCII_MARKER) { - throw new EOFException("attempted to read past EOF"); + continue; } - if (size > pfbdata.length - pointer) + byte[] ar = barrList.get(i); + if (i == typeList.size() - 1 && ar.length < 600 && + new String(ar, "US-ASCII").contains("cleartomark")) { - throw new EOFException("attempted to read " + size + " bytes at position " + pointer + - " into array of size " + pfbdata.length + ", but only space for " + - (pfbdata.length - pointer) + " bytes left"); + cleartomarkSegment = ar; + continue; } - int got = in.read(pfbdata, pointer, size); - if (got < 0) + System.arraycopy(ar, 0, pfbdata, dstPos, ar.length); + dstPos += ar.length; + } + lengths[0] = dstPos; + + // copy the binary segments + for (int i = 0; i < typeList.size(); ++i) + { + if (typeList.get(i) != BINARY_MARKER) { - throw new EOFException(); + continue; } - pointer += got; + byte[] ar = barrList.get(i); + System.arraycopy(ar, 0, pfbdata, dstPos, ar.length); + dstPos += ar.length; + } + lengths[1] = dstPos - lengths[0]; + + if (cleartomarkSegment != null) + { + System.arraycopy(cleartomarkSegment, 0, pfbdata, dstPos, cleartomarkSegment.length); + lengths[2] = cleartomarkSegment.length; } } diff --git a/library/src/main/java/com/tom_roush/fontbox/ttf/CmapSubtable.java b/library/src/main/java/com/tom_roush/fontbox/ttf/CmapSubtable.java index 82bb2ebca..a536b3cb1 100644 --- a/library/src/main/java/com/tom_roush/fontbox/ttf/CmapSubtable.java +++ b/library/src/main/java/com/tom_roush/fontbox/ttf/CmapSubtable.java @@ -23,10 +23,11 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Map.Entry; - +import java.util.Map; +import java.util.Set; /** * A "cmap" subtable. * @@ -34,6 +35,7 @@ */ public class CmapSubtable implements CmapLookup { + private static final long LEAD_OFFSET = 0xD800l - (0x10000 >> 10); private static final long SURROGATE_OFFSET = 0x10000l - (0xD800 << 10) - 0xDC00; @@ -42,7 +44,7 @@ public class CmapSubtable implements CmapLookup private long subTableOffset; private int[] glyphIdToCharacterCode; private final Map> glyphIdToCharacterCodeMultiple = new HashMap>(); - private Map characterCodeToGlyphId= new HashMap(); + private Map characterCodeToGlyphId = Collections.emptyMap(); /** * This will read the required data from the stream. @@ -86,35 +88,35 @@ void initSubtable(CmapTable cmap, int numGlyphs, TTFDataStream data) throws IOEx switch (subtableFormat) { - case 0: - processSubtype0(data); - break; - case 2: - processSubtype2(data, numGlyphs); - break; - case 4: - processSubtype4(data, numGlyphs); - break; - case 6: - processSubtype6(data, numGlyphs); - break; - case 8: - processSubtype8(data, numGlyphs); - break; - case 10: - processSubtype10(data, numGlyphs); - break; - case 12: - processSubtype12(data, numGlyphs); - break; - case 13: - processSubtype13(data, numGlyphs); - break; - case 14: - processSubtype14(data, numGlyphs); - break; - default: - throw new IOException("Unknown cmap format:" + subtableFormat); + case 0: + processSubtype0(data); + break; + case 2: + processSubtype2(data, numGlyphs); + break; + case 4: + processSubtype4(data, numGlyphs); + break; + case 6: + processSubtype6(data, numGlyphs); + break; + case 8: + processSubtype8(data, numGlyphs); + break; + case 10: + processSubtype10(data, numGlyphs); + break; + case 12: + processSubtype12(data, numGlyphs); + break; + case 13: + processSubtype13(data, numGlyphs); + break; + case 14: + processSubtype14(data, numGlyphs); + break; + default: + throw new IOException("Unknown cmap format:" + subtableFormat); } } @@ -218,10 +220,10 @@ void processSubtype10(TTFDataStream data, int numGlyphs) throws IOException } if (startCode < 0 || startCode > 0x0010FFFF || (startCode + numChars) > 0x0010FFFF - || ((startCode + numChars) >= 0x0000D800 && (startCode + numChars) <= 0x0000DFFF)) + || ((startCode + numChars) >= 0x0000D800 && (startCode + numChars) <= 0x0000DFFF)) { throw new IOException("Invalid character codes, " + - String.format("startCode: 0x%X, numChars: %d", startCode, numChars)); + String.format("startCode: 0x%X, numChars: %d", startCode, numChars)); } } @@ -319,7 +321,7 @@ void processSubtype13(TTFDataStream data, int numGlyphs) throws IOException } if ((endCode > 0 && endCode < firstCode) || endCode > 0x0010FFFF - || (endCode >= 0x0000D800 && endCode <= 0x0000DFFF)) + || (endCode >= 0x0000D800 && endCode <= 0x0000DFFF)) { throw new IOException("Invalid character code " + String.format("0x%X", endCode)); } @@ -411,11 +413,11 @@ void processSubtype4(TTFDataStream data, int numGlyphs) throws IOException { int start = startCount[i]; int end = endCount[i]; - int delta = idDelta[i]; - int rangeOffset = idRangeOffset[i]; - long segmentRangeOffset = idRangeOffsetPosition + (i * 2L) + rangeOffset; if (start != 65535 && end != 65535) { + int delta = idDelta[i]; + int rangeOffset = idRangeOffset[i]; + long segmentRangeOffset = idRangeOffsetPosition + (i * 2L) + rangeOffset; for (int j = start; j <= end; j++) { if (rangeOffset == 0) @@ -468,7 +470,7 @@ private void buildGlyphIdToCharacterCodeLookup(int maxGlyphId) List mappedValues = glyphIdToCharacterCodeMultiple.get(entry.getValue()); if (mappedValues == null) { - mappedValues = new ArrayList(); + mappedValues = new ArrayList(2); glyphIdToCharacterCodeMultiple.put(entry.getValue(), mappedValues); mappedValues.add(glyphIdToCharacterCode[entry.getValue()]); // mark value as multiple mapping @@ -515,6 +517,8 @@ void processSubtype2(TTFDataStream data, int numGlyphs) throws IOException Log.w("PdfBox-Android", "subtable has no glyphs"); return; } + Set logged = new HashSet(); + boolean maxLoggingReached = false; for (int i = 0; i <= maxSubHeaderIndex; ++i) { SubHeader sh = subHeaders[i]; @@ -546,7 +550,16 @@ void processSubtype2(TTFDataStream data, int numGlyphs) throws IOException if (p >= numGlyphs) { - Log.w("PdfBox-Android", "glyphId " + p + " for charcode " + charCode + " ignored, numGlyphs is " + numGlyphs); + if (!maxLoggingReached && !logged.contains(p)) + { + Log.w("PdfBox-Android", "glyphId " + p + " for charcode " + charCode + " ignored, numGlyphs is " + numGlyphs); + logged.add(p); + if (logged.size() > 10) + { + Log.w("PdfBox-Android", "too many bad glyphIds, more won't be reported for this table"); + maxLoggingReached = true; + } + } continue; } diff --git a/library/src/main/java/com/tom_roush/fontbox/ttf/GlyfCompositeComp.java b/library/src/main/java/com/tom_roush/fontbox/ttf/GlyfCompositeComp.java index 5d5c0892b..6cd053ffe 100644 --- a/library/src/main/java/com/tom_roush/fontbox/ttf/GlyfCompositeComp.java +++ b/library/src/main/java/com/tom_roush/fontbox/ttf/GlyfCompositeComp.java @@ -1,28 +1,25 @@ /* - - Licensed to the Apache Software Foundation (ASF) under one or more - contributor license agreements. See the NOTICE file distributed with - this work for additional information regarding copyright ownership. - The ASF licenses this file to You under the Apache License, Version 2.0 - (the "License"); you may not use this file except in compliance with - the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.tom_roush.fontbox.ttf; import java.io.IOException; - /** - * This class is based on code from Apache Batik a subproject of Apache XMLGraphics. see - * http://xmlgraphics.apache.org/batik/ for further details. + * This class is based on code from Apache Batik, a subproject of Apache XMLGraphics. See + * The Apache™ Batik Project for further details. */ public class GlyfCompositeComp { diff --git a/library/src/main/java/com/tom_roush/fontbox/ttf/GlyfCompositeDescript.java b/library/src/main/java/com/tom_roush/fontbox/ttf/GlyfCompositeDescript.java index 0ab61548e..16ecb9d77 100644 --- a/library/src/main/java/com/tom_roush/fontbox/ttf/GlyfCompositeDescript.java +++ b/library/src/main/java/com/tom_roush/fontbox/ttf/GlyfCompositeDescript.java @@ -1,20 +1,18 @@ /* - - Licensed to the Apache Software Foundation (ASF) under one or more - contributor license agreements. See the NOTICE file distributed with - this work for additional information regarding copyright ownership. - The ASF licenses this file to You under the Apache License, Version 2.0 - (the "License"); you may not use this file except in compliance with - the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.tom_roush.fontbox.ttf; @@ -22,17 +20,17 @@ Licensed to the Apache Software Foundation (ASF) under one or more import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; - /** * Glyph description for composite glyphs. Composite glyphs are made up of one * or more simple glyphs, usually with some sort of transformation applied to * each. * * This class is based on code from Apache Batik a subproject of Apache - * XMLGraphics. see http://xmlgraphics.apache.org/batik/ for further details. + * XMLGraphics. See The Apache™ Batik Project for further details. */ public class GlyfCompositeDescript extends GlyfDescript { @@ -49,9 +47,10 @@ public class GlyfCompositeDescript extends GlyfDescript * * @param bais the stream to be read * @param glyphTable the Glyphtable containing all glyphs + * @param level current level * @throws IOException is thrown if something went wrong */ - GlyfCompositeDescript(TTFDataStream bais, GlyphTable glyphTable) throws IOException + GlyfCompositeDescript(TTFDataStream bais, GlyphTable glyphTable, int level) throws IOException { super((short) -1, bais); @@ -71,7 +70,7 @@ public class GlyfCompositeDescript extends GlyfDescript { readInstructions(bais, (bais.readUnsignedShort())); } - initDescriptions(); + initDescriptions(level); } /** @@ -250,6 +249,16 @@ public int getComponentCount() return components.size(); } + /** + * Gets a view to the composite components. + * + * @return unmodifiable list of this composite glyph's {@linkplain GlyfCompositeComp components} + */ + public List getComponents() + { + return Collections.unmodifiableList(components); + } + private GlyfCompositeComp getCompositeComp(int i) { for (GlyfCompositeComp c : components) @@ -276,14 +285,14 @@ private GlyfCompositeComp getCompositeCompEndPt(int i) return null; } - private void initDescriptions() + private void initDescriptions(int level) { for (GlyfCompositeComp component : components) { try { int index = component.getGlyphIndex(); - GlyphData glyph = glyphTable.getGlyph(index); + GlyphData glyph = glyphTable.getGlyph(index, level); if (glyph != null) { descriptions.put(index, glyph.getDescription()); diff --git a/library/src/main/java/com/tom_roush/fontbox/ttf/GlyfDescript.java b/library/src/main/java/com/tom_roush/fontbox/ttf/GlyfDescript.java index 1cb9439e5..716db312d 100644 --- a/library/src/main/java/com/tom_roush/fontbox/ttf/GlyfDescript.java +++ b/library/src/main/java/com/tom_roush/fontbox/ttf/GlyfDescript.java @@ -1,28 +1,25 @@ /* - - Licensed to the Apache Software Foundation (ASF) under one or more - contributor license agreements. See the NOTICE file distributed with - this work for additional information regarding copyright ownership. - The ASF licenses this file to You under the Apache License, Version 2.0 - (the "License"); you may not use this file except in compliance with - the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.tom_roush.fontbox.ttf; import java.io.IOException; - /** - * This class is based on code from Apache Batik a subproject of Apache XMLGraphics. - * see http://xmlgraphics.apache.org/batik/ for further details. + * This class is based on code from Apache Batik, a subproject of Apache XMLGraphics. + * see The Apache™ Batik Project for further details. * */ public abstract class GlyfDescript implements GlyphDescription @@ -42,7 +39,7 @@ public abstract class GlyfDescript implements GlyphDescription */ public static final byte Y_SHORT_VECTOR = 0x04; /** - * if set, the next byte specifies the number of additional + * if set, the next byte specifies the number of additional * times this set of flags is to be repeated. */ public static final byte REPEAT = 0x08; @@ -109,7 +106,7 @@ public int[] getInstructions() /** * Read the hinting instructions. * @param bais the stream to be read - * @param count the number of instructions to be read + * @param count the number of instructions to be read * @throws IOException is thrown if something went wrong */ void readInstructions(TTFDataStream bais, int count) throws IOException diff --git a/library/src/main/java/com/tom_roush/fontbox/ttf/GlyfSimpleDescript.java b/library/src/main/java/com/tom_roush/fontbox/ttf/GlyfSimpleDescript.java index 95d08615e..39a237fc0 100644 --- a/library/src/main/java/com/tom_roush/fontbox/ttf/GlyfSimpleDescript.java +++ b/library/src/main/java/com/tom_roush/fontbox/ttf/GlyfSimpleDescript.java @@ -1,34 +1,30 @@ /* - - Licensed to the Apache Software Foundation (ASF) under one or more - contributor license agreements. See the NOTICE file distributed with - this work for additional information regarding copyright ownership. - The ASF licenses this file to You under the Apache License, Version 2.0 - (the "License"); you may not use this file except in compliance with - the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.tom_roush.fontbox.ttf; import android.util.Log; import java.io.IOException; - /** - * This class is based on code from Apache Batik a subproject of Apache XMLGraphics. see - * http://xmlgraphics.apache.org/batik/ for further details. + * This class is based on code from Apache Batik, a subproject of Apache XMLGraphics. see + * The Apache™ Batik Project for further details. */ public class GlyfSimpleDescript extends GlyfDescript { - private int[] endPtsOfContours; private byte[] flags; private short[] xCoordinates; @@ -59,7 +55,7 @@ public class GlyfSimpleDescript extends GlyfDescript super(numberOfContours, bais); /* - * https://developer.apple.com/fonts/TTRefMan/RM06/Chap6glyf.html + * https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6glyf.html * "If a glyph has zero contours, it need not have any glyph data." set the pointCount to zero to initialize * attributes and avoid nullpointer but maybe there shouldn't have GlyphDescript in the GlyphData? */ diff --git a/library/src/main/java/com/tom_roush/fontbox/ttf/GlyphData.java b/library/src/main/java/com/tom_roush/fontbox/ttf/GlyphData.java index 9f61d439c..c02efe99a 100644 --- a/library/src/main/java/com/tom_roush/fontbox/ttf/GlyphData.java +++ b/library/src/main/java/com/tom_roush/fontbox/ttf/GlyphData.java @@ -21,7 +21,6 @@ import java.io.IOException; import com.tom_roush.fontbox.util.BoundingBox; - /** * A glyph data record in the glyf table. * @@ -43,9 +42,11 @@ public class GlyphData * @param glyphTable The glyph table this glyph belongs to. * @param data The stream to read the data from. * @param leftSideBearing The left side bearing for this glyph. + * @param level composite level * @throws IOException If there is an error reading the data. */ - void initData( GlyphTable glyphTable, TTFDataStream data, int leftSideBearing ) throws IOException + void initData( GlyphTable glyphTable, TTFDataStream data, int leftSideBearing, int level) + throws IOException { numberOfContours = data.readSignedShort(); xMin = data.readSignedShort(); @@ -63,7 +64,7 @@ void initData( GlyphTable glyphTable, TTFDataStream data, int leftSideBearing ) else { // create a composite glyph - glyphDescription = new GlyfCompositeDescript(data, glyphTable); + glyphDescription = new GlyfCompositeDescript(data, glyphTable, level + 1); } } diff --git a/library/src/main/java/com/tom_roush/fontbox/ttf/GlyphDescription.java b/library/src/main/java/com/tom_roush/fontbox/ttf/GlyphDescription.java index ad8c5a090..7b7697cfb 100644 --- a/library/src/main/java/com/tom_roush/fontbox/ttf/GlyphDescription.java +++ b/library/src/main/java/com/tom_roush/fontbox/ttf/GlyphDescription.java @@ -1,28 +1,27 @@ /* - - Licensed to the Apache Software Foundation (ASF) under one or more - contributor license agreements. See the NOTICE file distributed with - this work for additional information regarding copyright ownership. - The ASF licenses this file to You under the Apache License, Version 2.0 - (the "License"); you may not use this file except in compliance with - the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.tom_roush.fontbox.ttf; + /** * Specifies access to glyph description classes, simple and composite. * - * This class is based on code from Apache Batik a subproject of Apache XMLGraphics. - * see http://xmlgraphics.apache.org/batik/ for further details. + * This class is based on code from Apache Batik, a subproject of Apache XMLGraphics. + * see The Apache™ Batik Project for further details. * */ public interface GlyphDescription diff --git a/library/src/main/java/com/tom_roush/fontbox/ttf/GlyphRenderer.java b/library/src/main/java/com/tom_roush/fontbox/ttf/GlyphRenderer.java index f882d7e29..c1bb2b730 100644 --- a/library/src/main/java/com/tom_roush/fontbox/ttf/GlyphRenderer.java +++ b/library/src/main/java/com/tom_roush/fontbox/ttf/GlyphRenderer.java @@ -24,7 +24,6 @@ import java.util.Locale; import com.tom_roush.pdfbox.android.PDFBoxConfig; - /** * This class provides a glyph to Path conversion for true type fonts. * Based on code from Apache Batik, a subproject of Apache XMLGraphics. @@ -40,6 +39,7 @@ */ class GlyphRenderer { + private GlyphDescription glyphDescription; GlyphRenderer(GlyphDescription glyphDescription) @@ -78,7 +78,7 @@ private Point[] describe(GlyphDescription gd) endPtOfContourIndex = -1; } points[i] = new Point(gd.getXCoordinate(i), gd.getYCoordinate(i), - (gd.getFlags(i) & GlyfDescript.ON_CURVE) != 0, endPt); + (gd.getFlags(i) & GlyfDescript.ON_CURVE) != 0, endPt); } return points; } @@ -100,7 +100,7 @@ private Path calculatePath(Point[] points) { Point firstPoint = points[start]; Point lastPoint = points[p]; - List contour = new ArrayList(); + List contour = new ArrayList((p - start) + 3); for (int q = start; q <= p; ++q) { contour.add(points[q]); @@ -171,7 +171,7 @@ private void quadTo(Path path, Point ctrlPoint, Point point) if (PDFBoxConfig.isDebugEnabled()) { Log.d("PdfBox-Android", "quadTo: " + String.format(Locale.US, "%d,%d %d,%d", ctrlPoint.x, ctrlPoint.y, - point.x, point.y)); + point.x, point.y)); } } @@ -214,7 +214,7 @@ private static class Point public String toString() { return String.format(Locale.US, "Point(%d,%d,%s,%s)", x, y, onCurve ? "onCurve" : "", - endOfContour ? "endOfContour" : ""); + endOfContour ? "endOfContour" : ""); } } diff --git a/library/src/main/java/com/tom_roush/fontbox/ttf/GlyphSubstitutionTable.java b/library/src/main/java/com/tom_roush/fontbox/ttf/GlyphSubstitutionTable.java index 16675ceb7..d3f80ca1b 100644 --- a/library/src/main/java/com/tom_roush/fontbox/ttf/GlyphSubstitutionTable.java +++ b/library/src/main/java/com/tom_roush/fontbox/ttf/GlyphSubstitutionTable.java @@ -30,7 +30,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; - +import java.util.regex.Pattern; /** * A glyph substitution 'GSUB' table in a TrueType or OpenType font. * @@ -38,6 +38,7 @@ */ public class GlyphSubstitutionTable extends TTFTable { + public static final String TAG = "GSUB"; private LinkedHashMap scriptList; @@ -50,6 +51,13 @@ public class GlyphSubstitutionTable extends TTFTable private String lastUsedSupportedScript; + /** + * The regex represents 4 'word characters' [a-zA-Z_0-9], + * see {@link java.util.regex.ASCII#WORD}
+ * Note: the ' '-character is not matched! + */ + private static final Pattern WORDPATTERN = Pattern.compile( "\\w{4}" ); + GlyphSubstitutionTable(TrueTypeFont font) { super(font); @@ -75,6 +83,8 @@ void read(TrueTypeFont ttf, TTFDataStream data) throws IOException scriptList = readScriptList(data, start + scriptListOffset); featureList = readFeatureList(data, start + featureListOffset); lookupList = readLookupList(data, start + lookupListOffset); + + initialized = true; } LinkedHashMap readScriptList(TTFDataStream data, long offset) throws IOException @@ -97,6 +107,11 @@ LinkedHashMap readScriptList(TTFDataStream data, long offse LinkedHashMap resultScriptList = new LinkedHashMap(scriptCount); for (ScriptRecord scriptRecord : scriptRecords) { + if (resultScriptList.get(scriptRecord.scriptTag) != null) + { + // PDFBOX-6146 + continue; + } resultScriptList.put(scriptRecord.scriptTag, scriptRecord.scriptTable); } return resultScriptList; @@ -120,7 +135,7 @@ ScriptTable readScriptTable(TTFDataStream data, long offset) throws IOException // PDFBOX-4489: catch corrupt file // https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#slTbl_sRec throw new IOException("LangSysRecords not alphabetically sorted by LangSys tag: " + - langSysRecord.langSysTag + " <= " + prevLangSysTag); + langSysRecord.langSysTag + " <= " + prevLangSysTag); } langSysOffsets[i] = data.readUnsignedShort(); langSysRecords[i] = langSysRecord; @@ -173,17 +188,18 @@ FeatureRecord[] readFeatureList(TTFDataStream data, long offset) throws IOExcept { // catch corrupt file // https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#flTbl - if (featureRecord.featureTag.matches("\\w{4}") && prevFeatureTag.matches("\\w{4}")) + if (WORDPATTERN.matcher(featureRecord.featureTag).matches() && + WORDPATTERN.matcher(prevFeatureTag).matches()) { // ArialUni.ttf has many warnings but isn't corrupt, so we assume that only // strings with trash characters indicate real corruption Log.d("PdfBox-Android", "FeatureRecord array not alphabetically sorted by FeatureTag: " + - featureRecord.featureTag + " < " + prevFeatureTag); + featureRecord.featureTag + " < " + prevFeatureTag); } else { Log.w("PdfBox-Android", "FeatureRecord array not alphabetically sorted by FeatureTag: " + - featureRecord.featureTag + " < " + prevFeatureTag); + featureRecord.featureTag + " < " + prevFeatureTag); return new FeatureRecord[0]; } } @@ -223,9 +239,16 @@ LookupTable[] readLookupList(TTFDataStream data, long offset) throws IOException lookups[i] = data.readUnsignedShort(); } LookupTable[] lookupTables = new LookupTable[lookupCount]; + Map lookupTableMap = new HashMap(); // PDFBOX-6146 for (int i = 0; i < lookupCount; i++) { - lookupTables[i] = readLookupTable(data, offset + lookups[i]); + LookupTable lookupTable = lookupTableMap.get(lookups[i]); + if (lookupTable == null) + { + lookupTable = readLookupTable(data, offset + lookups[i]); + lookupTableMap.put(lookups[i], lookupTable); + } + lookupTables[i] = lookupTable; } return lookupTables; } @@ -237,10 +260,10 @@ LookupTable readLookupTable(TTFDataStream data, long offset) throws IOException lookupTable.lookupType = data.readUnsignedShort(); lookupTable.lookupFlag = data.readUnsignedShort(); int subTableCount = data.readUnsignedShort(); - int[] subTableOffets = new int[subTableCount]; + int[] subTableOffsets = new int[subTableCount]; for (int i = 0; i < subTableCount; i++) { - subTableOffets[i] = data.readUnsignedShort(); + subTableOffsets[i] = data.readUnsignedShort(); } if ((lookupTable.lookupFlag & 0x0010) != 0) { @@ -249,15 +272,15 @@ LookupTable readLookupTable(TTFDataStream data, long offset) throws IOException lookupTable.subTables = new LookupSubTable[subTableCount]; switch (lookupTable.lookupType) { - case 1: // Single - for (int i = 0; i < subTableCount; i++) - { - lookupTable.subTables[i] = readLookupSubTable(data, offset + subTableOffets[i]); - } - break; - default: - // Other lookup types are not supported - Log.d("PdfBox-Android", "Type " + lookupTable.lookupType + " GSUB lookup table is not supported and will be ignored"); + case 1: // Single + for (int i = 0; i < subTableCount; i++) + { + lookupTable.subTables[i] = readLookupSubTable(data, offset + subTableOffsets[i]); + } + break; + default: + // Other lookup types are not supported + Log.d("PdfBox-Android", "Type " + lookupTable.lookupType + " GSUB lookup table is not supported and will be ignored"); } return lookupTable; } @@ -268,31 +291,32 @@ LookupSubTable readLookupSubTable(TTFDataStream data, long offset) throws IOExce int substFormat = data.readUnsignedShort(); switch (substFormat) { - case 1: - { - LookupTypeSingleSubstFormat1 lookupSubTable = new LookupTypeSingleSubstFormat1(); - lookupSubTable.substFormat = substFormat; - int coverageOffset = data.readUnsignedShort(); - lookupSubTable.deltaGlyphID = data.readSignedShort(); - lookupSubTable.coverageTable = readCoverageTable(data, offset + coverageOffset); - return lookupSubTable; - } - case 2: + case 1: + { + LookupTypeSingleSubstFormat1 lookupSubTable = new LookupTypeSingleSubstFormat1(); + lookupSubTable.substFormat = substFormat; + int coverageOffset = data.readUnsignedShort(); + lookupSubTable.deltaGlyphID = data.readSignedShort(); + lookupSubTable.coverageTable = readCoverageTable(data, offset + coverageOffset); + return lookupSubTable; + } + case 2: + { + LookupTypeSingleSubstFormat2 lookupSubTable = new LookupTypeSingleSubstFormat2(); + lookupSubTable.substFormat = substFormat; + int coverageOffset = data.readUnsignedShort(); + int glyphCount = data.readUnsignedShort(); + lookupSubTable.substituteGlyphIDs = new int[glyphCount]; + for (int i = 0; i < glyphCount; i++) { - LookupTypeSingleSubstFormat2 lookupSubTable = new LookupTypeSingleSubstFormat2(); - lookupSubTable.substFormat = substFormat; - int coverageOffset = data.readUnsignedShort(); - int glyphCount = data.readUnsignedShort(); - lookupSubTable.substituteGlyphIDs = new int[glyphCount]; - for (int i = 0; i < glyphCount; i++) - { - lookupSubTable.substituteGlyphIDs[i] = data.readUnsignedShort(); - } - lookupSubTable.coverageTable = readCoverageTable(data, offset + coverageOffset); - return lookupSubTable; + lookupSubTable.substituteGlyphIDs[i] = data.readUnsignedShort(); } - default: - throw new IOException("Unknown substFormat: " + substFormat); + lookupSubTable.coverageTable = readCoverageTable(data, offset + coverageOffset); + return lookupSubTable; + } + default: + Log.w("PdfBox-Android", "Unknown substFormat: " + substFormat); + return null; } } @@ -302,34 +326,34 @@ CoverageTable readCoverageTable(TTFDataStream data, long offset) throws IOExcept int coverageFormat = data.readUnsignedShort(); switch (coverageFormat) { - case 1: + case 1: + { + CoverageTableFormat1 coverageTable = new CoverageTableFormat1(); + coverageTable.coverageFormat = coverageFormat; + int glyphCount = data.readUnsignedShort(); + coverageTable.glyphArray = new int[glyphCount]; + for (int i = 0; i < glyphCount; i++) { - CoverageTableFormat1 coverageTable = new CoverageTableFormat1(); - coverageTable.coverageFormat = coverageFormat; - int glyphCount = data.readUnsignedShort(); - coverageTable.glyphArray = new int[glyphCount]; - for (int i = 0; i < glyphCount; i++) - { - coverageTable.glyphArray[i] = data.readUnsignedShort(); - } - return coverageTable; + coverageTable.glyphArray[i] = data.readUnsignedShort(); } - case 2: + return coverageTable; + } + case 2: + { + CoverageTableFormat2 coverageTable = new CoverageTableFormat2(); + coverageTable.coverageFormat = coverageFormat; + int rangeCount = data.readUnsignedShort(); + coverageTable.rangeRecords = new RangeRecord[rangeCount]; + for (int i = 0; i < rangeCount; i++) { - CoverageTableFormat2 coverageTable = new CoverageTableFormat2(); - coverageTable.coverageFormat = coverageFormat; - int rangeCount = data.readUnsignedShort(); - coverageTable.rangeRecords = new RangeRecord[rangeCount]; - for (int i = 0; i < rangeCount; i++) - { - coverageTable.rangeRecords[i] = readRangeRecord(data); - } - return coverageTable; - + coverageTable.rangeRecords[i] = readRangeRecord(data); } - default: - // Should not happen (the spec indicates only format 1 and format 2) - throw new IOException("Unknown coverage format: " + coverageFormat); + return coverageTable; + + } + default: + // Should not happen (the spec indicates only format 1 and format 2) + throw new IOException("Unknown coverage format: " + coverageFormat); } } @@ -347,7 +371,7 @@ private String selectScriptTag(String[] tags) { String tag = tags[0]; if (OpenTypeScript.INHERITED.equals(tag) - || (OpenTypeScript.TAG_DEFAULT.equals(tag) && !scriptList.containsKey(tag))) + || (OpenTypeScript.TAG_DEFAULT.equals(tag) && !scriptList.containsKey(tag))) { // We don't know what script this should be. if (lastUsedSupportedScript == null) @@ -406,7 +430,7 @@ private Collection getLangSysTables(String scriptTag) * @return The indicated {@code FeatureRecord}s */ private List getFeatureRecords(Collection langSysTables, - final List enabledFeatures) + final List enabledFeatures) { if (langSysTables.isEmpty()) { @@ -423,8 +447,8 @@ private List getFeatureRecords(Collection langSysTa for (int featureIndex : langSysTable.featureIndices) { if (featureIndex < featureList.length && - (enabledFeatures == null || - enabledFeatures.contains(featureList[featureIndex].featureTag))) + (enabledFeatures == null || + enabledFeatures.contains(featureList[featureIndex].featureTag))) { result.add(featureList[featureIndex]); } @@ -487,7 +511,7 @@ private int applyFeature(FeatureRecord featureRecord, int gid) if (lookupTable.lookupType != 1) { Log.d("PdfBox-Android", "Skipping GSUB feature '" + featureRecord.featureTag - + "' because it requires unsupported lookup table type " + lookupTable.lookupType); + + "' because it requires unsupported lookup table type " + lookupTable.lookupType); continue; } gid = doLookup(lookupTable, gid); @@ -531,7 +555,7 @@ public int getSubstitution(int gid, String[] scriptTags, List enabledFea if (cached != null) { // Because script detection for indeterminate scripts (COMMON, INHERIT, etc.) depends on context, - // it is possible to return a different substitution for the same input. However we don't want that, + // it is possible to return a different substitution for the same input. However, we don't want that, // as we need a one-to-one mapping. return cached; } @@ -549,13 +573,15 @@ public int getSubstitution(int gid, String[] scriptTags, List enabledFea } /** - * For a substitute-gid (obtained from {@link #getSubstitution(int, String[], List)}), retrieve - * the original gid. - * - * Only gids previously substituted by this instance can be un-substituted. If you are trying to - * unsubstitute before you substitute, something is wrong. + * For a substitute-gid (obtained from {@link #getSubstitution(int, String[], List)}), + * retrieve the original gid. + *

+ * Only gids previously substituted by this instance can be un-substituted. + * If you are trying to unsubstitute before you substitute, something is wrong. * * @param sgid Substitute GID + * + * @return the original gid of a substitute-gid */ public int getUnsubstitution(int sgid) { @@ -599,7 +625,7 @@ static class ScriptTable public String toString() { return String.format("ScriptTable[hasDefault=%s,langSysRecordsCount=%d]", - defaultLangSysTable != null, langSysTables.size()); + defaultLangSysTable != null, langSysTables.size()); } } @@ -647,8 +673,8 @@ static class FeatureTable @Override public String toString() { - return String.format("FeatureTable[lookupListIndiciesCount=%d]", - lookupListIndices.length); + return String.format("FeatureTable[lookupListIndicesCount=%d]", + lookupListIndices.length); } } @@ -663,7 +689,7 @@ static class LookupTable public String toString() { return String.format("LookupTable[lookupType=%d,lookupFlag=%d,markFilteringSet=%d]", - lookupType, lookupFlag, markFilteringSet); + lookupType, lookupFlag, markFilteringSet); } } @@ -689,7 +715,7 @@ int doSubstitution(int gid, int coverageIndex) public String toString() { return String.format("LookupTypeSingleSubstFormat1[substFormat=%d,deltaGlyphID=%d]", - substFormat, deltaGlyphID); + substFormat, deltaGlyphID); } } @@ -707,8 +733,8 @@ int doSubstitution(int gid, int coverageIndex) public String toString() { return String.format( - "LookupTypeSingleSubstFormat2[substFormat=%d,substituteGlyphIDs=%s]", - substFormat, Arrays.toString(substituteGlyphIDs)); + "LookupTypeSingleSubstFormat2[substFormat=%d,substituteGlyphIDs=%s]", + substFormat, Arrays.toString(substituteGlyphIDs)); } } @@ -733,7 +759,7 @@ int getCoverageIndex(int gid) public String toString() { return String.format("CoverageTableFormat1[coverageFormat=%d,glyphArray=%s]", - coverageFormat, Arrays.toString(glyphArray)); + coverageFormat, Arrays.toString(glyphArray)); } } @@ -771,7 +797,7 @@ static class RangeRecord public String toString() { return String.format("RangeRecord[startGlyphID=%d,endGlyphID=%d,startCoverageIndex=%d]", - startGlyphID, endGlyphID, startCoverageIndex); + startGlyphID, endGlyphID, startCoverageIndex); } } } diff --git a/library/src/main/java/com/tom_roush/fontbox/ttf/GlyphTable.java b/library/src/main/java/com/tom_roush/fontbox/ttf/GlyphTable.java index f0d5cb706..337977c9c 100644 --- a/library/src/main/java/com/tom_roush/fontbox/ttf/GlyphTable.java +++ b/library/src/main/java/com/tom_roush/fontbox/ttf/GlyphTable.java @@ -17,7 +17,6 @@ package com.tom_roush.fontbox.ttf; import java.io.IOException; - /** * A table in a true type font. * @@ -40,6 +39,7 @@ public class GlyphTable extends TTFTable private int cached = 0; private HorizontalMetricsTable hmt = null; + private MaximumProfileTable maxp = null; /** * Don't even bother to cache huge fonts. @@ -83,6 +83,8 @@ void read(TrueTypeFont ttf, TTFDataStream data) throws IOException // locks TrueTypeFont and then tries to lock "data" hmt = font.getHorizontalMetrics(); + maxp = ttf.getMaximumProfile(); + initialized = true; } @@ -139,7 +141,7 @@ public GlyphData[] getGlyphs() throws IOException { ++cached; } - glyphs[gid] = getGlyphData(gid); + glyphs[gid] = getGlyphData(gid, 0); } initialized = true; return glyphs; @@ -161,6 +163,11 @@ public void setGlyphs(GlyphData[] glyphsValue) * @throws IOException if the font cannot be read */ public GlyphData getGlyph(int gid) throws IOException + { + return getGlyph(gid, 0); + } + + GlyphData getGlyph(int gid, int level) throws IOException { if (gid < 0 || gid >= numGlyphs) { @@ -181,11 +188,12 @@ public GlyphData getGlyph(int gid) throws IOException // read a single glyph long[] offsets = loca.getOffsets(); - if (offsets[gid] == offsets[gid + 1]) + if (offsets[gid] == offsets[gid + 1] || offsets[gid] == data.getOriginalDataSize()) { // no outline // PDFBOX-5135: can't return null, must return an empty glyph because // sometimes this is used in a composite glyph. + // PDFBOX-5917: offset points to end of the stream glyph = new GlyphData(); glyph.initEmptyData(); } @@ -196,7 +204,7 @@ public GlyphData getGlyph(int gid) throws IOException data.seek(getOffset() + offsets[gid]); - glyph = getGlyphData(gid); + glyph = getGlyphData(gid, level); // restore data.seek(currentPosition); @@ -212,11 +220,15 @@ public GlyphData getGlyph(int gid) throws IOException } } - private GlyphData getGlyphData(int gid) throws IOException + private GlyphData getGlyphData(int gid, int level) throws IOException { + if (level > maxp.getMaxComponentDepth()) + { + throw new IOException("composite glyph maximum level (" + maxp.getMaxComponentDepth() + ") reached"); + } GlyphData glyph = new GlyphData(); int leftSideBearing = hmt == null ? 0 : hmt.getLeftSideBearing(gid); - glyph.initData(this, data, leftSideBearing); + glyph.initData(this, data, leftSideBearing, level); // resolve composite glyph if (glyph.getDescription().isComposite()) { diff --git a/library/src/main/java/com/tom_roush/fontbox/ttf/IndexToLocationTable.java b/library/src/main/java/com/tom_roush/fontbox/ttf/IndexToLocationTable.java index 69774eef7..81cb86331 100644 --- a/library/src/main/java/com/tom_roush/fontbox/ttf/IndexToLocationTable.java +++ b/library/src/main/java/com/tom_roush/fontbox/ttf/IndexToLocationTable.java @@ -17,7 +17,6 @@ package com.tom_roush.fontbox.ttf; import java.io.IOException; - /** * A table in a true type font. * @@ -71,6 +70,11 @@ else if( head.getIndexToLocFormat() == LONG_OFFSETS ) throw new IOException( "Error:TTF.loca unknown offset format."); } } + if (numGlyphs == 1 && offsets[0] == 0 && offsets[1] == 0) + { + // PDFBOX-5794 empty glyph + throw new IOException("The font has no glyphs"); + } initialized = true; } /** diff --git a/library/src/main/java/com/tom_roush/fontbox/ttf/KerningSubtable.java b/library/src/main/java/com/tom_roush/fontbox/ttf/KerningSubtable.java index 5b9ad665d..174120594 100644 --- a/library/src/main/java/com/tom_roush/fontbox/ttf/KerningSubtable.java +++ b/library/src/main/java/com/tom_roush/fontbox/ttf/KerningSubtable.java @@ -21,7 +21,6 @@ import java.io.IOException; import java.util.Arrays; import java.util.Comparator; - /** * A 'kern' table in a true type font. * @@ -29,6 +28,7 @@ */ public class KerningSubtable { + // coverage field bit masks and values private static final int COVERAGE_HORIZONTAL = 0x0001; private static final int COVERAGE_MINIMUMS = 0x0002; @@ -185,8 +185,8 @@ private void readSubtable0(TTFDataStream data) throws IOException int length = data.readUnsignedShort(); if (length < 6) { - throw new IOException("Kerning sub-table too short, got " + length - + " bytes, expect 6 or more."); + Log.w("PdfBox-Android", "Kerning sub-table too short, got " + length + " bytes, expect 6 or more."); + return; } int coverage = data.readUnsignedShort(); if (isBitsSet(coverage, COVERAGE_HORIZONTAL, COVERAGE_HORIZONTAL_SHIFT)) @@ -251,14 +251,13 @@ private interface PairData private static class PairData0Format0 implements Comparator, PairData { - private int searchRange; private int[][] pairs; @Override public void read(TTFDataStream data) throws IOException { int numPairs = data.readUnsignedShort(); - searchRange = data.readUnsignedShort()/6; + int searchRange = data.readUnsignedShort()/6; int entrySelector = data.readUnsignedShort(); int rangeShift = data.readUnsignedShort(); pairs = new int[numPairs][3]; @@ -288,10 +287,6 @@ public int getKerning(int l, int r) @Override public int compare(int[] p1, int[] p2) { - assert p1 != null; - assert p1.length >= 2; - assert p2 != null; - assert p2.length >= 2; int l1 = p1[0]; int l2 = p2[0]; if (l1 < l2) diff --git a/library/src/main/java/com/tom_roush/fontbox/ttf/MaximumProfileTable.java b/library/src/main/java/com/tom_roush/fontbox/ttf/MaximumProfileTable.java index 521fca446..0676abd3c 100644 --- a/library/src/main/java/com/tom_roush/fontbox/ttf/MaximumProfileTable.java +++ b/library/src/main/java/com/tom_roush/fontbox/ttf/MaximumProfileTable.java @@ -17,7 +17,6 @@ package com.tom_roush.fontbox.ttf; import java.io.IOException; - /** * A table in a true type font. * @@ -274,19 +273,27 @@ void read(TrueTypeFont ttf, TTFDataStream data) throws IOException { version = data.read32Fixed(); numGlyphs = data.readUnsignedShort(); - maxPoints = data.readUnsignedShort(); - maxContours = data.readUnsignedShort(); - maxCompositePoints = data.readUnsignedShort(); - maxCompositeContours = data.readUnsignedShort(); - maxZones = data.readUnsignedShort(); - maxTwilightPoints = data.readUnsignedShort(); - maxStorage = data.readUnsignedShort(); - maxFunctionDefs = data.readUnsignedShort(); - maxInstructionDefs = data.readUnsignedShort(); - maxStackElements = data.readUnsignedShort(); - maxSizeOfInstructions = data.readUnsignedShort(); - maxComponentElements = data.readUnsignedShort(); - maxComponentDepth = data.readUnsignedShort(); + if (version >= 1.0f) + { + maxPoints = data.readUnsignedShort(); + maxContours = data.readUnsignedShort(); + maxCompositePoints = data.readUnsignedShort(); + maxCompositeContours = data.readUnsignedShort(); + maxZones = data.readUnsignedShort(); + maxTwilightPoints = data.readUnsignedShort(); + maxStorage = data.readUnsignedShort(); + maxFunctionDefs = data.readUnsignedShort(); + maxInstructionDefs = data.readUnsignedShort(); + maxStackElements = data.readUnsignedShort(); + maxSizeOfInstructions = data.readUnsignedShort(); + maxComponentElements = data.readUnsignedShort(); + maxComponentDepth = data.readUnsignedShort(); + if (maxComponentDepth == 0) + { + // PDFBOX-6105 + maxComponentDepth = 1; + } + } initialized = true; } } diff --git a/library/src/main/java/com/tom_roush/fontbox/ttf/MemoryTTFDataStream.java b/library/src/main/java/com/tom_roush/fontbox/ttf/MemoryTTFDataStream.java index 86c7b6973..5c7b29cb3 100644 --- a/library/src/main/java/com/tom_roush/fontbox/ttf/MemoryTTFDataStream.java +++ b/library/src/main/java/com/tom_roush/fontbox/ttf/MemoryTTFDataStream.java @@ -21,7 +21,6 @@ import java.io.EOFException; import java.io.IOException; import java.io.InputStream; - /** * An interface into a data stream. * @@ -34,7 +33,7 @@ class MemoryTTFDataStream extends TTFDataStream private int currentPosition = 0; /** - * Constructor from a stream. + * Constructor from a stream. * @param is The stream to read from. It will be closed by this method. * @throws IOException If an error occurs while reading from the stream. */ @@ -42,7 +41,13 @@ class MemoryTTFDataStream extends TTFDataStream { try { - ByteArrayOutputStream output = new ByteArrayOutputStream( is.available() ); + int available = is.available(); + if (available > Integer.MAX_VALUE - 8) // https://www.baeldung.com/java-arrays-max-size + { + // PDFBOX-5991 + throw new IOException("Stream is too long, size: " + available); + } + ByteArrayOutputStream output = new ByteArrayOutputStream(available); byte[] buffer = new byte[1024]; int amountRead; while( (amountRead = is.read( buffer ) ) != -1 ) @@ -179,10 +184,10 @@ public void seek(long pos) throws IOException */ @Override public int read(byte[] b, - int off, - int len) - throws IOException - { + int off, + int len) + throws IOException + { if (currentPosition < data.length) { int amountRead = Math.min( len, data.length-currentPosition ); @@ -194,7 +199,7 @@ public int read(byte[] b, { return -1; } - } + } /** * Get the current position in the stream. diff --git a/library/src/main/java/com/tom_roush/fontbox/ttf/NameRecord.java b/library/src/main/java/com/tom_roush/fontbox/ttf/NameRecord.java index ceeee98f4..594bf59a6 100644 --- a/library/src/main/java/com/tom_roush/fontbox/ttf/NameRecord.java +++ b/library/src/main/java/com/tom_roush/fontbox/ttf/NameRecord.java @@ -17,7 +17,6 @@ package com.tom_roush.fontbox.ttf; import java.io.IOException; - /** * A name record in the name table. * @@ -39,7 +38,7 @@ public class NameRecord // Unicode encoding ids /** - * @deprecated use {@link #LANGUAGE_UNICODE} instead. + * @deprecated use {@link #LANGUAGE_UNICODE} instead. */ @Deprecated public static final int LANGUGAE_UNICODE = 0; @@ -52,7 +51,7 @@ public class NameRecord // Windows language ids /** - * @deprecated use {@link #LANGUAGE_WINDOWS_EN_US} instead. + * @deprecated use {@link #LANGUAGE_WINDOWS_EN_US} instead. */ @Deprecated public static final int LANGUGAE_WINDOWS_EN_US = 0x0409; @@ -63,7 +62,7 @@ public class NameRecord // Macintosh language ids /** - * @deprecated use {@link #LANGUAGE_MACINTOSH_ENGLISH} instead. + * @deprecated use {@link #LANGUAGE_MACINTOSH_ENGLISH} instead. */ @Deprecated public static final int LANGUGAE_MACINTOSH_ENGLISH = 0; @@ -195,14 +194,15 @@ void initData( TrueTypeFont ttf, TTFDataStream data ) throws IOException * * @return A string for this class. */ + @Override public String toString() { return "platform=" + platformId + - " pEncoding=" + platformEncodingId + - " language=" + languageId + - " name=" + nameId + - " " + string; + " pEncoding=" + platformEncodingId + + " language=" + languageId + + " name=" + nameId + + " " + string; } /** * @return Returns the string. diff --git a/library/src/main/java/com/tom_roush/fontbox/ttf/NamingTable.java b/library/src/main/java/com/tom_roush/fontbox/ttf/NamingTable.java index 2b710d6d0..c4db30c52 100644 --- a/library/src/main/java/com/tom_roush/fontbox/ttf/NamingTable.java +++ b/library/src/main/java/com/tom_roush/fontbox/ttf/NamingTable.java @@ -24,7 +24,6 @@ import java.util.Map; import com.tom_roush.fontbox.util.Charsets; - /** * A table in a true type font. * @@ -80,42 +79,22 @@ void read(TrueTypeFont ttf, TTFDataStream data) throws IOException continue; } - data.seek(getOffset() + (2*3)+numberOfNameRecords*2*6+nr.getStringOffset()); - int platform = nr.getPlatformId(); - int encoding = nr.getPlatformEncodingId(); - Charset charset = Charsets.ISO_8859_1; - if (platform == NameRecord.PLATFORM_WINDOWS && (encoding == NameRecord.ENCODING_WINDOWS_SYMBOL || encoding == NameRecord.ENCODING_WINDOWS_UNICODE_BMP)) - { - charset = Charsets.UTF_16; - } - else if (platform == NameRecord.PLATFORM_UNICODE) - { - charset = Charsets.UTF_16; - } - else if (platform == NameRecord.PLATFORM_ISO) - { - switch (encoding) - { - case 0: - charset = Charsets.US_ASCII; - break; - case 1: - //not sure is this is correct?? - charset = Charsets.ISO_10646; - break; - case 2: - charset = Charsets.ISO_8859_1; - break; - default: - break; - } - } + data.seek(getOffset() + (2*3L)+numberOfNameRecords*2*6+nr.getStringOffset()); + Charset charset = getCharset(nr); String string = data.readString(nr.getStringLength(), charset); nr.setString(string); } - // build multi-dimensional lookup table lookupTable = new HashMap>>>(nameRecords.size()); + fillLookupTable(); + readInterestingStrings(); + + initialized = true; + } + + private void fillLookupTable() + { + // build multi-dimensional lookup table for (NameRecord nr : nameRecords) { // name id @@ -136,35 +115,66 @@ else if (platform == NameRecord.PLATFORM_ISO) Map languageLookup = encodingLookup.get(nr.getPlatformEncodingId()); if (languageLookup == null) { - languageLookup = new HashMap(); + languageLookup = new HashMap(1); encodingLookup.put(nr.getPlatformEncodingId(), languageLookup); } // language id / string languageLookup.put(nr.getLanguageId(), nr.getString()); } + } - // extract strings of interest + private Charset getCharset(NameRecord nr) + { + int platform = nr.getPlatformId(); + int encoding = nr.getPlatformEncodingId(); + Charset charset = Charsets.ISO_8859_1; + if (platform == NameRecord.PLATFORM_WINDOWS && (encoding == NameRecord.ENCODING_WINDOWS_SYMBOL || encoding == NameRecord.ENCODING_WINDOWS_UNICODE_BMP)) + { + charset = Charsets.UTF_16; + } + else if (platform == NameRecord.PLATFORM_UNICODE) + { + charset = Charsets.UTF_16; + } + else if (platform == NameRecord.PLATFORM_ISO) + { + switch (encoding) + { + case 0: + charset = Charsets.US_ASCII; + break; + case 1: + //not sure is this is correct?? + charset = Charsets.ISO_10646; + break; + default: + break; + } + } + return charset; + } + + private void readInterestingStrings() + { fontFamily = getEnglishName(NameRecord.NAME_FONT_FAMILY_NAME); fontSubFamily = getEnglishName(NameRecord.NAME_FONT_SUB_FAMILY_NAME); // extract PostScript name, only these two formats are valid psName = getName(NameRecord.NAME_POSTSCRIPT_NAME, - NameRecord.PLATFORM_MACINTOSH, - NameRecord.ENCODING_MACINTOSH_ROMAN, - NameRecord.LANGUAGE_MACINTOSH_ENGLISH); + NameRecord.PLATFORM_MACINTOSH, + NameRecord.ENCODING_MACINTOSH_ROMAN, + NameRecord.LANGUAGE_MACINTOSH_ENGLISH); if (psName == null) { psName = getName(NameRecord.NAME_POSTSCRIPT_NAME, - NameRecord.PLATFORM_WINDOWS, - NameRecord.ENCODING_WINDOWS_UNICODE_BMP, - NameRecord.LANGUAGE_WINDOWS_EN_US); + NameRecord.PLATFORM_WINDOWS, + NameRecord.ENCODING_WINDOWS_UNICODE_BMP, + NameRecord.LANGUAGE_WINDOWS_EN_US); } if (psName != null) { psName = psName.trim(); } - - initialized = true; } /** @@ -176,10 +186,10 @@ private String getEnglishName(int nameId) for (int i = 4; i >= 0; i--) { String nameUni = - getName(nameId, - NameRecord.PLATFORM_UNICODE, - i, - NameRecord.LANGUAGE_UNICODE); + getName(nameId, + NameRecord.PLATFORM_UNICODE, + i, + NameRecord.LANGUAGE_UNICODE); if (nameUni != null) { return nameUni; @@ -188,10 +198,10 @@ private String getEnglishName(int nameId) // Windows, Unicode BMP, EN-US String nameWin = - getName(nameId, - NameRecord.PLATFORM_WINDOWS, - NameRecord.ENCODING_WINDOWS_UNICODE_BMP, - NameRecord.LANGUAGE_WINDOWS_EN_US); + getName(nameId, + NameRecord.PLATFORM_WINDOWS, + NameRecord.ENCODING_WINDOWS_UNICODE_BMP, + NameRecord.LANGUAGE_WINDOWS_EN_US); if (nameWin != null) { return nameWin; @@ -199,13 +209,13 @@ private String getEnglishName(int nameId) // Macintosh, Roman, English return getName(nameId, - NameRecord.PLATFORM_MACINTOSH, - NameRecord.ENCODING_MACINTOSH_ROMAN, - NameRecord.LANGUAGE_MACINTOSH_ENGLISH); + NameRecord.PLATFORM_MACINTOSH, + NameRecord.ENCODING_MACINTOSH_ROMAN, + NameRecord.LANGUAGE_MACINTOSH_ENGLISH); } /** - * Returns a name from the table, or null it it does not exist. + * Returns a name from the table, or null if it does not exist. * * @param nameId Name ID from NameRecord constants. * @param platformId Platform ID from NameRecord constants. diff --git a/library/src/main/java/com/tom_roush/fontbox/ttf/OpenTypeFont.java b/library/src/main/java/com/tom_roush/fontbox/ttf/OpenTypeFont.java index 8937da566..27065e717 100644 --- a/library/src/main/java/com/tom_roush/fontbox/ttf/OpenTypeFont.java +++ b/library/src/main/java/com/tom_roush/fontbox/ttf/OpenTypeFont.java @@ -20,7 +20,6 @@ import android.graphics.Path; import java.io.IOException; - /** * An OpenType (OTF/TTF) font. */ @@ -81,7 +80,25 @@ public Path getPath(String name) throws IOException */ public boolean isPostScript() { - return tables.containsKey(CFFTable.TAG); + return isPostScript || tables.containsKey(CFFTable.TAG) || tables.containsKey("CFF2"); + } + + /** + * Returns true if this font is supported. + * + * There are 3 kind of OpenType fonts, fonts using TrueType outlines, fonts using CFF outlines (version 1 and 2) + * + * Fonts using CFF outlines version 2 aren't supported yet. + * + * @return true if the font is supported + */ + public boolean isSupportedOTF() + { + // OTF using CFF2 based outlines aren't yet supported + return !(isPostScript // + && !tables.containsKey(CFFTable.TAG) // + && tables.containsKey("CFF2") // + ); } /** @@ -90,9 +107,9 @@ public boolean isPostScript() public boolean hasLayoutTables() { return tables.containsKey("BASE") || - tables.containsKey("GDEF") || - tables.containsKey("GPOS") || - tables.containsKey("GSUB") || - tables.containsKey("JSTF"); + tables.containsKey("GDEF") || + tables.containsKey("GPOS") || + tables.containsKey("GSUB") || + tables.containsKey("JSTF"); } } diff --git a/library/src/main/java/com/tom_roush/fontbox/ttf/OpenTypeScript.java b/library/src/main/java/com/tom_roush/fontbox/ttf/OpenTypeScript.java index feabbbeba..129c3a893 100644 --- a/library/src/main/java/com/tom_roush/fontbox/ttf/OpenTypeScript.java +++ b/library/src/main/java/com/tom_roush/fontbox/ttf/OpenTypeScript.java @@ -26,13 +26,12 @@ import java.util.Arrays; import java.util.Comparator; import java.util.HashMap; -import java.util.Map; import java.util.Map.Entry; +import java.util.Map; import java.util.StringTokenizer; import java.util.TreeMap; import com.tom_roush.pdfbox.android.PDFBoxResourceLoader; - /** * A class for mapping Unicode codepoints to OpenType script tags * @@ -44,6 +43,7 @@ */ public final class OpenTypeScript { + public static final String INHERITED = "Inherited"; public static final String UNKNOWN = "Unknown"; public static final String TAG_DEFAULT = "DFLT"; @@ -61,150 +61,150 @@ public final class OpenTypeScript static { Object[][] table = - { - {"Adlam", new String[] { "adlm" }}, - {"Ahom", new String[] { "ahom" }}, - {"Anatolian_Hieroglyphs", new String[] { "hluw" }}, - {"Arabic", new String[] { "arab" }}, - {"Armenian", new String[] { "armn" }}, - {"Avestan", new String[] { "avst" }}, - {"Balinese", new String[] { "bali" }}, - {"Bamum", new String[] { "bamu" }}, - {"Bassa_Vah", new String[] { "bass" }}, - {"Batak", new String[] { "batk" }}, - {"Bengali", new String[] { "bng2", "beng" }}, - {"Bhaiksuki", new String[] { "bhks" }}, - {"Bopomofo", new String[] { "bopo" }}, - {"Brahmi", new String[] { "brah" }}, - {"Braille", new String[] { "brai" }}, - {"Buginese", new String[] { "bugi" }}, - {"Buhid", new String[] { "buhd" }}, - // Byzantine Music: byzm - {"Canadian_Aboriginal", new String[] { "cans" }}, - {"Carian", new String[] { "cari" }}, - {"Caucasian_Albanian", new String[] { "aghb" }}, - {"Chakma", new String[] { "cakm" }}, - {"Cham", new String[] { "cham" }}, - {"Cherokee", new String[] { "cher" }}, - {"Common", new String[] { TAG_DEFAULT }}, // "Default" in OpenType - {"Coptic", new String[] { "copt" }}, - {"Cuneiform", new String[] { "xsux" }}, // "Sumero-Akkadian Cuneiform" in OpenType - {"Cypriot", new String[] { "cprt" }}, - {"Cyrillic", new String[] { "cyrl" }}, - {"Deseret", new String[] { "dsrt" }}, - {"Devanagari", new String[] { "dev2", "deva" }}, - {"Duployan", new String[] { "dupl" }}, - {"Egyptian_Hieroglyphs", new String[] { "egyp" }}, - {"Elbasan", new String[] { "elba" }}, - {"Ethiopic", new String[] { "ethi" }}, - {"Georgian", new String[] { "geor" }}, - {"Glagolitic", new String[] { "glag" }}, - {"Gothic", new String[] { "goth" }}, - {"Grantha", new String[] { "gran" }}, - {"Greek", new String[] { "grek" }}, - {"Gujarati", new String[] { "gjr2", "gujr" }}, - {"Gurmukhi", new String[] { "gur2", "guru" }}, - {"Han", new String[] { "hani" }}, // "CJK Ideographic" in OpenType - {"Hangul", new String[] { "hang" }}, - // Hangul Jamo: jamo - {"Hanunoo", new String[] { "hano" }}, - {"Hatran", new String[] { "hatr" }}, - {"Hebrew", new String[] { "hebr" }}, - {"Hiragana", new String[] { "kana" }}, - {"Imperial_Aramaic", new String[] { "armi" }}, - {INHERITED, new String[] { INHERITED }}, - {"Inscriptional_Pahlavi", new String[] { "phli" }}, - {"Inscriptional_Parthian", new String[] { "prti" }}, - {"Javanese", new String[] { "java" }}, - {"Kaithi", new String[] { "kthi" }}, - {"Kannada", new String[] { "knd2", "knda" }}, - {"Katakana", new String[] { "kana" }}, - {"Kayah_Li", new String[] { "kali" }}, - {"Kharoshthi", new String[] { "khar" }}, - {"Khmer", new String[] { "khmr" }}, - {"Khojki", new String[] { "khoj" }}, - {"Khudawadi", new String[] { "sind" }}, - {"Lao", new String[] { "lao " }}, - {"Latin", new String[] { "latn" }}, - {"Lepcha", new String[] { "lepc" }}, - {"Limbu", new String[] { "limb" }}, - {"Linear_A", new String[] { "lina" }}, - {"Linear_B", new String[] { "linb" }}, - {"Lisu", new String[] { "lisu" }}, - {"Lycian", new String[] { "lyci" }}, - {"Lydian", new String[] { "lydi" }}, - {"Mahajani", new String[] { "mahj" }}, - {"Malayalam", new String[] { "mlm2", "mlym" }}, - {"Mandaic", new String[] { "mand" }}, - {"Manichaean", new String[] { "mani" }}, - {"Marchen", new String[] { "marc" }}, - // Mathematical Alphanumeric Symbols: math - {"Meetei_Mayek", new String[] { "mtei" }}, - {"Mende_Kikakui", new String[] { "mend" }}, - {"Meroitic_Cursive", new String[] { "merc" }}, - {"Meroitic_Hieroglyphs", new String[] { "mero" }}, - {"Miao", new String[] { "plrd" }}, - {"Modi", new String[] { "modi" }}, - {"Mongolian", new String[] { "mong" }}, - {"Mro", new String[] { "mroo" }}, - {"Multani", new String[] { "mult" }}, - // Musical Symbols: musc - {"Myanmar", new String[] { "mym2", "mymr" }}, - {"Nabataean", new String[] { "nbat" }}, - {"Newa", new String[] { "newa" }}, - {"New_Tai_Lue", new String[] { "talu" }}, - {"Nko", new String[] { "nko " }}, - {"Ogham", new String[] { "ogam" }}, - {"Ol_Chiki", new String[] { "olck" }}, - {"Old_Italic", new String[] { "ital" }}, - {"Old_Hungarian", new String[] { "hung" }}, - {"Old_North_Arabian", new String[] { "narb" }}, - {"Old_Permic", new String[] { "perm" }}, - {"Old_Persian", new String[] { "xpeo" }}, - {"Old_South_Arabian", new String[] { "sarb" }}, - {"Old_Turkic", new String[] { "orkh" }}, - {"Oriya", new String[] { "ory2", "orya" }}, // "Odia (formerly Oriya)" in OpenType - {"Osage", new String[] { "osge" }}, - {"Osmanya", new String[] { "osma" }}, - {"Pahawh_Hmong", new String[] { "hmng" }}, - {"Palmyrene", new String[] { "palm" }}, - {"Pau_Cin_Hau", new String[] { "pauc" }}, - {"Phags_Pa", new String[] { "phag" }}, - {"Phoenician", new String[] { "phnx" }}, - {"Psalter_Pahlavi", new String[] { "phlp" }}, - {"Rejang", new String[] { "rjng" }}, - {"Runic", new String[] { "runr" }}, - {"Samaritan", new String[] { "samr" }}, - {"Saurashtra", new String[] { "saur" }}, - {"Sharada", new String[] { "shrd" }}, - {"Shavian", new String[] { "shaw" }}, - {"Siddham", new String[] { "sidd" }}, - {"SignWriting", new String[] { "sgnw" }}, - {"Sinhala", new String[] { "sinh" }}, - {"Sora_Sompeng", new String[] { "sora" }}, - {"Sundanese", new String[] { "sund" }}, - {"Syloti_Nagri", new String[] { "sylo" }}, - {"Syriac", new String[] { "syrc" }}, - {"Tagalog", new String[] { "tglg" }}, - {"Tagbanwa", new String[] { "tagb" }}, - {"Tai_Le", new String[] { "tale" }}, - {"Tai_Tham", new String[] { "lana" }}, - {"Tai_Viet", new String[] { "tavt" }}, - {"Takri", new String[] { "takr" }}, - {"Tamil", new String[] { "tml2", "taml" }}, - {"Tangut", new String[] { "tang" }}, - {"Telugu", new String[] { "tel2", "telu" }}, - {"Thaana", new String[] { "thaa" }}, - {"Thai", new String[] { "thai" }}, - {"Tibetan", new String[] { "tibt" }}, - {"Tifinagh", new String[] { "tfng" }}, - {"Tirhuta", new String[] { "tirh" }}, - {"Ugaritic", new String[] { "ugar" }}, - {UNKNOWN, new String[] { TAG_DEFAULT }}, - {"Vai", new String[] { "vai " }}, - {"Warang_Citi", new String[] { "wara" }}, - {"Yi", new String[] { "yi " }} - }; + { + {"Adlam", new String[] { "adlm" }}, + {"Ahom", new String[] { "ahom" }}, + {"Anatolian_Hieroglyphs", new String[] { "hluw" }}, + {"Arabic", new String[] { "arab" }}, + {"Armenian", new String[] { "armn" }}, + {"Avestan", new String[] { "avst" }}, + {"Balinese", new String[] { "bali" }}, + {"Bamum", new String[] { "bamu" }}, + {"Bassa_Vah", new String[] { "bass" }}, + {"Batak", new String[] { "batk" }}, + {"Bengali", new String[] { "bng2", "beng" }}, + {"Bhaiksuki", new String[] { "bhks" }}, + {"Bopomofo", new String[] { "bopo" }}, + {"Brahmi", new String[] { "brah" }}, + {"Braille", new String[] { "brai" }}, + {"Buginese", new String[] { "bugi" }}, + {"Buhid", new String[] { "buhd" }}, + // Byzantine Music: byzm + {"Canadian_Aboriginal", new String[] { "cans" }}, + {"Carian", new String[] { "cari" }}, + {"Caucasian_Albanian", new String[] { "aghb" }}, + {"Chakma", new String[] { "cakm" }}, + {"Cham", new String[] { "cham" }}, + {"Cherokee", new String[] { "cher" }}, + {"Common", new String[] { TAG_DEFAULT }}, // "Default" in OpenType + {"Coptic", new String[] { "copt" }}, + {"Cuneiform", new String[] { "xsux" }}, // "Sumero-Akkadian Cuneiform" in OpenType + {"Cypriot", new String[] { "cprt" }}, + {"Cyrillic", new String[] { "cyrl" }}, + {"Deseret", new String[] { "dsrt" }}, + {"Devanagari", new String[] { "dev2", "deva" }}, + {"Duployan", new String[] { "dupl" }}, + {"Egyptian_Hieroglyphs", new String[] { "egyp" }}, + {"Elbasan", new String[] { "elba" }}, + {"Ethiopic", new String[] { "ethi" }}, + {"Georgian", new String[] { "geor" }}, + {"Glagolitic", new String[] { "glag" }}, + {"Gothic", new String[] { "goth" }}, + {"Grantha", new String[] { "gran" }}, + {"Greek", new String[] { "grek" }}, + {"Gujarati", new String[] { "gjr2", "gujr" }}, + {"Gurmukhi", new String[] { "gur2", "guru" }}, + {"Han", new String[] { "hani" }}, // "CJK Ideographic" in OpenType + {"Hangul", new String[] { "hang" }}, + // Hangul Jamo: jamo + {"Hanunoo", new String[] { "hano" }}, + {"Hatran", new String[] { "hatr" }}, + {"Hebrew", new String[] { "hebr" }}, + {"Hiragana", new String[] { "kana" }}, + {"Imperial_Aramaic", new String[] { "armi" }}, + {INHERITED, new String[] { INHERITED }}, + {"Inscriptional_Pahlavi", new String[] { "phli" }}, + {"Inscriptional_Parthian", new String[] { "prti" }}, + {"Javanese", new String[] { "java" }}, + {"Kaithi", new String[] { "kthi" }}, + {"Kannada", new String[] { "knd2", "knda" }}, + {"Katakana", new String[] { "kana" }}, + {"Kayah_Li", new String[] { "kali" }}, + {"Kharoshthi", new String[] { "khar" }}, + {"Khmer", new String[] { "khmr" }}, + {"Khojki", new String[] { "khoj" }}, + {"Khudawadi", new String[] { "sind" }}, + {"Lao", new String[] { "lao " }}, + {"Latin", new String[] { "latn" }}, + {"Lepcha", new String[] { "lepc" }}, + {"Limbu", new String[] { "limb" }}, + {"Linear_A", new String[] { "lina" }}, + {"Linear_B", new String[] { "linb" }}, + {"Lisu", new String[] { "lisu" }}, + {"Lycian", new String[] { "lyci" }}, + {"Lydian", new String[] { "lydi" }}, + {"Mahajani", new String[] { "mahj" }}, + {"Malayalam", new String[] { "mlm2", "mlym" }}, + {"Mandaic", new String[] { "mand" }}, + {"Manichaean", new String[] { "mani" }}, + {"Marchen", new String[] { "marc" }}, + // Mathematical Alphanumeric Symbols: math + {"Meetei_Mayek", new String[] { "mtei" }}, + {"Mende_Kikakui", new String[] { "mend" }}, + {"Meroitic_Cursive", new String[] { "merc" }}, + {"Meroitic_Hieroglyphs", new String[] { "mero" }}, + {"Miao", new String[] { "plrd" }}, + {"Modi", new String[] { "modi" }}, + {"Mongolian", new String[] { "mong" }}, + {"Mro", new String[] { "mroo" }}, + {"Multani", new String[] { "mult" }}, + // Musical Symbols: musc + {"Myanmar", new String[] { "mym2", "mymr" }}, + {"Nabataean", new String[] { "nbat" }}, + {"Newa", new String[] { "newa" }}, + {"New_Tai_Lue", new String[] { "talu" }}, + {"Nko", new String[] { "nko " }}, + {"Ogham", new String[] { "ogam" }}, + {"Ol_Chiki", new String[] { "olck" }}, + {"Old_Italic", new String[] { "ital" }}, + {"Old_Hungarian", new String[] { "hung" }}, + {"Old_North_Arabian", new String[] { "narb" }}, + {"Old_Permic", new String[] { "perm" }}, + {"Old_Persian", new String[] { "xpeo" }}, + {"Old_South_Arabian", new String[] { "sarb" }}, + {"Old_Turkic", new String[] { "orkh" }}, + {"Oriya", new String[] { "ory2", "orya" }}, // "Odia (formerly Oriya)" in OpenType + {"Osage", new String[] { "osge" }}, + {"Osmanya", new String[] { "osma" }}, + {"Pahawh_Hmong", new String[] { "hmng" }}, + {"Palmyrene", new String[] { "palm" }}, + {"Pau_Cin_Hau", new String[] { "pauc" }}, + {"Phags_Pa", new String[] { "phag" }}, + {"Phoenician", new String[] { "phnx" }}, + {"Psalter_Pahlavi", new String[] { "phlp" }}, + {"Rejang", new String[] { "rjng" }}, + {"Runic", new String[] { "runr" }}, + {"Samaritan", new String[] { "samr" }}, + {"Saurashtra", new String[] { "saur" }}, + {"Sharada", new String[] { "shrd" }}, + {"Shavian", new String[] { "shaw" }}, + {"Siddham", new String[] { "sidd" }}, + {"SignWriting", new String[] { "sgnw" }}, + {"Sinhala", new String[] { "sinh" }}, + {"Sora_Sompeng", new String[] { "sora" }}, + {"Sundanese", new String[] { "sund" }}, + {"Syloti_Nagri", new String[] { "sylo" }}, + {"Syriac", new String[] { "syrc" }}, + {"Tagalog", new String[] { "tglg" }}, + {"Tagbanwa", new String[] { "tagb" }}, + {"Tai_Le", new String[] { "tale" }}, + {"Tai_Tham", new String[] { "lana" }}, + {"Tai_Viet", new String[] { "tavt" }}, + {"Takri", new String[] { "takr" }}, + {"Tamil", new String[] { "tml2", "taml" }}, + {"Tangut", new String[] { "tang" }}, + {"Telugu", new String[] { "tel2", "telu" }}, + {"Thaana", new String[] { "thaa" }}, + {"Thai", new String[] { "thai" }}, + {"Tibetan", new String[] { "tibt" }}, + {"Tifinagh", new String[] { "tfng" }}, + {"Tirhuta", new String[] { "tirh" }}, + {"Ugaritic", new String[] { "ugar" }}, + {UNKNOWN, new String[] { TAG_DEFAULT }}, + {"Vai", new String[] { "vai " }}, + {"Warang_Citi", new String[] { "wara" }}, + {"Yi", new String[] { "yi " }} + }; UNICODE_SCRIPT_TO_OPENTYPE_TAG_MAP = new HashMap(table.length); for (Object[] array : table) { @@ -234,7 +234,7 @@ public final class OpenTypeScript catch (IOException e) { Log.w("PdfBox-Android", "Could not parse Scripts.txt, mirroring char map will be empty: " - + e.getMessage()); + + e.getMessage()); } finally { @@ -266,7 +266,7 @@ public int compare(int[] o1, int[] o2) return o1[0] < o2[0] ? -1 : o1[0] == o2[0] ? 0 : 1; }; }); - LineNumberReader rd = new LineNumberReader(new InputStreamReader(inputStream)); + LineNumberReader rd = new LineNumberReader(new InputStreamReader(inputStream, "US-ASCII")); int[] lastRange = { Integer.MIN_VALUE, Integer.MIN_VALUE }; String lastScript = null; do diff --git a/library/src/main/java/com/tom_roush/fontbox/ttf/PostScriptTable.java b/library/src/main/java/com/tom_roush/fontbox/ttf/PostScriptTable.java index 5c7310de7..47c516337 100644 --- a/library/src/main/java/com/tom_roush/fontbox/ttf/PostScriptTable.java +++ b/library/src/main/java/com/tom_roush/fontbox/ttf/PostScriptTable.java @@ -19,7 +19,6 @@ import android.util.Log; import java.io.IOException; - /** * A table in a true type font. * @@ -68,7 +67,11 @@ void read(TrueTypeFont ttf, TTFDataStream data) throws IOException mimMemType1 = data.readUnsignedInt(); maxMemType1 = data.readUnsignedInt(); - if (formatType == 1.0f) + if (data.getCurrentPosition() == data.getOriginalDataSize()) + { + Log.w("PdfBox-Android", "No PostScript name data is provided for the font " + ttf.getName()); + } + else if (formatType == 1.0f) { /* * This TrueType font file contains exactly the 258 glyphs in the standard Macintosh TrueType. @@ -108,7 +111,7 @@ else if (formatType == 2.0f) { // PDFBOX-4851: EOF Log.w("PdfBox-Android", "Error reading names in PostScript table at entry " + i + " of " + - nameArray.length + ", setting remaining entries to .notdef", ex); + nameArray.length + ", setting remaining entries to .notdef", ex); for (int j = i; j < nameArray.length; ++j) { nameArray[j] = ".notdef"; @@ -159,7 +162,7 @@ else if (formatType == 2.5f) else { Log.d("PdfBox-Android", "incorrect glyph name index " + index + - ", valid numbers 0.." + WGL4Names.NUMBER_OF_MAC_GLYPHS); + ", valid numbers 0.." + WGL4Names.NUMBER_OF_MAC_GLYPHS); } } } diff --git a/library/src/main/java/com/tom_roush/fontbox/ttf/TTFDataStream.java b/library/src/main/java/com/tom_roush/fontbox/ttf/TTFDataStream.java index e50ad9055..a73ff4f9f 100644 --- a/library/src/main/java/com/tom_roush/fontbox/ttf/TTFDataStream.java +++ b/library/src/main/java/com/tom_roush/fontbox/ttf/TTFDataStream.java @@ -25,7 +25,6 @@ import java.util.TimeZone; import com.tom_roush.fontbox.util.Charsets; - /** * An interface into a data stream. * @@ -33,6 +32,8 @@ */ abstract class TTFDataStream implements Closeable { + private static final TimeZone TIMEZONE_UTC = TimeZone.getTimeZone("UTC"); // clone before using + TTFDataStream() { } @@ -118,9 +119,10 @@ public int readSignedByte() throws IOException } /** - * Read a unsigned byte. Similar to {@link #read()}, but throws an exception if EOF is unexpectedly reached. + * Read an unsigned byte. Similar to {@link #read()}, but throws an exception if EOF is + * unexpectedly reached. * - * @return A unsigned byte. + * @return An unsigned byte. * @throws IOException If there is an error reading the data. */ public int readUnsignedByte() throws IOException @@ -147,7 +149,8 @@ public long readUnsignedInt() throws IOException long byte4 = read(); if (byte4 < 0) { - throw new EOFException(); + throw new EOFException("EOF at " + getCurrentPosition() + + ", b1: " + byte1 + ", b2: " + byte2 + ", b3: " + byte3 + ", b4: " + byte4); } return (byte1 << 24) + (byte2 << 16) + (byte3 << 8) + byte4; } @@ -195,9 +198,9 @@ public int[] readUnsignedShortArray(int length) throws IOException } /** - * Read an signed short. + * Read a signed short. * - * @return An signed short. + * @return A signed short. * @throws IOException If there is an error reading the data. */ public abstract short readSignedShort() throws IOException; @@ -205,13 +208,13 @@ public int[] readUnsignedShortArray(int length) throws IOException /** * Read an eight byte international date. * - * @return An signed short. + * @return A signed short. * @throws IOException If there is an error reading the data. */ public Calendar readInternationalDate() throws IOException { long secondsSince1904 = readLong(); - Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + Calendar cal = Calendar.getInstance((TimeZone) TIMEZONE_UTC.clone()); cal.set(1904, 0, 1, 0, 0, 0); cal.set(Calendar.MILLISECOND, 0); long millisFor1904 = cal.getTimeInMillis(); @@ -251,7 +254,7 @@ public byte[] read(int numberOfBytes) throws IOException int totalAmountRead = 0; // read at most numberOfBytes bytes from the stream. while (totalAmountRead < numberOfBytes - && (amountRead = read(data, totalAmountRead, numberOfBytes - totalAmountRead)) != -1) + && (amountRead = read(data, totalAmountRead, numberOfBytes - totalAmountRead)) != -1) { totalAmountRead += amountRead; } diff --git a/library/src/main/java/com/tom_roush/fontbox/ttf/TTFParser.java b/library/src/main/java/com/tom_roush/fontbox/ttf/TTFParser.java index 0a0ad4597..b5885a1a5 100644 --- a/library/src/main/java/com/tom_roush/fontbox/ttf/TTFParser.java +++ b/library/src/main/java/com/tom_roush/fontbox/ttf/TTFParser.java @@ -21,7 +21,6 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; - /** * TrueType font file parser. * @@ -29,6 +28,7 @@ */ public class TTFParser { + private boolean isEmbedded = false; private boolean parseOnDemandOnly = false; @@ -147,9 +147,9 @@ TrueTypeFont parse(TTFDataStream raf) throws IOException { // PDFBOX-5285 if we're lucky, this is an "unimportant" table, e.g. vmtx Log.w("PdfBox-Android", "Skip table '" + table.getTag() + - "' which goes past the file size; offset: " + table.getOffset() + - ", size: " + table.getLength() + - ", font size: " + font.getOriginalDataSize()); + "' which goes past the file size; offset: " + table.getOffset() + + ", size: " + table.getLength() + + ", font size: " + font.getOriginalDataSize()); } else { @@ -188,7 +188,8 @@ private void parseTables(TrueTypeFont font) throws IOException } boolean hasCFF = font.tables.containsKey(CFFTable.TAG); - boolean isPostScript = allowCFF() && hasCFF; + boolean isOTF = font instanceof OpenTypeFont; + boolean isPostScript = isOTF ? ((OpenTypeFont) font).isPostScript() : hasCFF; HeaderTable head = font.getHeader(); if (head == null) @@ -217,22 +218,19 @@ private void parseTables(TrueTypeFont font) throws IOException if (!isPostScript) { - String messageSuffix = ""; - if (hasCFF) - { - messageSuffix = "; this an OpenType CFF font, but we expected a TrueType font here"; - } - IndexToLocationTable loc = font.getIndexToLocation(); - if (loc == null) + if (font.getIndexToLocation() == null) { - throw new IOException("'loca' table is mandatory" + messageSuffix); + throw new IOException("'loca' table is mandatory"); } - if (font.getGlyph() == null) { - throw new IOException("'glyf' table is mandatory" + messageSuffix); + throw new IOException("'glyf' table is mandatory"); } } + else if (!isOTF) + { + throw new IOException("True Type fonts using CFF outlines are not supported"); + } if (font.getNaming() == null && !isEmbedded) { diff --git a/library/src/main/java/com/tom_roush/fontbox/ttf/TTFSubsetter.java b/library/src/main/java/com/tom_roush/fontbox/ttf/TTFSubsetter.java index ffc07686e..f485b521c 100644 --- a/library/src/main/java/com/tom_roush/fontbox/ttf/TTFSubsetter.java +++ b/library/src/main/java/com/tom_roush/fontbox/ttf/TTFSubsetter.java @@ -27,18 +27,18 @@ import java.nio.charset.Charset; import java.util.Calendar; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import java.util.Map.Entry; +import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.SortedSet; import java.util.TimeZone; import java.util.TreeMap; import java.util.TreeSet; - /** * Subsetter for TrueType (TTF) fonts. * @@ -49,7 +49,10 @@ */ public final class TTFSubsetter { - private static final byte[] PAD_BUF = new byte[] { 0, 0, 0 }; + + private static final byte[] PAD_BUF = new byte[] { 0, 0, 0, 0 }; + + private static final TimeZone TIMEZONE_UTC = TimeZone.getTimeZone("UTC"); // clone before using private final TrueTypeFont ttf; private final CmapLookup unicodeCmap; @@ -57,6 +60,7 @@ public final class TTFSubsetter private final List keepTables; private final SortedSet glyphIds; // new glyph ids + private final Set invisibleGlyphIds; private String prefix; private boolean hasAddedCompoundReferences; @@ -83,6 +87,7 @@ public TTFSubsetter(TrueTypeFont ttf, List tables) throws IOException uniToGID = new TreeMap(); glyphIds = new TreeSet(); + invisibleGlyphIds = new HashSet(); // find the best Unicode cmap this.unicodeCmap = ttf.getUnicodeCmapLookup(); @@ -127,6 +132,23 @@ public void addAll(Set unicodeSet) } } + /** + * Forces the glyph for the specified character code to be zero-width and contour-free, + * regardless of what the glyph looks like in the original font. Note that the specified + * character code is not added to the subset unless it is also {@link #add(int) added} + * separately. + * + * @param unicode the character code whose glyph should be invisible + */ + public void forceInvisible(int unicode) + { + int gid = unicodeCmap.getGlyphId(unicode); + if (gid != 0) + { + invisibleGlyphIds.add(gid); + } + } + /** * Returns the map of new -> old GIDs. */ @@ -171,7 +193,7 @@ private long writeFileHeader(DataOutputStream out, int nTables) throws IOExcepti } private long writeTableHeader(DataOutputStream out, String tag, long offset, byte[] bytes) - throws IOException + throws IOException { long checksum = 0; for (int nup = 0, n = bytes.length; nup < n; nup++) @@ -203,7 +225,7 @@ private void writeTableBody(OutputStream os, byte[] bytes) throws IOException private byte[] buildHeadTable() throws IOException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ByteArrayOutputStream bos = new ByteArrayOutputStream(54); DataOutputStream out = new DataOutputStream(bos); HeaderTable h = ttf.getHeader(); @@ -232,7 +254,7 @@ private byte[] buildHeadTable() throws IOException private byte[] buildHheaTable() throws IOException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ByteArrayOutputStream bos = new ByteArrayOutputStream(36); DataOutputStream out = new DataOutputStream(bos); HorizontalHeaderTable h = ttf.getHorizontalHeader(); @@ -269,14 +291,14 @@ private byte[] buildHheaTable() throws IOException private boolean shouldCopyNameRecord(NameRecord nr) { return nr.getPlatformId() == NameRecord.PLATFORM_WINDOWS - && nr.getPlatformEncodingId() == NameRecord.ENCODING_WINDOWS_UNICODE_BMP - && nr.getLanguageId() == NameRecord.LANGUAGE_WINDOWS_EN_US - && nr.getNameId() >= 0 && nr.getNameId() < 7; + && nr.getPlatformEncodingId() == NameRecord.ENCODING_WINDOWS_UNICODE_BMP + && nr.getLanguageId() == NameRecord.LANGUAGE_WINDOWS_EN_US + && nr.getNameId() >= 0 && nr.getNameId() < 7; } private byte[] buildNameTable() throws IOException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ByteArrayOutputStream bos = new ByteArrayOutputStream(512); DataOutputStream out = new DataOutputStream(bos); NamingTable name = ttf.getNaming(); @@ -372,26 +394,28 @@ else if (encoding == 2) // ISO 8859-1 private byte[] buildMaxpTable() throws IOException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ByteArrayOutputStream bos = new ByteArrayOutputStream(32); DataOutputStream out = new DataOutputStream(bos); MaximumProfileTable p = ttf.getMaximumProfile(); writeFixed(out, 1.0); writeUint16(out, glyphIds.size()); - writeUint16(out, p.getMaxPoints()); - writeUint16(out, p.getMaxContours()); - writeUint16(out, p.getMaxCompositePoints()); - writeUint16(out, p.getMaxCompositeContours()); - writeUint16(out, p.getMaxZones()); - writeUint16(out, p.getMaxTwilightPoints()); - writeUint16(out, p.getMaxStorage()); - writeUint16(out, p.getMaxFunctionDefs()); - writeUint16(out, p.getMaxInstructionDefs()); - writeUint16(out, p.getMaxStackElements()); - writeUint16(out, p.getMaxSizeOfInstructions()); - writeUint16(out, p.getMaxComponentElements()); - writeUint16(out, p.getMaxComponentDepth()); - + if (p.getVersion() >= 1.0f) + { + writeUint16(out, p.getMaxPoints()); + writeUint16(out, p.getMaxContours()); + writeUint16(out, p.getMaxCompositePoints()); + writeUint16(out, p.getMaxCompositeContours()); + writeUint16(out, p.getMaxZones()); + writeUint16(out, p.getMaxTwilightPoints()); + writeUint16(out, p.getMaxStorage()); + writeUint16(out, p.getMaxFunctionDefs()); + writeUint16(out, p.getMaxInstructionDefs()); + writeUint16(out, p.getMaxStackElements()); + writeUint16(out, p.getMaxSizeOfInstructions()); + writeUint16(out, p.getMaxComponentElements()); + writeUint16(out, p.getMaxComponentDepth()); + } out.flush(); return bos.toByteArray(); } @@ -404,7 +428,7 @@ private byte[] buildOS2Table() throws IOException return null; } - ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ByteArrayOutputStream bos = new ByteArrayOutputStream(78); DataOutputStream out = new DataOutputStream(bos); writeUint16(out, os2.getVersion()); @@ -452,7 +476,7 @@ private byte[] buildOS2Table() throws IOException // never returns null private byte[] buildLocaTable(long[] newOffsets) throws IOException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ByteArrayOutputStream bos = new ByteArrayOutputStream(newOffsets.length * 4); DataOutputStream out = new DataOutputStream(bos); for (long offset : newOffsets) @@ -486,12 +510,12 @@ private void addCompoundReferences() throws IOException { is.skip(g.getOffset()); long lastOff = 0L; - for (Integer glyphId : glyphIds) + for (Integer gid : glyphIds) { - long offset = offsets[glyphId]; - long len = offsets[glyphId + 1] - offset; + long offset = offsets[gid]; + long length = offsets[gid + 1] - offset; is.skip(offset - lastOff); - byte[] buf = new byte[(int)len]; + byte[] buf = new byte[(int) length]; is.read(buf); // rewrite glyphIds for compound glyphs if (buf.length >= 2 && buf[0] == -1 && buf[1] == -1) @@ -540,18 +564,18 @@ else if ((flags & 1 << 3) != 0) while ((flags & 1 << 5) != 0); // MORE_COMPONENTS } - lastOff = offsets[glyphId + 1]; + lastOff = offsets[gid + 1]; } } finally { is.close(); } - if (glyphIdsToAdd != null) + hasNested = glyphIdsToAdd != null; + if (hasNested) { glyphIds.addAll(glyphIdsToAdd); } - hasNested = glyphIdsToAdd != null; } while (hasNested); } @@ -559,7 +583,7 @@ else if ((flags & 1 << 3) != 0) // never returns null private byte[] buildGlyfTable(long[] newOffsets) throws IOException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ByteArrayOutputStream bos = new ByteArrayOutputStream(512); GlyphTable g = ttf.getGlyph(); long[] offsets = ttf.getIndexToLocation().getOffsets(); @@ -568,7 +592,7 @@ private byte[] buildGlyfTable(long[] newOffsets) throws IOException { is.skip(g.getOffset()); - long prevEnd = 0; // previously read glyph offset + long lastOff = 0; // previously read glyph offset long newOffset = 0; // new offset for the glyph in the subset font int newGid = 0; // new GID in subset font @@ -579,7 +603,15 @@ private byte[] buildGlyfTable(long[] newOffsets) throws IOException long length = offsets[gid + 1] - offset; newOffsets[newGid++] = newOffset; - is.skip(offset - prevEnd); + is.skip(offset - lastOff); + + // glyphs with no outlines have an empty entry in the 'glyf' table, with a + // corresponding 'loca' table entry with length = 0 + if (invisibleGlyphIds.contains(gid)) + { + lastOff = offset; + continue; + } byte[] buf = new byte[(int)length]; is.read(buf); @@ -598,8 +630,11 @@ private byte[] buildGlyfTable(long[] newOffsets) throws IOException // glyphIndex int componentGid = (buf[off] & 0xff) << 8 | buf[off + 1] & 0xff; - glyphIds.add(componentGid); - + if (!glyphIds.contains(componentGid)) + { + // PDFBOX-6085 + throw new IOException("Internal error: componentGid " + componentGid + " not in glyphIds set"); + } int newComponentGid = getNewGlyphId(componentGid); buf[off] = (byte)(newComponentGid >>> 8); buf[off + 1] = (byte)newComponentGid; @@ -666,7 +701,7 @@ else if (buf.length > 0) newOffset += len; } - prevEnd = offset + length; + lastOff = offset + length; } newOffsets[newGid++] = newOffset; } @@ -690,7 +725,7 @@ private byte[] buildCmapTable() throws IOException return null; } - ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ByteArrayOutputStream bos = new ByteArrayOutputStream(64); DataOutputStream out = new DataOutputStream(bos); // cmap header @@ -804,12 +839,13 @@ else if (!lastChar.getKey().equals(prevChar.getKey())) private byte[] buildPostTable() throws IOException { PostScriptTable post = ttf.getPostScript(); - if (post == null || keepTables != null && !keepTables.contains("post")) + if (post == null || post.getGlyphNames() == null || + keepTables != null && !keepTables.contains("post")) { return null; } - ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ByteArrayOutputStream bos = new ByteArrayOutputStream(64); DataOutputStream out = new DataOutputStream(bos); writeFixed(out, 2.0); // version @@ -880,15 +916,24 @@ private byte[] buildHmtxTable() throws IOException { is.skip(hm.getOffset()); long lastOffset = 0; - for (Integer glyphId : glyphIds) + for (Integer gid : glyphIds) { // offset in original file long offset; - if (glyphId <= lastgid) + if (gid <= lastgid) { - // copy width and lsb - offset = glyphId * 4l; - lastOffset = copyBytes(is, bos, offset, lastOffset, 4); + if (invisibleGlyphIds.contains(gid)) + { + // force zero width (no change to last offset) + // 4 bytes total, 2 bytes each for: advance width = 0, left side bearing = 0 + bos.write(PAD_BUF, 0, 4); + } + else + { + // copy width and lsb + offset = gid * 4l; + lastOffset = copyBytes(is, bos, offset, lastOffset, 4); + } } else { @@ -904,7 +949,7 @@ private byte[] buildHmtxTable() throws IOException } // copy lsb only, as we are beyond numOfHMetrics - offset = h.getNumberOfHMetrics() * 4l + (glyphId - h.getNumberOfHMetrics()) * 2l; + offset = h.getNumberOfHMetrics() * 4l + (gid - h.getNumberOfHMetrics()) * 2l; lastOffset = copyBytes(is, bos, offset, lastOffset, 2); } } @@ -918,7 +963,7 @@ private byte[] buildHmtxTable() throws IOException } private long copyBytes(InputStream is, OutputStream os, long newOffset, long lastOffset, int count) - throws IOException + throws IOException { // skip over from last original offset long nskip = newOffset - lastOffset; @@ -1011,7 +1056,7 @@ public void writeToStream(OutputStream os) throws IOException for (Map.Entry entry : tables.entrySet()) { checksum += writeTableHeader(out, entry.getKey(), offset, entry.getValue()); - offset += (entry.getValue().length + 3) / 4 * 4; + offset += (entry.getValue().length + 3L) / 4 * 4; } checksum = 0xB1B0AFBAL - (checksum & 0xffffffffL); @@ -1062,7 +1107,7 @@ private void writeUint8(DataOutputStream out, int i) throws IOException private void writeLongDateTime(DataOutputStream out, Calendar calendar) throws IOException { // inverse operation of TTFDataStream.readInternationalDate() - Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + Calendar cal = Calendar.getInstance((TimeZone) TIMEZONE_UTC.clone()); cal.set(1904, 0, 1, 0, 0, 0); cal.set(Calendar.MILLISECOND, 0); long millisFor1904 = cal.getTimeInMillis(); @@ -1078,9 +1123,9 @@ private long toUInt32(int high, int low) private long toUInt32(byte[] bytes) { return (bytes[0] & 0xffL) << 24 - | (bytes[1] & 0xffL) << 16 - | (bytes[2] & 0xffL) << 8 - | bytes[3] & 0xffL; + | (bytes[1] & 0xffL) << 16 + | (bytes[2] & 0xffL) << 8 + | bytes[3] & 0xffL; } private int log2(int num) diff --git a/library/src/main/java/com/tom_roush/fontbox/ttf/TrueTypeCollection.java b/library/src/main/java/com/tom_roush/fontbox/ttf/TrueTypeCollection.java index 9e5851512..5ee1f638f 100644 --- a/library/src/main/java/com/tom_roush/fontbox/ttf/TrueTypeCollection.java +++ b/library/src/main/java/com/tom_roush/fontbox/ttf/TrueTypeCollection.java @@ -21,7 +21,6 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; - /** * A TrueType Collection, now more properly known as a "Font Collection" as it may contain either * TrueType or OpenType fonts. @@ -57,9 +56,9 @@ public TrueTypeCollection(InputStream stream) throws IOException } /** - * Creates a new TrueTypeCollection from a TTC stream. + * Creates a new TrueTypeCollection from a TTFDataStream. * - * @param stream The TTF file. + * @param stream A data stream to read. * @throws IOException If the font could not be parsed. */ TrueTypeCollection(TTFDataStream stream) throws IOException diff --git a/library/src/main/java/com/tom_roush/fontbox/ttf/TrueTypeFont.java b/library/src/main/java/com/tom_roush/fontbox/ttf/TrueTypeFont.java index bb7ef5091..90845c8a5 100644 --- a/library/src/main/java/com/tom_roush/fontbox/ttf/TrueTypeFont.java +++ b/library/src/main/java/com/tom_roush/fontbox/ttf/TrueTypeFont.java @@ -31,7 +31,6 @@ import com.tom_roush.fontbox.FontBoxFont; import com.tom_roush.fontbox.util.BoundingBox; - /** * A TrueType font file. * @@ -67,7 +66,7 @@ public void close() throws IOException protected void finalize() throws Throwable { super.finalize(); - // PDFBOX-4963: risk of memory leaks due to SoftReference in FontCache + // PDFBOX-4963: risk of memory leaks due to SoftReference in FontCache close(); } @@ -321,7 +320,7 @@ public GlyphSubstitutionTable getGsub() throws IOException /** * Get the data of the TrueType Font - * program representing the stream used to build this + * program representing the stream used to build this * object (normally from the TTFParser object). * * @return COSStream TrueType font program stream @@ -330,7 +329,7 @@ public GlyphSubstitutionTable getGsub() throws IOException */ public InputStream getOriginalData() throws IOException { - return data.getOriginalData(); + return data.getOriginalData(); } /** @@ -341,7 +340,7 @@ public InputStream getOriginalData() throws IOException */ public long getOriginalDataSize() { - return data.getOriginalDataSize(); + return data.getOriginalDataSize(); } /** @@ -548,7 +547,7 @@ public CmapLookup getUnicodeCmapLookup(boolean isStrict) throws IOException if (table != null) { return new SubstitutingCmapLookup(cmap, (GlyphSubstitutionTable) table, - Collections.unmodifiableList(enabledGsubFeatures)); + Collections.unmodifiableList(enabledGsubFeatures)); } } return cmap; @@ -570,28 +569,34 @@ private CmapSubtable getUnicodeCmapImpl(boolean isStrict) throws IOException } CmapSubtable cmap = cmapTable.getSubtable(CmapTable.PLATFORM_UNICODE, - CmapTable.ENCODING_UNICODE_2_0_FULL); + CmapTable.ENCODING_UNICODE_2_0_FULL); if (cmap == null) { cmap = cmapTable.getSubtable(CmapTable.PLATFORM_WINDOWS, - CmapTable.ENCODING_WIN_UNICODE_FULL); + CmapTable.ENCODING_WIN_UNICODE_FULL); } if (cmap == null) { cmap = cmapTable.getSubtable(CmapTable.PLATFORM_UNICODE, - CmapTable.ENCODING_UNICODE_2_0_BMP); + CmapTable.ENCODING_UNICODE_2_0_BMP); } if (cmap == null) { cmap = cmapTable.getSubtable(CmapTable.PLATFORM_WINDOWS, - CmapTable.ENCODING_WIN_UNICODE_BMP); + CmapTable.ENCODING_WIN_UNICODE_BMP); } if (cmap == null) { // Microsoft's "Recommendations for OpenType Fonts" says that "Symbol" encoding // actually means "Unicode, non-standard character set" cmap = cmapTable.getSubtable(CmapTable.PLATFORM_WINDOWS, - CmapTable.ENCODING_WIN_SYMBOL); + CmapTable.ENCODING_WIN_SYMBOL); + } + if (cmap == null) + { + // PDFBOX-6015 + cmap = cmapTable.getSubtable(CmapTable.PLATFORM_UNICODE, + CmapTable.ENCODING_UNICODE_1_1); } if (cmap == null) { @@ -633,6 +638,12 @@ public int nameToGID(String name) throws IOException return cmap.getGlyphId(uni); } + // PDFBOX-5604: assume gnnnnn is a gid + if (name.matches("g\\d+")) + { + return Integer.parseInt(name.substring(1)); + } + return 0; } diff --git a/library/src/main/java/com/tom_roush/fontbox/ttf/VerticalOriginTable.java b/library/src/main/java/com/tom_roush/fontbox/ttf/VerticalOriginTable.java index f3d115838..48a18a7ac 100644 --- a/library/src/main/java/com/tom_roush/fontbox/ttf/VerticalOriginTable.java +++ b/library/src/main/java/com/tom_roush/fontbox/ttf/VerticalOriginTable.java @@ -19,7 +19,6 @@ import java.io.IOException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; - /** * A vertical origin 'VORG' table in an OpenType font. * @@ -91,13 +90,7 @@ public float getVersion() */ public int getOriginY(int gid) { - if (origins.containsKey(gid)) - { - return origins.get(gid); - } - else - { - return defaultVertOriginY; - } + Integer originY = origins.get(gid); + return originY != null ? originY : defaultVertOriginY; } } diff --git a/library/src/main/java/com/tom_roush/fontbox/type1/Type1Font.java b/library/src/main/java/com/tom_roush/fontbox/type1/Type1Font.java index d1a5505a2..14b24a407 100644 --- a/library/src/main/java/com/tom_roush/fontbox/type1/Type1Font.java +++ b/library/src/main/java/com/tom_roush/fontbox/type1/Type1Font.java @@ -35,7 +35,6 @@ import com.tom_roush.fontbox.encoding.Encoding; import com.tom_roush.fontbox.pfb.PfbParser; import com.tom_roush.fontbox.util.BoundingBox; - /** * Represents an Adobe Type 1 (.pfb) font. Thread safe. * @@ -92,8 +91,8 @@ public static Type1Font createWithSegments(byte[] segment1, byte[] segment2) thr Encoding encoding = null; int paintType; int fontType; - List fontMatrix = new ArrayList(); - List fontBBox = new ArrayList(); + List fontMatrix = Collections.emptyList(); + List fontBBox = Collections.emptyList(); int uniqueID; float strokeWidth; String fontID = ""; @@ -110,17 +109,17 @@ public static Type1Font createWithSegments(byte[] segment1, byte[] segment2) thr float underlineThickness; // Private dictionary - List blueValues = new ArrayList(); - List otherBlues = new ArrayList(); - List familyBlues = new ArrayList(); - List familyOtherBlues = new ArrayList(); + List blueValues = Collections.emptyList(); + List otherBlues = Collections.emptyList(); + List familyBlues = Collections.emptyList(); + List familyOtherBlues = Collections.emptyList(); float blueScale; int blueShift; int blueFuzz; - List stdHW = new ArrayList(); - List stdVW = new ArrayList(); - List stemSnapH = new ArrayList(); - List stemSnapV = new ArrayList(); + List stdHW = Collections.emptyList(); + List stdVW = Collections.emptyList(); + List stemSnapH = Collections.emptyList(); + List stemSnapV = Collections.emptyList(); boolean forceBold; int languageGroup; @@ -130,7 +129,7 @@ public static Type1Font createWithSegments(byte[] segment1, byte[] segment2) thr // private caches private final Map charStringCache = - new ConcurrentHashMap(); + new ConcurrentHashMap(); // raw data private final byte[] segment1; @@ -199,6 +198,10 @@ public Type1CharString getType1CharString(String name) throws IOException if (bytes == null) { bytes = charstrings.get(".notdef"); + if (bytes == null) + { + throw new IOException(".notdef is not defined"); + } } Type1CharStringParser parser = new Type1CharStringParser(fontName, name); List sequence = parser.parse(bytes, subrs); @@ -255,6 +258,7 @@ public int getFontType() * * @return the font matrix */ + @Override public List getFontMatrix() { return Collections.unmodifiableList(fontMatrix); @@ -552,7 +556,7 @@ public byte[] getBinarySegment() public String toString() { return getClass().getName() + "[fontName=" + fontName + ", fullName=" + fullName - + ", encoding=" + encoding + ", charStringsDict=" + charstrings - + "]"; + + ", encoding=" + encoding + ", charStringsDict=" + charstrings + + "]"; } } diff --git a/library/src/main/java/com/tom_roush/fontbox/type1/Type1Lexer.java b/library/src/main/java/com/tom_roush/fontbox/type1/Type1Lexer.java index 73eb8abb1..a9f8bd96e 100644 --- a/library/src/main/java/com/tom_roush/fontbox/type1/Type1Lexer.java +++ b/library/src/main/java/com/tom_roush/fontbox/type1/Type1Lexer.java @@ -24,7 +24,6 @@ import java.io.IOException; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; - /** * Lexer for the ASCII portions of an Adobe Type 1 font. * @@ -98,7 +97,7 @@ private char getChar() throws IOException { try { - return (char) buffer.get(); + return (char) buffer.get(); } catch (BufferUnderflowException exception) { @@ -158,7 +157,7 @@ else if (c == '/') { // the stream is corrupt throw new DamagedFontException("Could not read token at position " + - buffer.position()); + buffer.position()); } return new Token(regular, Token.LITERAL); } @@ -217,7 +216,7 @@ else if (c == 0) { // the stream is corrupt throw new DamagedFontException("Could not read token at position " + - buffer.position()); + buffer.position()); } if (name.equals("RD") || name.equals("-|")) @@ -361,7 +360,7 @@ else if (c != 'e' && c != 'E') } catch (NumberFormatException ex) { - throw new IOException("Invalid number '" + sb.toString() + "'", ex); + throw new IOException("Invalid number '" + sb + "'", ex); } return new Token(Integer.toString(val), Token.INTEGER); } @@ -497,12 +496,16 @@ private Token readString() throws IOException */ private Token readCharString(int length) throws IOException { + if (length > buffer.array().length) + { + throw new IOException("String length " + length + " is larger than input"); + } try { - buffer.get(); // space - byte[] data = new byte[length]; - buffer.get(data); - return new Token(data, Token.CHARSTRING); + buffer.get(); // space + byte[] data = new byte[length]; + buffer.get(data); + return new Token(data, Token.CHARSTRING); } catch (BufferUnderflowException exception) { diff --git a/library/src/main/java/com/tom_roush/fontbox/type1/Type1Parser.java b/library/src/main/java/com/tom_roush/fontbox/type1/Type1Parser.java index 317bb013f..798ef2097 100644 --- a/library/src/main/java/com/tom_roush/fontbox/type1/Type1Parser.java +++ b/library/src/main/java/com/tom_roush/fontbox/type1/Type1Parser.java @@ -25,7 +25,6 @@ import com.tom_roush.fontbox.encoding.BuiltInEncoding; import com.tom_roush.fontbox.encoding.StandardEncoding; - /** * Parses an Adobe Type 1 (.pfb) font. It is used exclusively by Type1Font. * @@ -218,16 +217,19 @@ private void readEncoding() throws IOException // we have to check "readonly" and "def" too // as some fonts don't provide any dup-values, see PDFBOX-2134 while (!(lexer.peekKind(Token.NAME) && - (lexer.peekToken().getText().equals("dup") || + (lexer.peekToken().getText().equals("dup") || lexer.peekToken().getText().equals("readonly") || lexer.peekToken().getText().equals("def")))) { - lexer.nextToken(); + if ( lexer.nextToken() == null ) + { + throw new IOException( "Incomplete data while reading encoding of type 1 font" ); + } } Map codeToName = new HashMap(); while (lexer.peekKind(Token.NAME) && - lexer.peekToken().getText().equals("dup")) + lexer.peekToken().getText().equals("dup")) { read(Token.NAME, "dup"); int code = read(Token.INTEGER).intValue(); @@ -246,7 +248,7 @@ private void readEncoding() throws IOException */ private List arrayToNumbers(List value) throws IOException { - List numbers = new ArrayList(); + List numbers = new ArrayList(value.size()); for (int i = 1, size = value.size() - 1; i < size; i++) { Token token = value.get(i); @@ -261,7 +263,7 @@ else if (token.getKind() == Token.INTEGER) else { throw new IOException("Expected INTEGER or REAL but got " + token + - " at array position " + i); + " at array position " + i); } } return numbers; @@ -327,6 +329,13 @@ private Map> readSimpleDict() throws IOException int length = read(Token.INTEGER).intValue(); read(Token.NAME, "dict"); readMaybe(Token.NAME, "dup"); + + if (readMaybe(Token.NAME, "def") != null) + { + // PDFBOX-5942 empty dict + return dict; + } + read(Token.NAME, "begin"); for (int i = 0; i < length; i++) @@ -336,7 +345,7 @@ private Map> readSimpleDict() throws IOException break; } if (lexer.peekKind(Token.NAME) && - !lexer.peekToken().getText().equals("end")) + !lexer.peekToken().getText().equals("end")) { read(Token.NAME); } @@ -642,9 +651,12 @@ else if ("RD".equals(key)) // sometimes followed by "put". Either way, we just skip until // the /CharStrings dict is found while (!(lexer.peekKind(Token.LITERAL) - && lexer.peekToken().getText().equals("CharStrings"))) + && lexer.peekToken().getText().equals("CharStrings"))) { - lexer.nextToken(); + if ( lexer.nextToken() == null ) + { + throw new IOException( "Missing 'CharStrings' dictionary in type 1 font" ); + } } // CharStrings dict @@ -733,7 +745,7 @@ private void readSubrs(int lenIV) throws IOException break; } if (!(lexer.peekKind(Token.NAME) && - lexer.peekToken().getText().equals("dup"))) + lexer.peekToken().getText().equals("dup"))) { break; } @@ -891,8 +903,6 @@ private Token read(Token.Kind kind) throws IOException /** * Reads the next token and throws an exception if it is not of the given kind * and does not have the given value. - * - * @return token, never null */ private void read(Token.Kind kind, String name) throws IOException { @@ -969,7 +979,7 @@ private boolean isBinary(byte[] bytes) { byte by = bytes[i]; if (by != 0x0a && by != 0x0d && by != 0x20 && by != '\t' && - Character.digit((char) by, 16) == -1) + Character.digit((char) by, 16) == -1) { return true; } diff --git a/library/src/main/java/com/tom_roush/fontbox/util/autodetect/UnixFontDirFinder.java b/library/src/main/java/com/tom_roush/fontbox/util/autodetect/UnixFontDirFinder.java index 5a21f0a62..d29053114 100644 --- a/library/src/main/java/com/tom_roush/fontbox/util/autodetect/UnixFontDirFinder.java +++ b/library/src/main/java/com/tom_roush/fontbox/util/autodetect/UnixFontDirFinder.java @@ -17,6 +17,7 @@ package com.tom_roush.fontbox.util.autodetect; + /** * Unix font directory finder. This class is based on a class provided by Apache FOP. see * com.tom_roush.fop.fonts.autodetect.UnixFontDirFinder @@ -27,15 +28,15 @@ public class UnixFontDirFinder extends NativeFontDirFinder /** * Some guesses at possible unix font directory locations. * - * @return a list of possible font locations + * @return an array of possible font locations */ @Override protected String[] getSearchableDirectories() { return new String[] { System.getProperty("user.home") + "/.fonts", // user - "/usr/local/fonts", // local - "/usr/local/share/fonts", // local shared - "/usr/share/fonts", // system + "/usr/local/fonts", // local + "/usr/local/share/fonts", // local shared + "/usr/share/fonts", // system "/usr/X11R6/lib/X11/fonts" // X }; } diff --git a/library/src/main/java/com/tom_roush/fontbox/util/autodetect/WindowsFontDirFinder.java b/library/src/main/java/com/tom_roush/fontbox/util/autodetect/WindowsFontDirFinder.java index 5dea19516..2e30b3eb5 100644 --- a/library/src/main/java/com/tom_roush/fontbox/util/autodetect/WindowsFontDirFinder.java +++ b/library/src/main/java/com/tom_roush/fontbox/util/autodetect/WindowsFontDirFinder.java @@ -17,44 +17,15 @@ package com.tom_roush.fontbox.util.autodetect; -import java.io.BufferedReader; import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; import java.util.List; - -import com.tom_roush.fontbox.util.Charsets; - /** * FontFinder for native Windows platforms. This class is based on a class provided by Apache FOP. see - * com.tom_roush.fop.fonts.autodetect.WindowsFontDirFinder + * org.apache.fop.fonts.autodetect.WindowsFontDirFinder */ public class WindowsFontDirFinder implements FontDirFinder { - /** - * Attempts to read windir environment variable on windows (disclaimer: This is a bit dirty but seems to work - * nicely). - */ - private String getWinDir(String osName) throws IOException - { - Process process; - Runtime runtime = Runtime.getRuntime(); - if (osName.startsWith("Windows 9")) - { - process = runtime.exec("command.com /c echo %windir%"); - } - else - { - process = runtime.exec("cmd.exe /c echo %windir%"); - } - BufferedReader bufferedReader = new BufferedReader(new InputStreamReader( - process.getInputStream(), Charsets.ISO_8859_1)); - String winDir = bufferedReader.readLine(); - bufferedReader.close(); - return winDir; - } - /** * {@inheritDoc} * @@ -73,16 +44,11 @@ public List find() { // should continue if this fails } - String osName = System.getProperty("os.name"); if (windir == null) { try { - windir = getWinDir(osName); - } - catch (IOException e) - { - // should continue if this fails + windir = System.getenv("windir"); } catch (SecurityException e) { @@ -111,12 +77,13 @@ public List find() } else { + String osName = System.getProperty("os.name"); String windowsDirName = osName.endsWith("NT") ? "WINNT" : "WINDOWS"; // look for true type font folder for (char driveLetter = 'C'; driveLetter <= 'E'; driveLetter++) { osFontsDir = new File(driveLetter + ":" + File.separator + windowsDirName - + File.separator + "FONTS"); + + File.separator + "FONTS"); try { if (osFontsDir.exists() && osFontsDir.canRead()) @@ -155,7 +122,7 @@ public List find() if (localAppData != null && !localAppData.isEmpty()) { File localFontDir = new File(localAppData + File.separator + "Microsoft" + - File.separator + "Windows" + File.separator + "Fonts"); + File.separator + "Windows" + File.separator + "Fonts"); if (localFontDir.exists() && localFontDir.canRead()) { fontDirList.add(localFontDir); diff --git a/library/src/main/java/com/tom_roush/pdfbox/android/PDFBoxResourceLoader.java b/library/src/main/java/com/tom_roush/pdfbox/android/PDFBoxResourceLoader.java index f3c471235..db1e16dc2 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/android/PDFBoxResourceLoader.java +++ b/library/src/main/java/com/tom_roush/pdfbox/android/PDFBoxResourceLoader.java @@ -63,8 +63,7 @@ public static InputStream getStream(String path) throws IOException { if (ASSET_MANAGER == null) { - Log.e("PdfBox-Android", - "PDFBoxResourceLoader is not initialized, call PDFBoxResourceLoader.init() before use"); + throw new IOException("PDFBoxResourceLoader is not initialized, call PDFBoxResourceLoader.init() before use"); } return ASSET_MANAGER.open(path); } diff --git a/library/src/main/java/com/tom_roush/pdfbox/contentstream/PDFStreamEngine.java b/library/src/main/java/com/tom_roush/pdfbox/contentstream/PDFStreamEngine.java index 553fc5cdf..8ff32f29f 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/contentstream/PDFStreamEngine.java +++ b/library/src/main/java/com/tom_roush/pdfbox/contentstream/PDFStreamEngine.java @@ -30,9 +30,11 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.zip.DataFormatException; import com.tom_roush.pdfbox.contentstream.operator.MissingOperandException; import com.tom_roush.pdfbox.contentstream.operator.Operator; +import com.tom_roush.pdfbox.contentstream.operator.OperatorName; import com.tom_roush.pdfbox.contentstream.operator.OperatorProcessor; import com.tom_roush.pdfbox.contentstream.operator.state.EmptyGraphicsStackException; import com.tom_roush.pdfbox.cos.COSArray; @@ -55,6 +57,7 @@ import com.tom_roush.pdfbox.pdmodel.graphics.blend.BlendMode; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColor; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColorSpace; +import com.tom_roush.pdfbox.pdmodel.graphics.color.PDDeviceGray; import com.tom_roush.pdfbox.pdmodel.graphics.form.PDFormXObject; import com.tom_roush.pdfbox.pdmodel.graphics.form.PDTransparencyGroup; import com.tom_roush.pdfbox.pdmodel.graphics.pattern.PDTilingPattern; @@ -64,7 +67,6 @@ import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream; import com.tom_roush.pdfbox.util.Matrix; import com.tom_roush.pdfbox.util.Vector; - /** * Processes a PDF content stream and executes certain operations. * Provides a callback interface for clients that want to do things with the stream. @@ -73,10 +75,8 @@ */ public abstract class PDFStreamEngine { - private final Map operators = new HashMap(80); - private Matrix textMatrix; - private Matrix textLineMatrix; + private final Map operators = new HashMap(); private Deque graphicsStack = new ArrayDeque(); @@ -88,6 +88,9 @@ public abstract class PDFStreamEngine // used to monitor potentially recursive operations. private int level = 0; + // false in certain cases, e.g. type3 charprocs with d1 or uncolored tiling patterns + private boolean shouldProcessColorOperators; + /** * Creates a new PDFStreamEngine. */ @@ -132,8 +135,6 @@ private void initPage(PDPage page) currentPage = page; graphicsStack.clear(); graphicsStack.push(new PDGraphicsState(page.getCropBox())); - textMatrix = null; - textLineMatrix = null; resources = null; initialMatrix = page.getMatrix(); } @@ -177,7 +178,7 @@ public void showForm(PDFormXObject form) throws IOException if (currentPage == null) { throw new IllegalStateException("No current page, call " + - "#processChildStream(PDContentStream, PDPage) instead"); + "#processChildStream(PDContentStream, PDPage) instead"); } if (form.getCOSObject().getLength() > 0) { @@ -195,10 +196,23 @@ public void showForm(PDFormXObject form) throws IOException protected void processSoftMask(PDTransparencyGroup group) throws IOException { saveGraphicsState(); - Matrix softMaskCTM = getGraphicsState().getSoftMask().getInitialTransformationMatrix(); - getGraphicsState().setCurrentTransformationMatrix(softMaskCTM); - processTransparencyGroup(group); - restoreGraphicsState(); + PDGraphicsState graphicsState = getGraphicsState(); + Matrix softMaskCTM = graphicsState.getSoftMask().getInitialTransformationMatrix(); + graphicsState.setCurrentTransformationMatrix(softMaskCTM); + graphicsState.setTextMatrix(new Matrix()); + graphicsState.setTextLineMatrix(new Matrix()); + graphicsState.setNonStrokingColorSpace(PDDeviceGray.INSTANCE); + graphicsState.setNonStrokingColor(PDDeviceGray.INSTANCE.getInitialColor()); + graphicsState.setStrokingColorSpace(PDDeviceGray.INSTANCE); + graphicsState.setStrokingColor(PDDeviceGray.INSTANCE.getInitialColor()); + try + { + processTransparencyGroup(group); + } + finally + { + restoreGraphicsState(); + } } /** @@ -213,7 +227,7 @@ protected void processTransparencyGroup(PDTransparencyGroup group) throws IOExce if (currentPage == null) { throw new IllegalStateException("No current page, call " + - "#processChildStream(PDContentStream, PDPage) instead"); + "#processChildStream(PDContentStream, PDPage) instead"); } PDResources parent = pushResources(group); @@ -228,8 +242,8 @@ protected void processTransparencyGroup(PDTransparencyGroup group) throws IOExce // transform the CTM using the stream's matrix graphicsState.getCurrentTransformationMatrix().concatenate(group.getMatrix()); - // Before execution of the transparency group XObject’s content stream, - // the current blend mode in the graphics state shall be initialized to Normal, + // Before execution of the transparency group XObject’s content stream, + // the current blend mode in the graphics state shall be initialized to Normal, // the current stroking and nonstroking alpha constants to 1.0, and the current soft mask to None. graphicsState.setBlendMode(BlendMode.NORMAL); graphicsState.setAlphaConstant(1); @@ -260,30 +274,28 @@ protected void processTransparencyGroup(PDTransparencyGroup group) throws IOExce * @throws IOException if there is an error reading or parsing the character content stream. */ protected void processType3Stream(PDType3CharProc charProc, Matrix textRenderingMatrix) - throws IOException + throws IOException { if (currentPage == null) { throw new IllegalStateException("No current page, call " + - "#processChildStream(PDContentStream, PDPage) instead"); + "#processChildStream(PDContentStream, PDPage) instead"); } PDResources parent = pushResources(charProc); Deque savedStack = saveGraphicsStack(); + PDGraphicsState graphicsState = getGraphicsState(); // replace the CTM with the TRM - getGraphicsState().setCurrentTransformationMatrix(textRenderingMatrix); + graphicsState.setCurrentTransformationMatrix(textRenderingMatrix); // transform the CTM using the stream's matrix (this is the FontMatrix) textRenderingMatrix.concatenate(charProc.getMatrix()); // note: we don't clip to the BBox as it is often wrong, see PDFBOX-1917 - // save text matrices (Type 3 stream may contain BT/ET, see PDFBOX-2137) - Matrix textMatrixOld = textMatrix; - textMatrix = new Matrix(); - Matrix textLineMatrixOld = textLineMatrix; - textLineMatrix = new Matrix(); + graphicsState.setTextMatrix(new Matrix()); + graphicsState.setTextLineMatrix(new Matrix()); try { @@ -291,10 +303,6 @@ protected void processType3Stream(PDType3CharProc charProc, Matrix textRendering } finally { - // restore text matrices - textMatrix = textMatrixOld; - textLineMatrix = textLineMatrixOld; - restoreGraphicsStack(savedStack); popResources(parent); } @@ -308,23 +316,25 @@ protected void processType3Stream(PDType3CharProc charProc, Matrix textRendering * @throws IOException If there is an error reading or parsing the appearance content stream. */ protected void processAnnotation(PDAnnotation annotation, PDAppearanceStream appearance) - throws IOException + throws IOException { PDRectangle bbox = appearance.getBBox(); PDRectangle rect = annotation.getRectangle(); - // zero-sized rectangles are not valid + // PDFBOX-4783: zero-sized rectangles are not valid if (rect != null && rect.getWidth() > 0 && rect.getHeight() > 0 && bbox != null && bbox.getWidth() > 0 && bbox.getHeight() > 0) { - PDResources parent = pushResources(appearance); - Deque savedStack = saveGraphicsStack(); - Matrix matrix = appearance.getMatrix(); // transformed appearance box fixme: may be an arbitrary shape RectF transformedBox = new RectF(); bbox.transform(matrix).computeBounds(transformedBox, true); + if (transformedBox.isEmpty()) + { + // PDFBOX-6095: zero-sized rectangles are not valid + return; + } // compute a matrix which scales and translates the transformed appearance box to align // with the edges of the annotation's rectangle @@ -337,10 +347,13 @@ protected void processAnnotation(PDAnnotation annotation, PDAppearanceStream app // Matrix shall be concatenated with A to form a matrix AA that maps from the appearance's // coordinate system to the annotation's rectangle in default user space // - // HOWEVER only the opposite order works for rotated pages with + // HOWEVER only the opposite order works for rotated pages with // filled fields / annotations that have a matrix in the appearance stream, see PDFBOX-3083 Matrix aa = Matrix.concatenate(a, matrix); + PDResources parent = pushResources(appearance); + Deque savedStack = saveGraphicsStack(); + // make matrix AA the CTM getGraphicsState().setCurrentTransformationMatrix(aa); @@ -371,7 +384,7 @@ protected void processAnnotation(PDAnnotation annotation, PDAppearanceStream app * @throws IOException if there is an error reading or parsing the tiling pattern content stream. */ protected final void processTilingPattern(PDTilingPattern tilingPattern, PDColor color, - PDColorSpace colorSpace) throws IOException + PDColorSpace colorSpace) throws IOException { processTilingPattern(tilingPattern, color, colorSpace, tilingPattern.getMatrix()); } @@ -387,8 +400,8 @@ protected final void processTilingPattern(PDTilingPattern tilingPattern, PDColor * @throws IOException if there is an error reading or parsing the tiling pattern content stream. */ protected final void processTilingPattern(PDTilingPattern tilingPattern, PDColor color, - PDColorSpace colorSpace, Matrix patternMatrix) - throws IOException + PDColorSpace colorSpace, Matrix patternMatrix) + throws IOException { PDResources parent = pushResources(tilingPattern); @@ -423,18 +436,12 @@ protected final void processTilingPattern(PDTilingPattern tilingPattern, PDColor // clip to bounding box clipToRect(tilingBBox); - // save text matrices (pattern stream may contain BT/ET, see PDFBOX-4896) - Matrix textMatrixSave = textMatrix; - Matrix textLineMatrixSave = textLineMatrix; - try { processStreamOperators(tilingPattern); } finally { - textMatrix = textMatrixSave; - textLineMatrix = textLineMatrixSave; initialMatrix = parentMatrix; restoreGraphicsStack(savedStack); popResources(parent); @@ -481,7 +488,7 @@ protected void processChildStream(PDContentStream contentStream, PDPage page) th if (isProcessingPage) { throw new IllegalStateException("Current page has already been set via " + - " #processPage(PDPage) call #processChildStream(PDContentStream) instead"); + " #processPage(PDPage) call #processChildStream(PDContentStream) instead"); } initPage(page); processStream(contentStream); @@ -534,18 +541,40 @@ private void processStreamOperators(PDContentStream contentStream) throws IOExce List arguments = new ArrayList(); PDFStreamParser parser = new PDFStreamParser(contentStream); Object token = parser.parseNextToken(); - while (token != null) + + boolean isFirstOperator = true; + boolean oldShouldProcessColorOperators = shouldProcessColorOperators; + shouldProcessColorOperators = true; + if (contentStream instanceof PDTilingPattern && + ((PDTilingPattern) contentStream).getPaintType() == PDTilingPattern.PAINT_UNCOLORED) { - if (token instanceof Operator) - { - processOperator((Operator) token, arguments); - arguments.clear(); - } - else + shouldProcessColorOperators = false; + } + try + { + while (token != null) { - arguments.add((COSBase) token); + if (token instanceof Operator) + { + if (isFirstOperator && contentStream instanceof PDType3CharProc && + OperatorName.TYPE3_D1.equals(((Operator) token).getName())) + { + shouldProcessColorOperators = false; + } + isFirstOperator = false; + processOperator((Operator) token, arguments); + arguments.clear(); + } + else + { + arguments.add((COSBase) token); + } + token = parser.parseNextToken(); } - token = parser.parseNextToken(); + } + finally + { + shouldProcessColorOperators = oldShouldProcessColorOperators; } } @@ -687,7 +716,7 @@ else if (obj instanceof COSArray) else { Log.e("PdfBox-Android", "Unknown type " + obj.getClass().getSimpleName() - + " in array for TJ operation: " + obj); + + " in array for TJ operation: " + obj); } } } @@ -703,7 +732,7 @@ else if (obj instanceof COSArray) protected void applyTextAdjustment(float tx, float ty) throws IOException { // update the text matrix - textMatrix.concatenate(Matrix.getTranslateInstance(tx, ty)); + getGraphicsState().getTextMatrix().translate(tx, ty); } /** @@ -732,9 +761,11 @@ protected void showText(byte[] string) throws IOException // put the text state parameters into matrix form Matrix parameters = new Matrix( - fontSize * horizontalScaling, 0, // 0 - 0, fontSize, // 0 - 0, textState.getRise()); // 1 + fontSize * horizontalScaling, 0, // 0 + 0, fontSize, // 0 + 0, textState.getRise()); // 1 + + Matrix textMatrix = state.getTextMatrix(); // read the stream until it is empty InputStream in = new ByteArrayInputStream(string); @@ -790,7 +821,7 @@ protected void showText(byte[] string) throws IOException } // update the text matrix - textMatrix.concatenate(Matrix.getTranslateInstance(tx, ty)); + textMatrix.translate(tx, ty); } } @@ -808,8 +839,8 @@ protected void showText(byte[] string) throws IOException * @deprecated use {@link #showGlyph(Matrix, PDFont, int, Vector)} instead */ protected void showGlyph(Matrix textRenderingMatrix, PDFont font, int code, - String unicode, - Vector displacement) throws IOException + String unicode, + Vector displacement) throws IOException { if (font instanceof PDType3Font) { @@ -832,7 +863,7 @@ protected void showGlyph(Matrix textRenderingMatrix, PDFont font, int code, * @throws IOException if the glyph cannot be processed */ protected void showGlyph(Matrix textRenderingMatrix, PDFont font, int code, Vector displacement) - throws IOException + throws IOException { // call deprecated method to ensure binary compatibility if not overridden showGlyph(textRenderingMatrix, font, code, font.toUnicode(code), displacement); @@ -852,7 +883,7 @@ protected void showGlyph(Matrix textRenderingMatrix, PDFont font, int code, Vect * @deprecated use {@link #showFontGlyph(Matrix, PDFont, int, Vector)} instead */ protected void showFontGlyph(Matrix textRenderingMatrix, PDFont font, int code, String unicode, - Vector displacement) throws IOException + Vector displacement) throws IOException { // overridden in subclasses } @@ -868,7 +899,7 @@ protected void showFontGlyph(Matrix textRenderingMatrix, PDFont font, int code, * @throws IOException if the glyph cannot be processed */ protected void showFontGlyph(Matrix textRenderingMatrix, PDFont font, int code, - Vector displacement) throws IOException + Vector displacement) throws IOException { // overridden in subclasses // call deprecated method to ensure binary compatibility if not overridden @@ -889,7 +920,7 @@ protected void showFontGlyph(Matrix textRenderingMatrix, PDFont font, int code, * @deprecated use {@link #showType3Glyph(Matrix, PDType3Font, int, Vector)} instead */ protected void showType3Glyph(Matrix textRenderingMatrix, PDType3Font font, int code, - String unicode, Vector displacement) throws IOException + String unicode, Vector displacement) throws IOException { PDType3CharProc charProc = font.getCharProc(code); if (charProc != null) @@ -909,7 +940,7 @@ protected void showType3Glyph(Matrix textRenderingMatrix, PDType3Font font, int * @throws IOException if the glyph cannot be processed */ protected void showType3Glyph(Matrix textRenderingMatrix, PDType3Font font, int code, - Vector displacement) throws IOException + Vector displacement) throws IOException { // call deprecated method to ensure binary compatibility if not overridden showType3Glyph(textRenderingMatrix, font, code, font.toUnicode(code), displacement); @@ -999,23 +1030,27 @@ protected void unsupportedOperator(Operator operator, List operands) th * @throws IOException if something went wrong */ protected void operatorException(Operator operator, List operands, IOException e) - throws IOException + throws IOException { if (e instanceof MissingOperandException || e instanceof MissingResourceException || e instanceof MissingImageReaderException) { - Log.e("PdfBox-Android", e.getMessage()); + Log.e("PdfBox-Android", e.getMessage(), e); } else if (e instanceof EmptyGraphicsStackException) { - Log.w("PdfBox-Android", e.getMessage()); + Log.w("PdfBox-Android", e.getMessage(), e); } else if (operator.getName().equals("Do")) { // todo: this too forgiving, but PDFBox has always worked this way for DrawObject // some careful refactoring is needed - Log.w("PdfBox-Android", e.getMessage()); + Log.w("PdfBox-Android", e.getMessage(), e); + } + else if (e.getCause() instanceof DataFormatException) + { + Log.w("PdfBox-Android", e.getMessage(), e); } else { @@ -1083,7 +1118,7 @@ public PDGraphicsState getGraphicsState() */ public Matrix getTextLineMatrix() { - return textLineMatrix; + return getGraphicsState().getTextLineMatrix(); } /** @@ -1091,7 +1126,7 @@ public Matrix getTextLineMatrix() */ public void setTextLineMatrix(Matrix value) { - textLineMatrix = value; + getGraphicsState().setTextLineMatrix(value); } /** @@ -1099,7 +1134,7 @@ public void setTextLineMatrix(Matrix value) */ public Matrix getTextMatrix() { - return textMatrix; + return getGraphicsState().getTextMatrix(); } /** @@ -1107,7 +1142,7 @@ public Matrix getTextMatrix() */ public void setTextMatrix(Matrix value) { - textMatrix = value; + getGraphicsState().setTextMatrix(value); } /** @@ -1116,11 +1151,6 @@ public void setTextMatrix(Matrix value) */ public void setLineDashPattern(COSArray array, int phase) { - if (phase < 0) - { - Log.w("PdfBox-Android", "Dash phase has negative value " + phase + ", set to 0"); - phase = 0; - } PDLineDashPattern lineDash = new PDLineDashPattern(array, phase); getGraphicsState().setLineDashPattern(lineDash); } @@ -1164,7 +1194,7 @@ public PointF transformedPoint(float x, float y) { float[] position = { x, y }; getGraphicsState().getCurrentTransformationMatrix().createAffineTransform() - .transform(position, 0, position, 0, 1); + .transform(position, 0, position, 0, 1); return new PointF(position[0], position[1]); } @@ -1215,4 +1245,16 @@ public void decreaseLevel() Log.e("PdfBox-Android", "level is " + level); } } + + /** + * Tells whether color operators should be processed. To be used in some OperatorProcessor + * classes. + * + * @return true if color operators should be processed, false if not, e.g. in type3 charprocs + * with d1 or in uncolored tiling patterns. + */ + public boolean isShouldProcessColorOperators() + { + return shouldProcessColorOperators; + } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetColor.java b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetColor.java index 5928da486..262351625 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetColor.java +++ b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetColor.java @@ -27,7 +27,6 @@ import com.tom_roush.pdfbox.cos.COSNumber; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColor; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColorSpace; - /** * sc,scn,SC,SCN: Sets the color to use for stroking or non-stroking operations. * @@ -39,7 +38,8 @@ public abstract class SetColor extends OperatorProcessor public void process(Operator operator, List arguments) throws IOException { PDColorSpace colorSpace = getColorSpace(); -// if (!(colorSpace instanceof PDPattern)) TODO: PdfBox-Android + // TODO: PdfBox-Android: Pattern colorspace not supported +// if (!(colorSpace instanceof PDPattern)) { if (arguments.size() < colorSpace.getNumberOfComponents()) { @@ -47,6 +47,9 @@ public void process(Operator operator, List arguments) throws IOExcepti } if (!checkArrayTypesClass(arguments, COSNumber.class)) { + // PDFBOX-5851: set an invalid color because Pattern colorspace is missing + // this will produce transparency in PageDrawer + setColor(new PDColor(new float[0], null)); return; } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetNonStrokingColorSpace.java b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetNonStrokingColorSpace.java index 9e7c4cacb..30fd793a1 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetNonStrokingColorSpace.java +++ b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetNonStrokingColorSpace.java @@ -19,13 +19,12 @@ import java.io.IOException; import java.util.List; -import com.tom_roush.pdfbox.cos.COSBase; -import com.tom_roush.pdfbox.cos.COSName; -import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColorSpace; import com.tom_roush.pdfbox.contentstream.operator.Operator; import com.tom_roush.pdfbox.contentstream.operator.OperatorName; import com.tom_roush.pdfbox.contentstream.operator.OperatorProcessor; - +import com.tom_roush.pdfbox.cos.COSBase; +import com.tom_roush.pdfbox.cos.COSName; +import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColorSpace; /** * cs: Sets the non-stroking color space. * @@ -37,6 +36,10 @@ public class SetNonStrokingColorSpace extends OperatorProcessor @Override public void process(Operator operator, List arguments) throws IOException { + if (!context.isShouldProcessColorOperators()) + { + return; + } if (arguments.isEmpty()) { return; diff --git a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetNonStrokingDeviceCMYKColor.java b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetNonStrokingDeviceCMYKColor.java index 7af3617a2..b11cbe87b 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetNonStrokingDeviceCMYKColor.java +++ b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetNonStrokingDeviceCMYKColor.java @@ -24,7 +24,6 @@ import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColorSpace; - /** * k: Set the non-stroking colour space to DeviceCMYK and set the colour to * use for non-stroking operations. @@ -36,6 +35,10 @@ public class SetNonStrokingDeviceCMYKColor extends SetNonStrokingColor @Override public void process(Operator operator, List arguments) throws IOException { + if (!context.isShouldProcessColorOperators()) + { + return; + } PDColorSpace cs = context.getResources().getColorSpace(COSName.DEVICECMYK); context.getGraphicsState().setNonStrokingColorSpace(cs); super.process(operator, arguments); diff --git a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetNonStrokingDeviceGrayColor.java b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetNonStrokingDeviceGrayColor.java index 13d656a9d..ec2dfd65f 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetNonStrokingDeviceGrayColor.java +++ b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetNonStrokingDeviceGrayColor.java @@ -24,7 +24,6 @@ import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColorSpace; - /** * g: Set the non-stroking colour space to DeviceGray and set the gray * level to use for non-stroking operations. @@ -36,6 +35,10 @@ public class SetNonStrokingDeviceGrayColor extends SetNonStrokingColor @Override public void process(Operator operator, List arguments) throws IOException { + if (!context.isShouldProcessColorOperators()) + { + return; + } PDColorSpace cs = context.getResources().getColorSpace(COSName.DEVICEGRAY); context.getGraphicsState().setNonStrokingColorSpace(cs); super.process(operator, arguments); diff --git a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetNonStrokingDeviceRGBColor.java b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetNonStrokingDeviceRGBColor.java index d545ab7c9..ae26054dc 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetNonStrokingDeviceRGBColor.java +++ b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetNonStrokingDeviceRGBColor.java @@ -24,7 +24,6 @@ import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColorSpace; - /** * rg: Set the non-stroking colour space to DeviceRGB and set the colour to * use for non-stroking operations. @@ -36,6 +35,10 @@ public class SetNonStrokingDeviceRGBColor extends SetNonStrokingColor @Override public void process(Operator operator, List arguments) throws IOException { + if (!context.isShouldProcessColorOperators()) + { + return; + } PDColorSpace cs = context.getResources().getColorSpace(COSName.DEVICERGB); context.getGraphicsState().setNonStrokingColorSpace(cs); super.process(operator, arguments); diff --git a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetStrokingColorSpace.java b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetStrokingColorSpace.java index 76e147de4..9cac6135f 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetStrokingColorSpace.java +++ b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetStrokingColorSpace.java @@ -19,13 +19,12 @@ import java.io.IOException; import java.util.List; -import com.tom_roush.pdfbox.cos.COSBase; -import com.tom_roush.pdfbox.cos.COSName; -import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColorSpace; import com.tom_roush.pdfbox.contentstream.operator.Operator; import com.tom_roush.pdfbox.contentstream.operator.OperatorName; import com.tom_roush.pdfbox.contentstream.operator.OperatorProcessor; - +import com.tom_roush.pdfbox.cos.COSBase; +import com.tom_roush.pdfbox.cos.COSName; +import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColorSpace; /** * CS: Set color space for stroking operations. * @@ -37,6 +36,10 @@ public class SetStrokingColorSpace extends OperatorProcessor @Override public void process(Operator operator, List arguments) throws IOException { + if (!context.isShouldProcessColorOperators()) + { + return; + } if (arguments.isEmpty()) { return; diff --git a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetStrokingDeviceCMYKColor.java b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetStrokingDeviceCMYKColor.java index 48cb1d860..3cd7c8921 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetStrokingDeviceCMYKColor.java +++ b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetStrokingDeviceCMYKColor.java @@ -24,7 +24,6 @@ import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColorSpace; - /** * K: Set the stroking colour space to DeviceCMYK and set the colour to use for stroking operations. * @@ -35,6 +34,10 @@ public class SetStrokingDeviceCMYKColor extends SetStrokingColor @Override public void process(Operator operator, List arguments) throws IOException { + if (!context.isShouldProcessColorOperators()) + { + return; + } PDColorSpace cs = context.getResources().getColorSpace(COSName.DEVICECMYK); context.getGraphicsState().setStrokingColorSpace(cs); super.process(operator, arguments); diff --git a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetStrokingDeviceGrayColor.java b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetStrokingDeviceGrayColor.java index 78e0f2842..157ae85f5 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetStrokingDeviceGrayColor.java +++ b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetStrokingDeviceGrayColor.java @@ -24,7 +24,6 @@ import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColorSpace; - /** * G: Set the stroking colour space to DeviceGray and set the gray level to use for stroking * operations. @@ -33,8 +32,13 @@ */ public class SetStrokingDeviceGrayColor extends SetStrokingColor { + @Override public void process(Operator operator, List arguments) throws IOException { + if (!context.isShouldProcessColorOperators()) + { + return; + } PDColorSpace cs = context.getResources().getColorSpace(COSName.DEVICEGRAY); context.getGraphicsState().setStrokingColorSpace(cs); super.process(operator, arguments); diff --git a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetStrokingDeviceRGBColor.java b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetStrokingDeviceRGBColor.java index 649736fe6..1c38079b0 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetStrokingDeviceRGBColor.java +++ b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/color/SetStrokingDeviceRGBColor.java @@ -24,7 +24,6 @@ import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColorSpace; - /** * RG: Set the stroking colour space to DeviceRGB and set the colour to use for stroking operations. * @@ -40,8 +39,13 @@ public class SetStrokingDeviceRGBColor extends SetStrokingColor * @param arguments List * @throws IOException If the color space cannot be read. */ + @Override public void process(Operator operator, List arguments) throws IOException { + if (!context.isShouldProcessColorOperators()) + { + return; + } PDColorSpace cs = context.getResources().getColorSpace(COSName.DEVICERGB); context.getGraphicsState().setStrokingColorSpace(cs); super.process(operator, arguments); diff --git a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/graphics/BeginInlineImage.java b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/graphics/BeginInlineImage.java index 8531baeb0..edb12d7dd 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/graphics/BeginInlineImage.java +++ b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/graphics/BeginInlineImage.java @@ -19,12 +19,11 @@ import java.io.IOException; import java.util.List; +import com.tom_roush.pdfbox.contentstream.operator.Operator; +import com.tom_roush.pdfbox.contentstream.operator.OperatorName; import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.pdmodel.graphics.image.PDImage; import com.tom_roush.pdfbox.pdmodel.graphics.image.PDInlineImage; -import com.tom_roush.pdfbox.contentstream.operator.Operator; -import com.tom_roush.pdfbox.contentstream.operator.OperatorName; - /** * BI Begins an inline image. * @@ -40,8 +39,12 @@ public void process(Operator operator, List operands) throws IOExceptio return; } PDImage image = new PDInlineImage(operator.getImageParameters(), - operator.getImageData(), - context.getResources()); + operator.getImageData(), + context.getResources()); + if (!image.isStencil() && !context.isShouldProcessColorOperators()) + { + return; + } context.drawImage(image); } diff --git a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/graphics/CurveToReplicateFinalPoint.java b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/graphics/CurveToReplicateFinalPoint.java index ee2b78472..9ae0a4079 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/graphics/CurveToReplicateFinalPoint.java +++ b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/graphics/CurveToReplicateFinalPoint.java @@ -17,6 +17,7 @@ package com.tom_roush.pdfbox.contentstream.operator.graphics; import android.graphics.PointF; +import android.util.Log; import java.io.IOException; import java.util.List; @@ -26,7 +27,6 @@ import com.tom_roush.pdfbox.contentstream.operator.OperatorName; import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSNumber; - /** * y Append curved segment to path with final point replicated. * @@ -34,6 +34,7 @@ */ public final class CurveToReplicateFinalPoint extends GraphicsOperatorProcessor { + @Override public void process(Operator operator, List operands) throws IOException { @@ -50,12 +51,22 @@ public void process(Operator operator, List operands) throws IOExceptio COSNumber x3 = (COSNumber)operands.get(2); COSNumber y3 = (COSNumber)operands.get(3); + PointF currentPoint = context.getCurrentPoint(); + PointF point1 = context.transformedPoint(x1.floatValue(), y1.floatValue()); PointF point3 = context.transformedPoint(x3.floatValue(), y3.floatValue()); - context.curveTo(point1.x, point1.y, - point3.x, point3.y, - point3.x, point3.y); + if (currentPoint == null) + { + Log.w("PdfBox-Android", "curveTo (" + point3.x + "," + point3.y + ") without initial MoveTo"); + context.moveTo(point3.x, point3.y); + } + else + { + context.curveTo(point1.x, point1.y, + point3.x, point3.y, + point3.x, point3.y); + } } @Override diff --git a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/graphics/DrawObject.java b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/graphics/DrawObject.java index 99dce1969..e78c9e17e 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/graphics/DrawObject.java +++ b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/graphics/DrawObject.java @@ -31,7 +31,6 @@ import com.tom_roush.pdfbox.pdmodel.graphics.form.PDFormXObject; import com.tom_roush.pdfbox.pdmodel.graphics.form.PDTransparencyGroup; import com.tom_roush.pdfbox.pdmodel.graphics.image.PDImageXObject; - /** * Do: Draws an XObject. * @@ -40,6 +39,7 @@ */ public final class DrawObject extends GraphicsOperatorProcessor { + @Override public void process(Operator operator, List operands) throws IOException { @@ -61,7 +61,11 @@ public void process(Operator operator, List operands) throws IOExceptio } else if (xobject instanceof PDImageXObject) { - PDImageXObject image = (PDImageXObject)xobject; + PDImageXObject image = (PDImageXObject) xobject; + if (!image.isStencil() && !context.isShouldProcessColorOperators()) + { + return; + } context.drawImage(image); } else if (xobject instanceof PDFormXObject) diff --git a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/graphics/ShadingFill.java b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/graphics/ShadingFill.java index a9e9b9297..ec8720514 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/graphics/ShadingFill.java +++ b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/graphics/ShadingFill.java @@ -20,11 +20,10 @@ import java.util.List; import com.tom_roush.pdfbox.contentstream.operator.MissingOperandException; -import com.tom_roush.pdfbox.cos.COSBase; -import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.contentstream.operator.Operator; import com.tom_roush.pdfbox.contentstream.operator.OperatorName; - +import com.tom_roush.pdfbox.cos.COSBase; +import com.tom_roush.pdfbox.cos.COSName; /** * sh Fills the clipping area with the given shading pattern. * @@ -39,7 +38,12 @@ public void process(Operator operator, List operands) throws IOExceptio { throw new MissingOperandException(operator, operands); } - context.shadingFill((COSName) operands.get(0)); + COSBase base = operands.get(0); + if (!(base instanceof COSName)) + { + throw new MissingOperandException(operator, operands); + } + context.shadingFill((COSName) base); } @Override diff --git a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/markedcontent/BeginMarkedContentSequenceWithProperties.java b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/markedcontent/BeginMarkedContentSequenceWithProperties.java index 01b485be0..b14de2dd1 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/markedcontent/BeginMarkedContentSequenceWithProperties.java +++ b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/markedcontent/BeginMarkedContentSequenceWithProperties.java @@ -19,13 +19,14 @@ import java.io.IOException; import java.util.List; -import com.tom_roush.pdfbox.cos.COSBase; -import com.tom_roush.pdfbox.cos.COSDictionary; -import com.tom_roush.pdfbox.cos.COSName; +import com.tom_roush.pdfbox.contentstream.operator.MissingOperandException; import com.tom_roush.pdfbox.contentstream.operator.Operator; import com.tom_roush.pdfbox.contentstream.operator.OperatorName; import com.tom_roush.pdfbox.contentstream.operator.OperatorProcessor; - +import com.tom_roush.pdfbox.cos.COSBase; +import com.tom_roush.pdfbox.cos.COSDictionary; +import com.tom_roush.pdfbox.cos.COSName; +import com.tom_roush.pdfbox.pdmodel.documentinterchange.markedcontent.PDPropertyList; /** * BDC : Begins a marked-content sequence with property list. * @@ -34,22 +35,38 @@ public class BeginMarkedContentSequenceWithProperties extends OperatorProcessor { @Override - public void process(Operator operator, List arguments) throws IOException + public void process(Operator operator, List operands) throws IOException { - COSName tag = null; - COSDictionary properties = null; - for (COSBase argument : arguments) + if (operands.size() < 2) { - if (argument instanceof COSName) - { - tag = (COSName) argument; - } - else if (argument instanceof COSDictionary) + throw new MissingOperandException(operator, operands); + } + if (!(operands.get(0) instanceof COSName)) + { + return; + } + COSName tag = (COSName) operands.get(0); + COSBase op1 = operands.get(1); + COSDictionary propDict = null; + if (op1 instanceof COSName) + { + // PDFBOX-5980 and SO79549651 + PDPropertyList prop = context.getResources().getProperties((COSName) op1); + if (prop != null) { - properties = (COSDictionary) argument; + propDict = prop.getCOSObject(); } } - context.beginMarkedContentSequence(tag, properties); + else if (op1 instanceof COSDictionary) + { + propDict = (COSDictionary) op1; + } + if (propDict == null) + { + // wrong type or property not found + return; + } + context.beginMarkedContentSequence(tag, propDict); } @Override diff --git a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/state/SetLineCapStyle.java b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/state/SetLineCapStyle.java index 84702f0b6..d555c8b6a 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/state/SetLineCapStyle.java +++ b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/state/SetLineCapStyle.java @@ -27,7 +27,6 @@ import com.tom_roush.pdfbox.contentstream.operator.OperatorProcessor; import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSNumber; - /** * J: Set the line cap style. * @@ -41,6 +40,10 @@ public void process(Operator operator, List arguments) throws IOExcepti { throw new MissingOperandException(operator, arguments); } + if (!checkArrayTypesClass(arguments, COSNumber.class)) + { + return; + } Paint.Cap lineCapStyle; switch (((COSNumber)arguments.get( 0 )).intValue()) { diff --git a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/state/SetLineJoinStyle.java b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/state/SetLineJoinStyle.java index 2e4ae8215..70e937aff 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/state/SetLineJoinStyle.java +++ b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/state/SetLineJoinStyle.java @@ -27,7 +27,6 @@ import com.tom_roush.pdfbox.contentstream.operator.OperatorProcessor; import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSNumber; - /** * j: Set the line join style. * @@ -41,6 +40,10 @@ public void process(Operator operator, List arguments) throws IOExcepti { throw new MissingOperandException(operator, arguments); } + if (!checkArrayTypesClass(arguments, COSNumber.class)) + { + return; + } Paint.Join lineJoinStyle; switch (((COSNumber)arguments.get( 0 )).intValue()) { diff --git a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/state/SetLineMiterLimit.java b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/state/SetLineMiterLimit.java index 49d2dee38..74137486e 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/state/SetLineMiterLimit.java +++ b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/state/SetLineMiterLimit.java @@ -25,8 +25,6 @@ import com.tom_roush.pdfbox.contentstream.operator.OperatorProcessor; import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSNumber; - - /** * M: Set miter limit. * @@ -40,6 +38,10 @@ public void process(Operator operator, List arguments) throws IOExcepti { throw new MissingOperandException(operator, arguments); } + if (!checkArrayTypesClass(arguments, COSNumber.class)) + { + return; + } COSNumber miterLimit = (COSNumber)arguments.get( 0 ); context.getGraphicsState().setMiterLimit(miterLimit.floatValue()); } diff --git a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/state/SetLineWidth.java b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/state/SetLineWidth.java index 5655f510b..e08599c9f 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/state/SetLineWidth.java +++ b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/state/SetLineWidth.java @@ -25,7 +25,6 @@ import com.tom_roush.pdfbox.contentstream.operator.OperatorProcessor; import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSNumber; - /** * w: Set line width. * @@ -40,6 +39,10 @@ public void process(Operator operator, List arguments) throws IOExcepti { throw new MissingOperandException(operator, arguments); } + if (!checkArrayTypesClass(arguments, COSNumber.class)) + { + return; + } COSNumber width = (COSNumber) arguments.get(0); context.getGraphicsState().setLineWidth(width.floatValue()); } diff --git a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/text/SetFontAndSize.java b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/text/SetFontAndSize.java index 2f7218931..2d48e68f6 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/text/SetFontAndSize.java +++ b/library/src/main/java/com/tom_roush/pdfbox/contentstream/operator/text/SetFontAndSize.java @@ -30,7 +30,7 @@ import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.cos.COSNumber; import com.tom_roush.pdfbox.pdmodel.font.PDFont; - +import com.tom_roush.pdfbox.pdmodel.graphics.state.PDTextState; /** * Tf: Set text font and size. * @@ -38,6 +38,7 @@ */ public class SetFontAndSize extends OperatorProcessor { + @Override public void process(Operator operator, List arguments) throws IOException { @@ -58,13 +59,16 @@ public void process(Operator operator, List arguments) throws IOExcepti } COSName fontName = (COSName) base0; float fontSize = ((COSNumber) base1).floatValue(); - context.getGraphicsState().getTextState().setFontSize(fontSize); + PDTextState textState = context.getGraphicsState().getTextState(); + textState.setFontSize(fontSize); + // Get the font after the size has been set in case there is an exception + // so that PDFBox will use a default font PDFont font = context.getResources().getFont(fontName); if (font == null) { Log.w("PdfBox-Android", "font '" + fontName.getName() + "' not found in resources"); } - context.getGraphicsState().getTextState().setFont(font); + textState.setFont(font); } @Override diff --git a/library/src/main/java/com/tom_roush/pdfbox/cos/COSArray.java b/library/src/main/java/com/tom_roush/pdfbox/cos/COSArray.java index a2fa86819..f9607c727 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/cos/COSArray.java +++ b/library/src/main/java/com/tom_roush/pdfbox/cos/COSArray.java @@ -23,7 +23,6 @@ import java.util.List; import com.tom_roush.pdfbox.pdmodel.common.COSObjectable; - /** * An array of PDFBase objects as part of the PDF document. * @@ -117,12 +116,14 @@ public void addAll( Collection objectsList ) * * @param objectList The list of objects to add. */ - public void addAll( COSArray objectList ) + public void addAll(COSArray objectList) { - if( objectList != null ) + if (objectList == null) { - objects.addAll( objectList.objects ); + return; } + + objects.addAll(objectList.objects); } /** @@ -472,11 +473,18 @@ public int indexOfObject(COSBase object) return i; } } - else if (item.equals(object) - || item instanceof COSObject && ((COSObject) item).getObject().equals(object)) + else if (item.equals(object)) { return i; } + else if (item instanceof COSObject) + { + COSBase cosBase = ((COSObject) item).getObject(); + if (cosBase != null && cosBase.equals(object)) + { + return i; + } + } } return -1; } @@ -525,7 +533,7 @@ public Object accept(ICOSVisitor visitor) throws IOException @Override public boolean isNeedToBeUpdated() { - return needToBeUpdated; + return needToBeUpdated; } /** @@ -540,7 +548,7 @@ public boolean isNeedToBeUpdated() @Override public void setNeedToBeUpdated(boolean flag) { - needToBeUpdated = flag; + needToBeUpdated = flag; } /** diff --git a/library/src/main/java/com/tom_roush/pdfbox/cos/COSDictionary.java b/library/src/main/java/com/tom_roush/pdfbox/cos/COSDictionary.java index 1a56f17bc..8f1e9cd30 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/cos/COSDictionary.java +++ b/library/src/main/java/com/tom_roush/pdfbox/cos/COSDictionary.java @@ -16,6 +16,8 @@ */ package com.tom_roush.pdfbox.cos; +import android.graphics.Rect; + import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; @@ -31,7 +33,6 @@ import com.tom_roush.pdfbox.pdmodel.common.COSObjectable; import com.tom_roush.pdfbox.util.DateConverter; import com.tom_roush.pdfbox.util.SmallMap; - /** * This class represents a dictionary where name/value pairs reside. * @@ -96,8 +97,9 @@ public COSName getKeyForValue(Object value) { Object nextValue = entry.getValue(); if (nextValue.equals(value) - || (nextValue instanceof COSObject && ((COSObject) nextValue).getObject() - .equals(value))) + || (nextValue instanceof COSObject && + ((COSObject) nextValue).getObject() != null && + ((COSObject) nextValue).getObject().equals(value))) { return entry.getKey(); } @@ -937,7 +939,7 @@ public Calendar getEmbeddedDate(String embedded, COSName key) throws IOException * @throws IOException If there is an error converting to a date. */ public Calendar getEmbeddedDate(String embedded, String key, Calendar defaultValue) - throws IOException + throws IOException { return getEmbeddedDate(embedded, COSName.getPDFName(key), defaultValue); } @@ -952,7 +954,7 @@ public Calendar getEmbeddedDate(String embedded, String key, Calendar defaultVal * @throws IOException If there is an error converting to a date. */ public Calendar getEmbeddedDate(String embedded, COSName key, Calendar defaultValue) - throws IOException + throws IOException { Calendar retval = defaultValue; COSDictionary eDic = (COSDictionary) getDictionaryObject(embedded); @@ -1431,13 +1433,13 @@ public Object accept(ICOSVisitor visitor) throws IOException @Override public boolean isNeedToBeUpdated() { - return needToBeUpdated; + return needToBeUpdated; } @Override public void setNeedToBeUpdated(boolean flag) { - needToBeUpdated = flag; + needToBeUpdated = flag; } /** @@ -1561,11 +1563,11 @@ private static String getDictionaryString(COSBase base, List objs) thro if (objs.contains(base)) { // avoid endless recursion - return String.valueOf(base.hashCode()); + return "hash:" + base.hashCode(); } - objs.add(base); if (base instanceof COSDictionary) { + objs.add(base); StringBuilder sb = new StringBuilder("COSDictionary{"); for (Map.Entry x : ((COSDictionary) base).entrySet()) { @@ -1586,6 +1588,7 @@ private static String getDictionaryString(COSBase base, List objs) thro } if (base instanceof COSArray) { + objs.add(base); StringBuilder sb = new StringBuilder("COSArray{"); for (COSBase x : ((COSArray) base)) { @@ -1597,6 +1600,7 @@ private static String getDictionaryString(COSBase base, List objs) thro } if (base instanceof COSObject) { + objs.add(base); COSObject obj = (COSObject) base; return "COSObject{" + getDictionaryString(obj.getObject(), objs) + "}"; } diff --git a/library/src/main/java/com/tom_roush/pdfbox/cos/COSDocument.java b/library/src/main/java/com/tom_roush/pdfbox/cos/COSDocument.java index 572820bf2..2035b9ba0 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/cos/COSDocument.java +++ b/library/src/main/java/com/tom_roush/pdfbox/cos/COSDocument.java @@ -30,7 +30,6 @@ import com.tom_roush.pdfbox.multipdf.Splitter; import com.tom_roush.pdfbox.pdfparser.PDFObjectStreamParser; import com.tom_roush.pdfbox.pdmodel.PDDocument; - /** * This is the in-memory representation of the PDF document. You need to call * close() on this object when you are done using it!! @@ -40,7 +39,6 @@ */ public class COSDocument extends COSBase implements Closeable { - private float version = 1.4f; /** @@ -57,7 +55,7 @@ public class COSDocument extends COSBase implements Closeable new HashMap(); /** - * List containing all streams which are created when creating a new pdf. + * List containing all streams which are created when creating a new pdf. */ private final List streams = new ArrayList(); @@ -69,7 +67,7 @@ public class COSDocument extends COSBase implements Closeable private boolean warnMissingClose = true; /** - * Signal that document is already decrypted. + * Signal that document is already decrypted. */ private boolean isDecrypted = false; @@ -350,7 +348,10 @@ public COSArray getDocumentID() } /** - * This will set the document ID. + * This will set the document ID. This should be an array of two strings. This method cannot be + * used to remove the document id by passing null or an empty array; it will be recreated. Only + * the first existing string is used when writing, the second one is always recreated. If you + * don't want this, you'll have to modify the {@code COSWriter} class, look for {@link COSName#ID}. * * @param id The document id. */ @@ -516,7 +517,7 @@ protected void finalize() throws IOException { if (warnMissingClose) { - Log.w("PdfBox-Android", "Warning: You did not close a PDF Document" ); + Log.w("PdfBox-Android", "Warning: You did not close a PDF Document" ); } close(); } @@ -551,9 +552,9 @@ public void dereferenceObjectStreams() throws IOException { COSObjectKey key = new COSObjectKey(next); if (objectPool.get(key) == null || objectPool.get(key).getObject() == null - // xrefTable stores negated objNr of objStream for objects in objStreams - || (xrefTable.containsKey(key) - && xrefTable.get(key) == -objStream.getObjectNumber())) + // xrefTable stores negated objNr of objStream for objects in objStreams + || (xrefTable.containsKey(key) + && xrefTable.get(key) == -objStream.getObjectNumber())) { COSObject obj = getObjectFromPool(key); obj.setObject(next.getObject()); @@ -623,7 +624,7 @@ public Map getXrefTable() } /** - * This method set the startxref value of the document. This will only + * This method set the startxref value of the document. This will only * be needed for incremental updates. * * @param startXrefValue the value for startXref @@ -640,7 +641,7 @@ public void setStartXref(long startXrefValue) */ public long getStartXref() { - return startXref; + return startXref; } /** diff --git a/library/src/main/java/com/tom_roush/pdfbox/cos/COSFloat.java b/library/src/main/java/com/tom_roush/pdfbox/cos/COSFloat.java index f4604de3d..d8bc8eb87 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/cos/COSFloat.java +++ b/library/src/main/java/com/tom_roush/pdfbox/cos/COSFloat.java @@ -19,7 +19,6 @@ import java.io.IOException; import java.io.OutputStream; import java.math.BigDecimal; - /** * This class represents a floating point number in a PDF document. * @@ -38,7 +37,7 @@ public class COSFloat extends COSNumber */ public COSFloat( float aFloat ) { - // use a BigDecimal as intermediate state to avoid + // use a BigDecimal as intermediate state to avoid // a floating point string representation of the float value value = new BigDecimal(String.valueOf(aFloat)); valueAsString = removeNullDigits(value.toPlainString()); @@ -73,6 +72,11 @@ else if (aFloat.matches("^0\\.0*\\-\\d+")) // PDFBOX-3500 has 0.-262 valueAsString = "-" + valueAsString.replaceFirst("\\-", ""); } + else if (aFloat.matches("^-\\d+\\.-\\d+")) + { + // PDFBOX-5829 has -12.-1 + valueAsString = "-" + valueAsString.replace("-", ""); + } else { throw new IOException("Error expected floating point number actual='" + aFloat + "'", e); @@ -184,7 +188,7 @@ public int intValue() public boolean equals( Object o ) { return o instanceof COSFloat && - Float.floatToIntBits(((COSFloat)o).value.floatValue()) == Float.floatToIntBits(value.floatValue()); + Float.floatToIntBits(((COSFloat)o).value.floatValue()) == Float.floatToIntBits(value.floatValue()); } /** diff --git a/library/src/main/java/com/tom_roush/pdfbox/cos/COSInputStream.java b/library/src/main/java/com/tom_roush/pdfbox/cos/COSInputStream.java index 5df86e1bf..943b80238 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/cos/COSInputStream.java +++ b/library/src/main/java/com/tom_roush/pdfbox/cos/COSInputStream.java @@ -17,13 +17,14 @@ package com.tom_roush.pdfbox.cos; +import android.util.Log; + import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -35,7 +36,6 @@ import com.tom_roush.pdfbox.io.RandomAccessInputStream; import com.tom_roush.pdfbox.io.RandomAccessOutputStream; import com.tom_roush.pdfbox.io.ScratchFile; - /** * An InputStream which reads from an encoded COS stream. * @@ -43,6 +43,7 @@ */ public final class COSInputStream extends FilterInputStream { + /** * Creates a new COSInputStream from an encoded input stream. * @@ -50,22 +51,17 @@ public final class COSInputStream extends FilterInputStream * @param parameters Filter parameters. * @param in Encoded input stream. * @param scratchFile Scratch file to use, or null. + * @param options decode options for the encoded stream * @return Decoded stream. * @throws IOException If the stream could not be read. */ static COSInputStream create(List filters, COSDictionary parameters, InputStream in, - ScratchFile scratchFile) throws IOException - { - return create(filters, parameters, in, scratchFile, DecodeOptions.DEFAULT); - } - - static COSInputStream create(List filters, COSDictionary parameters, InputStream in, - ScratchFile scratchFile, DecodeOptions options) throws IOException + ScratchFile scratchFile, DecodeOptions options) throws IOException { InputStream input = in; if (filters.isEmpty()) { - return new COSInputStream(in, Collections.emptyList()); + return new COSInputStream(in, null); } List results = new ArrayList(filters.size()); @@ -74,7 +70,17 @@ static COSInputStream create(List filters, COSDictionary parameters, Inp Set filterSet = new HashSet(filters); if (filterSet.size() != filters.size()) { - throw new IOException("Duplicate"); + List reducedFilterList = new ArrayList(); + for (Filter filter : filters) + { + if (!reducedFilterList.contains(filter)) + { + reducedFilterList.add(filter); + } + } + // replace origin list with the reduced one + filters = reducedFilterList; + Log.w("PdfBox-Android", "Removed duplicated filter entries"); } } // apply filters @@ -104,37 +110,38 @@ public void close() throws IOException input = new ByteArrayInputStream(output.toByteArray()); } } - return new COSInputStream(input, results); + if (results.isEmpty()) + { + return new COSInputStream(in, null); + } + return new COSInputStream(input, results.get(results.size() - 1)); } - private final List decodeResults; + private final DecodeResult decodeResult; /** * Constructor. * * @param input decoded stream - * @param decodeResults results of decoding + * @param decodeResult result of decoding */ - private COSInputStream(InputStream input, List decodeResults) + private COSInputStream(InputStream input, DecodeResult decodeResult) { super(input); - this.decodeResults = decodeResults; + this.decodeResult = decodeResult; } /** * Returns the result of the last filter, for use by repair mechanisms. * - * @return the result of the decoding. + * @return the result of the last filter */ public DecodeResult getDecodeResult() { - if (decodeResults.isEmpty()) + if (decodeResult == null) { return DecodeResult.DEFAULT; } - else - { - return decodeResults.get(decodeResults.size() - 1); - } + return decodeResult; } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/cos/COSInteger.java b/library/src/main/java/com/tom_roush/pdfbox/cos/COSInteger.java index 27cfdc646..c1c55fd3d 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/cos/COSInteger.java +++ b/library/src/main/java/com/tom_roush/pdfbox/cos/COSInteger.java @@ -18,7 +18,6 @@ import java.io.IOException; import java.io.OutputStream; - /** * This class represents an integer number in a PDF document. * @@ -70,12 +69,12 @@ public final class COSInteger extends COSNumber /** * Constant for an out of range value which is bigger than Log.MAX_VALUE. */ - protected static final COSInteger OUT_OF_RANGE_MAX = getInvalid(true); + static final COSInteger OUT_OF_RANGE_MAX = getInvalid(true); /** * Constant for an out of range value which is smaller than Log.MIN_VALUE. */ - protected static final COSInteger OUT_OF_RANGE_MIN = getInvalid(false); + static final COSInteger OUT_OF_RANGE_MIN = getInvalid(false); /** * Returns a COSInteger instance with the given value. @@ -101,7 +100,7 @@ public static COSInteger get(long val) private static COSInteger getInvalid(boolean maxValue) { return maxValue ? new COSInteger(Long.MAX_VALUE, false) - : new COSInteger(Long.MIN_VALUE, false); + : new COSInteger(Long.MIN_VALUE, false); } private final long value; diff --git a/library/src/main/java/com/tom_roush/pdfbox/cos/COSName.java b/library/src/main/java/com/tom_roush/pdfbox/cos/COSName.java index 7935be826..227b11052 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/cos/COSName.java +++ b/library/src/main/java/com/tom_roush/pdfbox/cos/COSName.java @@ -16,15 +16,19 @@ */ package com.tom_roush.pdfbox.cos; +import android.graphics.Path; +import android.graphics.Rect; + import java.io.IOException; import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import com.tom_roush.pdfbox.util.Charsets; import com.tom_roush.pdfbox.util.Hex; - /** * A PDF Name object. * @@ -33,12 +37,12 @@ public final class COSName extends COSBase implements Comparable { // using ConcurrentHashMap because this can be accessed by multiple threads - private static final Map nameMap = new ConcurrentHashMap( - 8192); + private static final Map nameMap = new ConcurrentHashMap( + 8192); // all common COSName values are stored in this HashMap // they are already defined as static constants and don't need to be synchronized - private static final Map commonNameMap = new HashMap(768); + private static final Map commonNameMap = new HashMap(768); // // IMPORTANT: this list is *alphabetized* and does not need any JavaDoc @@ -51,12 +55,15 @@ public final class COSName extends COSBase implements Comparable public static final COSName AC = new COSName("AC"); public static final COSName ACRO_FORM = new COSName("AcroForm"); public static final COSName ACTUAL_TEXT = new COSName("ActualText"); + public static final COSName ADBE = new COSName("ADBE"); public static final COSName ADBE_PKCS7_DETACHED = new COSName("adbe.pkcs7.detached"); public static final COSName ADBE_PKCS7_SHA1 = new COSName("adbe.pkcs7.sha1"); public static final COSName ADBE_X509_RSA_SHA1 = new COSName("adbe.x509.rsa_sha1"); public static final COSName ADOBE_PPKLITE = new COSName("Adobe.PPKLite"); public static final COSName AESV2 = new COSName("AESV2"); public static final COSName AESV3 = new COSName("AESV3"); + public static final COSName AF = new COSName("AF"); + public static final COSName AF_RELATIONSHIP = new COSName("AFRelationship"); public static final COSName AFTER = new COSName("After"); public static final COSName AI_META_DATA = new COSName("AIMetaData"); public static final COSName AIS = new COSName("AIS"); @@ -90,6 +97,7 @@ public final class COSName extends COSBase implements Comparable public static final COSName BASE_ENCODING = new COSName("BaseEncoding"); public static final COSName BASE_FONT = new COSName("BaseFont"); public static final COSName BASE_STATE = new COSName("BaseState"); + public static final COSName BASE_VERSION = new COSName("BaseVersion"); public static final COSName BBOX = new COSName("BBox"); public static final COSName BC = new COSName("BC"); public static final COSName BE = new COSName("BE"); @@ -125,6 +133,7 @@ public final class COSName extends COSBase implements Comparable public static final COSName CCITTFAX_DECODE_ABBREVIATION = new COSName("CCF"); public static final COSName CENTER_WINDOW = new COSName("CenterWindow"); public static final COSName CERT = new COSName("Cert"); + public static final COSName CERTS = new COSName("Certs"); public static final COSName CF = new COSName("CF"); public static final COSName CFM = new COSName("CFM"); //** Acro form field type for choice fields. @@ -139,6 +148,7 @@ public final class COSName extends COSBase implements Comparable public static final COSName CID_SET = new COSName("CIDSet"); public static final COSName CIDSYSTEMINFO = new COSName("CIDSystemInfo"); public static final COSName CL = new COSName("CL"); + public static final COSName CLASS_MAP = new COSName("ClassMap"); public static final COSName CLR_F = new COSName("ClrF"); public static final COSName CLR_FF = new COSName("ClrFf"); public static final COSName CMAP = new COSName("CMap"); @@ -167,6 +177,8 @@ public final class COSName extends COSBase implements Comparable public static final COSName CP = new COSName("CP"); public static final COSName CREATION_DATE = new COSName("CreationDate"); public static final COSName CREATOR = new COSName("Creator"); + public static final COSName CRL = new COSName("CRL"); + public static final COSName CRLS = new COSName("CRLs"); public static final COSName CROP_BOX = new COSName("CropBox"); public static final COSName CRYPT = new COSName("Crypt"); public static final COSName CS = new COSName("CS"); @@ -217,6 +229,7 @@ public final class COSName extends COSBase implements Comparable public static final COSName DP = new COSName("DP"); public static final COSName DR = new COSName("DR"); public static final COSName DS = new COSName("DS"); + public static final COSName DSS = new COSName("DSS"); public static final COSName DUPLEX = new COSName("Duplex"); public static final COSName DUR = new COSName("Dur"); public static final COSName DV = new COSName("DV"); @@ -238,9 +251,13 @@ public final class COSName extends COSBase implements Comparable public static final COSName ENCODING_ETEN_B5_V = new COSName("ETen-B5-V"); public static final COSName ENCRYPT = new COSName("Encrypt"); public static final COSName ENCRYPT_META_DATA = new COSName("EncryptMetadata"); + public static final COSName ENCRYPTED_PAYLOAD = new COSName("EncryptedPayload"); public static final COSName END_OF_LINE = new COSName("EndOfLine"); public static final COSName ENTRUST_PPKEF = new COSName("Entrust.PPKEF"); public static final COSName EXCLUSION = new COSName("Exclusion"); + public static final COSName EXTENSIONS = new COSName("Extensions"); + public static final COSName EXTENSION_LEVEL = new COSName("ExtensionLevel"); + public static final COSName EX_DATA = new COSName("ExData"); public static final COSName EXT_G_STATE = new COSName("ExtGState"); public static final COSName EXTEND = new COSName("Extend"); public static final COSName EXTENDS = new COSName("Extends"); @@ -345,6 +362,7 @@ public final class COSName extends COSBase implements Comparable public static final COSName LENGTH2 = new COSName("Length2"); public static final COSName LIGHTEN = new COSName("Lighten"); public static final COSName LIMITS = new COSName("Limits"); + public static final COSName LINK = getPDFName("Link"); public static final COSName LJ = new COSName("LJ"); public static final COSName LL = new COSName("LL"); public static final COSName LLE = new COSName("LLE"); @@ -366,6 +384,7 @@ public final class COSName extends COSBase implements Comparable public static final COSName MAX_LEN = new COSName("MaxLen"); public static final COSName MAX_WIDTH = new COSName("MaxWidth"); public static final COSName MCID = new COSName("MCID"); + public static final COSName MCR = getPDFName("MCR"); public static final COSName MDP = new COSName("MDP"); public static final COSName MEDIA_BOX = new COSName("MediaBox"); public static final COSName MEASURE = new COSName("Measure"); @@ -394,12 +413,15 @@ public final class COSName extends COSBase implements Comparable // O public static final COSName O = new COSName("O"); public static final COSName OBJ = new COSName("Obj"); + public static final COSName OBJR = new COSName("OBJR"); public static final COSName OBJ_STM = new COSName("ObjStm"); public static final COSName OC = new COSName("OC"); public static final COSName OCG = new COSName("OCG"); public static final COSName OCGS = new COSName("OCGs"); public static final COSName OCMD = new COSName("OCMD"); public static final COSName OCPROPERTIES = new COSName("OCProperties"); + public static final COSName OCSP = new COSName("OCSP"); + public static final COSName OCSPS = new COSName("OCSPs"); public static final COSName OE = new COSName("OE"); public static final COSName OID = new COSName("OID"); @@ -426,7 +448,7 @@ public final class COSName extends COSBase implements Comparable public static final COSName OUTLINES = new COSName("Outlines"); public static final COSName OUTPUT_CONDITION = new COSName("OutputCondition"); public static final COSName OUTPUT_CONDITION_IDENTIFIER = new COSName( - "OutputConditionIdentifier"); + "OutputConditionIdentifier"); public static final COSName OUTPUT_INTENT = new COSName("OutputIntent"); public static final COSName OUTPUT_INTENTS = new COSName("OutputIntents"); public static final COSName OVERLAY = new COSName("Overlay"); @@ -452,6 +474,7 @@ public final class COSName extends COSBase implements Comparable public static final COSName PERCEPTUAL = new COSName("Perceptual"); public static final COSName PIECE_INFO = new COSName("PieceInfo"); public static final COSName PG = new COSName("Pg"); + public static final COSName POPUP = new COSName("Popup"); public static final COSName PRE_RELEASE = new COSName("PreRelease"); public static final COSName PREDICTOR = new COSName("Predictor"); public static final COSName PREV = new COSName("Prev"); @@ -512,6 +535,7 @@ public final class COSName extends COSBase implements Comparable public static final COSName SIZE = new COSName("Size"); public static final COSName SM = new COSName("SM"); public static final COSName SMASK = new COSName("SMask"); + public static final COSName SMASK_IN_DATA = new COSName("SMaskInData"); public static final COSName SOFT_LIGHT = new COSName("SoftLight"); public static final COSName SORT = new COSName("Sort"); public static final COSName SOUND = new COSName("Sound"); @@ -548,6 +572,7 @@ public final class COSName extends COSBase implements Comparable public static final COSName TARGET = new COSName("Target"); public static final COSName TEMPLATES = new COSName("Templates"); public static final COSName THREADS = new COSName("Threads"); + public static final COSName THREE_DD = new COSName("3DD"); public static final COSName THUMB = new COSName("Thumb"); public static final COSName TI = new COSName("TI"); public static final COSName TILING_TYPE = new COSName("TilingType"); @@ -597,6 +622,7 @@ public final class COSName extends COSBase implements Comparable public static final COSName VIEWER_PREFERENCES = new COSName("ViewerPreferences"); public static final COSName VOLUME = new COSName("Volume"); public static final COSName VP = new COSName("VP"); + public static final COSName VRI = new COSName("VRI"); // W public static final COSName W = new COSName("W"); public static final COSName W2 = new COSName("W2"); @@ -620,100 +646,180 @@ public final class COSName extends COSBase implements Comparable public static final COSName ZA_DB = new COSName("ZaDb"); // fields - private final String name; - private final int hashCode; /** - * This will get a COSName object with that name. + *

Per PDF 32000-1:2008 §7.3.5: Beginning with PDF 1.2 a name object is an atomic symbol + * uniquely defined by a sequence of any characters (8-bit values) except null + * (character code 0).

+ */ + private final byte[] nameBytes; + + /** + * Returns a {@code COSName} whose byte sequence is the UTF-8 encoding of {@code aName}. * - * @param aName The name of the object. + *

This is the standard factory for names defined in Java source code (e.g. the static + * constants above). All well-formed PDF names defined by the spec are ASCII, so the UTF-8 + * encoding is a transparent identity transform for those cases.

* - * @return A COSName with the specified name. + * @param aName the name string; must not be {@code null} + * @return a canonicalised {@code COSName} instance */ public static COSName getPDFName(String aName) { - COSName name = null; - if (aName != null) + return getPDFName(aName.getBytes(Charsets.UTF_8)); + } + + /** + * Returns a {@code COSName} whose byte sequence is exactly {@code bytes}. + * + *

This is the preferred factory when constructing a name directly from a PDF byte stream + * (i.e. after the parser has stripped the leading {@code /} and expanded all {@code #XX} + * escape sequences). Using this method preserves the spec-correct, byte-level identity of the + * name even when the bytes are not valid UTF-8.

+ * + *

Null bytes (0x00) are rejected; the spec explicitly excludes them.

+ * + * @param bytes the raw decoded byte sequence; must not be {@code null} and must not contain 0x00 + * @return a canonicalised {@code COSName} instance + * @throws IllegalArgumentException if {@code bytes} contains a null byte + */ + public static COSName getPDFName(byte[] bytes) + { + if (bytes == null) + { + return null; + } + + // Wrap for lookup only to avoid unnecessary copying of the byte array for the key. + ByteBuffer lookupKey = ByteBuffer.wrap(bytes); + + // Is it a common COSName ?? + COSName name = commonNameMap.get(lookupKey); + if (name == null) { - // Is it a common COSName ?? - name = commonNameMap.get(aName); + // It seems to be a document specific COSName + name = nameMap.get(lookupKey); if (name == null) { - // It seems to be a document specific COSName - name = nameMap.get(aName); - if (name == null) - { - // name is added to the synchronized map in the constructor - name = new COSName(aName, false); - } + // name is added to the synchronized map in the constructor + name = new COSName(bytes, false); } } + return name; } /** - * Private constructor. This will limit the number of COSName objects. that are created. + * Private constructor. This will limit the number of COSName objects that are created. * - * @param aName The name of the COSName object. + * @param storedBytes The name of the COSName object. * @param staticValue Indicates if the COSName object is static so that it can be stored in the HashMap without * synchronizing. */ - private COSName(String aName, boolean staticValue) + private COSName(byte[] storedBytes, boolean staticValue) { - name = aName; - hashCode = name.hashCode(); + this.nameBytes = storedBytes; + ByteBuffer storedKey = ByteBuffer.wrap(storedBytes); if (staticValue) { - commonNameMap.put(aName, this); + commonNameMap.put(storedKey, this); } else { - nameMap.put(aName, this); + nameMap.put(storedKey, this); } } /** - * Private constructor. This will limit the number of COSName objects. that are created. + * Private constructor. This will limit the number of COSName objects that are created. * * @param aName The name of the COSName object. */ private COSName(String aName) { - this(aName, true); + this(aName.getBytes(Charsets.UTF_8), true); + } + + /** + * Returns the raw byte sequence that defines this name. + * + *

This is the atomic content/identity of the name. Prefer this over + * {@link #getName()} whenever you need to write name bytes to an output stream, compare names + * parsed from a PDF, or otherwise operate at the byte level.

+ * + * @return a defensive copy of the internal byte array; never {@code null} + */ + public byte[] getBytes() + { + return Arrays.copyOf(nameBytes, nameBytes.length); } /** - * This will get the name of this COSName object. + * Returns the name decoded as a UTF-8 {@code String}. + * + *

This method exists primarily for backward compatibility and for cases where the + * readable value needs to be stored.

+ * + *

Per PDF 32000-1:2008 §7.3.5, ... However, occasionally the need arises to treat a name object + * as text, such as one that represents a font ...

+ * + *

... In such situations, the sequence of bytes (after expansion of NUMBER SIGN sequences, if any) + * should be interpreted according to UTF-8...

+ * + *

Use {@link #getBytes()} when byte-level fidelity is required.

* * @return The name of the object. */ public String getName() { - return name; + String utf8String = new String(nameBytes, Charsets.UTF_8); + + //check for lossy decoding, which can happen if the name contains + // bytes that are not valid UTF-8 + if (utf8String.indexOf('\uFFFD') >= 0) + { + // fall back to ISO-8859-1, which is a single-byte encoding that can decode any + // byte sequence without loss + return new String(nameBytes, Charsets.ISO_8859_1); + } + return utf8String; } @Override public String toString() { - return "COSName{" + name + "}"; + return "COSName{" + getName() + "}"; } @Override public boolean equals(Object object) { - return object instanceof COSName && name.equals(((COSName) object).name); + return object instanceof COSName && Arrays.equals(nameBytes, ((COSName) object).nameBytes); } @Override public int hashCode() { - return hashCode; + return Arrays.hashCode(nameBytes); } @Override public int compareTo(COSName other) { - return name.compareTo(other.name); + if (this == other) + { + return 0; + } + int len = Math.min(nameBytes.length, other.nameBytes.length); + for (int i = 0; i < len; i++) + { + int diff = (nameBytes[i] & 0xFF) - (other.nameBytes[i] & 0xFF); + if (diff != 0) + { + return diff; + } + } + return nameBytes.length - other.nameBytes.length; } /** @@ -722,7 +828,7 @@ public int compareTo(COSName other) */ public boolean isEmpty() { - return name.isEmpty(); + return nameBytes.length == 0; } @Override @@ -740,23 +846,23 @@ public Object accept(ICOSVisitor visitor) throws IOException public void writePDF(OutputStream output) throws IOException { output.write('/'); - byte[] bytes = getName().getBytes(Charsets.UTF_8); + byte[] bytes = getBytes(); for (byte b : bytes) { int current = b & 0xFF; // be more restrictive than the PDF spec, "Name Objects", see PDFBOX-2073 if (current >= 'A' && current <= 'Z' || - current >= 'a' && current <= 'z' || - current >= '0' && current <= '9' || - current == '+' || - current == '-' || - current == '_' || - current == '@' || - current == '*' || - current == '$' || - current == ';' || - current == '.') + current >= 'a' && current <= 'z' || + current >= '0' && current <= '9' || + current == '+' || + current == '-' || + current == '_' || + current == '@' || + current == '*' || + current == '$' || + current == ';' || + current == '.') { output.write(current); } diff --git a/library/src/main/java/com/tom_roush/pdfbox/cos/COSNumber.java b/library/src/main/java/com/tom_roush/pdfbox/cos/COSNumber.java index abbfc5773..189eaa834 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/cos/COSNumber.java +++ b/library/src/main/java/com/tom_roush/pdfbox/cos/COSNumber.java @@ -17,7 +17,6 @@ package com.tom_roush.pdfbox.cos; import java.io.IOException; - /** * This class represents an abstract number in a PDF document. * @@ -113,14 +112,14 @@ else if (digit == '-' || digit == '.') { // check if the given string could be a number at all String numberString = number.startsWith("+") || number.startsWith("-") - ? number.substring(1) : number; - if (!numberString.matches("[0-9]*")) + ? number.substring(1) : number; + if (!numberString.matches("\\d*")) { throw new IOException("Not a number: " + number); } // return a limited COSInteger value which is marked as invalid return number.startsWith("-") ? COSInteger.OUT_OF_RANGE_MIN - : COSInteger.OUT_OF_RANGE_MAX; + : COSInteger.OUT_OF_RANGE_MAX; } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/cos/COSObject.java b/library/src/main/java/com/tom_roush/pdfbox/cos/COSObject.java index 2b3c5f519..f54584625 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/cos/COSObject.java +++ b/library/src/main/java/com/tom_roush/pdfbox/cos/COSObject.java @@ -17,7 +17,6 @@ package com.tom_roush.pdfbox.cos; import java.io.IOException; - /** * This class represents a PDF object. * @@ -51,7 +50,11 @@ public COSObject( COSBase object ) throws IOException * @param key The key to the value that we are searching for. * * @return The pdf object that matches the key. + * + * @deprecated This will be removed in 3.0. Call {@link #getObject()} to find out if it has the + * correct type and then call {@link #getDictionaryObject(com.tom_roush.pdfbox.cos.COSName)}. */ + @Deprecated public COSBase getDictionaryObject( COSName key ) { COSBase retval =null; @@ -68,7 +71,11 @@ public COSBase getDictionaryObject( COSName key ) * @param key The key to the value that we are searching for. * * @return The pdf object that matches the key. + * + * @deprecated This will be removed in 3.0. Call {@link #getObject()} to find out if it has the + * correct type and then call {@link #getDictionaryObject(com.tom_roush.pdfbox.cos.COSName)}. */ + @Deprecated public COSBase getItem( COSName key ) { COSBase retval =null; diff --git a/library/src/main/java/com/tom_roush/pdfbox/cos/COSString.java b/library/src/main/java/com/tom_roush/pdfbox/cos/COSString.java index 36a4b07dd..34d47f713 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/cos/COSString.java +++ b/library/src/main/java/com/tom_roush/pdfbox/cos/COSString.java @@ -24,7 +24,6 @@ import com.tom_roush.pdfbox.util.Charsets; import com.tom_roush.pdfbox.util.Hex; - /** * A string object, which may be a text string, a PDFDocEncoded string, ASCII string, or byte string. * @@ -45,12 +44,13 @@ */ public final class COSString extends COSBase { + private byte[] bytes; private boolean forceHexForm; // legacy behaviour for old PDFParser public static final boolean FORCE_PARSING = - Boolean.getBoolean("com.tom_roush.pdfbox.forceParsing"); + Boolean.getBoolean("com.tom_roush.pdfbox.forceParsing"); /** * Creates a new PDF string from a byte array. This method can be used to read a string from @@ -106,36 +106,60 @@ public COSString(String text) */ public static COSString parseHex(String hex) throws IOException { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - StringBuilder hexBuffer = new StringBuilder(hex.trim()); - - // if odd number then the last hex digit is assumed to be 0 - if (hexBuffer.length() % 2 != 0) + // skip leading and trailing whitespace + int end = hex.length(); + while (end > 0 && Character.isWhitespace(hex.charAt(end - 1))) + { + end--; + } + int start = 0; + while (start < end && Character.isWhitespace(hex.charAt(start))) { - hexBuffer.append('0'); + start++; } - int length = hexBuffer.length(); + int length = end - start; + ByteArrayOutputStream bytes = new ByteArrayOutputStream((length + 1) / 2); + + boolean isLengthUneven = length % 2 != 0; + if (isLengthUneven) + { + length--; + } for (int i = 0; i < length; i += 2) { - try + int value = 16 * Hex.getHexValue(hex.charAt(i)) + Hex.getHexValue(hex.charAt(i + 1)); + if (value >= 0) + { + bytes.write(value); + } + else if (FORCE_PARSING) { - bytes.write(Integer.parseInt(hexBuffer.substring(i, i + 2), 16)); + Log.w("PdfBox-Android", "Encountered a malformed hex string"); + bytes.write('?'); // todo: what does Acrobat do? Any example PDFs? } - catch (NumberFormatException e) + else { - if (FORCE_PARSING) - { - Log.w("PdfBox-Android", "Encountered a malformed hex string"); - bytes.write('?'); // todo: what does Acrobat do? Any example PDFs? - } - else - { - throw new IOException("Invalid hex string: " + hex, e); - } + throw new IOException("Invalid hex string: " + hex); + } + } + if (isLengthUneven) + { + int value = 16 * Hex.getHexValue(hex.charAt(length)); + if (value >= 0) + { + bytes.write(value); + } + else if (FORCE_PARSING) + { + Log.w("PdfBox-Android", "Encountered a malformed hex string"); + bytes.write('?'); // todo: what does Acrobat do? Any example PDFs? + } + else + { + throw new IOException("Invalid hex string: " + hex); } } - return new COSString(bytes.toByteArray()); } @@ -246,7 +270,7 @@ public boolean equals(Object obj) { COSString strObj = (COSString) obj; return getString().equals(strObj.getString()) && - forceHexForm == strObj.forceHexForm; + forceHexForm == strObj.forceHexForm; } return false; } diff --git a/library/src/main/java/com/tom_roush/pdfbox/cos/PDFDocEncoding.java b/library/src/main/java/com/tom_roush/pdfbox/cos/PDFDocEncoding.java index 2aaec0baf..98ea27faa 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/cos/PDFDocEncoding.java +++ b/library/src/main/java/com/tom_roush/pdfbox/cos/PDFDocEncoding.java @@ -20,7 +20,6 @@ import java.io.ByteArrayOutputStream; import java.util.HashMap; import java.util.Map; - /** * The "PDFDocEncoding" encoding. Note that this is *not* a Type 1 font encoding, it is used only * within PDF "text strings". @@ -120,7 +119,7 @@ private static void set(int code, char unicode) */ public static String toString(byte[] bytes) { - StringBuilder sb = new StringBuilder(); + StringBuilder sb = new StringBuilder(bytes.length); for (byte b : bytes) { if ((b & 0xff) >= CODE_TO_UNI.length) @@ -140,7 +139,7 @@ public static String toString(byte[] bytes) */ public static byte[] getBytes(String text) { - ByteArrayOutputStream out = new ByteArrayOutputStream(); + ByteArrayOutputStream out = new ByteArrayOutputStream(text.length()); for (char c : text.toCharArray()) { Integer code = UNI_TO_CODE.get(c); diff --git a/library/src/main/java/com/tom_roush/pdfbox/filter/ASCIIHexFilter.java b/library/src/main/java/com/tom_roush/pdfbox/filter/ASCIIHexFilter.java index 913e52655..c05f7e4f6 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/filter/ASCIIHexFilter.java +++ b/library/src/main/java/com/tom_roush/pdfbox/filter/ASCIIHexFilter.java @@ -24,7 +24,6 @@ import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.util.Hex; - /** * Decodes data encoded in an ASCII hexadecimal form, reproducing the original binary data. * @@ -32,38 +31,39 @@ */ final class ASCIIHexFilter extends Filter { + private static final int[] REVERSE_HEX = { - /* 0 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - /* 10 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - /* 20 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - /* 30 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - /* 40 */ -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, - /* 50 */ 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, - /* 60 */ -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, - /* 70 */ 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, - /* 80 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - /* 90 */ -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, - /* 100 */ 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, - /* 110 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - /* 120 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - /* 130 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - /* 140 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - /* 150 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - /* 160 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - /* 170 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - /* 180 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - /* 190 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - /* 200 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - /* 210 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - /* 220 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - /* 230 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - /* 240 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - /* 250 */ -1, -1, -1, -1, -1, -1 + /* 0 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 10 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 20 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 30 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 40 */ -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, + /* 50 */ 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, + /* 60 */ -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, + /* 70 */ 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 80 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 90 */ -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, + /* 100 */ 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, + /* 110 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 120 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 130 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 140 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 150 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 160 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 170 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 180 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 190 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 200 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 210 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 220 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 230 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 240 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* 250 */ -1, -1, -1, -1, -1, -1 }; @Override public DecodeResult decode(InputStream encoded, OutputStream decoded, - COSDictionary parameters, int index) throws IOException + COSDictionary parameters, int index) throws IOException { int value, firstByte, secondByte; while ((firstByte = encoded.read()) != -1) @@ -109,12 +109,23 @@ public DecodeResult decode(InputStream encoded, OutputStream decoded, // 12 0x0C Form feed (FF) // 13 0x0D Carriage return (CR) // 32 0x20 Space (SP) - private boolean isWhitespace(int c) + private static boolean isWhitespace(int c) { - return c == 0 || c == 9 || c == 10 || c == 12 || c == 13 || c == 32; + switch (c) + { + case 0: + case 9: + case 10: + case 12: + case 13: + case 32: + return true; + default: + return false; + } } - private boolean isEOD(int c) + private static boolean isEOD(int c) { return c == '>'; } diff --git a/library/src/main/java/com/tom_roush/pdfbox/filter/CCITTFaxDecoderStream.java b/library/src/main/java/com/tom_roush/pdfbox/filter/CCITTFaxDecoderStream.java index 430a838c5..31adcde45 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/filter/CCITTFaxDecoderStream.java +++ b/library/src/main/java/com/tom_roush/pdfbox/filter/CCITTFaxDecoderStream.java @@ -35,7 +35,6 @@ import java.io.IOException; import java.io.InputStream; import java.util.Arrays; - /** * CCITT Modified Huffman RLE, Group 3 (T4) and Group 4 (T6) fax compression. * @@ -86,8 +85,8 @@ final class CCITTFaxDecoderStream extends FilterInputStream { * @param options CCITT T.4 or T.6 options. * @param byteAligned enable byte alignment used in PDF files (EncodedByteAlign). */ - public CCITTFaxDecoderStream(final InputStream stream, final int columns, final int type, - final long options, final boolean byteAligned) { + CCITTFaxDecoderStream(final InputStream stream, final int columns, final int type, + final long options, final boolean byteAligned) { super(stream); this.columns = columns; @@ -591,148 +590,148 @@ void fill(final int depth, final int path, final Node node) throws IOException { } static final short[][] BLACK_CODES = { - { // 2 bits - 0x2, 0x3, - }, - { // 3 bits - 0x2, 0x3, - }, - { // 4 bits - 0x2, 0x3, - }, - { // 5 bits - 0x3, - }, - { // 6 bits - 0x4, 0x5, - }, - { // 7 bits - 0x4, 0x5, 0x7, - }, - { // 8 bits - 0x4, 0x7, - }, - { // 9 bits - 0x18, - }, - { // 10 bits - 0x17, 0x18, 0x37, 0x8, 0xf, - }, - { // 11 bits - 0x17, 0x18, 0x28, 0x37, 0x67, 0x68, 0x6c, 0x8, 0xc, 0xd, - }, - { // 12 bits - 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1c, 0x1d, 0x1e, 0x1f, 0x24, 0x27, 0x28, 0x2b, 0x2c, 0x33, - 0x34, 0x35, 0x37, 0x38, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x64, 0x65, - 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xd2, 0xd3, - 0xd4, 0xd5, 0xd6, 0xd7, 0xda, 0xdb, - }, - { // 13 bits - 0x4a, 0x4b, 0x4c, 0x4d, 0x52, 0x53, 0x54, 0x55, 0x5a, 0x5b, 0x64, 0x65, 0x6c, 0x6d, 0x72, 0x73, - 0x74, 0x75, 0x76, 0x77, - } + { // 2 bits + 0x2, 0x3, + }, + { // 3 bits + 0x2, 0x3, + }, + { // 4 bits + 0x2, 0x3, + }, + { // 5 bits + 0x3, + }, + { // 6 bits + 0x4, 0x5, + }, + { // 7 bits + 0x4, 0x5, 0x7, + }, + { // 8 bits + 0x4, 0x7, + }, + { // 9 bits + 0x18, + }, + { // 10 bits + 0x17, 0x18, 0x37, 0x8, 0xf, + }, + { // 11 bits + 0x17, 0x18, 0x28, 0x37, 0x67, 0x68, 0x6c, 0x8, 0xc, 0xd, + }, + { // 12 bits + 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1c, 0x1d, 0x1e, 0x1f, 0x24, 0x27, 0x28, 0x2b, 0x2c, 0x33, + 0x34, 0x35, 0x37, 0x38, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x64, 0x65, + 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xd2, 0xd3, + 0xd4, 0xd5, 0xd6, 0xd7, 0xda, 0xdb, + }, + { // 13 bits + 0x4a, 0x4b, 0x4c, 0x4d, 0x52, 0x53, 0x54, 0x55, 0x5a, 0x5b, 0x64, 0x65, 0x6c, 0x6d, 0x72, 0x73, + 0x74, 0x75, 0x76, 0x77, + } }; static final short[][] BLACK_RUN_LENGTHS = { - { // 2 bits - 3, 2, - }, - { // 3 bits - 1, 4, - }, - { // 4 bits - 6, 5, - }, - { // 5 bits - 7, - }, - { // 6 bits - 9, 8, - }, - { // 7 bits - 10, 11, 12, - }, - { // 8 bits - 13, 14, - }, - { // 9 bits - 15, - }, - { // 10 bits - 16, 17, 0, 18, 64, - }, - { // 11 bits - 24, 25, 23, 22, 19, 20, 21, 1792, 1856, 1920, - }, - { // 12 bits - 1984, 2048, 2112, 2176, 2240, 2304, 2368, 2432, 2496, 2560, 52, 55, 56, 59, 60, 320, 384, 448, 53, - 54, 50, 51, 44, 45, 46, 47, 57, 58, 61, 256, 48, 49, 62, 63, 30, 31, 32, 33, 40, 41, 128, 192, 26, - 27, 28, 29, 34, 35, 36, 37, 38, 39, 42, 43, - }, - { // 13 bits - 640, 704, 768, 832, 1280, 1344, 1408, 1472, 1536, 1600, 1664, 1728, 512, 576, 896, 960, 1024, 1088, - 1152, 1216, - } + { // 2 bits + 3, 2, + }, + { // 3 bits + 1, 4, + }, + { // 4 bits + 6, 5, + }, + { // 5 bits + 7, + }, + { // 6 bits + 9, 8, + }, + { // 7 bits + 10, 11, 12, + }, + { // 8 bits + 13, 14, + }, + { // 9 bits + 15, + }, + { // 10 bits + 16, 17, 0, 18, 64, + }, + { // 11 bits + 24, 25, 23, 22, 19, 20, 21, 1792, 1856, 1920, + }, + { // 12 bits + 1984, 2048, 2112, 2176, 2240, 2304, 2368, 2432, 2496, 2560, 52, 55, 56, 59, 60, 320, 384, 448, 53, + 54, 50, 51, 44, 45, 46, 47, 57, 58, 61, 256, 48, 49, 62, 63, 30, 31, 32, 33, 40, 41, 128, 192, 26, + 27, 28, 29, 34, 35, 36, 37, 38, 39, 42, 43, + }, + { // 13 bits + 640, 704, 768, 832, 1280, 1344, 1408, 1472, 1536, 1600, 1664, 1728, 512, 576, 896, 960, 1024, 1088, + 1152, 1216, + } }; public static final short[][] WHITE_CODES = { - { // 4 bits - 0x7, 0x8, 0xb, 0xc, 0xe, 0xf, - }, - { // 5 bits - 0x12, 0x13, 0x14, 0x1b, 0x7, 0x8, - }, - { // 6 bits - 0x17, 0x18, 0x2a, 0x2b, 0x3, 0x34, 0x35, 0x7, 0x8, - }, - { // 7 bits - 0x13, 0x17, 0x18, 0x24, 0x27, 0x28, 0x2b, 0x3, 0x37, 0x4, 0x8, 0xc, - }, - { // 8 bits - 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1a, 0x1b, 0x2, 0x24, 0x25, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, - 0x3, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x4, 0x4a, 0x4b, 0x5, 0x52, 0x53, 0x54, 0x55, 0x58, 0x59, - 0x5a, 0x5b, 0x64, 0x65, 0x67, 0x68, 0xa, 0xb, - }, - { // 9 bits - 0x98, 0x99, 0x9a, 0x9b, 0xcc, 0xcd, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb, - }, - { // 10 bits - }, - { // 11 bits - 0x8, 0xc, 0xd, - }, - { // 12 bits - 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1c, 0x1d, 0x1e, 0x1f, - } + { // 4 bits + 0x7, 0x8, 0xb, 0xc, 0xe, 0xf, + }, + { // 5 bits + 0x12, 0x13, 0x14, 0x1b, 0x7, 0x8, + }, + { // 6 bits + 0x17, 0x18, 0x2a, 0x2b, 0x3, 0x34, 0x35, 0x7, 0x8, + }, + { // 7 bits + 0x13, 0x17, 0x18, 0x24, 0x27, 0x28, 0x2b, 0x3, 0x37, 0x4, 0x8, 0xc, + }, + { // 8 bits + 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1a, 0x1b, 0x2, 0x24, 0x25, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, + 0x3, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x4, 0x4a, 0x4b, 0x5, 0x52, 0x53, 0x54, 0x55, 0x58, 0x59, + 0x5a, 0x5b, 0x64, 0x65, 0x67, 0x68, 0xa, 0xb, + }, + { // 9 bits + 0x98, 0x99, 0x9a, 0x9b, 0xcc, 0xcd, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb, + }, + { // 10 bits + }, + { // 11 bits + 0x8, 0xc, 0xd, + }, + { // 12 bits + 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1c, 0x1d, 0x1e, 0x1f, + } }; public static final short[][] WHITE_RUN_LENGTHS = { - { // 4 bits - 2, 3, 4, 5, 6, 7, - }, - { // 5 bits - 128, 8, 9, 64, 10, 11, - }, - { // 6 bits - 192, 1664, 16, 17, 13, 14, 15, 1, 12, - }, - { // 7 bits - 26, 21, 28, 27, 18, 24, 25, 22, 256, 23, 20, 19, - }, - { // 8 bits - 33, 34, 35, 36, 37, 38, 31, 32, 29, 53, 54, 39, 40, 41, 42, 43, 44, 30, 61, 62, 63, 0, 320, 384, 45, - 59, 60, 46, 49, 50, 51, 52, 55, 56, 57, 58, 448, 512, 640, 576, 47, 48, - }, - { // 9 bits - 1472, 1536, 1600, 1728, 704, 768, 832, 896, 960, 1024, 1088, 1152, 1216, 1280, 1344, 1408, - }, - { // 10 bits - }, - { // 11 bits - 1792, 1856, 1920, - }, - { // 12 bits - 1984, 2048, 2112, 2176, 2240, 2304, 2368, 2432, 2496, 2560, - } + { // 4 bits + 2, 3, 4, 5, 6, 7, + }, + { // 5 bits + 128, 8, 9, 64, 10, 11, + }, + { // 6 bits + 192, 1664, 16, 17, 13, 14, 15, 1, 12, + }, + { // 7 bits + 26, 21, 28, 27, 18, 24, 25, 22, 256, 23, 20, 19, + }, + { // 8 bits + 33, 34, 35, 36, 37, 38, 31, 32, 29, 53, 54, 39, 40, 41, 42, 43, 44, 30, 61, 62, 63, 0, 320, 384, 45, + 59, 60, 46, 49, 50, 51, 52, 55, 56, 57, 58, 448, 512, 640, 576, 47, 48, + }, + { // 9 bits + 1472, 1536, 1600, 1728, 704, 768, 832, 896, 960, 1024, 1088, 1152, 1216, 1280, 1344, 1408, + }, + { // 10 bits + }, + { // 11 bits + 1792, 1856, 1920, + }, + { // 12 bits + 1984, 2048, 2112, 2176, 2240, 2304, 2368, 2432, 2496, 2560, + } }; final static Node EOL; diff --git a/library/src/main/java/com/tom_roush/pdfbox/filter/CCITTFaxEncoderStream.java b/library/src/main/java/com/tom_roush/pdfbox/filter/CCITTFaxEncoderStream.java index 06d250f53..00844c004 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/filter/CCITTFaxEncoderStream.java +++ b/library/src/main/java/com/tom_roush/pdfbox/filter/CCITTFaxEncoderStream.java @@ -29,7 +29,6 @@ import java.io.IOException; import java.io.OutputStream; - /** * CCITT Modified Group 4 (T6) fax compression. * @@ -46,7 +45,7 @@ final class CCITTFaxEncoderStream extends OutputStream { private int currentBufferLength = 0; - private final byte[] inputBuffer; + private byte[] inputBuffer; private final int inputBufferLength; private final int columns; private final int rows; @@ -93,6 +92,9 @@ public void flush() throws IOException { @Override public void close() throws IOException { + changesCurrentRow = null; + changesReferenceRow = null; + inputBuffer = null; stream.close(); } diff --git a/library/src/main/java/com/tom_roush/pdfbox/filter/CCITTFaxFilter.java b/library/src/main/java/com/tom_roush/pdfbox/filter/CCITTFaxFilter.java index ca52e425f..cedfb1bcf 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/filter/CCITTFaxFilter.java +++ b/library/src/main/java/com/tom_roush/pdfbox/filter/CCITTFaxFilter.java @@ -24,7 +24,6 @@ import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.io.IOUtils; - /** * Decodes image data that has been encoded using either Group 3 or Group 4 * CCITT facsimile (fax) encoding, and encodes image data to Group 4. @@ -37,7 +36,7 @@ final class CCITTFaxFilter extends Filter { @Override public DecodeResult decode(InputStream encoded, OutputStream decoded, - COSDictionary parameters, int index) throws IOException + COSDictionary parameters, int index) throws IOException { // get decode parameters COSDictionary decodeParms = getDecodeParams(parameters, index); @@ -60,33 +59,82 @@ public DecodeResult decode(InputStream encoded, OutputStream decoded, // decompress data int k = decodeParms.getInt(COSName.K, 0); boolean encodedByteAlign = decodeParms.getBoolean(COSName.ENCODED_BYTE_ALIGN, false); - int arraySize = (cols + 7) / 8 * rows; - // TODO possible options?? + if (cols <= 0 || rows <= 0) + { + throw new IOException("Invalid CCITT image dimensions: cols=" + cols + ", rows=" + rows); + } + + long arraySizeLong = ((long) cols + 7) / 8 * rows; + + long maxBytes = 256 * 1024 * 1024L; + String sysProp = System.getProperty(Filter.SYSPROP_CCITTFAX_MAXBYTES); + + if (sysProp != null) + { + try + { + long parsed = Long.parseLong(sysProp); + if (parsed > 0) + { + maxBytes = parsed; + } + // else ignore zero/negative values + } + catch (NumberFormatException e) + { + // ignore invalid value, keep default + } + } + + if (arraySizeLong > maxBytes) + { + throw new IOException( + "CCITT decode buffer too large (" + arraySizeLong + " bytes) for cols=" + cols + + ", rows=" + rows + "; max allowed=" + maxBytes + + "; increase " + Filter.SYSPROP_CCITTFAX_MAXBYTES + " to override" + ); + } + + int arraySize = (int) arraySizeLong; byte[] decompressed = new byte[arraySize]; CCITTFaxDecoderStream s; int type; long tiffOptions = 0; if (k == 0) { - type = TIFFExtension.COMPRESSION_CCITT_T4; // Group 3 1D - byte[] streamData = new byte[20]; - int bytesRead = encoded.read(streamData); - PushbackInputStream pushbackInputStream = new PushbackInputStream(encoded, streamData.length); - pushbackInputStream.unread(streamData, 0, bytesRead); - encoded = pushbackInputStream; - if (streamData[0] != 0 || (streamData[1] >> 4 != 1 && streamData[1] != 1)) + if (decodeParms.containsKey(COSName.END_OF_LINE)) + { + // PDFBOX-6080: respect the parameter if it exists + boolean hasEndOfLine = decodeParms.getBoolean(COSName.END_OF_LINE, false); + type = hasEndOfLine ? TIFFExtension.COMPRESSION_CCITT_T4 : TIFFExtension.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE; + } + else { - // leading EOL (0b000000000001) not found, search further and try RLE if not - // found - type = TIFFExtension.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE; - short b = (short) (((streamData[0] << 8) + (streamData[1] & 0xff)) >> 4); - for (int i = 12; i < bytesRead * 8; i++) + // In twelvemonkeys, this part is found in CCITTFaxDecoderStream.findCompressionType() + // needed for 015315-p8-ccitt.pdf, PDFBOX-2123-1bit.pdf, PDFBOX-2778.pdf + type = TIFFExtension.COMPRESSION_CCITT_T4; // Group 3 1D + byte[] streamData = new byte[20]; + int bytesRead = encoded.read(streamData); + if (bytesRead == -1) + { + throw new IOException("EOF while reading CCITT header"); + } + PushbackInputStream pushbackInputStream = new PushbackInputStream(encoded, streamData.length); + pushbackInputStream.unread(streamData, 0, bytesRead); + encoded = pushbackInputStream; + if (streamData[0] != 0 || (streamData[1] >> 4 != 1 && streamData[1] != 1)) { - b = (short) ((b << 1) + ((streamData[(i / 8)] >> (7 - (i % 8))) & 0x01)); - if ((b & 0xFFF) == 1) + // leading EOL (0b000000000001) not found, search further and try RLE if not found + type = TIFFExtension.COMPRESSION_CCITT_MODIFIED_HUFFMAN_RLE; + short b = (short) (((streamData[0] << 8) + (streamData[1] & 0xff)) >> 4); + for (int i = 12; i < bytesRead * 8; i++) { - type = TIFFExtension.COMPRESSION_CCITT_T4; - break; + b = (short) ((b << 1) + ((streamData[(i / 8)] >> (7 - (i % 8))) & 0x01)); + if ((b & 0xFFF) == 1) + { + type = TIFFExtension.COMPRESSION_CCITT_T4; + break; + } } } } @@ -121,7 +169,7 @@ else if (k > 0) } void readFromDecoderStream(CCITTFaxDecoderStream decoderStream, byte[] result) - throws IOException + throws IOException { int pos = 0; int read; @@ -145,12 +193,12 @@ private void invertBitmap(byte[] bufferData) @Override protected void encode(InputStream input, OutputStream encoded, COSDictionary parameters) - throws IOException + throws IOException { int cols = parameters.getInt(COSName.COLUMNS); int rows = parameters.getInt(COSName.ROWS); CCITTFaxEncoderStream ccittFaxEncoderStream = - new CCITTFaxEncoderStream(encoded, cols, rows, TIFFExtension.FILL_LEFT_TO_RIGHT); + new CCITTFaxEncoderStream(encoded, cols, rows, TIFFExtension.FILL_LEFT_TO_RIGHT); IOUtils.copy(input, ccittFaxEncoderStream); } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/filter/CryptFilter.java b/library/src/main/java/com/tom_roush/pdfbox/filter/CryptFilter.java index 08123634f..798c032f0 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/filter/CryptFilter.java +++ b/library/src/main/java/com/tom_roush/pdfbox/filter/CryptFilter.java @@ -22,7 +22,6 @@ import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; - /** * Decrypts data encrypted by a security handler, reproducing the data as it was before encryption. * @author Adam Nichols @@ -31,9 +30,9 @@ final class CryptFilter extends Filter { @Override public DecodeResult decode(InputStream encoded, OutputStream decoded, - COSDictionary parameters, int index) throws IOException + COSDictionary parameters, int index) throws IOException { - COSName encryptionName = (COSName) parameters.getDictionaryObject(COSName.NAME); + COSName encryptionName = parameters.getCOSName(COSName.NAME); if(encryptionName == null || encryptionName.equals(COSName.IDENTITY)) { // currently the only supported implementation is the Identity crypt filter @@ -46,9 +45,9 @@ public DecodeResult decode(InputStream encoded, OutputStream decoded, @Override protected void encode(InputStream input, OutputStream encoded, COSDictionary parameters) - throws IOException + throws IOException { - COSName encryptionName = (COSName) parameters.getDictionaryObject(COSName.NAME); + COSName encryptionName = parameters.getCOSName(COSName.NAME); if(encryptionName == null || encryptionName.equals(COSName.IDENTITY)) { // currently the only supported implementation is the Identity crypt filter diff --git a/library/src/main/java/com/tom_roush/pdfbox/filter/DecodeResult.java b/library/src/main/java/com/tom_roush/pdfbox/filter/DecodeResult.java index 69ef3dc83..f902aea4c 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/filter/DecodeResult.java +++ b/library/src/main/java/com/tom_roush/pdfbox/filter/DecodeResult.java @@ -16,6 +16,8 @@ */ package com.tom_roush.pdfbox.filter; +import android.graphics.Bitmap; + import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDJPXColorSpace; @@ -32,6 +34,7 @@ public final class DecodeResult private final COSDictionary parameters; private PDJPXColorSpace colorSpace; + private Bitmap smask; DecodeResult(COSDictionary parameters) { @@ -67,4 +70,14 @@ void setColorSpace(PDJPXColorSpace colorSpace) { this.colorSpace = colorSpace; } + void setJPXSMask(Bitmap smask) + { + this.smask = smask; + } + + public Bitmap getJPXSMask() + { + return smask; + } + } diff --git a/library/src/main/java/com/tom_roush/pdfbox/filter/Filter.java b/library/src/main/java/com/tom_roush/pdfbox/filter/Filter.java index 536b83b9f..637f264ef 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/filter/Filter.java +++ b/library/src/main/java/com/tom_roush/pdfbox/filter/Filter.java @@ -21,13 +21,13 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.Iterator; import java.util.zip.Deflater; import com.tom_roush.pdfbox.cos.COSArray; import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; - /** * A filter for stream data. * @@ -36,6 +36,7 @@ */ public abstract class Filter { + /** * Compression Level System Property. Set this to a value from 0 to 9 to change the zlib deflate * compression level used to compress /Flate streams. The default value is -1 which is @@ -44,6 +45,16 @@ public abstract class Filter */ public static final String SYSPROP_DEFLATELEVEL = "com.tom_roush.pdfbox.filter.deflatelevel"; + /** + * CCITTFax decode buffer size cap System Property. Sets the maximum number of bytes that + * CCITTFaxFilter is allowed to pre-allocate for a single image decode buffer. PDF-controlled + * /Columns and /Rows values are validated against this limit before allocation to prevent + * denial-of-service via crafted image dimensions. The default is 256 MB. To raise the cap for + * high-resolution legitimate documents, use + * {@code System.setProperty(Filter.SYSPROP_CCITTFAX_MAXBYTES, String.valueOf(512 * 1024 * 1024L));} + */ + public static final String SYSPROP_CCITTFAX_MAXBYTES = "com.tom_roush.pdfbox.filter.ccittmaxbytes"; + /** * Constructor. */ @@ -61,7 +72,7 @@ protected Filter() * @throws IOException if the stream cannot be decoded */ public abstract DecodeResult decode(InputStream encoded, OutputStream decoded, COSDictionary parameters, - int index) throws IOException; + int index) throws IOException; /** * Decodes data, with optional DecodeOptions. Not all filters support all options, and so @@ -76,7 +87,7 @@ public abstract DecodeResult decode(InputStream encoded, OutputStream decoded, C * @throws IOException if the stream cannot be decoded */ public DecodeResult decode(InputStream encoded, OutputStream decoded, COSDictionary parameters, - int index, DecodeOptions options) throws IOException + int index, DecodeOptions options) throws IOException { return decode(encoded, decoded, parameters, index); } @@ -90,14 +101,14 @@ public DecodeResult decode(InputStream encoded, OutputStream decoded, COSDiction * @throws IOException if the stream cannot be encoded */ public final void encode(InputStream input, OutputStream encoded, COSDictionary parameters, - int index) throws IOException + int index) throws IOException { encode(input, encoded, parameters.asUnmodifiableDictionary()); } // implemented in subclasses protected abstract void encode(InputStream input, OutputStream encoded, - COSDictionary parameters) throws IOException; + COSDictionary parameters) throws IOException; // gets the decode params for a specific filter index, this is used to // normalise the DecodeParams entry so that it is always a dictionary @@ -107,8 +118,8 @@ protected COSDictionary getDecodeParams(COSDictionary dictionary, int index) COSBase obj = dictionary.getDictionaryObject(COSName.DP, COSName.DECODE_PARMS); if (filter instanceof COSName && obj instanceof COSDictionary) { - // PDFBOX-3932: The PDF specification requires "If there is only one filter and that - // filter has parameters, DecodeParms shall be set to the filter’s parameter dictionary" + // PDFBOX-3932: The PDF specification requires "If there is only one filter and that + // filter has parameters, DecodeParms shall be set to the filter’s parameter dictionary" // but tests show that Adobe means "one filter name object". return (COSDictionary)obj; } @@ -127,12 +138,61 @@ else if (filter instanceof COSArray && obj instanceof COSArray) else if (obj != null && !(filter instanceof COSArray || obj instanceof COSArray)) { Log.e("PdfBox-Android", "Expected DecodeParams to be an Array or Dictionary but found " + - obj.getClass().getName()); + obj.getClass().getName()); } return new COSDictionary(); } -// protected static ImageReader findImageReader(String formatName, String errorCause) throws MissingImageReaderException TODO: PdfBox-Android +// TODO: PdfBox-Android: javax.imageio not available +// /** +// * Finds a suitable image reader for a format. +// * +// * @param formatName The format to search for. +// * @param errorCause The probably cause if something goes wrong. +// * @return The image reader for the format. +// * @throws MissingImageReaderException if no image reader is found. +// */ +// protected static ImageReader findImageReader(String formatName, String errorCause) throws MissingImageReaderException +// { +// Iterator readers = ImageIO.getImageReadersByFormatName(formatName); +// ImageReader reader; +// while (readers.hasNext()) +// { +// reader = readers.next(); +// if (reader != null) +// { +// return reader; +// } +// } +// throw new MissingImageReaderException("Cannot read " + formatName + " image: " + errorCause); +// } +// +// /** +// * Finds a suitable image reader for an image format. +// * +// * @param formatName The image format to search for. +// * @param errorCause The probably cause if something goes wrong. +// * @return The image reader for the format. +// * @throws MissingImageReaderException if no image reader is found. +// */ +// public static ImageReader findRasterReader(String formatName, String errorCause) +// throws MissingImageReaderException +// { +// Iterator readers = ImageIO.getImageReadersByFormatName(formatName); +// while (readers.hasNext()) +// { +// ImageReader reader = readers.next(); +// if (reader != null) +// { +// if (reader.canReadRaster()) +// { +// return reader; +// } +// reader.dispose(); +// } +// } +// throw new MissingImageReaderException("Cannot read " + formatName + " image: " + errorCause); +// } /** * @return the ZIP compression level configured for PDFBox diff --git a/library/src/main/java/com/tom_roush/pdfbox/filter/JPXFilter.java b/library/src/main/java/com/tom_roush/pdfbox/filter/JPXFilter.java index be006da3a..ad0c2302a 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/filter/JPXFilter.java +++ b/library/src/main/java/com/tom_roush/pdfbox/filter/JPXFilter.java @@ -18,6 +18,7 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.graphics.Canvas; import android.graphics.Color; import android.os.Build; @@ -131,6 +132,19 @@ private Bitmap readJPX(InputStream input, DecodeOptions options, DecodeResult re // extract embedded color space if (!parameters.containsKey(COSName.COLORSPACE) && Build.VERSION.SDK_INT > Build.VERSION_CODES.O) { + if (image.hasAlpha() && parameters.getInt(COSName.SMASK_IN_DATA) > 0) + { + // PDFBOX-5657: save the soft mask in DecodeResult and use it later + // we never had SMaskInData = 2, maybe more work is needed + Bitmap smask = image.extractAlpha(); + result.setJPXSMask(smask); + // create opaque image + Bitmap bim = Bitmap.createBitmap( + image.getWidth(), image.getHeight(), Bitmap.Config.RGB_565); + Canvas canvas = new Canvas(bim); + canvas.drawBitmap(image, 0, 0, null); + image = bim; + } result.setColorSpace(new PDJPXColorSpace(image.getColorSpace())); } diff --git a/library/src/main/java/com/tom_roush/pdfbox/filter/LZWFilter.java b/library/src/main/java/com/tom_roush/pdfbox/filter/LZWFilter.java index e5683e010..1af439801 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/filter/LZWFilter.java +++ b/library/src/main/java/com/tom_roush/pdfbox/filter/LZWFilter.java @@ -29,7 +29,6 @@ import com.tom_roush.harmony.javax.imageio.stream.MemoryCacheImageOutputStream; import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; - /** * * This is the filter used for the LZWDecode filter. @@ -39,6 +38,10 @@ */ public class LZWFilter extends Filter { + /** + * Log instance. + */ + /** * The LZW clear table code. */ @@ -49,35 +52,27 @@ public class LZWFilter extends Filter */ public static final long EOD = 257; - //BEWARE: codeTable must be local to each method, because there is only - // one instance of each filter - /** * {@inheritDoc} */ @Override public DecodeResult decode(InputStream encoded, OutputStream decoded, - COSDictionary parameters, int index) throws IOException + COSDictionary parameters, int index) throws IOException { COSDictionary decodeParams = getDecodeParams(parameters, index); - int earlyChange = decodeParams.getInt(COSName.EARLY_CHANGE, 1); - - if (earlyChange != 0 && earlyChange != 1) - { - earlyChange = 1; - } - + boolean earlyChange = decodeParams.getInt(COSName.EARLY_CHANGE, 1) != 0; doLZWDecode(encoded, Predictor.wrapPredictor(decoded, decodeParams), earlyChange); return new DecodeResult(parameters); } - private void doLZWDecode(InputStream encoded, OutputStream decoded, int earlyChange) throws IOException + private void doLZWDecode(InputStream encoded, OutputStream decoded, boolean earlyChange) throws IOException { - List codeTable = new ArrayList(); + List codeTable = createCodeTable(); // includes CLEAR/EOD handling as needed int chunk = 9; final MemoryCacheImageInputStream in = new MemoryCacheImageInputStream(encoded); + + byte[] prev = null; // no previous string yet long nextCommand; - long prevCommand = -1; try { @@ -87,60 +82,50 @@ private void doLZWDecode(InputStream encoded, OutputStream decoded, int earlyCha { chunk = 9; codeTable = createCodeTable(); - prevCommand = -1; + prev = null; + continue; } - else + + byte[] curr; + + if (nextCommand < codeTable.size()) { - if (nextCommand < codeTable.size()) - { - byte[] data = codeTable.get((int) nextCommand); - byte firstByte = data[0]; - decoded.write(data); - if (prevCommand != -1) - { - checkIndexBounds(codeTable, prevCommand, in); - data = codeTable.get((int) prevCommand); - byte[] newData = Arrays.copyOf(data, data.length + 1); - newData[data.length] = firstByte; - codeTable.add(newData); - } - } - else + // Normal case: code exists + curr = codeTable.get((int) nextCommand); + decoded.write(curr); + + if (prev != null) { - checkIndexBounds(codeTable, prevCommand, in); - byte[] data = codeTable.get((int) prevCommand); - byte[] newData = Arrays.copyOf(data, data.length + 1); - newData[data.length] = data[0]; - decoded.write(newData); - codeTable.add(newData); + // Add prev + first(curr) + byte[] entry = Arrays.copyOf(prev, prev.length + 1); + entry[prev.length] = curr[0]; + codeTable.add(entry); } - - chunk = calculateChunk(codeTable.size(), earlyChange); - prevCommand = nextCommand; } + else if (nextCommand == codeTable.size() && prev != null) + { + // KwKwK case: code equals next available index + curr = Arrays.copyOf(prev, prev.length + 1); + curr[prev.length] = prev[0]; + decoded.write(curr); + codeTable.add(curr); + } + else + { + // Corrupt stream (code out of range, or KwKwK without prev) + throw new EOFException("Invalid LZW code: " + nextCommand); + } + + prev = curr; // move forward + chunk = calculateChunk(codeTable.size(), earlyChange); } } catch (EOFException ex) { - Log.w("PdfBox-Android", "Premature EOF in LZW stream, EOD code missing"); + Log.w("PdfBox-Android", "Premature EOF in LZW stream, EOD code missing", ex); } - decoded.flush(); - } - private void checkIndexBounds(List codeTable, long index, MemoryCacheImageInputStream in) - throws IOException - { - if (index < 0) - { - throw new IOException("negative array index: " + index + " near offset " - + in.getStreamPosition()); - } - if (index >= codeTable.size()) - { - throw new IOException("array index overflow: " + index + - " >= " + codeTable.size() + " near offset " - + in.getStreamPosition()); - } + decoded.flush(); } /** @@ -148,7 +133,7 @@ private void checkIndexBounds(List codeTable, long index, MemoryCacheIma */ @Override protected void encode(InputStream rawData, OutputStream encoded, COSDictionary parameters) - throws IOException + throws IOException { List codeTable = createCodeTable(); int chunk = 9; @@ -174,7 +159,7 @@ protected void encode(InputStream rawData, OutputStream encoded, COSDictionary p if (newFoundCode == -1) { // use previous - chunk = calculateChunk(codeTable.size() - 1, 1); + chunk = calculateChunk(codeTable.size() - 1, true); out.writeBits(foundCode, chunk); // create new table entry codeTable.add(inputPattern); @@ -197,16 +182,16 @@ protected void encode(InputStream rawData, OutputStream encoded, COSDictionary p } if (foundCode != -1) { - chunk = calculateChunk(codeTable.size() - 1, 1); + chunk = calculateChunk(codeTable.size() - 1, true); out.writeBits(foundCode, chunk); } - // PPDFBOX-1977: the decoder wouldn't know that the encoder would output - // an EOD as code, so he would have increased his own code table and - // possibly adjusted the chunk. Therefore, the encoder must behave as + // PPDFBOX-1977: the decoder wouldn't know that the encoder would output + // an EOD as code, so he would have increased his own code table and + // possibly adjusted the chunk. Therefore, the encoder must behave as // if the code table had just grown and thus it must be checked it is // needed to adjust the chunk, based on an increased table size parameter - chunk = calculateChunk(codeTable.size(), 1); + chunk = calculateChunk(codeTable.size(), true); out.writeBits(EOD, chunk); @@ -219,50 +204,47 @@ protected void encode(InputStream rawData, OutputStream encoded, COSDictionary p } /** - * Find the longest matching pattern in the code table. + * Find a matching pattern in the code table. * * @param codeTable The LZW code table. * @param pattern The pattern to be searched for. - * @return The index of the longest matching pattern or -1 if nothing is - * found. + * @return The index of the matching pattern or -1 if nothing is found. */ - private int findPatternCode(List codeTable, byte[] pattern) + private static int findPatternCode(List codeTable, byte[] pattern) { - int foundCode = -1; - int foundLen = 0; - for (int i = codeTable.size() - 1; i >= 0; --i) + // for the first 256 entries, index matches value + if (pattern.length == 1) { - if (i <= EOD) - { - // we're in the single byte area - if (foundCode != -1) - { - // we already found pattern with size > 1 - return foundCode; - } - else if (pattern.length > 1) - { - // we won't find anything here anyway - return -1; - } - } - byte[] tryPattern = codeTable.get(i); - if ((foundCode != -1 || tryPattern.length > foundLen) && Arrays.equals(tryPattern, pattern)) + return pattern[0]; + } + + // no need to test the first 256 + 2 entries against longer patterns + for (int i = 257; i < codeTable.size(); i++) + { + if (Arrays.equals(codeTable.get(i), pattern)) { - foundCode = i; - foundLen = tryPattern.length; + return i; } } - return foundCode; + + return -1; } /** - * Init the code table with 1 byte entries and the EOD and CLEAR_TABLE - * markers. + * Init the code table with 1 byte entries and the EOD and CLEAR_TABLE markers. */ - private List createCodeTable() + private static List createCodeTable() { List codeTable = new ArrayList(4096); + codeTable.addAll(INITIAL_CODE_TABLE); + return codeTable; + } + + private static final List INITIAL_CODE_TABLE = createInitialCodeTable(); + + private static List createInitialCodeTable() + { + List codeTable = new ArrayList(258); for (int i = 0; i < 256; ++i) { codeTable.add(new byte[] { (byte) (i & 0xFF) }); @@ -276,21 +258,22 @@ private List createCodeTable() * Calculate the appropriate chunk size * * @param tabSize the size of the code table - * @param earlyChange 0 or 1 for early chunk increase + * @param earlyChange true for early chunk increase * * @return a value between 9 and 12 */ - private int calculateChunk(int tabSize, int earlyChange) + private static int calculateChunk(int tabSize, boolean earlyChange) { - if (tabSize >= 2048 - earlyChange) + int i = tabSize + (earlyChange ? 1 : 0); + if (i >= 2048) { return 12; } - if (tabSize >= 1024 - earlyChange) + if (i >= 1024) { return 11; } - if (tabSize >= 512 - earlyChange) + if (i >= 512) { return 10; } diff --git a/library/src/main/java/com/tom_roush/pdfbox/filter/Predictor.java b/library/src/main/java/com/tom_roush/pdfbox/filter/Predictor.java index 9f0647dbd..a53bf4f37 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/filter/Predictor.java +++ b/library/src/main/java/com/tom_roush/pdfbox/filter/Predictor.java @@ -22,9 +22,8 @@ import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; - /** - * Helper class to contain predictor decoding used by Flate and LZW filter. + * Helper class to contain predictor decoding used by Flate and LZW filter. * To see the history, look at the FlateFilter class. */ public final class Predictor @@ -77,7 +76,7 @@ static void decodePredictorRow(int predictor, int colors, int bitsPerComponent, { int sub = ((actline[p] & 0xff) << 8) + (actline[p + 1] & 0xff); int left = (((actline[p - bytesPerPixel] & 0xff) << 8) - + (actline[p - bytesPerPixel + 1] & 0xff)); + + (actline[p - bytesPerPixel + 1] & 0xff)); actline[p] = (byte) (((sub + left) >> 8) & 0xff); actline[p + 1] = (byte) ((sub + left) & 0xff); } @@ -231,8 +230,9 @@ static int calcSetBitSeq(int by, int startBit, int bitSize, int val) * @param decodeParams Decode parameters for the stream * @return An OutputStream is returned, which will write decoded data * into the given stream. If no predictor is specified, the original stream is returned. + * @throws IOException */ - static OutputStream wrapPredictor(OutputStream out, COSDictionary decodeParams) + static OutputStream wrapPredictor(OutputStream out, COSDictionary decodeParams) throws IOException { int predictor = decodeParams.getInt(COSName.PREDICTOR); if (predictor > 1) @@ -276,6 +276,7 @@ private static final class PredictorOutputStream extends FilterOutputStream private boolean predictorRead = false; PredictorOutputStream(OutputStream out, int predictor, int colors, int bitsPerComponent, int columns) + throws IOException { super(out); this.predictor = predictor; @@ -283,6 +284,10 @@ private static final class PredictorOutputStream extends FilterOutputStream this.bitsPerComponent = bitsPerComponent; this.columns = columns; this.rowLength = calculateRowLength(colors, bitsPerComponent, columns); + if (rowLength < 0) + { + throw new IOException("Calculated row length is negative: " + rowLength); + } this.predictorPerRow = predictor >= 10; currentRow = new byte[rowLength]; lastRow = new byte[rowLength]; diff --git a/library/src/main/java/com/tom_roush/pdfbox/filter/RunLengthDecodeFilter.java b/library/src/main/java/com/tom_roush/pdfbox/filter/RunLengthDecodeFilter.java index 9ffb623ca..2a6cb28c4 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/filter/RunLengthDecodeFilter.java +++ b/library/src/main/java/com/tom_roush/pdfbox/filter/RunLengthDecodeFilter.java @@ -23,12 +23,12 @@ import java.io.OutputStream; import com.tom_roush.pdfbox.cos.COSDictionary; - /** * Decompresses data encoded using a byte-oriented run-length encoding algorithm, * reproducing the original text or binary data * * @author Ben Litchfield + * @author Tilman Hausherr */ final class RunLengthDecodeFilter extends Filter { @@ -36,7 +36,7 @@ final class RunLengthDecodeFilter extends Filter @Override public DecodeResult decode(InputStream encoded, OutputStream decoded, - COSDictionary parameters, int index) throws IOException + COSDictionary parameters, int index) throws IOException { int dupAmount; byte[] buffer = new byte[128]; @@ -77,8 +77,115 @@ public DecodeResult decode(InputStream encoded, OutputStream decoded, @Override protected void encode(InputStream input, OutputStream encoded, COSDictionary parameters) - throws IOException + throws IOException { - Log.w("PdfBox-Android", "RunLengthDecodeFilter.encode is not implemented yet, skipping this stream."); + // Not used in PDFBox except for testing the decoder. + int lastVal = -1; + int byt; + int count = 0; + boolean equality = false; + + // buffer for "unequal" runs, size between 2 and 128 + byte[] buf = new byte[128]; + + while ((byt = input.read()) != -1) + { + if (lastVal == -1) + { + // first time + lastVal = byt; + count = 1; + } + else + { + if (count == 128) + { + if (equality) + { + // max length of equals + encoded.write(129); // = 257 - 128 + encoded.write(lastVal); + } + else + { + // max length of unequals + encoded.write(127); + encoded.write(buf, 0, 128); + } + equality = false; + lastVal = byt; + count = 1; + } + else if (count == 1) + { + if (byt == lastVal) + { + equality = true; + } + else + { + buf[0] = (byte) lastVal; + buf[1] = (byte) byt; + lastVal = byt; + } + count = 2; + } + else + { + // 1 < count < 128 + if (byt == lastVal) + { + if (equality) + { + ++count; + } + else + { + // write all we got except the last + encoded.write(count - 2); + encoded.write(buf, 0, count - 1); + count = 2; + equality = true; + } + } + else + { + if (equality) + { + // equality ends here + encoded.write(257 - count); + encoded.write(lastVal); + equality = false; + count = 1; + } + else + { + buf[count] = (byte) byt; + ++count; + } + lastVal = byt; + } + } + } + } + if (count > 0) + { + if (count == 1) + { + encoded.write(0); + encoded.write(lastVal); + } + else if (equality) + { + encoded.write(257 - count); + encoded.write(lastVal); + } + else + { + encoded.write(count - 1); + encoded.write(buf, 0, count); + } + } + encoded.write(RUN_LENGTH_EOD); } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/io/ScratchFile.java b/library/src/main/java/com/tom_roush/pdfbox/io/ScratchFile.java index 1abcdaabe..078a6659e 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/io/ScratchFile.java +++ b/library/src/main/java/com/tom_roush/pdfbox/io/ScratchFile.java @@ -20,12 +20,14 @@ import java.io.Closeable; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.BitSet; +import java.util.List; import com.tom_roush.pdfbox.android.PDFBoxConfig; - /** * Implements a memory page handling mechanism as base for creating (multiple) * {@link RandomAccess} buffers each having its set of pages (implemented by @@ -51,6 +53,7 @@ */ public class ScratchFile implements Closeable { + /** number of pages by which we enlarge the scratch file (reduce I/O-operations) */ private static final int ENLARGE_PAGE_COUNT = 16; /** in case of unrestricted main memory usage this is the initial number of pages @@ -75,6 +78,8 @@ public class ScratchFile implements Closeable private final boolean useScratchFile; private final boolean maxMainMemoryIsRestricted; + private final List buffers = new ArrayList(); + private volatile boolean isClosed = false; /** @@ -100,7 +105,7 @@ public ScratchFile(File scratchFileDirectory) throws IOException *

Depending on the size of allowed memory usage a number of pages (memorySize/{@link #PAGE_SIZE}) * will be stored in-memory and only additional pages will be written to/read from scratch file.

* - * @param memUsageSetting set how memory/temporary files are used for buffering streams etc. + * @param memUsageSetting set how memory/temporary files are used for buffering streams etc. * * @throws IOException If scratch file directory was given but don't exist. */ @@ -116,14 +121,14 @@ public ScratchFile(MemoryUsageSetting memUsageSetting) throws IOException } maxPageCount = memUsageSetting.isStorageRestricted() ? - (int) Math.min(Integer.MAX_VALUE, memUsageSetting.getMaxStorageBytes() / PAGE_SIZE) : - Integer.MAX_VALUE; + (int) Math.min(Integer.MAX_VALUE, memUsageSetting.getMaxStorageBytes() / PAGE_SIZE) : + Integer.MAX_VALUE; inMemoryMaxPageCount = memUsageSetting.useMainMemory() ? - (memUsageSetting.isMainMemoryRestricted() ? - (int) Math.min(Integer.MAX_VALUE, memUsageSetting.getMaxMainMemoryBytes() / PAGE_SIZE) : - Integer.MAX_VALUE) : - 0; + (memUsageSetting.isMainMemoryRestricted() ? + (int) Math.min(Integer.MAX_VALUE, memUsageSetting.getMaxMainMemoryBytes() / PAGE_SIZE) : + Integer.MAX_VALUE) : + 0; inMemoryPages = new byte[maxMainMemoryIsRestricted ? inMemoryMaxPageCount : INIT_UNRESTRICTED_MAINMEM_PAGECOUNT][]; freePages.set(0, inMemoryPages.length); @@ -184,7 +189,7 @@ int getNewPage() throws IOException } /** - * This will provide new free pages by either enlarging the scratch file + * This will provide new free pages by either enlarging the scratch file * by a number of pages defined by {@link #ENLARGE_PAGE_COUNT} - in case * scratch file usage is allowed - or increase the {@link #inMemoryPages} * array in case main memory was not restricted. If neither of both is @@ -218,7 +223,7 @@ private void enlarge() throws IOException { raf = new java.io.RandomAccessFile(file, "rw"); } - catch (IOException e) + catch (FileNotFoundException e) { if (!file.delete()) { @@ -234,7 +239,7 @@ private void enlarge() throws IOException if (expectedFileLen != fileLen) { throw new IOException("Expected scratch file size of " + expectedFileLen + - " but found " + fileLen + " in file " + file); + " but found " + fileLen + " in file " + file); } // enlarge if we do not int overflow @@ -244,7 +249,7 @@ private void enlarge() throws IOException { Log.d("PdfBox-Android", "file: " + file); Log.d("PdfBox-Android", "fileLen before: " + fileLen + ", raf length: " + raf.length() + - ", file length: " + file.length()); + ", file length: " + file.length()); } fileLen += ENLARGE_PAGE_COUNT * PAGE_SIZE; @@ -252,7 +257,7 @@ private void enlarge() throws IOException if (PDFBoxConfig.isDebugEnabled()) { Log.d("PdfBox-Android", "fileLen after1: " + fileLen + ", raf length: " + raf.length() + - ", file length: " + file.length()); + ", file length: " + file.length()); } if (fileLen != raf.length()) { @@ -299,7 +304,7 @@ int getPageSize() * * @param pageIdx index of page to read * - * @return byte array of size {@link #PAGE_SIZE} filled with page data read from file + * @return byte array of size {@link #PAGE_SIZE} filled with page data read from file * * @throws IOException */ @@ -420,7 +425,20 @@ void checkClosed() throws IOException */ public RandomAccess createBuffer() throws IOException { - return new ScratchFileBuffer(this); + ScratchFileBuffer newBuffer = new ScratchFileBuffer(this); + synchronized (buffers) + { + buffers.add(newBuffer); + } + return newBuffer; + } + + void removeBuffer(ScratchFileBuffer buffer) + { + synchronized (buffers) + { + buffers.remove(buffer); + } } /** @@ -435,7 +453,7 @@ public RandomAccess createBuffer() throws IOException */ public RandomAccess createBuffer(InputStream input) throws IOException { - ScratchFileBuffer buf = new ScratchFileBuffer(this); + RandomAccess buf = createBuffer(); byte[] byteBuffer = new byte[8192]; int bytesRead; @@ -452,7 +470,7 @@ public RandomAccess createBuffer(InputStream input) throws IOException * Allows a buffer which is cleared/closed to release its pages to be re-used. * * @param pageIndexes pages indexes of pages to release - * @param count number of page indexes contained in provided array + * @param count number of page indexes contained in provided array */ void markPagesAsFree(int[] pageIndexes, int off, int count) { @@ -467,7 +485,7 @@ void markPagesAsFree(int[] pageIndexes, int off, int count) { if (pageIdx < inMemoryMaxPageCount) { inMemoryPages[pageIdx] = null; // remark: not in ioLock synchronization since behavior won't - // change even in case of parallel called 'enlarge' method + // change even in case of parallel called 'enlarge' method } } @@ -496,6 +514,15 @@ public void close() throws IOException isClosed = true; + for (ScratchFileBuffer buffer : buffers) + { + if (buffer != null && !buffer.isClosed()) + { + buffer.close(false); + } + } + buffers.clear(); + if (raf != null) { try diff --git a/library/src/main/java/com/tom_roush/pdfbox/io/ScratchFileBuffer.java b/library/src/main/java/com/tom_roush/pdfbox/io/ScratchFileBuffer.java index d3052d202..4a6435c2f 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/io/ScratchFileBuffer.java +++ b/library/src/main/java/com/tom_roush/pdfbox/io/ScratchFileBuffer.java @@ -16,14 +16,8 @@ */ package com.tom_roush.pdfbox.io; -import android.util.Log; - import java.io.EOFException; import java.io.IOException; - -import com.tom_roush.pdfbox.android.PDFBoxConfig; -import com.tom_roush.pdfbox.cos.COSStream; - /** * Implementation of {@link RandomAccess} as sequence of multiple fixed size pages handled * by {@link ScratchFile}. @@ -483,44 +477,30 @@ public int read(byte[] b, int off, int len) throws IOException @Override public void close() throws IOException { - if (pageHandler != null) { - - pageHandler.markPagesAsFree(pageIndexes, 0, pageCount); - pageHandler = null; - - pageIndexes = null; - currentPage = null; - currentPageOffset = 0; - currentPagePositionInPageIndexes = -1; - positionInPage = 0; - size = 0; - } + close(true); } /** - * While calling finalize is normally discouraged we will have to - * use it here as long as closing a scratch file buffer is not - * done in every case. Currently {@link COSStream} creates new - * buffers without closing the old one - which might still be - * used. + * Release all resources and remove this buffer from ScratchFile. * - *

Enabling debugging one will see if there are still cases - * where the buffer is not closed.

+ * @param removeBuffer remove buffer from ScratchFile if set to true */ - @Override - protected void finalize() throws Throwable + void close(boolean removeBuffer) { - try + if (pageHandler != null) { - if ((pageHandler != null) && PDFBoxConfig.isDebugEnabled()) + pageHandler.markPagesAsFree(pageIndexes, 0, pageCount); + if (removeBuffer) { - Log.d("PdfBox-Android", "ScratchFileBuffer not closed!"); + pageHandler.removeBuffer(this); } - close(); - } - finally - { - super.finalize(); + pageHandler = null; + pageIndexes = null; + currentPage = null; + currentPageOffset = 0; + currentPagePositionInPageIndexes = -1; + positionInPage = 0; + size = 0; } } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/multipdf/LayerUtility.java b/library/src/main/java/com/tom_roush/pdfbox/multipdf/LayerUtility.java index 397c59682..0831ae2a6 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/multipdf/LayerUtility.java +++ b/library/src/main/java/com/tom_roush/pdfbox/multipdf/LayerUtility.java @@ -35,8 +35,8 @@ import com.tom_roush.pdfbox.pdmodel.PDDocument; import com.tom_roush.pdfbox.pdmodel.PDDocumentCatalog; import com.tom_roush.pdfbox.pdmodel.PDPage; -import com.tom_roush.pdfbox.pdmodel.PDPageContentStream; import com.tom_roush.pdfbox.pdmodel.PDPageContentStream.AppendMode; +import com.tom_roush.pdfbox.pdmodel.PDPageContentStream; import com.tom_roush.pdfbox.pdmodel.PDResources; import com.tom_roush.pdfbox.pdmodel.common.PDRectangle; import com.tom_roush.pdfbox.pdmodel.common.PDStream; @@ -44,7 +44,6 @@ import com.tom_roush.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentGroup; import com.tom_roush.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentProperties; import com.tom_roush.pdfbox.util.Matrix; - /** * This class allows to import pages as Form XObjects into a document and use them to create layers * (optional content groups). It should used only on loaded documents, not on generated documents @@ -52,6 +51,7 @@ */ public class LayerUtility { + private static final boolean DEBUG = true; private final PDDocument targetDoc; @@ -59,12 +59,12 @@ public class LayerUtility /** * Creates a new instance. - * @param document the PDF document to modify + * @param targetDoc the PDF document to modify */ - public LayerUtility(PDDocument document) + public LayerUtility(PDDocument targetDoc) { - this.targetDoc = document; - this.cloner = new PDFCloneUtility(document); + this.targetDoc = targetDoc; + this.cloner = new PDFCloneUtility(targetDoc); } /** @@ -131,7 +131,7 @@ else if( contents instanceof COSArray ) * make sure that the graphics state is reset. * * @param sourceDoc the source PDF document that contains the page to be copied - * @param pageNumber the page number of the page to be copied + * @param pageNumber the 0-based page number of the page to be copied * @return a Form XObject containing the original page's content * @throws IOException if an I/O error occurs */ @@ -142,7 +142,7 @@ public PDFormXObject importPageAsForm(PDDocument sourceDoc, int pageNumber) thro } private static final Set PAGE_TO_FORM_FILTER = - new HashSet(Arrays.asList("Group", "LastModified", "Metadata")); + new HashSet(Arrays.asList("Group", "LastModified", "Metadata")); /** * Imports a page from some PDF file as a Form XObject so it can be placed on another page @@ -184,25 +184,25 @@ public PDFormXObject importPageAsForm(PDDocument sourceDoc, PDPage page) throws //Transform to FOP's user space //at.scale(1 / viewBox.getWidth(), 1 / viewBox.getHeight()); at.translate(mediaBox.getLowerLeftX() - viewBox.getLowerLeftX(), - mediaBox.getLowerLeftY() - viewBox.getLowerLeftY()); + mediaBox.getLowerLeftY() - viewBox.getLowerLeftY()); switch (rotation) { - case 90: - at.scale(viewBox.getWidth() / viewBox.getHeight(), viewBox.getHeight() / viewBox.getWidth()); - at.translate(0, viewBox.getWidth()); - at.rotate(-Math.PI / 2.0); - break; - case 180: - at.translate(viewBox.getWidth(), viewBox.getHeight()); - at.rotate(-Math.PI); - break; - case 270: - at.scale(viewBox.getWidth() / viewBox.getHeight(), viewBox.getHeight() / viewBox.getWidth()); - at.translate(viewBox.getHeight(), 0); - at.rotate(-Math.PI * 1.5); - break; - default: - //no additional transformations necessary + case 90: + at.scale(viewBox.getWidth() / viewBox.getHeight(), viewBox.getHeight() / viewBox.getWidth()); + at.translate(0, viewBox.getWidth()); + at.rotate(-Math.PI / 2.0); + break; + case 180: + at.translate(viewBox.getWidth(), viewBox.getHeight()); + at.rotate(-Math.PI); + break; + case 270: + at.scale(viewBox.getWidth() / viewBox.getHeight(), viewBox.getHeight() / viewBox.getWidth()); + at.translate(viewBox.getHeight(), 0); + at.rotate(-Math.PI * 1.5); + break; + default: + //no additional transformations necessary } //Compensate for Crop Boxes not starting at 0,0 at.translate(-viewBox.getLowerLeftX(), -viewBox.getLowerLeftY()); @@ -240,8 +240,8 @@ public PDFormXObject importPageAsForm(PDDocument sourceDoc, PDPage page) throws * @throws IOException if an I/O error occurs */ public PDOptionalContentGroup appendFormAsLayer(PDPage targetPage, - PDFormXObject form, AffineTransform transform, - String layerName) throws IOException + PDFormXObject form, AffineTransform transform, + String layerName) throws IOException { PDDocumentCatalog catalog = targetDoc.getDocumentCatalog(); PDOptionalContentProperties ocprops = catalog.getOCProperties(); @@ -258,16 +258,16 @@ public PDOptionalContentGroup appendFormAsLayer(PDPage targetPage, PDRectangle cropBox = targetPage.getCropBox(); if ((cropBox.getLowerLeftX() < 0 || cropBox.getLowerLeftY() < 0) && transform.isIdentity()) { - // PDFBOX-4044 + // PDFBOX-4044 Log.w("PdfBox-Android", "Negative cropBox " + cropBox + - " and identity transform may make your form invisible"); + " and identity transform may make your form invisible"); } PDOptionalContentGroup layer = new PDOptionalContentGroup(layerName); ocprops.addGroup(layer); PDPageContentStream contentStream = new PDPageContentStream( - targetDoc, targetPage, AppendMode.APPEND, !DEBUG); + targetDoc, targetPage, AppendMode.APPEND, !DEBUG); contentStream.beginMarkedContent(COSName.OC, layer); contentStream.saveGraphicsState(); contentStream.transform(new Matrix(transform)); @@ -280,7 +280,7 @@ public PDOptionalContentGroup appendFormAsLayer(PDPage targetPage, } private void transferDict(COSDictionary orgDict, COSDictionary targetDict, Set filter) - throws IOException + throws IOException { for (Map.Entry entry : orgDict.entrySet()) { @@ -314,7 +314,7 @@ private void importOcProperties(PDDocument srcDoc) throws IOException if (dstOCProperties == null) { dstCatalog.setOCProperties(new PDOptionalContentProperties( - (COSDictionary) cloner.cloneForNewDocument(srcOCProperties))); + (COSDictionary) cloner.cloneForNewDocument(srcOCProperties))); } else { diff --git a/library/src/main/java/com/tom_roush/pdfbox/multipdf/Overlay.java b/library/src/main/java/com/tom_roush/pdfbox/multipdf/Overlay.java index ec8c18996..807606e43 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/multipdf/Overlay.java +++ b/library/src/main/java/com/tom_roush/pdfbox/multipdf/Overlay.java @@ -16,6 +16,8 @@ */ package com.tom_roush.pdfbox.multipdf; +import android.util.Log; + import java.io.Closeable; import java.io.File; import java.io.IOException; @@ -31,6 +33,7 @@ import java.util.Set; import com.tom_roush.harmony.awt.geom.AffineTransform; +import com.tom_roush.pdfbox.android.PDFBoxConfig; import com.tom_roush.pdfbox.cos.COSArray; import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSDictionary; @@ -44,7 +47,6 @@ import com.tom_roush.pdfbox.pdmodel.PDResources; import com.tom_roush.pdfbox.pdmodel.common.PDRectangle; import com.tom_roush.pdfbox.pdmodel.graphics.form.PDFormXObject; - /** * Adds an overlay to an existing PDF document. * @@ -53,6 +55,7 @@ */ public class Overlay implements Closeable { + /** * Possible location of the overlaid pages: foreground or background. */ @@ -62,13 +65,14 @@ public enum Position } private LayoutPage defaultOverlayPage; + private final Map rotatedDefaultOverlayPagesMap = new HashMap(); private LayoutPage firstPageOverlayPage; private LayoutPage lastPageOverlayPage; private LayoutPage oddPageOverlayPage; private LayoutPage evenPageOverlayPage; - private final Set openDocuments = new HashSet(); - private Map specificPageOverlayPage = new HashMap(); + private final Set openDocumentsSet = new HashSet(); + private Map specificPageOverlayLayoutPageMap = new HashMap(); private Position position = Position.BACKGROUND; @@ -76,30 +80,31 @@ public enum Position private PDDocument inputPDFDocument = null; private String defaultOverlayFilename = null; - private PDDocument defaultOverlay = null; + private PDDocument defaultOverlayDocument = null; private String firstPageOverlayFilename = null; - private PDDocument firstPageOverlay = null; + private PDDocument firstPageOverlayDocument = null; private String lastPageOverlayFilename = null; - private PDDocument lastPageOverlay = null; + private PDDocument lastPageOverlayDocument = null; private String allPagesOverlayFilename = null; - private PDDocument allPagesOverlay = null; + private PDDocument allPagesOverlayDocument = null; private String oddPageOverlayFilename = null; - private PDDocument oddPageOverlay = null; + private PDDocument oddPageOverlayDocument = null; private String evenPageOverlayFilename = null; - private PDDocument evenPageOverlay = null; + private PDDocument evenPageOverlayDocument = null; private int numberOfOverlayPages = 0; private boolean useAllOverlayPages = false; + private boolean adjustRotation = false; /** * This will add overlays to a document. * - * @param specificPageOverlayFile Optional map of overlay files of which the first page will be + * @param specificPageOverlayMap Optional map of overlay files of which the first page will be * used for specific pages of the input document. The page numbers are 1-based. The map must be * empty (but not null) if no specific mappings are used. * @@ -107,24 +112,26 @@ public enum Position * the input document was passed by {@link #setInputPDF(PDDocument) setInputPDF(PDDocument)} * then it is that object that is returned. * - * @throws IOException if something went wrong + * @throws IOException if something went wrong. + * @throws IllegalArgumentException if the input document is missing. */ - public PDDocument overlay(Map specificPageOverlayFile) throws IOException + public PDDocument overlay(Map specificPageOverlayMap) throws IOException { Map layouts = new HashMap(); String path; loadPDFs(); - for (Map.Entry e : specificPageOverlayFile.entrySet()) + for (Map.Entry e : specificPageOverlayMap.entrySet()) { path = e.getValue(); LayoutPage layoutPage = layouts.get(path); if (layoutPage == null) { PDDocument doc = loadPDF(path); - layouts.put(path, getLayoutPage(doc)); - openDocuments.add(doc); + layoutPage = createLayoutPageFromDocument(doc); + layouts.put(path, layoutPage); + openDocumentsSet.add(doc); } - specificPageOverlayPage.put(e.getKey(), layoutPage); + specificPageOverlayLayoutPageMap.put(e.getKey(), layoutPage); } processPages(inputPDFDocument); return inputPDFDocument; @@ -134,7 +141,7 @@ public PDDocument overlay(Map specificPageOverlayFile) throws I * This will add overlays documents to a document. If you created the overlay documents with * subsetted fonts, you need to save them first so that the subsetting gets done. * - * @param specificPageOverlayDocuments Optional map of overlay documents for specific pages. The + * @param specificPageOverlayDocumentMap Optional map of overlay documents for specific pages. The * page numbers are 1-based. The map must be empty (but not null) if no specific mappings are * used. * @@ -144,15 +151,15 @@ public PDDocument overlay(Map specificPageOverlayFile) throws I * * @throws IOException if something went wrong */ - public PDDocument overlayDocuments(Map specificPageOverlayDocuments) throws IOException + public PDDocument overlayDocuments(Map specificPageOverlayDocumentMap) throws IOException { loadPDFs(); - for (Map.Entry e : specificPageOverlayDocuments.entrySet()) + for (Map.Entry e : specificPageOverlayDocumentMap.entrySet()) { PDDocument doc = e.getValue(); if (doc != null) { - specificPageOverlayPage.put(e.getKey(), getLayoutPage(doc)); + specificPageOverlayLayoutPageMap.put(e.getKey(), createLayoutPageFromDocument(doc)); } } processPages(inputPDFDocument); @@ -167,36 +174,37 @@ public PDDocument overlayDocuments(Map specificPageOverlayD @Override public void close() throws IOException { - if (defaultOverlay != null) + if (defaultOverlayDocument != null) { - defaultOverlay.close(); + defaultOverlayDocument.close(); } - if (firstPageOverlay != null) + if (firstPageOverlayDocument != null) { - firstPageOverlay.close(); + firstPageOverlayDocument.close(); } - if (lastPageOverlay != null) + if (lastPageOverlayDocument != null) { - lastPageOverlay.close(); + lastPageOverlayDocument.close(); } - if (allPagesOverlay != null) + if (allPagesOverlayDocument != null) { - allPagesOverlay.close(); + allPagesOverlayDocument.close(); } - if (oddPageOverlay != null) + if (oddPageOverlayDocument != null) { - oddPageOverlay.close(); + oddPageOverlayDocument.close(); } - if (evenPageOverlay != null) + if (evenPageOverlayDocument != null) { - evenPageOverlay.close(); + evenPageOverlayDocument.close(); } - for (PDDocument doc : openDocuments) + for (PDDocument doc : openDocumentsSet) { doc.close(); } - openDocuments.clear(); - specificPageOverlayPage.clear(); + openDocumentsSet.clear(); + specificPageOverlayLayoutPageMap.clear(); + rotatedDefaultOverlayPagesMap.clear(); } private void loadPDFs() throws IOException @@ -206,61 +214,65 @@ private void loadPDFs() throws IOException { inputPDFDocument = loadPDF(inputFileName); } + if (inputPDFDocument == null) + { + throw new IllegalArgumentException("No input document"); + } // default overlay PDF if (defaultOverlayFilename != null) { - defaultOverlay = loadPDF(defaultOverlayFilename); + defaultOverlayDocument = loadPDF(defaultOverlayFilename); } - if (defaultOverlay != null) + if (defaultOverlayDocument != null) { - defaultOverlayPage = getLayoutPage(defaultOverlay); + defaultOverlayPage = createLayoutPageFromDocument(defaultOverlayDocument); } // first page overlay PDF if (firstPageOverlayFilename != null) { - firstPageOverlay = loadPDF(firstPageOverlayFilename); + firstPageOverlayDocument = loadPDF(firstPageOverlayFilename); } - if (firstPageOverlay != null) + if (firstPageOverlayDocument != null) { - firstPageOverlayPage = getLayoutPage(firstPageOverlay); + firstPageOverlayPage = createLayoutPageFromDocument(firstPageOverlayDocument); } // last page overlay PDF if (lastPageOverlayFilename != null) { - lastPageOverlay = loadPDF(lastPageOverlayFilename); + lastPageOverlayDocument = loadPDF(lastPageOverlayFilename); } - if (lastPageOverlay != null) + if (lastPageOverlayDocument != null) { - lastPageOverlayPage = getLayoutPage(lastPageOverlay); + lastPageOverlayPage = createLayoutPageFromDocument(lastPageOverlayDocument); } // odd pages overlay PDF if (oddPageOverlayFilename != null) { - oddPageOverlay = loadPDF(oddPageOverlayFilename); + oddPageOverlayDocument = loadPDF(oddPageOverlayFilename); } - if (oddPageOverlay != null) + if (oddPageOverlayDocument != null) { - oddPageOverlayPage = getLayoutPage(oddPageOverlay); + oddPageOverlayPage = createLayoutPageFromDocument(oddPageOverlayDocument); } // even pages overlay PDF if (evenPageOverlayFilename != null) { - evenPageOverlay = loadPDF(evenPageOverlayFilename); + evenPageOverlayDocument = loadPDF(evenPageOverlayFilename); } - if (evenPageOverlay != null) + if (evenPageOverlayDocument != null) { - evenPageOverlayPage = getLayoutPage(evenPageOverlay); + evenPageOverlayPage = createLayoutPageFromDocument(evenPageOverlayDocument); } // all pages overlay PDF if (allPagesOverlayFilename != null) { - allPagesOverlay = loadPDF(allPagesOverlayFilename); + allPagesOverlayDocument = loadPDF(allPagesOverlayFilename); } - if (allPagesOverlay != null) + if (allPagesOverlayDocument != null) { - specificPageOverlayPage = getLayoutPages(allPagesOverlay); + specificPageOverlayLayoutPageMap = createPageOverlayLayoutPageMap(allPagesOverlayDocument); useAllOverlayPages = true; - numberOfOverlayPages = specificPageOverlayPage.size(); + numberOfOverlayPages = specificPageOverlayLayoutPageMap.size(); } } @@ -275,14 +287,14 @@ private PDDocument loadPDF(String pdfName) throws IOException private static final class LayoutPage { private final PDRectangle overlayMediaBox; - private final COSStream overlayContentStream; + private final COSStream overlayCOSStream; private final COSDictionary overlayResources; - private final short overlayRotation; + private int overlayRotation; - private LayoutPage(PDRectangle mediaBox, COSStream contentStream, COSDictionary resources, short rotation) + private LayoutPage(PDRectangle mediaBox, COSStream contentStream, COSDictionary resources, int rotation) { overlayMediaBox = mediaBox; - overlayContentStream = contentStream; + overlayCOSStream = contentStream; overlayResources = resources; overlayRotation = rotation; } @@ -295,7 +307,7 @@ private LayoutPage(PDRectangle mediaBox, COSStream contentStream, COSDictionary * @return * @throws IOException */ - private LayoutPage getLayoutPage(PDDocument doc) throws IOException + private LayoutPage createLayoutPageFromDocument(PDDocument doc) throws IOException { return createLayoutPage(doc.getPage(0)); } @@ -315,14 +327,15 @@ private LayoutPage createLayoutPage(PDPage page) throws IOException resources = new PDResources(); } return new LayoutPage(page.getMediaBox(), createCombinedContentStream(contents), - resources.getCOSObject(), (short) page.getRotation()); + resources.getCOSObject(), page.getRotation()); } - private Map getLayoutPages(PDDocument doc) throws IOException + private Map createPageOverlayLayoutPageMap(PDDocument doc) throws IOException { int i = 0; - Map layoutPages = new HashMap(); - for (PDPage page : doc.getPages()) + PDPageTree pageTree = doc.getPages(); + Map layoutPages = new HashMap(pageTree.getCount()); + for (PDPage page : pageTree) { layoutPages.put(i, createLayoutPage(page)); i++; @@ -381,6 +394,7 @@ else if (contents instanceof COSObject) private void processPages(PDDocument document) throws IOException { int pageCounter = 0; + PDFCloneUtility cloner = new PDFCloneUtility(document); PDPageTree pageTree = document.getPages(); int numberOfPages = pageTree.getCount(); for (PDPage page : pageTree) @@ -440,7 +454,7 @@ else if (contents instanceof COSArray) } private void overlayPage(PDPage page, LayoutPage layoutPage, COSArray array) - throws IOException + throws IOException { PDResources resources = page.getResources(); if (resources == null) @@ -448,16 +462,17 @@ private void overlayPage(PDPage page, LayoutPage layoutPage, COSArray array) resources = new PDResources(); page.setResources(resources); } - COSName xObjectId = createOverlayXObject(page, layoutPage); - array.add(createOverlayStream(page, layoutPage, xObjectId)); + PDFormXObject overlayFormXObject = createOverlayFormXObject(layoutPage); + COSName formXObjectId = resources.add(overlayFormXObject, "OL"); + array.add(createOverlayStream(page, layoutPage, formXObjectId)); } - private LayoutPage getLayoutPage(int pageNumber, int numberOfPages) + private LayoutPage getLayoutPage(int pageNumber, int numberOfPages) throws IOException { LayoutPage layoutPage = null; - if (!useAllOverlayPages && specificPageOverlayPage.containsKey(pageNumber)) + if (!useAllOverlayPages && specificPageOverlayLayoutPageMap.containsKey(pageNumber)) { - layoutPage = specificPageOverlayPage.get(pageNumber); + layoutPage = specificPageOverlayLayoutPageMap.get(pageNumber); } else if ((pageNumber == 1) && (firstPageOverlayPage != null)) { @@ -478,18 +493,45 @@ else if ((pageNumber % 2 == 0) && (evenPageOverlayPage != null)) else if (defaultOverlayPage != null) { layoutPage = defaultOverlayPage; + + if (adjustRotation) + { + // PDFBOX-6049: consider the rotation of the document page + // Note that this segment is only the second best solution to the problem. The best + // would be to make appropriate transforms in calculateAffineTransform() + PDPage page = inputPDFDocument.getPage(pageNumber - 1); + int rotation = page.getRotation(); + if (rotation != 0) + { + return createAdjustedLayoutPage(rotation); + } + } } else if (useAllOverlayPages) { int usePageNum = (pageNumber -1 ) % numberOfOverlayPages; - layoutPage = specificPageOverlayPage.get(usePageNum); + layoutPage = specificPageOverlayLayoutPageMap.get(usePageNum); } return layoutPage; } - private COSName createOverlayXObject(PDPage page, LayoutPage layoutPage) + private LayoutPage createAdjustedLayoutPage(int rotation) throws IOException + { + LayoutPage rotatedLayoutPage = rotatedDefaultOverlayPagesMap.get(rotation); + if (rotatedLayoutPage == null) + { + // createLayoutPage must be called because we can't reuse the COSStream + rotatedLayoutPage = createLayoutPage(defaultOverlayDocument.getPage(0)); + int newRotation = (rotatedLayoutPage.overlayRotation - rotation + 360) % 360; + rotatedLayoutPage.overlayRotation = newRotation; + rotatedDefaultOverlayPagesMap.put(rotation, rotatedLayoutPage); + } + return rotatedLayoutPage; + } + + private PDFormXObject createOverlayFormXObject(LayoutPage layoutPage) throws IOException { - PDFormXObject xobjForm = new PDFormXObject(layoutPage.overlayContentStream); + PDFormXObject xobjForm = new PDFormXObject(layoutPage.overlayCOSStream); xobjForm.setResources(new PDResources(layoutPage.overlayResources)); xobjForm.setFormType(1); xobjForm.setBBox(layoutPage.overlayMediaBox.createRetranslatedRectangle()); @@ -498,26 +540,25 @@ private COSName createOverlayXObject(PDPage page, LayoutPage layoutPage) { case 90: at.translate(0, layoutPage.overlayMediaBox.getWidth()); - at.rotate(Math.toRadians(-90)); + at.rotate(-Math.PI / 2.0); break; case 180: at.translate(layoutPage.overlayMediaBox.getWidth(), layoutPage.overlayMediaBox.getHeight()); - at.rotate(Math.toRadians(-180)); + at.rotate(-Math.PI); break; case 270: at.translate(layoutPage.overlayMediaBox.getHeight(), 0); - at.rotate(Math.toRadians(-270)); + at.rotate(-Math.PI * 1.5); break; default: break; } xobjForm.setMatrix(at); - PDResources resources = page.getResources(); - return resources.add(xobjForm, "OL"); + return xobjForm; } private COSStream createOverlayStream(PDPage page, LayoutPage layoutPage, COSName xObjectId) - throws IOException + throws IOException { // create a new content stream that executes the XObject content StringBuilder overlayStream = new StringBuilder(); @@ -536,7 +577,7 @@ private COSStream createOverlayStream(PDPage page, LayoutPage layoutPage, COSNam for (double v : flatmatrix) { overlayStream.append(float2String((float) v)); - overlayStream.append(" "); + overlayStream.append(' '); } overlayStream.append(" cm\n"); @@ -552,8 +593,9 @@ private COSStream createOverlayStream(PDPage page, LayoutPage layoutPage, COSNam /** * Calculate the transform to be used when positioning the overlay. The default implementation - * centers on the destination. Override this method to do your own, e.g. move to a corner, or - * rotate. + * centers on the destination and assumes (0,0) to be the lower left (This will be changed in + * 4.0, see PDFBOX-6048 why). Override this method to do your own, e.g. move to a corner, + * rotate, or zoom. * * @param page The page that will get the overlay. * @param overlayMediaBox The overlay media box. @@ -565,13 +607,17 @@ protected AffineTransform calculateAffineTransform(PDPage page, PDRectangle over PDRectangle pageMediaBox = page.getMediaBox(); float hShift = (pageMediaBox.getWidth() - overlayMediaBox.getWidth()) / 2.0f; float vShift = (pageMediaBox.getHeight() - overlayMediaBox.getHeight()) / 2.0f; + if (PDFBoxConfig.isDebugEnabled()) + { + Log.d("PdfBox-Android", "Overlay position: (" + hShift + "," + vShift + ")"); + } at.translate(hShift, vShift); return at; } private String float2String(float floatValue) { - // use a BigDecimal as intermediate state to avoid + // use a BigDecimal as intermediate state to avoid // a floating point string representation of the float value BigDecimal value = new BigDecimal(String.valueOf(floatValue)); String stringValue = value.toPlainString(); @@ -590,7 +636,7 @@ private COSStream createStream(String content) throws IOException { COSStream stream = inputPDFDocument.getDocument().createCOSStream(); OutputStream out = stream.createOutputStream( - content.length() > 20 ? COSName.FLATE_DECODE : null); + content.length() > 20 ? COSName.FLATE_DECODE : null); out.write(content.getBytes("ISO-8859-1")); out.close(); return stream; @@ -657,7 +703,7 @@ public void setDefaultOverlayFile(String defaultOverlayFile) */ public void setDefaultOverlayPDF(PDDocument defaultOverlayPDF) { - defaultOverlay = defaultOverlayPDF; + defaultOverlayDocument = defaultOverlayPDF; } /** @@ -688,7 +734,7 @@ public void setFirstPageOverlayFile(String firstPageOverlayFile) */ public void setFirstPageOverlayPDF(PDDocument firstPageOverlayPDF) { - firstPageOverlay = firstPageOverlayPDF; + firstPageOverlayDocument = firstPageOverlayPDF; } /** @@ -709,7 +755,7 @@ public void setLastPageOverlayFile(String lastPageOverlayFile) */ public void setLastPageOverlayPDF(PDDocument lastPageOverlayPDF) { - lastPageOverlay = lastPageOverlayPDF; + lastPageOverlayDocument = lastPageOverlayPDF; } /** @@ -731,7 +777,7 @@ public void setAllPagesOverlayFile(String allPagesOverlayFile) */ public void setAllPagesOverlayPDF(PDDocument allPagesOverlayPDF) { - allPagesOverlay = allPagesOverlayPDF; + allPagesOverlayDocument = allPagesOverlayPDF; } /** @@ -752,7 +798,7 @@ public void setOddPageOverlayFile(String oddPageOverlayFile) */ public void setOddPageOverlayPDF(PDDocument oddPageOverlayPDF) { - oddPageOverlay = oddPageOverlayPDF; + oddPageOverlayDocument = oddPageOverlayPDF; } /** @@ -773,6 +819,24 @@ public void setEvenPageOverlayFile(String evenPageOverlayFile) */ public void setEvenPageOverlayPDF(PDDocument evenPageOverlayPDF) { - evenPageOverlay = evenPageOverlayPDF; + evenPageOverlayDocument = evenPageOverlayPDF; + } + + /** + * This sets whether the overlay is to be rotated according to the rotation of the pages of the + * source document. This may look weird if the content of the document is also rotated. So it's + * really a users decision to activate this option if the overlay appears rotated in some pages + * of the result document and this isn't wanted. + *

+ * This setting will only apply to usage of the default overlay, because it is assumed that when + * using specific overlays for specific pages, it is known in advance what kind of input there + * is. + * + * @param adjustRotation if true, the overlay will always look the same when the result file is + * displayed on the screen. If false (default) then it will be rotated if the page is rotated. + */ + public void setAdjustRotation(boolean adjustRotation) + { + this.adjustRotation = adjustRotation; } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/multipdf/PDFMergerUtility.java b/library/src/main/java/com/tom_roush/pdfbox/multipdf/PDFMergerUtility.java index 93598a1e7..f42c15c68 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/multipdf/PDFMergerUtility.java +++ b/library/src/main/java/com/tom_roush/pdfbox/multipdf/PDFMergerUtility.java @@ -64,6 +64,8 @@ import com.tom_roush.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureElement; import com.tom_roush.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureTreeRoot; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDOutputIntent; +import com.tom_roush.pdfbox.pdmodel.interactive.action.PDAction; +import com.tom_roush.pdfbox.pdmodel.interactive.action.PDActionFactory; import com.tom_roush.pdfbox.pdmodel.interactive.action.PDActionGoTo; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotation; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; @@ -75,7 +77,6 @@ import com.tom_roush.pdfbox.pdmodel.interactive.form.PDField; import com.tom_roush.pdfbox.pdmodel.interactive.form.PDNonTerminalField; import com.tom_roush.pdfbox.pdmodel.interactive.viewerpreferences.PDViewerPreferences; - /** * This class will take a list of pdf documents and merge them, saving the * result in a new document. @@ -103,7 +104,7 @@ public class PDFMergerUtility * the PDFBOX_LEGACY_MODE. Currently supported are: *

    *
  • Page content and resources - *
+ * *
  • {@link DocumentMergeMode#PDFBOX_LEGACY_MODE} Keeps all files open until the * merge has been completed. This is currently necessary to merge documents * containing a Structure Tree.
    This is the standard mode for PDFBox 2.0. @@ -120,7 +121,7 @@ public enum DocumentMergeMode * *
      *
    • {@link AcroFormMergeMode#JOIN_FORM_FIELDS_MODE} fields with the same fully qualified name - * will be merged into one with the widget annotations of the merged fields + * will be merged into one with the widget annotations of the merged fields * becoming part of the same field.
      * Although the API is finalized processing of different form field types is still in * development. Currently only (nested) text fields do work with intermediate nodes @@ -326,11 +327,13 @@ public void mergeDocuments() throws IOException } /** - * Merge the list of source documents, saving the result in the destination - * file. + * Merge the list of source documents, saving the result in the destination file. The source + * list is not reset after merge. If you want to merge one document at a time, then it's better + * to use + * {@link #appendDocument(com.tom_roush.pdfbox.pdmodel.PDDocument, com.tom_roush.pdfbox.pdmodel.PDDocument)}. * * @param memUsageSetting defines how memory is used for buffering PDF streams; - * in case of null unrestricted main memory is used + * in case of null unrestricted main memory is used * * @throws IOException If there is an error saving the document. */ @@ -414,14 +417,14 @@ private void optimizedMergeDocuments(MemoryUsageSetting memUsageSetting) throws * file. * * @param memUsageSetting defines how memory is used for buffering PDF streams; - * in case of null unrestricted main memory is used + * in case of null unrestricted main memory is used * * @throws IOException If there is an error saving the document. */ private void legacyMergeDocuments(MemoryUsageSetting memUsageSetting) throws IOException { PDDocument destination = null; - if (sources.size() > 0) + if (!sources.isEmpty()) { // Make sure that: // - first Exception is kept @@ -429,19 +432,16 @@ private void legacyMergeDocuments(MemoryUsageSetting memUsageSetting) throws IOE // - all PDDocuments are closed // - all FileInputStreams are closed // - there's a way to see which errors occurred - - List tobeclosed = new ArrayList(sources.size()); - try { MemoryUsageSetting partitionedMemSetting = memUsageSetting != null ? - memUsageSetting.getPartitionedCopy(sources.size()+1) : - MemoryUsageSetting.setupMainMemoryOnly(); + memUsageSetting.getPartitionedCopy(sources.size()+1) : + MemoryUsageSetting.setupMainMemoryOnly(); destination = new PDDocument(partitionedMemSetting); for (Object sourceObject : sources) { - PDDocument sourceDoc = null; + PDDocument sourceDoc; if (sourceObject instanceof File) { sourceDoc = PDDocument.load((File) sourceObject, partitionedMemSetting); @@ -449,10 +449,16 @@ private void legacyMergeDocuments(MemoryUsageSetting memUsageSetting) throws IOE else { sourceDoc = PDDocument.load((InputStream) sourceObject, - partitionedMemSetting); + partitionedMemSetting); + } + try + { + appendDocument(destination, sourceDoc); + } + finally + { + IOUtils.closeAndLogException(sourceDoc, "PDDocument", null); } - tobeclosed.add(sourceDoc); - appendDocument(destination, sourceDoc); } // optionally set meta data @@ -480,11 +486,6 @@ private void legacyMergeDocuments(MemoryUsageSetting memUsageSetting) throws IOE { IOUtils.closeAndLogException(destination, "PDDocument", null); } - - for (PDDocument doc : tobeclosed) - { - IOUtils.closeAndLogException(doc, "PDDocument", null); - } } } } @@ -500,6 +501,7 @@ private void legacyMergeDocuments(MemoryUsageSetting memUsageSetting) throws IOE */ public void appendDocument(PDDocument destination, PDDocument source) throws IOException { + PDFCloneUtility cloner = new PDFCloneUtility(destination); if (source.getDocument().isClosed()) { throw new IOException("Error: source PDF is closed."); @@ -517,7 +519,7 @@ public void appendDocument(PDDocument destination, PDDocument source) throws IOE PDDocumentInformation destInfo = destination.getDocumentInformation(); PDDocumentInformation srcInfo = source.getDocumentInformation(); - mergeInto(srcInfo.getCOSObject(), destInfo.getCOSObject(), Collections.emptySet()); + mergeInto(srcInfo.getCOSObject(), destInfo.getCOSObject(), cloner, Collections.emptySet()); // use the highest version number for the resulting pdf float destVersion = destination.getVersion(); @@ -528,50 +530,12 @@ public void appendDocument(PDDocument destination, PDDocument source) throws IOE destination.setVersion(srcVersion); } - int pageIndexOpenActionDest = -1; PDDocumentCatalog destCatalog = destination.getDocumentCatalog(); - if (destCatalog.getOpenAction() == null) - { - // PDFBOX-3972: get local dest page index, it must be reassigned after the page cloning - PDDestinationOrAction openAction = null; - try - { - openAction = srcCatalog.getOpenAction(); - } - catch (IOException ex) - { - // PDFBOX-4223 - Log.e("PdfBox-Android", "Invalid OpenAction ignored", ex); - } - PDDestination openActionDestination = null; - if (openAction instanceof PDActionGoTo) - { - openActionDestination = ((PDActionGoTo) openAction).getDestination(); - } - else if (openAction instanceof PDDestination) - { - openActionDestination = (PDDestination) openAction; - } - // note that it can also be something else, e.g. PDActionJavaScript, then do nothing. - - if (openActionDestination instanceof PDPageDestination) - { - PDPage page = ((PDPageDestination) openActionDestination).getPage(); - if (page != null) - { - pageIndexOpenActionDest = srcCatalog.getPages().indexOf(page); - } - } - - destCatalog.setOpenAction(openAction); - } - - PDFCloneUtility cloner = new PDFCloneUtility(destination); mergeAcroForm(cloner, destCatalog, srcCatalog); - COSArray destThreads = (COSArray) destCatalog.getCOSObject().getDictionaryObject(COSName.THREADS); - COSArray srcThreads = (COSArray) cloner.cloneForNewDocument(destCatalog.getCOSObject().getDictionaryObject( - COSName.THREADS)); + COSArray destThreads = destCatalog.getCOSObject().getCOSArray(COSName.THREADS); + COSArray srcThreads = (COSArray) cloner.cloneForNewDocument(destCatalog.getCOSObject().getCOSArray( + COSName.THREADS)); if (destThreads == null) { destCatalog.getCOSObject().setItem(COSName.THREADS, srcThreads); @@ -628,9 +592,15 @@ else if (openAction instanceof PDDestination) else { // search last sibling for dest, because /Last entry is sometimes wrong + Set visited = new HashSet(); PDOutlineItem destLastOutlineItem = destOutline.getFirstChild(); while (true) { + if (!visited.add(destLastOutlineItem.getCOSObject())) + { + Log.w("PdfBox-Android", "Outline ignored: " + destLastOutlineItem.getCOSObject()); + break; // Cycle detected + } PDOutlineItem outlineItem = destLastOutlineItem.getNextSibling(); if (outlineItem == null) { @@ -674,9 +644,9 @@ else if (openAction instanceof PDDestination) } else { - destNums = (COSArray) destLabels.getDictionaryObject(COSName.NUMS); + destNums = destLabels.getCOSArray(COSName.NUMS); } - COSArray srcNums = (COSArray) srcLabels.getDictionaryObject(COSName.NUMS); + COSArray srcNums = srcLabels.getCOSArray(COSName.NUMS); if (srcNums != null) { int startSize = destNums.size(); @@ -708,13 +678,13 @@ else if (openAction instanceof PDDestination) try { PDStream newStream = new PDStream(destination, srcMetadata.createInputStream(), (COSName) null); - mergeInto(srcMetadata, newStream.getCOSObject(), - new HashSet(Arrays.asList(COSName.FILTER, COSName.LENGTH))); + mergeInto(srcMetadata, newStream.getCOSObject(), cloner, + new HashSet(Arrays.asList(COSName.FILTER, COSName.LENGTH))); destCatalog.getCOSObject().setItem(COSName.METADATA, newStream); } catch (IOException ex) { - // PDFBOX-4227 cleartext XMP stream with /Flate + // PDFBOX-4227 cleartext XMP stream with /Flate Log.e("PdfBox-Android", "Metadata skipped because it could not be read", ex); } } @@ -730,7 +700,7 @@ else if (destOCP != null && srcOCP != null) cloner.cloneMerge(srcOCP, destOCP); } - mergeOutputIntents(cloner, srcCatalog, destCatalog); + mergeOutputIntents(srcCatalog, destCatalog, cloner); // merge logical structure hierarchy boolean mergeStructTree = false; @@ -790,7 +760,6 @@ else if (destOCP != null && srcOCP != null) } Map objMapping = new HashMap(); - int pageIndex = 0; PDPageTree destinationPageTree = destination.getPages(); // cache PageTree for (PDPage page : srcCatalog.getPages()) { @@ -819,7 +788,7 @@ else if (destOCP != null && srcOCP != null) } if (mergeStructTree) { - // add the value of the destination ParentTreeNextKey to every source element + // add the value of the destination ParentTreeNextKey to every source element // StructParent(s) value so that these don't overlap with the existing values updateStructParentEntries(newPage, destParentTreeNextKey); objMapping.put(page.getCOSObject(), newPage.getCOSObject()); @@ -832,25 +801,8 @@ else if (destOCP != null && srcOCP != null) // TODO update mapping for XObjects } destinationPageTree.add(newPage); - - if (pageIndex == pageIndexOpenActionDest) - { - // PDFBOX-3972: reassign the page. - // The openAction is either a PDActionGoTo or a PDPageDestination - PDDestinationOrAction openAction = destCatalog.getOpenAction(); - PDPageDestination pageDestination; - if (openAction instanceof PDActionGoTo) - { - pageDestination = (PDPageDestination) ((PDActionGoTo) openAction).getDestination(); - } - else - { - pageDestination = (PDPageDestination) openAction; - } - pageDestination.setPage(newPage); - } - ++pageIndex; } + mergeOpenAction(srcCatalog, destCatalog, cloner); if (mergeStructTree) { updatePageReferences(cloner, srcNumberTreeAsMap, objMapping); @@ -859,14 +811,19 @@ else if (destOCP != null && srcOCP != null) { int srcKey = entry.getKey(); maxSrcKey = Math.max(srcKey, maxSrcKey); - destNumberTreeAsMap.put(destParentTreeNextKey + srcKey, cloner.cloneForNewDocument(entry.getValue())); + COSObjectable value = entry.getValue(); + if (value != null) + { + value = cloner.cloneForNewDocument(value.getCOSObject()); + destNumberTreeAsMap.put(destParentTreeNextKey + srcKey, value); + } } destParentTreeNextKey += maxSrcKey + 1; PDNumberTreeNode newParentTreeNode = new PDNumberTreeNode(PDParentTreeValue.class); // Note that all elements are stored flatly. This could become a problem for large files // when these are opened in a viewer that uses the tagging information. - // If this happens, then ​PDNumberTreeNode should be improved with a convenience method that + // If this happens, then PDNumberTreeNode should be improved with a convenience method that // stores the map into a B+Tree, see https://en.wikipedia.org/wiki/B+_tree newParentTreeNode.setNumbers(destNumberTreeAsMap); @@ -874,15 +831,61 @@ else if (destOCP != null && srcOCP != null) destStructTree.setParentTreeNextKey(destParentTreeNextKey); mergeKEntries(cloner, srcStructTree, destStructTree); - mergeRoleMap(srcStructTree, destStructTree); + mergeRoleMap(srcStructTree, destStructTree, cloner); mergeIDTree(cloner, srcStructTree, destStructTree); mergeMarkInfo(destCatalog, srcCatalog); mergeLanguage(destCatalog, srcCatalog); - mergeViewerPreferences(destCatalog, srcCatalog); + mergeViewerPreferences(destCatalog, srcCatalog, cloner); } } - private void mergeViewerPreferences(PDDocumentCatalog destCatalog, PDDocumentCatalog srcCatalog) + private void mergeOpenAction(PDDocumentCatalog srcCatalog, PDDocumentCatalog dstCatalog, + PDFCloneUtility cloner) throws IOException + { + PDDestinationOrAction srcOpenAction = null; + PDDestinationOrAction dstOpenAction = null; + try + { + dstOpenAction = dstCatalog.getOpenAction(); + srcOpenAction = srcCatalog.getOpenAction(); + } + catch (IOException ex) + { + // PDFBOX-4223 + Log.e("PdfBox-Android", "Invalid OpenAction ignored", ex); + } + if (dstOpenAction == null && srcOpenAction != null) + { + COSBase clonedOpenActionBase = cloner.cloneForNewDocument(srcOpenAction.getCOSObject()); + PDDestination openActionDestination = null; + if (clonedOpenActionBase instanceof COSDictionary) + { + PDAction action = PDActionFactory.createAction((COSDictionary) clonedOpenActionBase); + if (action instanceof PDActionGoTo) + { + openActionDestination = ((PDActionGoTo) action).getDestination(); + } + dstCatalog.setOpenAction(action); + } + else if (clonedOpenActionBase instanceof COSArray) + { + openActionDestination = PDDestination.create(clonedOpenActionBase); + dstCatalog.setOpenAction(openActionDestination); + } + if (openActionDestination instanceof PDPageDestination) + { + PDPage page = ((PDPageDestination) openActionDestination).getPage(); + if (page != null && dstCatalog.getPages().indexOf(page) == -1) + { + Log.w("PdfBox-Android", "OpenAction entry ignored because destination page doesn't exist"); + dstCatalog.setOpenAction(null); + } + } + } + } + + private void mergeViewerPreferences(PDDocumentCatalog destCatalog, PDDocumentCatalog srcCatalog, + PDFCloneUtility cloner) throws IOException { PDViewerPreferences srcViewerPreferences = srcCatalog.getViewerPreferences(); if (srcViewerPreferences == null) @@ -892,11 +895,11 @@ private void mergeViewerPreferences(PDDocumentCatalog destCatalog, PDDocumentCat PDViewerPreferences destViewerPreferences = destCatalog.getViewerPreferences(); if (destViewerPreferences == null) { - destViewerPreferences = new PDViewerPreferences(new COSDictionary()); + destViewerPreferences = new PDViewerPreferences(); destCatalog.setViewerPreferences(destViewerPreferences); } mergeInto(srcViewerPreferences.getCOSObject(), destViewerPreferences.getCOSObject(), - Collections.emptySet()); + cloner, Collections.emptySet()); // check the booleans - set to true if one is set and true if (srcViewerPreferences.hideToolbar() || destViewerPreferences.hideToolbar()) @@ -956,8 +959,8 @@ private void mergeMarkInfo(PDDocumentCatalog destCatalog, PDDocumentCatalog srcC } private void mergeKEntries(PDFCloneUtility cloner, - PDStructureTreeRoot srcStructTree, - PDStructureTreeRoot destStructTree) throws IOException + PDStructureTreeRoot srcStructTree, + PDStructureTreeRoot destStructTree) throws IOException { COSBase srcKEntry = srcStructTree.getK(); COSArray srcKArray = new COSArray(); @@ -1072,9 +1075,13 @@ private void updateParentEntry(COSArray kArray, COSDictionary newParent, COSName } private void mergeIDTree(PDFCloneUtility cloner, - PDStructureTreeRoot srcStructTree, - PDStructureTreeRoot destStructTree) throws IOException + PDStructureTreeRoot srcStructTree, + PDStructureTreeRoot destStructTree) throws IOException { + if (srcStructTree == null) + { + return; + } PDNameTreeNode srcIDTree = srcStructTree.getIDTree(); if (srcIDTree == null) { @@ -1091,12 +1098,16 @@ private void mergeIDTree(PDFCloneUtility cloner, { if (destNames.containsKey(entry.getKey())) { - Log.w("PdfBox-Android", "key " + entry.getKey() + " already exists in destination IDTree"); + Log.w("PdfBox-Android", "key '" + entry.getKey() + "' already exists in destination IDTree"); } else { - destNames.put(entry.getKey(), - new PDStructureElement((COSDictionary) cloner.cloneForNewDocument(entry.getValue().getCOSObject()))); + if (entry.getValue() != null) + { + PDStructureElement structureElement = new PDStructureElement( + (COSDictionary) cloner.cloneForNewDocument(entry.getValue().getCOSObject())); + destNames.put(entry.getKey(), structureElement); + } } } destIDTree = new PDStructureElementNameTreeNode(); @@ -1111,8 +1122,12 @@ private void mergeIDTree(PDFCloneUtility cloner, // PDNameTreeNode.getNames() only brings one level, this is why we need this // might be made public at a later time, or integrated into PDNameTreeNode with template. static Map getIDTreeAsMap(PDNameTreeNode idTree) - throws IOException + throws IOException { + if (idTree == null) + { + return new LinkedHashMap(); + } Map names = idTree.getNames(); if (names == null) { @@ -1137,8 +1152,12 @@ static Map getIDTreeAsMap(PDNameTreeNode getNumberTreeAsMap(PDNumberTreeNode tree) - throws IOException + throws IOException { + if (tree == null) + { + return new LinkedHashMap(); + } Map numbers = tree.getNumbers(); if (numbers == null) { @@ -1160,17 +1179,18 @@ static Map getNumberTreeAsMap(PDNumberTreeNode tree) return numbers; } - private void mergeRoleMap(PDStructureTreeRoot srcStructTree, PDStructureTreeRoot destStructTree) + private void mergeRoleMap(PDStructureTreeRoot srcStructTree, PDStructureTreeRoot destStructTree, + PDFCloneUtility cloner) throws IOException { COSDictionary srcDict = srcStructTree.getCOSObject().getCOSDictionary(COSName.ROLE_MAP); - COSDictionary destDict = destStructTree.getCOSObject().getCOSDictionary(COSName.ROLE_MAP); if (srcDict == null) { return; } + COSDictionary destDict = destStructTree.getCOSObject().getCOSDictionary(COSName.ROLE_MAP); if (destDict == null) { - destStructTree.getCOSObject().setItem(COSName.ROLE_MAP, srcDict); // clone not needed + destStructTree.getCOSObject().setItem(COSName.ROLE_MAP, cloner.cloneForNewDocument(srcDict)); return; } for (Map.Entry entry : srcDict.entrySet()) @@ -1183,19 +1203,18 @@ private void mergeRoleMap(PDStructureTreeRoot srcStructTree, PDStructureTreeRoot } if (destDict.containsKey(entry.getKey())) { - Log.w("PdfBox-Android", "key " + entry.getKey() + " already exists in destination RoleMap"); + Log.w("PdfBox-Android", "key '" + entry.getKey().getName() + "' already exists in destination RoleMap"); } else { - destDict.setItem(entry.getKey(), entry.getValue()); + destDict.setItem(entry.getKey(), cloner.cloneForNewDocument(entry.getValue())); } } } // copy outputIntents to destination, but avoid duplicate OutputConditionIdentifier, // except when it is missing or is named "Custom". - private void mergeOutputIntents(PDFCloneUtility cloner, - PDDocumentCatalog srcCatalog, PDDocumentCatalog destCatalog) throws IOException + private void mergeOutputIntents(PDDocumentCatalog srcCatalog, PDDocumentCatalog destCatalog, PDFCloneUtility cloner) throws IOException { List srcOutputIntents = srcCatalog.getOutputIntents(); List dstOutputIntents = destCatalog.getOutputIntents(); @@ -1208,7 +1227,7 @@ private void mergeOutputIntents(PDFCloneUtility cloner, boolean skip = false; for (PDOutputIntent dstOI : dstOutputIntents) { - if (dstOI.getOutputConditionIdentifier().equals(srcOCI)) + if (srcOCI.equals(dstOI.getOutputConditionIdentifier())) { skip = true; break; @@ -1234,7 +1253,7 @@ private void mergeOutputIntents(PDFCloneUtility cloner, * @throws IOException If an error occurs while adding the field. */ private void mergeAcroForm(PDFCloneUtility cloner, PDDocumentCatalog destCatalog, - PDDocumentCatalog srcCatalog ) throws IOException + PDDocumentCatalog srcCatalog ) throws IOException { try { @@ -1244,7 +1263,7 @@ private void mergeAcroForm(PDFCloneUtility cloner, PDDocumentCatalog destCatalog if (destAcroForm == null && srcAcroForm != null) { destCatalog.getCOSObject().setItem(COSName.ACRO_FORM, - cloner.cloneForNewDocument(srcAcroForm.getCOSObject())); + cloner.cloneForNewDocument(srcAcroForm.getCOSObject())); } else @@ -1282,12 +1301,12 @@ else if (acroFormMergeMode == AcroFormMergeMode.JOIN_FORM_FIELDS_MODE) * @throws IOException If an error occurs while adding the field. */ private void acroFormJoinFieldsMode(PDFCloneUtility cloner, PDAcroForm destAcroForm, PDAcroForm srcAcroForm) - throws IOException + throws IOException { List srcFields = srcAcroForm.getFields(); COSArray destFields; - if (srcFields != null && !srcFields.isEmpty()) + if (!srcFields.isEmpty()) { // get the destinations root fields. Could be that the entry doesn't exist // or is of wrong type @@ -1329,7 +1348,7 @@ private void mergeFields(PDFCloneUtility cloner, PDField destField, PDField srcF return; } - if (destField.getFieldType() == "Tx" && destField.getFieldType() == "Tx") + if ("Tx".equals(srcField.getFieldType()) && "Tx".equals(destField.getFieldType())) { // if the field already has multiple widgets we can add to the array if (destField.getCOSObject().containsKey(COSName.KIDS)) @@ -1426,15 +1445,15 @@ private void cleanupWidgetCOSDictionary(COSDictionary widgetCos, boolean removeD * @throws IOException If an error occurs while adding the field. */ private void acroFormLegacyMode(PDFCloneUtility cloner, PDAcroForm destAcroForm, PDAcroForm srcAcroForm) - throws IOException + throws IOException { List srcFields = srcAcroForm.getFields(); COSArray destFields; - if (srcFields != null && !srcFields.isEmpty()) + if (!srcFields.isEmpty()) { // if a form is merged multiple times using PDFBox the newly generated - // fields starting with dummyFieldName may already exist. We need to determine the last unique + // fields starting with dummyFieldName may already exist. We need to determine the last unique // number used and increment that. final String prefix = "dummyFieldName"; final int prefixLength = prefix.length(); @@ -1442,7 +1461,7 @@ private void acroFormLegacyMode(PDFCloneUtility cloner, PDAcroForm destAcroForm, for (PDField destField : destAcroForm.getFieldTree()) { String fieldName = destField.getPartialName(); - if (fieldName.startsWith(prefix)) + if (fieldName != null && fieldName.startsWith(prefix)) { String suffix = fieldName.substring(prefixLength); if (suffix.matches("\\d+")) @@ -1506,8 +1525,8 @@ public void setIgnoreAcroFormErrors(boolean ignoreAcroFormErrorsValue) * Update the Pg and Obj references to the new (merged) page. */ private void updatePageReferences(PDFCloneUtility cloner, - Map numberTreeAsMap, - Map objMapping) throws IOException + Map numberTreeAsMap, + Map objMapping) throws IOException { for (COSObjectable obj : numberTreeAsMap.values()) { @@ -1535,8 +1554,8 @@ private void updatePageReferences(PDFCloneUtility cloner, * @param objMapping mapping between old and new references */ private void updatePageReferences(PDFCloneUtility cloner, - COSDictionary parentTreeEntry, Map objMapping) - throws IOException + COSDictionary parentTreeEntry, Map objMapping) + throws IOException { COSDictionary pageDict = parentTreeEntry.getCOSDictionary(COSName.PG); if (objMapping.containsKey(pageDict)) @@ -1559,17 +1578,17 @@ private void updatePageReferences(PDFCloneUtility cloner, if (item instanceof COSObject) { Log.d("PdfBox-Android", "clone potential orphan object in structure tree: " + item + - ", Type: " + objDict.getNameAsString(COSName.TYPE) + - ", Subtype: " + objDict.getNameAsString(COSName.SUBTYPE) + - ", T: " + objDict.getNameAsString(COSName.T)); + ", Type: " + objDict.getNameAsString(COSName.TYPE) + + ", Subtype: " + objDict.getNameAsString(COSName.SUBTYPE) + + ", T: " + objDict.getNameAsString(COSName.T)); } else { // don't display in full because of stack overflow Log.d("PdfBox-Android", "clone potential orphan object in structure tree" + - ", Type: " + objDict.getNameAsString(COSName.TYPE) + - ", Subtype: " + objDict.getNameAsString(COSName.SUBTYPE) + - ", T: " + objDict.getNameAsString(COSName.T)); + ", Type: " + objDict.getNameAsString(COSName.TYPE) + + ", Subtype: " + objDict.getNameAsString(COSName.SUBTYPE) + + ", T: " + objDict.getNameAsString(COSName.T)); } parentTreeEntry.setItem(COSName.OBJ, cloner.cloneForNewDocument(obj)); } @@ -1586,8 +1605,8 @@ else if (kSubEntry instanceof COSDictionary) } private void updatePageReferences(PDFCloneUtility cloner, - COSArray parentTreeEntry, Map objMapping) - throws IOException + COSArray parentTreeEntry, Map objMapping) + throws IOException { for (int i = 0; i < parentTreeEntry.size(); i++) { @@ -1651,13 +1670,13 @@ private boolean isDynamicXfa(PDAcroForm acroForm) * @param dst The destination dictionary to merge the keys/values into. * @param exclude Names of keys that shall be skipped. */ - private void mergeInto(COSDictionary src, COSDictionary dst, Set exclude) + private void mergeInto(COSDictionary src, COSDictionary dst, PDFCloneUtility cloner, Set exclude) throws IOException { for (Map.Entry entry : src.entrySet()) { if (!exclude.contains(entry.getKey()) && !dst.containsKey(entry.getKey())) { - dst.setItem(entry.getKey(), entry.getValue()); + dst.setItem(entry.getKey(), cloner.cloneForNewDocument(entry.getValue())); } } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/multipdf/Splitter.java b/library/src/main/java/com/tom_roush/pdfbox/multipdf/Splitter.java index b27105af0..465596fea 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/multipdf/Splitter.java +++ b/library/src/main/java/com/tom_roush/pdfbox/multipdf/Splitter.java @@ -20,30 +20,57 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Set; +import com.tom_roush.pdfbox.cos.COSArray; import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSDictionary; +import com.tom_roush.pdfbox.cos.COSInteger; import com.tom_roush.pdfbox.cos.COSName; +import com.tom_roush.pdfbox.cos.COSObject; import com.tom_roush.pdfbox.io.MemoryUsageSetting; import com.tom_roush.pdfbox.pdmodel.PDDocument; import com.tom_roush.pdfbox.pdmodel.PDDocumentInformation; import com.tom_roush.pdfbox.pdmodel.PDPage; +import com.tom_roush.pdfbox.pdmodel.PDPageTree; +import com.tom_roush.pdfbox.pdmodel.PDResources; +import com.tom_roush.pdfbox.pdmodel.PDStructureElementNameTreeNode; +import com.tom_roush.pdfbox.pdmodel.common.COSObjectable; +import com.tom_roush.pdfbox.pdmodel.common.PDNameTreeNode; +import com.tom_roush.pdfbox.pdmodel.common.PDNumberTreeNode; +import com.tom_roush.pdfbox.pdmodel.documentinterchange.logicalstructure.PDParentTreeValue; +import com.tom_roush.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureElement; +import com.tom_roush.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureTreeRoot; +import com.tom_roush.pdfbox.pdmodel.graphics.PDXObject; +import com.tom_roush.pdfbox.pdmodel.graphics.form.PDFormXObject; +import com.tom_roush.pdfbox.pdmodel.graphics.image.PDImageXObject; import com.tom_roush.pdfbox.pdmodel.interactive.action.PDAction; +import com.tom_roush.pdfbox.pdmodel.interactive.action.PDActionFactory; import com.tom_roush.pdfbox.pdmodel.interactive.action.PDActionGoTo; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotation; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink; +import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationMarkup; +import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationPopup; +import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; +import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream; import com.tom_roush.pdfbox.pdmodel.interactive.documentnavigation.destination.PDDestination; +import com.tom_roush.pdfbox.pdmodel.interactive.documentnavigation.destination.PDNamedDestination; import com.tom_roush.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageDestination; - /** * Split a document into several other documents. * * @author Mario Ivankovits * @author Ben Litchfield + * @author Tilman Hausherr */ public class Splitter { + private PDDocument sourceDocument; private PDDocument currentDestinationDocument; @@ -51,6 +78,14 @@ public class Splitter private int startPage = Integer.MIN_VALUE; private int endPage = Integer.MAX_VALUE; private List destinationDocuments; + private Map pageDictMap; // map old page => new page for the current destination document + private List> pageDictMaps; // list of these maps for all destination documents + private Map structDictMap; + private List> annotDictMaps; // map old annotation => new annotation for the current destination document + private Map annotDictMap; // list of these maps for all destination documents + private Map destToFixMap; + private Set idSet; + private Set roleSet; private int currentPageNumber; @@ -92,10 +127,498 @@ public List split(PDDocument document) throws IOException currentPageNumber = 0; destinationDocuments = new ArrayList(); sourceDocument = document; + pageDictMaps = new ArrayList>(); + annotDictMaps = new ArrayList>(); + destToFixMap = new HashMap(); + idSet = new HashSet(); + roleSet = new HashSet(); + processPages(); + + for (int i = 0; i < destinationDocuments.size(); ++i) + { + PDDocument destinationDocument = destinationDocuments.get(i); + pageDictMap = pageDictMaps.get(i); + annotDictMap = annotDictMaps.get(i); + cloneStructureTree(destinationDocument); + fixDestinations(destinationDocument); + } + return destinationDocuments; } + /** + * Replace the page destinations, if the source and destination pages are in the target + * document. This must be called after all pages (and its annotations) are processed. + * + * @param destinationDocument + */ + private void fixDestinations(PDDocument destinationDocument) + { + PDPageTree pageTree = destinationDocument.getPages(); + for (Map.Entry entry : destToFixMap.entrySet()) + { + PDPageDestination pageDestination = entry.getKey(); + // Find whether source page is inside or outside + PDPage srcPage = entry.getValue(); + if (pageTree.indexOf(srcPage) < 0) + { + continue; + } + COSDictionary srcPageDict = pageDestination.getPage().getCOSObject(); + COSDictionary dstPageDict = pageDictMap.get(srcPageDict); + PDPage dstPage = new PDPage(dstPageDict); + // Find whether destination page is inside or outside + if (pageTree.indexOf(dstPage) >= 0) + { + pageDestination.setPage(dstPage); + } + else + { + pageDestination.setPage(null); + } + } + } + + /** + * Clone the structure tree from the source to the current destination document. This must be + * called after all pages are processed. + * + * @param destinationDocument + * @throws IOException + */ + private void cloneStructureTree(PDDocument destinationDocument) throws IOException + { + PDStructureTreeRoot srcStructureTreeRoot = sourceDocument.getDocumentCatalog().getStructureTreeRoot(); + if (srcStructureTreeRoot == null) + { + return; + } + structDictMap = new HashMap(); + PDStructureTreeRoot dstStructureTreeRoot = new PDStructureTreeRoot(); + PDPageTree dstPageTree = destinationDocument.getPages(); + + // clone /K, also fills dictMap + COSBase k1 = srcStructureTreeRoot.getK(); + COSBase k2 = new KCloner(dstPageTree).createClone(k1, dstStructureTreeRoot.getCOSObject(), null); + dstStructureTreeRoot.setK(k2); + + // transfer ParentTree using the map because the dictionaries are all found in the /K structure. + PDNumberTreeNode srcParentTree = srcStructureTreeRoot.getParentTree(); + Map srcNumberTreeAsMap = PDFMergerUtility.getNumberTreeAsMap(srcParentTree); + Map dstNumberTreeAsMap = new LinkedHashMap(); + int dstPageTreeCount = dstPageTree.getCount(); + for (int p = 0; p < dstPageTreeCount; ++p) + { + PDPage page = dstPageTree.get(p); + int sp1 = page.getStructParents(); + if (sp1 != -1) + { + cloneTreeElement(srcNumberTreeAsMap, dstNumberTreeAsMap, sp1); + } + for (PDAnnotation ann : page.getAnnotations()) + { + int sp2 = ann.getStructParent(); + if (sp2 != -1) + { + cloneTreeElement(srcNumberTreeAsMap, dstNumberTreeAsMap, sp2); + } + PDAppearanceStream normalAppearanceStream = ann.getNormalAppearanceStream(); + if (normalAppearanceStream != null) + { + processResources(normalAppearanceStream.getResources(), + srcNumberTreeAsMap, dstNumberTreeAsMap, new HashSet()); + } + } + processResources(page.getResources(), + srcNumberTreeAsMap, dstNumberTreeAsMap, new HashSet()); + } + PDNumberTreeNode dstNumberTreeNode = new PDNumberTreeNode(PDParentTreeValue.class); + dstNumberTreeNode.setNumbers(dstNumberTreeAsMap); + dstStructureTreeRoot.setParentTree(dstNumberTreeNode); + Integer upperLimit = dstNumberTreeNode.getUpperLimit(); + if (upperLimit != null) + { + dstStructureTreeRoot.setParentTreeNextKey(upperLimit + 1); + } + dstStructureTreeRoot.setClassMap(srcStructureTreeRoot.getClassMap()); + cloneRoleMap(srcStructureTreeRoot, dstStructureTreeRoot); + cloneIDTree(srcStructureTreeRoot, dstStructureTreeRoot); + + destinationDocument.getDocumentCatalog().setStructureTreeRoot(dstStructureTreeRoot); + } + + private void cloneIDTree(PDStructureTreeRoot srcStructTree, PDStructureTreeRoot destStructTree) + throws IOException + { + PDNameTreeNode srcIDTree = srcStructTree.getIDTree(); + if (srcIDTree == null) + { + return; + } + Map srcIDTreeAsMap = PDFMergerUtility.getIDTreeAsMap(srcIDTree); + Map destNames = new HashMap(); + for (Map.Entry entry : srcIDTreeAsMap.entrySet()) + { + String key = entry.getKey(); + if (!idSet.contains(key)) + { + continue; + } + COSDictionary dstDict = structDictMap.get(entry.getValue().getCOSObject()); + if (dstDict != null) + { + destNames.put(key, new PDStructureElement(dstDict)); + } + } + PDNameTreeNode destIDTree = new PDStructureElementNameTreeNode(); + destIDTree.setNames(destNames); + destStructTree.setIDTree(destIDTree); + // See comment at the end of PDFMergerUtility.mergeIDTree() + } + + // needed because getRoleMap() and setRoleMap() habe different map types?! + private void cloneRoleMap(PDStructureTreeRoot srcStructTree, PDStructureTreeRoot destStructTree) + { + COSDictionary srcDict = srcStructTree.getCOSObject().getCOSDictionary(COSName.ROLE_MAP); + if (srcDict == null) + { + return; + } + COSDictionary dstDict = new COSDictionary(); + for (Map.Entry entry : srcDict.entrySet()) + { + if (roleSet.contains(entry.getKey())) + { + dstDict.setItem(entry.getKey(), entry.getValue()); + } + } + destStructTree.getCOSObject().setItem(COSName.ROLE_MAP, dstDict); + } + + // clone tree element using the map so that structure elements are replaced + private void cloneTreeElement( + Map srcNumberTreeAsMap, + Map dstNumberTreeAsMap, + int sp) + { + COSObjectable srcObj = srcNumberTreeAsMap.get(sp); // this is a PDParentTreeValue class + COSObjectable dstObj = null; + if (srcObj != null) + { + COSBase actualSrcObj = srcObj.getCOSObject(); + // structure element or array + if (actualSrcObj instanceof COSArray) + { + // create a clone of the array + COSArray srcArray = (COSArray) actualSrcObj; + COSArray dstArray = new COSArray(); + for (int i = 0; i < srcArray.size(); ++i) + { + COSBase srcElement = srcArray.getObject(i); + dstArray.add(structDictMap.get(srcElement)); // may be null + } + dstObj = dstArray; + } + else if (actualSrcObj instanceof COSDictionary) + { + // get the clone from the map + dstObj = structDictMap.get(actualSrcObj); + if (dstObj == null) + { + // 164421.pdf, structure tree is weird. + // also 250052.pdf, 250198.pdf, 257012.pdf, 271459.pdf (multiple), + // 670045.pdf (multiple) + // In 71459.pdf annotations on page 1 have StructParent numbers + // that point to structure elements in the /ParentTree that point to + // a different page. + Log.w("PdfBox-Android", "ParentTree index " + sp + " dictionary not found in /K"); + } + } + else + { + Log.w("PdfBox-Android", "tree element neither dictionary nor array, but " + + (actualSrcObj == null ? "(null)" : actualSrcObj.getClass().getSimpleName())); + } + if (dstObj != null) + { + dstNumberTreeAsMap.put(sp, dstObj); + } + } + } + + /** + * Class to help clone the /K tree. It clones structure elements and fills the structure + * elements map. Pages are replaced with the help of the page map. Elements with pages that + * don't belong to the destination are removed from the clone. + */ + private class KCloner + { + PDPageTree dstPageTree; + + KCloner(PDPageTree dstPageTree) + { + this.dstPageTree = dstPageTree; + } + + /** + * Creates a clone of the source. + * + * @param src source dictionary or array. + * @param dstParent for the /P entry; parameter needed because arrays don't keep a parent. + * @param currentPageDict used to remember whether we have a page parent somewhere or not. + * Starts with null. + * @return a clone, or null if source is null or if there is no clone because it belongs to a + * different page or to no page. + */ + COSBase createClone(COSBase src, COSBase dstParent, COSDictionary currentPageDict) + { + if (src instanceof COSArray) + { + return createArrayClone(src, dstParent, currentPageDict); + } + else if (src instanceof COSDictionary) + { + return createDictionaryClone(src, dstParent, currentPageDict); + } + else + { + return src; + } + } + + private COSBase createArrayClone(COSBase src, COSBase dstParent, COSDictionary currentPageDict) + { + COSArray dst = new COSArray(); + for (COSBase base2 : (COSArray) src) + { + COSBase rc; + if (base2 instanceof COSObject) + { + rc = createClone(((COSObject) base2).getObject(), dstParent, currentPageDict); + } + else + { + rc = createClone(base2, dstParent, currentPageDict); + } + // if this is null then they don't belong to the destination document + if (rc != null) + { + dst.add(rc); + } + } + return dst.size() > 0 ? dst : null; + } + + private COSBase createDictionaryClone(COSBase src, COSBase dstParent, COSDictionary currentPageDict) + { + COSDictionary srcDict = (COSDictionary) src; + COSDictionary dstDict = structDictMap.get(srcDict); + if (dstDict != null) + { + return dstDict; + } + COSDictionary srcPageDict = srcDict.getCOSDictionary(COSName.PG); + COSDictionary dstPageDict = null; + COSBase kid = srcDict.getDictionaryObject(COSName.K); + COSName type = srcDict.getCOSName(COSName.TYPE); + if (srcPageDict != null) + { + dstPageDict = pageDictMap.get(srcPageDict); + if (dstPageDict != null) + { + PDPage dstPage = new PDPage(dstPageDict); + if (dstPageTree.indexOf(dstPage) == -1) + { + return null; + } + } + else + { + // PDFBOX-6009: "wrong" /Pg entry + // quit if MCIDs because these need a /Pg entry + // or if MCR/OBJR dicts + if (COSName.MCR.equals(type) || COSName.OBJR.equals(type) || hasMCIDs(kid)) + { + return null; + } + // else keep this as an intermediate element for now + } + } + + // special handling for MCR items ("marked-content reference dictionary") + if (COSName.MCR.equals(type) && dstPageDict == null && + dstParent instanceof COSDictionary && ((COSDictionary) dstParent).getCOSDictionary(COSName.PG) == null) + { + // PAC: Pg entry of marked-content reference and of parent structure is null + return null; + } + + // Create and fill clone + dstDict = new COSDictionary(); + structDictMap.put(srcDict, dstDict); + for (Map.Entry entry : srcDict.entrySet()) + { + COSName key = entry.getKey(); + if (!COSName.K.equals(key) && + !COSName.PG.equals(key) && + !COSName.P.equals(key)) + { + dstDict.setItem(key, entry.getValue()); + } + } + + // special handling for OBJR items ("object reference dictionary") + // see e.g. file 488300.pdf and Root/StructTreeRoot/K/K/[2]/K/[1]/K/[0]/Obj + if (COSName.OBJR.equals(type)) + { + COSDictionary srcObj = srcDict.getCOSDictionary(COSName.OBJ); + COSDictionary dstObj = annotDictMap.get(srcObj); + if (dstObj != null) + { + // replace annotation with clone + dstDict.setItem(COSName.OBJ, dstObj); + } + else if (srcObj != null) // 079177.pdf + { + removePossibleOrphanAnnotation(srcObj, srcDict, currentPageDict, dstDict); + } + if (dstDict.size() == 1) + { + return null; + } + if (dstPageDict == null && + dstParent instanceof COSDictionary && ((COSDictionary) dstParent).getCOSDictionary(COSName.PG) == null) + { + // Pg entry of object reference dictionary and of parent structure is null + return null; + } + } + + if (!COSName.OBJR.equals(type) && !COSName.MCR.equals(type)) + { + // /P not needed for OBJR or MCR items + dstDict.setItem(COSName.P, dstParent); + } + + dstDict.setItem(COSName.PG, dstPageDict); + + // stack overflow here with 207658.pdf, too complex + COSBase cloneKid = createClone(kid, dstDict, dstPageDict != null ? dstPageDict : currentPageDict); + if (cloneKid == null && kid != null) + { + return null; // kids array wasn't empty, but is empty now => ignore + } + + // removes orphan nodes, example: + // Root/StructTreeRoot/K/[7]/K/[3]/K/[5]/K/[2] in 271459.pdf + // decide about keeping source dictionaries with no /K and no /PG + if (dstPageDict == null && cloneKid == null && currentPageDict == null) + { + // if no parent page and no page here and no kids, assume this is an orphan + return null; + } + dstDict.setItem(COSName.K, cloneKid); + String id = dstDict.getString(COSName.ID); + if (id != null) + { + idSet.add(id); + } + COSName s = dstDict.getCOSName(COSName.S); + if (s != null) + { + roleSet.add(s); + } + return dstDict; + } + + private boolean hasMCIDs(COSBase kid) + { + if (kid instanceof COSInteger) + { + return true; + } + if (kid instanceof COSArray) + { + COSArray ar = (COSArray) kid; + for (int i = 0; i < ar.size(); ++i) + { + if (ar.getObject(i) instanceof COSInteger) + { + return true; + } + } + } + return false; + } + + private void removePossibleOrphanAnnotation(COSDictionary srcObj, COSDictionary srcDict, + COSDictionary currentPageDict, COSDictionary dstDict) + { + // PDFBOX-5929: Check whether this is an "orphan" annotation that isn't in the page + COSBase objType = srcObj.getDictionaryObject(COSName.TYPE); + COSBase objSubtype = srcObj.getDictionaryObject(COSName.SUBTYPE); + if (COSName.ANNOT.equals(objType) || COSName.LINK.equals(objSubtype)) + { + COSDictionary srcPageDict = srcDict.getCOSDictionary(COSName.PG); + if (srcPageDict == null) + { + // /Pg entry is not always on this level + srcPageDict = currentPageDict; + } + if (srcPageDict != null) + { + COSArray annotationArray = srcPageDict.getCOSArray(COSName.ANNOTS); + if (annotationArray == null || annotationArray.indexOfObject(srcObj) == -1) + { + // Ideally the entire OBJR entry should be removed. + // Removing the OBJ entry is done to avoid potential page orphans + // from the annotation destination. + Log.w("PdfBox-Android", "An annotation OBJ that isn't in the page has been removed from the structure tree"); + dstDict.removeItem(COSName.OBJ); + } + } + } + } + } + + // Look for /StructParent and /StructParents and add them to the destination tree + private void processResources(PDResources res, + Map srcNumberTreeAsMap, + Map dstNumberTreeAsMap, + Set visited) throws IOException + { + if (res == null) + { + return; + } + if (visited.contains(res.getCOSObject())) + { + // avoid endless recursion, e.g. with 002874.pdf + return; + } + visited.add(res.getCOSObject()); + + for (COSName name : res.getXObjectNames()) + { + PDXObject xObject = res.getXObject(name); + int sp2 = -1; + if (xObject instanceof PDFormXObject) + { + sp2 = ((PDFormXObject) xObject).getStructParents(); + processResources(((PDFormXObject) xObject).getResources(), srcNumberTreeAsMap, dstNumberTreeAsMap, visited); + } + else if (xObject instanceof PDImageXObject) + { + sp2 = ((PDImageXObject) xObject).getStructParent(); + } + if (sp2 != -1) + { + cloneTreeElement(srcNumberTreeAsMap, dstNumberTreeAsMap, sp2); + } + } + } + /** * This will tell the splitting algorithm where to split the pages. The default * is 1, so every page will become a new document. If it was two then each document would @@ -134,7 +657,7 @@ public void setStartPage(int start) * This will set the end page. * * @param end the 1-based end page - * @throws IllegalArgumentException if the end page is smaller than one. + * @throws IllegalArgumentException if the end page is smaller than one or than the start page. */ public void setEndPage(int end) { @@ -142,6 +665,10 @@ public void setEndPage(int end) { throw new IllegalArgumentException("End page is smaller than one"); } + if (end < startPage) + { + throw new IllegalArgumentException("End page is smaller than startPage"); + } endPage = end; } @@ -184,6 +711,10 @@ private void createNewDocumentIfNecessary() throws IOException { currentDestinationDocument = createNewDocument(); destinationDocuments.add(currentDestinationDocument); + pageDictMap = new HashMap(); + pageDictMaps.add(pageDictMap); + annotDictMap = new HashMap(); + annotDictMaps.add(annotDictMap); } } @@ -210,13 +741,13 @@ protected boolean splitAtPage(int pageNumber) /** * Create a new document to write the split contents to. * - * @return the newly created PDDocument. + * @return the newly created PDDocument. * @throws IOException If there is an problem creating the new document. */ protected PDDocument createNewDocument() throws IOException { PDDocument document = memoryUsageSetting == null ? - new PDDocument() : new PDDocument(memoryUsageSetting); + new PDDocument() : new PDDocument(memoryUsageSetting); document.getDocument().setVersion(getSourceDocument().getVersion()); PDDocumentInformation sourceDocumentInformation = getSourceDocument().getDocumentInformation(); if (sourceDocumentInformation != null) @@ -231,9 +762,9 @@ protected PDDocument createNewDocument() throws IOException if (value instanceof COSDictionary) { Log.w("PdfBox-Android", "Nested entry for key '" + key.getName() - + "' skipped in document information dictionary"); + + "' skipped in document information dictionary"); if (sourceDocument.getDocumentCatalog().getCOSObject() == - sourceDocument.getDocumentInformation().getCOSObject()) + sourceDocument.getDocumentInformation().getCOSObject()) { Log.w("PdfBox-Android", "/Root and /Info share the same dictionary"); } @@ -248,7 +779,13 @@ protected PDDocument createNewDocument() throws IOException document.setDocumentInformation(new PDDocumentInformation(destDocumentInformationDictionary)); } document.getDocumentCatalog().setViewerPreferences( - getSourceDocument().getDocumentCatalog().getViewerPreferences()); + getSourceDocument().getDocumentCatalog().getViewerPreferences()); + document.getDocumentCatalog().setLanguage( + getSourceDocument().getDocumentCatalog().getLanguage()); + document.getDocumentCatalog().setMarkInfo( + getSourceDocument().getDocumentCatalog().getMarkInfo()); + document.getDocumentCatalog().setMetadata( + getSourceDocument().getDocumentCatalog().getMetadata()); return document; } @@ -269,34 +806,175 @@ protected void processPage(PDPage page) throws IOException imported.setResources(page.getResources()); Log.i("PdfBox-Android", "Resources imported in Splitter"); // follow-up to warning in importPage } - // remove page links to avoid copying not needed resources + if (imported.getCOSObject().containsKey(COSName.B)) + { + imported.getCOSObject().removeItem(COSName.B); + Log.w("PdfBox-Android", "/B entry (beads) removed by splitter"); + } + // remove page links to avoid copying not needed resources processAnnotations(imported); + + pageDictMap.put(page.getCOSObject(), imported.getCOSObject()); } + /** + * Clone all annotations because of changes possibly made, and because the structure tree is + * cloned. + * + * @param imported + * @throws IOException + */ private void processAnnotations(PDPage imported) throws IOException { List annotations = imported.getAnnotations(); + if (annotations.isEmpty()) + { + return; + } + List clonedAnnotations = new ArrayList(annotations.size()); for (PDAnnotation annotation : annotations) { - if (annotation instanceof PDAnnotationLink) + // create a shallow clone + COSDictionary clonedDict = new COSDictionary(annotation.getCOSObject()); + PDAnnotation annotationClone = PDAnnotation.createAnnotation(clonedDict); + annotDictMap.put(annotation.getCOSObject(), clonedDict); + clonedAnnotations.add(annotationClone); + if (annotationClone instanceof PDAnnotationLink) { - PDAnnotationLink link = (PDAnnotationLink)annotation; - PDDestination destination = link.getDestination(); - PDAction action = link.getAction(); - if (destination == null && action instanceof PDActionGoTo) + PDAnnotationLink link = (PDAnnotationLink) annotationClone; + PDDestination srcDestination = null; + try + { + srcDestination = link.getDestination(); + } + catch (IOException ex) { - destination = ((PDActionGoTo) action).getDestination(); + Log.w("PdfBox-Android", "Incorrect destination in link annotation on page " + + (currentPageNumber + 1) + " is removed", ex); + link.setDestination(null); + } + PDAction action = null; + if (srcDestination == null) + { + action = link.getAction(); + if (action instanceof PDActionGoTo) + { + PDActionGoTo goToAction = (PDActionGoTo) action; + try + { + srcDestination = goToAction.getDestination(); + } + catch (IOException ex) + { + Log.w("PdfBox-Android", "GoToAction with incorrect destination in link annotation on page " + + (currentPageNumber + 1) + " is removed", ex); + link.setAction(null); + } + } } - if (destination instanceof PDPageDestination) + if (srcDestination instanceof PDNamedDestination) { - // TODO preserve links to pages within the split result - ((PDPageDestination) destination).setPage(null); + srcDestination = sourceDocument.getDocumentCatalog(). + findNamedDestinationPage((PDNamedDestination) srcDestination); + // we do not use the named destination anymore because names get modified, e.g. + // 0xAD becomes 0, see file 410609.pdf where the name no longer matches with the + // entry in the new name tree; plus the original solution was 40 additional loc + } + if (srcDestination instanceof PDPageDestination) + { + // preserve links to pages within the split result: + // not fully possible here because we don't have the full target document yet. + // However we're cloning as needed and remember what to do later. + PDPage destinationPage = ((PDPageDestination) srcDestination).getPage(); + if (destinationPage != null) + { + // clone destination + COSArray clonedDestinationArray = new COSArray(); + COSArray srcDestinationArray = ((PDPageDestination) srcDestination).getCOSObject(); + for (int i = 0; i < srcDestinationArray.size(); ++i) + { + clonedDestinationArray.add(srcDestinationArray.getObject(i)); + } + PDPageDestination dstDestination = + (PDPageDestination) PDDestination.create(clonedDestinationArray); + + // remember the destination to adjust / remove page later + destToFixMap.put(dstDestination, imported); + + if (action != null) + { + // if action is not null, then the destination came from an action, + // thus clone action as well, then assign destination clone, then action + COSDictionary clonedActionDict = new COSDictionary(action.getCOSObject()); + PDActionGoTo dstAction = + (PDActionGoTo) PDActionFactory.createAction(clonedActionDict); + dstAction.setDestination(dstDestination); + link.setAction(dstAction); + } + else + { + // just assign destination clone + link.setDestination(dstDestination); + } + } + } + } + if (annotationClone instanceof PDAnnotationWidget && + annotationClone.getCOSObject().containsKey(COSName.PARENT)) + { + // remove non-terminal field /Parent reference, because this may lead to orphan pages + annotationClone.getCOSObject().removeItem(COSName.PARENT); + } + if (annotation.getPage() != null) + { + annotationClone.setPage(imported); + } + } + // Second loop for markup and popup annotations, which reference annotations themselves + for (PDAnnotation annotation : clonedAnnotations) + { + if (annotation instanceof PDAnnotationMarkup) + { + PDAnnotationPopup annotationPopup = ((PDAnnotationMarkup) annotation).getPopup(); + if (annotationPopup == null) + { + continue; + } + COSDictionary clonedPopupDict = annotDictMap.get(annotationPopup.getCOSObject()); + if (clonedPopupDict != null) + { + annotation.getCOSObject().setItem(COSName.POPUP, clonedPopupDict); + } + else + { + // orphan popup (not in annotation list); clone it and fix references + clonedPopupDict = new COSDictionary(annotationPopup.getCOSObject()); + annotDictMap.put(annotationPopup.getCOSObject(), clonedPopupDict); + PDAnnotationPopup annotationPopupClone = + (PDAnnotationPopup) PDAnnotation.createAnnotation(clonedPopupDict); + annotationPopupClone.setParent((PDAnnotationMarkup) annotation); + ((PDAnnotationMarkup) annotation).setPopup(annotationPopupClone); + if (annotationPopupClone.getPage() != null) + { + annotationPopupClone.setPage(imported); + } } } - // TODO preserve links to pages within the split result - annotation.setPage(null); + if (annotation instanceof PDAnnotationPopup) + { + PDAnnotationMarkup annotationMarkup = ((PDAnnotationPopup) annotation).getParent(); + if (annotationMarkup == null) + { + continue; + } + COSDictionary clonedMarkupDict = annotDictMap.get(annotationMarkup.getCOSObject()); + // clonedMarkupDict will be null if markup annotation is an orphan (not in annotation list) + annotation.getCOSObject().setItem(COSName.PARENT, clonedMarkupDict); + } } + imported.setAnnotations(clonedAnnotations); } + /** * The source PDF document. * diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdfparser/BaseParser.java b/library/src/main/java/com/tom_roush/pdfbox/pdfparser/BaseParser.java index a238ab36b..84006b6b0 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdfparser/BaseParser.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdfparser/BaseParser.java @@ -20,9 +20,12 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; +import java.nio.charset.CodingErrorAction; import com.tom_roush.pdfbox.cos.COSArray; import com.tom_roush.pdfbox.cos.COSBase; @@ -37,9 +40,6 @@ import com.tom_roush.pdfbox.cos.COSObjectKey; import com.tom_roush.pdfbox.cos.COSString; import com.tom_roush.pdfbox.util.Charsets; - -import static com.tom_roush.pdfbox.util.Charsets.ISO_8859_1; - /** * This class is used to contain parsing logic that will be used by both the * PDFParser and the COSStreamParser. @@ -52,9 +52,41 @@ public abstract class BaseParser private static final long GENERATION_NUMBER_THRESHOLD = 65535; - static final int MAX_LENGTH_LONG = Long.toString(Long.MAX_VALUE).length(); + private static final int MAX_LENGTH_LONG = Long.toString(Long.MAX_VALUE).length(); + + private static final Charset ALTERNATIVE_CHARSET; - private final CharsetDecoder utf8Decoder = Charsets.UTF_8.newDecoder(); + private static final int MAX_RECURSION_DEPTH = 500; + private static final String MAX_RECUSRION_MSG = // + "Reached maximum recursion depth " + Integer.toString(MAX_RECURSION_DEPTH); + + private int recursionDepth = 0; + + static + { + Charset cs; + String charsetName = "Windows-1252"; + try + { + cs = Charset.forName(charsetName); + } + catch (IllegalArgumentException e) + { + cs = Charsets.ISO_8859_1; + Log.w("PdfBox-Android", "Charset is not supported: " + charsetName + ", falling back to " + cs.name(), e); + } + catch (UnsupportedOperationException e) + { + cs = Charsets.ISO_8859_1; + Log.w("PdfBox-Android", "Charset is not supported: " + charsetName + ", falling back to " + cs.name(), e); + } + ALTERNATIVE_CHARSET = cs; + } + + // CharSetDecoders are not threadsafe so not static + private final CharsetDecoder utf8Decoder = Charsets.UTF_8.newDecoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT); protected static final int E = 'e'; protected static final int N = 'n'; @@ -132,8 +164,8 @@ public abstract class BaseParser private static boolean isHexDigit(char ch) { return isDigit(ch) || - (ch >= 'a' && ch <= 'f') || - (ch >= 'A' && ch <= 'F'); + (ch >= 'a' && ch <= 'f') || + (ch >= 'A' && ch <= 'F'); } /** @@ -189,7 +221,7 @@ private COSBase getObjectFromPool(COSObjectKey key) throws IOException if (document == null) { throw new IOException("object reference " + key + " at offset " + seqSource.getPosition() - + " in content stream"); + + " in content stream"); } return document.getObjectFromPool(key); } @@ -203,42 +235,63 @@ private COSBase getObjectFromPool(COSObjectKey key) throws IOException */ protected COSDictionary parseCOSDictionary() throws IOException { - readExpectedChar('<'); - readExpectedChar('<'); - skipSpaces(); - COSDictionary obj = new COSDictionary(); - boolean done = false; - while (!done) + try { - skipSpaces(); - char c = (char) seqSource.peek(); - if (c == '>') + recursionDepth++; + if (recursionDepth > MAX_RECURSION_DEPTH) { - done = true; + throw new IOException(MAX_RECUSRION_MSG); } - else if (c == '/') + readExpectedChar('<'); + readExpectedChar('<'); + skipSpaces(); + COSDictionary obj = new COSDictionary(); + boolean done = false; + while (!done) { - // something went wrong, most likely the dictionary is corrupted - // stop immediately and return everything read so far - if (!parseCOSDictionaryNameValuePair(obj)) + skipSpaces(); + char c = (char) seqSource.peek(); + if (c == '>') { - return obj; + done = true; } - } - else - { - // invalid dictionary, we were expecting a /Name, read until the end or until we can recover - Log.w("PdfBox-Android", "Invalid dictionary, found: '" + c + "' but expected: '/' at offset " + seqSource.getPosition()); - if (readUntilEndOfCOSDictionary()) + else if (c == '/') { - // we couldn't recover - return obj; + // something went wrong, most likely the dictionary is corrupted + // stop immediately and return everything read so far + if (!parseCOSDictionaryNameValuePair(obj)) + { + return obj; + } } + else + { + // invalid dictionary, we were expecting a /Name, read until the end or until we can recover + Log.w("PdfBox-Android", "Invalid dictionary, found: '" + c + "' but expected: '/' at offset " + + seqSource.getPosition()); + if (readUntilEndOfCOSDictionary()) + { + // we couldn't recover + return obj; + } + } + } + try + { + readExpectedChar('>'); + readExpectedChar('>'); + } + catch (IOException exception) + { + Log.w("PdfBox-Android", "Invalid dictionary, can't find end of dictionary at offset " + + seqSource.getPosition()); } + return obj; + } + finally + { + recursionDepth--; } - readExpectedChar('>'); - readExpectedChar('>'); - return obj; } /** @@ -267,7 +320,7 @@ private boolean readUntilEndOfCOSDictionary() throws IOException { c = seqSource.read(); boolean isStream = c == S && seqSource.read() == T && seqSource.read() == R - && seqSource.read() == E && seqSource.read() == A && seqSource.read() == M; + && seqSource.read() == E && seqSource.read() == A && seqSource.read() == M; boolean isObj = !isStream && c == O && seqSource.read() == B && seqSource.read() == J; if (isStream || isObj) { @@ -314,6 +367,11 @@ else if (value instanceof COSInteger && !((COSInteger)value).isValid()) return true; } + /** + * Skip the upcoming CRLF or LF which are supposed to follow a stream. Trailing spaces are removed as well. + * + * @throws IOException if something went wrong + */ protected void skipWhiteSpaces() throws IOException { //PDF Ref 3.2.7 A stream must be followed by either @@ -328,24 +386,56 @@ protected void skipWhiteSpaces() throws IOException { whitespace = seqSource.read(); } + if (!skipLinebreak(whitespace)) + { + seqSource.unread(whitespace); + } + } - if (ASCII_CR == whitespace) + /** + * Skip one line break, such as CR, LF or CRLF. + * + * @return true if a line break was found and removed. + * + * @throws IOException if something went wrong + */ + protected boolean skipLinebreak() throws IOException + { + int whitespace = seqSource.read(); + // a line break is a CR, or LF or CRLF + if (!skipLinebreak(whitespace)) { - whitespace = seqSource.read(); - if (ASCII_LF != whitespace) + seqSource.unread(whitespace); + return false; + } + return true; + } + + /** + * Skip one line break, such as CR, LF or CRLF. + * + * @param linebreak the first character to be checked. + * + * @return true if a line break was found and removed. + * + * @throws IOException if something went wrong + */ + private boolean skipLinebreak(int linebreak) throws IOException + { + // a line break is a CR, or LF or CRLF + if (isCR(linebreak)) + { + int next = seqSource.read(); + if (!isLF(next)) { - seqSource.unread(whitespace); - //The spec says this is invalid but it happens in the real - //world so we must support it. + seqSource.unread(next); } } - else if (ASCII_LF != whitespace) + else if (!isLF(linebreak)) { - //we are in an error. - //but again we will do a lenient parsing and just assume that everything - //is fine - seqSource.unread(whitespace); + return false; } + return true; } /** @@ -356,8 +446,6 @@ else if (ASCII_LF != whitespace) * * The second bug was in this format /Title (c:\) /Producer * - * This patch moves this code out of the parseCOSString method, so it can be used twice. - * * @param bracesParameter the number of braces currently open. * * @return the corrected value of the brace counter @@ -365,29 +453,38 @@ else if (ASCII_LF != whitespace) */ private int checkForEndOfString(final int bracesParameter) throws IOException { - int braces = bracesParameter; + if (bracesParameter == 0) + { + return 0; + } + // Check the next 3 bytes if available byte[] nextThreeBytes = new byte[3]; int amountRead = seqSource.read(nextThreeBytes); - - // Check the next 3 bytes if available // The following cases are valid indicators for the end of the string // 1. Next line contains another COSObject: CR + LF + '/' // 2. COSDictionary ends in the next line: CR + LF + '>' - // 3. Next line contains another COSObject: CR + '/' - // 4. COSDictionary ends in the next line: CR + '>' - if (amountRead == 3 && nextThreeBytes[0] == ASCII_CR) - { - if ( (nextThreeBytes[1] == ASCII_LF && (nextThreeBytes[2] == '/') || nextThreeBytes[2] == '>') - || nextThreeBytes[1] == '/' || nextThreeBytes[1] == '>') - { - braces = 0; - } - } + // 3. Next line contains another COSObject: LF + '/' + // 4. COSDictionary ends in the next line: LF + '>' + // 5. Next line contains another COSObject: CR + '/' + // 6. COSDictionary ends in the next line: CR + '>' if (amountRead > 0) { seqSource.unread(nextThreeBytes, 0, amountRead); } - return braces; + if (amountRead < 3) + { + return bracesParameter; + } + if (((nextThreeBytes[0] == ASCII_CR || nextThreeBytes[0] == ASCII_LF) + && (nextThreeBytes[1] == '/' || nextThreeBytes[1] == '>')) // + || // + (nextThreeBytes[0] == ASCII_CR && nextThreeBytes[1] == ASCII_LF + && (nextThreeBytes[2] == '/' || nextThreeBytes[2] == '>')) // + ) + { + return 0; + } + return bracesParameter; } /** @@ -407,7 +504,7 @@ protected COSString parseCOSString() throws IOException else if (nextChar != '(') { throw new IOException( "parseCOSString string should start with '(' or '<' and not '" + - nextChar + "' at offset " + seqSource.getPosition()); + nextChar + "' at offset " + seqSource.getPosition()); } ByteArrayOutputStream out = new ByteArrayOutputStream(); @@ -458,7 +555,7 @@ else if( ch == '\\' ) break; case ')': // PDFBox 276 /Title (c:\) - braces = checkForEndOfString(braces); + braces = checkForEndOfString(braces); if( braces != 0 ) { out.write(next); @@ -581,8 +678,8 @@ else if ( c < 0 ) throw new IOException( "Missing closing bracket for hex string. Reached EOS." ); } else if ( ( c == ' ' ) || ( c == '\n' ) || - ( c == '\t' ) || ( c == '\r' ) || - ( c == '\b' ) || ( c == '\f' ) ) + ( c == '\t' ) || ( c == '\r' ) || + ( c == '\b' ) || ( c == '\f' ) ) { continue; } @@ -626,68 +723,81 @@ else if ( ( c == ' ' ) || ( c == '\n' ) || */ protected COSArray parseCOSArray() throws IOException { - long startPosition = seqSource.getPosition(); - readExpectedChar('['); - COSArray po = new COSArray(); - COSBase pbo; - skipSpaces(); - int i; - while( ((i = seqSource.peek()) > 0) && ((char)i != ']') ) + try { - pbo = parseDirObject(); - if( pbo instanceof COSObject ) + recursionDepth++; + if (recursionDepth > MAX_RECURSION_DEPTH) { - // We have to check if the expected values are there or not PDFBOX-385 - if (po.size() > 0 && po.get(po.size() - 1) instanceof COSInteger) + throw new IOException(MAX_RECUSRION_MSG); + } + long startPosition = seqSource.getPosition(); + readExpectedChar('['); + COSArray po = new COSArray(); + COSBase pbo; + skipSpaces(); + int i; + while (((i = seqSource.peek()) > 0) && ((char) i != ']')) + { + pbo = parseDirObject(); + if (pbo instanceof COSObject) { - COSInteger genNumber = (COSInteger)po.remove( po.size() -1 ); + // We have to check if the expected values are there or not PDFBOX-385 if (po.size() > 0 && po.get(po.size() - 1) instanceof COSInteger) { - COSInteger number = (COSInteger)po.remove( po.size() -1 ); - COSObjectKey key = new COSObjectKey(number.longValue(), genNumber.intValue()); - pbo = getObjectFromPool(key); + COSInteger genNumber = (COSInteger) po.remove(po.size() - 1); + if (po.size() > 0 && po.get(po.size() - 1) instanceof COSInteger) + { + COSInteger number = (COSInteger) po.remove(po.size() - 1); + COSObjectKey key = new COSObjectKey(number.longValue(), + genNumber.intValue()); + pbo = getObjectFromPool(key); + } + else + { + // the object reference is somehow wrong + pbo = null; + } } else { - // the object reference is somehow wrong pbo = null; } } - else - { - pbo = null; - } - } - if( pbo != null ) - { - po.add( pbo ); - } - else - { - //it could be a bad object in the array which is just skipped - Log.w("PdfBox-Android", "Corrupt array element at offset " - + seqSource.getPosition() + ", start offset: " + startPosition); - String isThisTheEnd = readString(); - // return immediately if a corrupt element is followed by another array - // to avoid a possible infinite recursion as most likely the whole array is corrupted - if (isThisTheEnd.isEmpty() && seqSource.peek() == '[') + if (pbo != null) { - return po; + po.add(pbo); } - seqSource.unread(isThisTheEnd.getBytes(ISO_8859_1)); - // This could also be an "endobj" or "endstream" which means we can assume that - // the array has ended. - if(ENDOBJ_STRING.equals(isThisTheEnd) || ENDSTREAM_STRING.equals(isThisTheEnd)) + else { - return po; + // it could be a bad object in the array which is just skipped + Log.w("PdfBox-Android", "Corrupt array element at offset " + seqSource.getPosition() + + ", start offset: " + startPosition); + String isThisTheEnd = readString(); + // return immediately if a corrupt element is followed by another array + // to avoid a possible infinite recursion as most likely the whole array is corrupted + if (isThisTheEnd.isEmpty() && seqSource.peek() == '[') + { + return po; + } + seqSource.unread(isThisTheEnd.getBytes(Charsets.ISO_8859_1)); + // This could also be an "endobj" or "endstream" which means we can assume that + // the array has ended. + if (ENDOBJ_STRING.equals(isThisTheEnd) || ENDSTREAM_STRING.equals(isThisTheEnd)) + { + return po; + } } + skipSpaces(); } + // read ']' + seqSource.read(); skipSpaces(); + return po; + } + finally + { + recursionDepth--; } - // read ']' - seqSource.read(); - skipSpaces(); - return po; } /** @@ -699,8 +809,8 @@ protected COSArray parseCOSArray() throws IOException protected boolean isEndOfName(int ch) { return ch == ASCII_SPACE || ch == ASCII_CR || ch == ASCII_LF || ch == 9 || ch == '>' || - ch == '<' || ch == '[' || ch =='/' || ch ==']' || ch ==')' || ch =='(' || - ch == 0 || ch == '\f' || ch == '%'; + ch == '<' || ch == '[' || ch =='/' || ch ==']' || ch ==')' || ch =='(' || + ch == 0 || ch == '\f' || ch == '%'; } /** @@ -769,33 +879,27 @@ else if (isEndOfName(ch)) seqSource.unread(c); } - byte[] bytes = buffer.toByteArray(); - String string; - if (isValidUTF8(bytes)) - { - string = new String(bytes, Charsets.UTF_8); - } - else - { - // some malformed PDFs don't use UTF-8 see PDFBOX-3347 - string = new String(bytes, Charsets.WINDOWS_1252); - } - return COSName.getPDFName(string); + return COSName.getPDFName(buffer.toByteArray()); } /** - * Returns true if a byte sequence is valid UTF-8. + * Tries to decode the buffer cotent to an UTF-8 String. + * If that fails, tries the alternative Encoding. + * @param buffer the {@link ByteArrayOutputStream} containing the bytes to decode + * @return the decoded String */ - private boolean isValidUTF8(byte[] input) + private String decodeBuffer(ByteArrayOutputStream buffer) throws UnsupportedEncodingException { try { - utf8Decoder.decode(ByteBuffer.wrap(input)); - return true; + return utf8Decoder.decode(ByteBuffer.wrap(buffer.toByteArray())).toString(); } catch (CharacterCodingException e) { - return false; + // some malformed PDFs don't use UTF-8 see PDFBOX-3347 + Log.d("PdfBox-Android", "Buffer could not be decoded using StandardCharsets.UTF_8 - " + + "trying " + ALTERNATIVE_CHARSET.name(), e); + return buffer.toString(ALTERNATIVE_CHARSET.name()); } } @@ -812,11 +916,11 @@ protected COSBoolean parseBoolean() throws IOException char c = (char) seqSource.peek(); if( c == 't' ) { - String trueString = new String( seqSource.readFully( 4 ), ISO_8859_1 ); + String trueString = new String( seqSource.readFully( 4 ), Charsets.ISO_8859_1 ); if( !trueString.equals( TRUE ) ) { throw new IOException( "Error parsing boolean: expected='true' actual='" + trueString - + "' at offset " + seqSource.getPosition()); + + "' at offset " + seqSource.getPosition()); } else { @@ -825,11 +929,11 @@ protected COSBoolean parseBoolean() throws IOException } else if( c == 'f' ) { - String falseString = new String( seqSource.readFully( 5 ), ISO_8859_1 ); + String falseString = new String( seqSource.readFully( 5 ), Charsets.ISO_8859_1 ); if( !falseString.equals( FALSE ) ) { throw new IOException( "Error parsing boolean: expected='true' actual='" + falseString - + "' at offset " + seqSource.getPosition()); + + "' at offset " + seqSource.getPosition()); } else { @@ -839,7 +943,7 @@ else if( c == 'f' ) else { throw new IOException( "Error parsing boolean expected='t or f' actual='" + c - + "' at offset " + seqSource.getPosition()); + + "' at offset " + seqSource.getPosition()); } return retval; } @@ -853,10 +957,17 @@ else if( c == 'f' ) */ protected COSBase parseDirObject() throws IOException { - skipSpaces(); - char c = (char)seqSource.peek(); - switch(c) + try { + recursionDepth++; + if (recursionDepth > MAX_RECURSION_DEPTH) + { + throw new IOException(MAX_RECUSRION_MSG); + } + skipSpaces(); + char c = (char) seqSource.peek(); + switch (c) + { case '<': // pull off first left bracket int leftBracket = seqSource.read(); @@ -877,34 +988,34 @@ protected COSBase parseDirObject() throws IOException readExpectedString(NULL); return COSNull.NULL; case 't': - String trueString = new String( seqSource.readFully(4), ISO_8859_1 ); - if( trueString.equals( TRUE ) ) + String trueString = new String(seqSource.readFully(4), Charsets.ISO_8859_1); + if (trueString.equals(TRUE)) { return COSBoolean.TRUE; } else { - throw new IOException( "expected true actual='" + trueString + "' " + seqSource + - "' at offset " + seqSource.getPosition()); + throw new IOException("expected true actual='" + trueString + "' " + seqSource + + "' at offset " + seqSource.getPosition()); } case 'f': - String falseString = new String( seqSource.readFully(5), ISO_8859_1 ); - if( falseString.equals( FALSE ) ) + String falseString = new String(seqSource.readFully(5), Charsets.ISO_8859_1); + if (falseString.equals(FALSE)) { return COSBoolean.FALSE; } else { - throw new IOException( "expected false actual='" + falseString + "' " + seqSource + - "' at offset " + seqSource.getPosition()); + throw new IOException("expected false actual='" + falseString + "' " + seqSource + + "' at offset " + seqSource.getPosition()); } case 'R': seqSource.read(); return new COSObject(null); - case (char)-1: + case (char) -1: return null; default: - if( Character.isDigit(c) || c == '-' || c == '+' || c == '.') + if (Character.isDigit(c) || c == '-' || c == '+' || c == '.') { return parseCOSNumber(); } @@ -917,24 +1028,29 @@ protected COSBase parseDirObject() throws IOException { int peek = seqSource.peek(); // we can end up in an infinite loop otherwise - throw new IOException( - "Unknown dir object c='" + c + "' cInt=" + (int) c + " peek='" + (char) peek - + "' peekInt=" + peek + " at offset " + seqSource.getPosition() - + " (start offset: " + startOffset + ")"); + throw new IOException("Unknown dir object c='" + c + "' cInt=" + (int) c + + " peek='" + (char) peek + "' peekInt=" + peek + " at offset " + + seqSource.getPosition() + " (start offset: " + startOffset + ")"); } // if it's an endstream/endobj, we want to put it back so the caller will see it if (ENDOBJ_STRING.equals(badString) || ENDSTREAM_STRING.equals(badString)) { - seqSource.unread(badString.getBytes(ISO_8859_1)); + seqSource.unread(badString.getBytes(Charsets.ISO_8859_1)); } else { Log.w("PdfBox-Android", "Skipped unexpected dir object = '" + badString + "' at offset " - + seqSource.getPosition() + " (start offset: " + startOffset + ")"); + + seqSource.getPosition() + " (start offset: " + startOffset + ")"); + return this instanceof PDFStreamParser ? null : COSNull.NULL; } + } + return null; + } + finally + { + recursionDepth--; } - return null; } private COSNumber parseCOSNumber() throws IOException @@ -952,6 +1068,15 @@ private COSNumber parseCOSNumber() throws IOException { seqSource.unread(ic); } + + // PDFBOX-5025: catch "74191endobj" + char lastc = buf.charAt(buf.length() - 1); + if (lastc == 'e' || lastc == 'E') + { + buf.deleteCharAt(buf.length() - 1); + seqSource.unread(lastc); + } + return COSNumber.get(buf.toString()); } @@ -1006,8 +1131,8 @@ protected final void readExpectedString(final char[] expectedString, boolean ski if (seqSource.read() != c) { throw new IOException("Expected string '" + new String(expectedString) - + "' but missed at character '" + c + "' at offset " - + seqSource.getPosition()); + + "' but missed at character '" + c + "' at offset " + + seqSource.getPosition()); } } skipSpaces(); @@ -1048,10 +1173,10 @@ protected String readString( int length ) throws IOException //about 16 so lets save some space. StringBuilder buffer = new StringBuilder(length); while( !isWhitespace(c) && !isClosing(c) && c != -1 && buffer.length() < length && - c != '[' && - c != '<' && - c != '(' && - c != '/' ) + c != '[' && + c != '<' && + c != '(' && + c != '/' ) { buffer.append( (char)c ); c = seqSource.read(); @@ -1100,7 +1225,7 @@ protected String readLine() throws IOException if (seqSource.isEOF()) { throw new IOException( "Error: End-of-File, expected line at offset " + - seqSource.getPosition()); + seqSource.getPosition()); } StringBuilder buffer = new StringBuilder( 11 ); @@ -1115,7 +1240,7 @@ protected String readLine() throws IOException } buffer.append( (char)c ); } - // CR+LF is also a valid EOL + // CR+LF is also a valid EOL if (isCR(c) && isLF(seqSource.peek())) { seqSource.read(); @@ -1177,7 +1302,7 @@ protected boolean isWhitespace() throws IOException protected boolean isWhitespace( int c ) { return c == 0 || c == 9 || c == 12 || c == ASCII_LF - || c == ASCII_CR || c == ASCII_SPACE; + || c == ASCII_CR || c == ASCII_SPACE; } /** @@ -1311,10 +1436,10 @@ protected int readInt() throws IOException } catch( NumberFormatException e ) { - seqSource.unread(intBuffer.toString().getBytes(ISO_8859_1)); + seqSource.unread(intBuffer.toString().getBytes(Charsets.ISO_8859_1)); throw new IOException("Error: Expected an integer type at offset " + - seqSource.getPosition() + - ", instead got '" + intBuffer + "'", e); + seqSource.getPosition() + + ", instead got '" + intBuffer + "'", e); } return retval; } @@ -1340,9 +1465,9 @@ protected long readLong() throws IOException } catch( NumberFormatException e ) { - seqSource.unread(longBuffer.toString().getBytes(ISO_8859_1)); + seqSource.unread(longBuffer.toString().getBytes(Charsets.ISO_8859_1)); throw new IOException( "Error: Expected a long type at offset " - + seqSource.getPosition() + ", instead got '" + longBuffer + "'", e); + + seqSource.getPosition() + ", instead got '" + longBuffer + "'", e); } return retval; } @@ -1364,7 +1489,7 @@ protected final StringBuilder readStringNumber() throws IOException if (buffer.length() > MAX_LENGTH_LONG) { throw new IOException("Number '" + buffer + - "' is getting too long, stop reading at offset " + seqSource.getPosition()); + "' is getting too long, stop reading at offset " + seqSource.getPosition()); } } if( lastByte != -1 ) diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdfparser/COSParser.java b/library/src/main/java/com/tom_roush/pdfbox/pdfparser/COSParser.java index 7227bcd2f..cbce5157b 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdfparser/COSParser.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdfparser/COSParser.java @@ -30,8 +30,8 @@ import java.util.HashSet; import java.util.LinkedList; import java.util.List; -import java.util.Map; import java.util.Map.Entry; +import java.util.Map; import java.util.Queue; import java.util.Set; import java.util.TreeMap; @@ -56,9 +56,8 @@ import com.tom_roush.pdfbox.pdmodel.encryption.PublicKeyDecryptionMaterial; import com.tom_roush.pdfbox.pdmodel.encryption.SecurityHandler; import com.tom_roush.pdfbox.pdmodel.encryption.StandardDecryptionMaterial; - +import com.tom_roush.pdfbox.util.StringUtil; import static com.tom_roush.pdfbox.util.Charsets.ISO_8859_1; - /** * PDF-Parser which first reads startxref and xref tables in order to know valid objects and parse only these objects. * @@ -103,14 +102,14 @@ public class COSParser extends BaseParser * Only parse the PDF file minimally allowing access to basic information. */ public static final String SYSPROP_PARSEMINIMAL = - "com.tom_roush.pdfbox.pdfparser.nonSequentialPDFParser.parseMinimal"; + "com.tom_roush.pdfbox.pdfparser.nonSequentialPDFParser.parseMinimal"; /** * The range within the %%EOF marker will be searched. - * Useful if there are additional characters after %%EOF within the PDF. + * Useful if there are additional characters after %%EOF within the PDF. */ public static final String SYSPROP_EOFLOOKUPRANGE = - "com.tom_roush.pdfbox.pdfparser.nonSequentialPDFParser.eofLookupRange"; + "com.tom_roush.pdfbox.pdfparser.nonSequentialPDFParser.eofLookupRange"; /** * How many trailing bytes to read for EOF marker. @@ -169,15 +168,16 @@ public class COSParser extends BaseParser */ private int readTrailBytes = DEFAULT_TRAIL_BYTECOUNT; + /** * Collects all Xref/trailer objects and resolves them into single - * object using startxref reference. + * object using startxref reference. */ protected XrefTrailerResolver xrefTrailerResolver = new XrefTrailerResolver(); /** - * The prefix for the temp file being used. + * The prefix for the temp file being used. */ public static final String TMP_FILE_PREFIX = "tmpPDF"; @@ -202,7 +202,7 @@ public COSParser(RandomAccessRead source) * */ public COSParser(RandomAccessRead source, String password, InputStream keyStore, - String keyAlias) + String keyAlias) { super(new RandomAccessSource(source)); this.source = source; @@ -330,10 +330,10 @@ protected COSDictionary parseXref(long startXRefOffset) throws IOException if (!parseXrefTable(prev) || !parseTrailer()) { throw new IOException("Expected trailer object at offset " - + source.getPosition()); + + source.getPosition()); } trailer = xrefTrailerResolver.getCurrentTrailer(); - // check for a XRef stream, it may contain some object ids of compressed objects + // check for a XRef stream, it may contain some object ids of compressed objects if(trailer.containsKey(COSName.XREF_STM)) { int streamOffset = trailer.getInt(COSName.XREF_STM); @@ -465,8 +465,8 @@ protected final long getStartxrefOffset() throws IOException if (readBytes < 1) { throw new IOException( - "No more bytes to read for trailing buffer, but expected: " - + (trailByteCount - off)); + "No more bytes to read for trailing buffer, but expected: " + + (trailByteCount - off)); } off += readBytes; } @@ -582,7 +582,7 @@ private long getObjectId(final COSObject obj) * we didn't add this COSObject already (checked via addedObjects). */ private void addNewToList(final Queue toBeParsedList, - final Collection newObjects, final Set addedObjects) + final Collection newObjects, final Set addedObjects) { for (COSBase newObject : newObjects) { @@ -597,7 +597,7 @@ private void addNewToList(final Queue toBeParsedList, * processed. */ private void addNewToList(final Queue toBeParsedList, final COSBase newObject, - final Set addedObjects) + final Set addedObjects) { if (newObject instanceof COSObject) { @@ -698,21 +698,21 @@ else if (baseObj instanceof COSObject) if (fileOffset != null) { Log.d("PdfBox-Android", "Set missing " + fileOffset + " for object " - + key); + + key); document.getXrefTable().put(key, fileOffset); } else { Log.w("PdfBox-Android", "Invalid object stream xref object reference for key '" - + objKey + "': " + fileOffset); + + objKey + "': " + fileOffset); continue; } } else { String msg = - "Invalid object stream xref object reference for key '" - + objKey + "': " + fileOffset; + "Invalid object stream xref object reference for key '" + + objKey + "': " + fileOffset; if (isLenient && fileOffset == null) { Log.w("PdfBox-Android", msg); @@ -732,7 +732,7 @@ else if (baseObj instanceof COSObject) else if (!(stmObjects instanceof ArrayList)) { throw new IOException(obj + " cannot be assigned to offset " + - fileOffset + ", this belongs to " + stmObjects.get(0)); + fileOffset + ", this belongs to " + stmObjects.get(0)); } stmObjects.add(obj); } @@ -784,7 +784,7 @@ private void addExcludedToList(COSName[] excludeObjects, COSDictionary dict, fin } /** - * This will parse the next object from the stream and add it to the local state. + * This will parse the next object from the stream and add it to the local state. * * @param obj object to be parsed (we only take object number and generation number for lookup start offset) * @param requireExistingNotCompressedObj if true object to be parsed must not be contained within @@ -794,14 +794,14 @@ private void addExcludedToList(COSName[] excludeObjects, COSDictionary dict, fin * @throws IOException If an IO error occurs. */ protected final COSBase parseObjectDynamically(COSObject obj, - boolean requireExistingNotCompressedObj) throws IOException + boolean requireExistingNotCompressedObj) throws IOException { return parseObjectDynamically(obj.getObjectNumber(), - obj.getGenerationNumber(), requireExistingNotCompressedObj); + obj.getGenerationNumber(), requireExistingNotCompressedObj); } /** - * This will parse the next object from the stream and add it to the local state. + * This will parse the next object from the stream and add it to the local state. * It's reduced to parsing an indirect object. * * @param objNr object number of object to be parsed @@ -815,7 +815,7 @@ protected final COSBase parseObjectDynamically(COSObject obj, * @throws IOException If an IO error occurs. */ protected COSBase parseObjectDynamically(long objNr, int objGenNr, - boolean requireExistingNotCompressedObj) throws IOException + boolean requireExistingNotCompressedObj) throws IOException { // ---- create object key and get object (container) from pool final COSObjectKey objKey = new COSObjectKey(objNr, objGenNr); @@ -841,16 +841,16 @@ protected COSBase parseObjectDynamically(long objNr, int objGenNr, // test to circumvent loops with broken documents if (requireExistingNotCompressedObj - && ((offsetOrObjstmObNr == null) || (offsetOrObjstmObNr <= 0))) + && ((offsetOrObjstmObNr == null) || (offsetOrObjstmObNr <= 0))) { throw new IOException("Object must be defined and must not be compressed object: " - + objKey.getNumber() + ":" + objKey.getGeneration()); + + objKey.getNumber() + ":" + objKey.getGeneration()); } // check if some dereferencing is already in progress if (pdfObject.derefencingInProgress()) { throw new IOException("Possible recursion detected when dereferencing object " - + objNr + " " + objGenNr); + + objNr + " " + objGenNr); } // change status of COSObject pdfObject.dereferencingStarted(); @@ -912,8 +912,8 @@ private void parseFileObject(Long offsetOrObjstmObNr, final COSObjectKey objKey, if ((readObjNr != objKey.getNumber()) || (readObjGen != objKey.getGeneration())) { throw new IOException("XREF for " + objKey.getNumber() + ":" - + objKey.getGeneration() + " points to wrong object: " + readObjNr - + ":" + readObjGen + " at offset " + offsetOrObjstmObNr); + + objKey.getGeneration() + " points to wrong object: " + readObjNr + + ":" + readObjGen + " at offset " + offsetOrObjstmObNr); } skipSpaces(); @@ -939,7 +939,7 @@ private void parseFileObject(Long offsetOrObjstmObNr, final COSObjectKey objKey, // the combination of a dict and the stream/endstream // forms a complete stream object throw new IOException("Stream not preceded by dictionary (offset: " - + offsetOrObjstmObNr + ")."); + + offsetOrObjstmObNr + ")."); } skipSpaces(); endObjectKey = readLine(); @@ -968,14 +968,14 @@ else if (securityHandler != null) if (isLenient) { Log.w("PdfBox-Android", "Object (" + readObjNr + ":" + readObjGen + ") at offset " - + offsetOrObjstmObNr + " does not end with 'endobj' but with '" - + endObjectKey + "'"); + + offsetOrObjstmObNr + " does not end with 'endobj' but with '" + + endObjectKey + "'"); } else { throw new IOException("Object (" + readObjNr + ":" + readObjGen - + ") at offset " + offsetOrObjstmObNr - + " does not end with 'endobj' but with '" + endObjectKey + "'"); + + ") at offset " + offsetOrObjstmObNr + + " does not end with 'endobj' but with '" + endObjectKey + "'"); } } } @@ -1036,7 +1036,7 @@ private void parseObjectStream(int objstmObjNr) throws IOException } /** - * Returns length value referred to or defined in given object. + * Returns length value referred to or defined in given object. */ private COSNumber getLength(final COSBase lengthBaseObj, final COSName streamType) throws IOException { @@ -1072,20 +1072,20 @@ else if (lengthBaseObj instanceof COSObject) if (COSNull.NULL == length) { Log.w("PdfBox-Android", "Length object (" + lengthObj.getObjectNumber() + " " - + lengthObj.getGenerationNumber() + ") not found"); + + lengthObj.getGenerationNumber() + ") not found"); return null; } if (!(length instanceof COSNumber)) { throw new IOException("Wrong type of referenced length object " + lengthObj - + ": " + length.getClass().getSimpleName()); + + ": " + length.getClass().getSimpleName()); } retVal = (COSNumber) length; } else { throw new IOException("Wrong type of length object: " - + lengthBaseObj.getClass().getSimpleName()); + + lengthBaseObj.getClass().getSimpleName()); } return retVal; } @@ -1124,7 +1124,7 @@ protected COSStream parseCOSStream(COSDictionary dic) throws IOException { if (isLenient) { - Log.w("PdfBox-Android", "The stream doesn't provide any stream length, using fallback readUntilEnd, at offset " + Log.w("PdfBox-Android", "The stream doesn't provide any stream length, using fallback readUntilEnd, at offset " + source.getPosition()); } else @@ -1158,8 +1158,8 @@ protected COSStream parseCOSStream(COSDictionary dic) throws IOException finally { out.close(); - // restore original (possibly incorrect) length - if (streamLengthObj != null) + // restore original (possibly incorrect) positive length value + if (streamLengthObj != null && streamLengthObj.longValue() > 0) { stream.setItem(COSName.LENGTH, streamLengthObj); } @@ -1169,21 +1169,21 @@ protected COSStream parseCOSStream(COSDictionary dic) throws IOException if (endStream.equals("endobj") && isLenient) { Log.w("PdfBox-Android", "stream ends with 'endobj' instead of 'endstream' at offset " - + source.getPosition()); + + source.getPosition()); // avoid follow-up warning about missing endobj source.rewind(ENDOBJ.length); } else if (endStream.length() > 9 && isLenient && endStream.startsWith(ENDSTREAM_STRING)) { Log.w("PdfBox-Android", "stream ends with '" + endStream + "' instead of 'endstream' at offset " - + source.getPosition()); + + source.getPosition()); // unread the "extra" bytes source.rewind(endStream.substring(9).getBytes(ISO_8859_1).length); } else if (!endStream.equals(ENDSTREAM_STRING)) { throw new IOException( - "Error reading stream, expected='endstream' actual='" + "Error reading stream, expected='endstream' actual='" + endStream + "' at offset " + source.getPosition()); } @@ -1226,7 +1226,7 @@ private void readUntilEndStream( final OutputStream out ) throws IOException { // reduce compare operations by first test last character we would have to // match if current one matches; if it is not a character from keywords - // we can move behind the test character; this shortcut is inspired by the + // we can move behind the test character; this shortcut is inspired by the // Boyer-Moore string search algorithm and can reduce parsing time by approx. 20% quickTestIdx = bIdx + quickTestOffset; if (charMatchCount == 0 && quickTestIdx < maxQuicktestIdx) @@ -1263,10 +1263,10 @@ private void readUntilEndStream( final OutputStream out ) throws IOException } else { - // no match; incrementing match start by 1 would be dumb since we already know - // matched chars depending on current char read we may already have beginning - // of a new match: 'e': first char matched; 'n': if we are at match position - // idx 7 we already read 'e' thus 2 chars matched for each other char we have + // no match; incrementing match start by 1 would be dumb since we already know + // matched chars depending on current char read we may already have beginning + // of a new match: 'e': first char matched; 'n': if we are at match position + // idx 7 we already read 'e' thus 2 chars matched for each other char we have // to start matching first keyword char beginning with next read position charMatchCount = ( ch == E ) ? 1 : ( ( ch == N ) && ( charMatchCount == 7 ) ) ? 2 : 0; // search again for 'endstream' @@ -1309,7 +1309,7 @@ private void readValidStream(OutputStream out, COSNumber streamLengthObj) throws { // shouldn't happen, the stream length has already been validated throw new IOException("read error at offset " + source.getPosition() - + ": expected " + chunk + " bytes, but read() returns " + readBytes); + + ": expected " + chunk + " bytes, but read() returns " + readBytes); } out.write(streamCopyBuf, 0, readBytes); remainBytes -= readBytes; @@ -1318,30 +1318,39 @@ private void readValidStream(OutputStream out, COSNumber streamLengthObj) throws private boolean validateStreamLength(long streamLength) throws IOException { - boolean streamLengthIsValid = true; long originOffset = source.getPosition(); + if (streamLength == 0) + { + // This may be valid (PDFBOX-5954), or not (PDFBOX-5880) + Log.d("PdfBox-Android", "Suspicious stream length 0, stream position: " + originOffset); + return false; + } + else if (streamLength < 0) + { + Log.w("PdfBox-Android", "Invalid stream length: " + streamLength + ", start position: " + originOffset); + return false; + } long expectedEndOfStream = originOffset + streamLength; if (expectedEndOfStream > fileLen) { - streamLengthIsValid = false; Log.w("PdfBox-Android", "The end of the stream is out of range, using workaround to read the stream, " - + "stream start position: " + originOffset + ", length: " + streamLength - + ", expected end position: " + expectedEndOfStream); - } - else - { - source.seek(expectedEndOfStream); - skipSpaces(); - if (!isString(ENDSTREAM)) - { - streamLengthIsValid = false; - Log.w("PdfBox-Android", "The end of the stream doesn't point to the correct offset, using workaround to read the stream, " + "stream start position: " + originOffset + ", length: " + streamLength + ", expected end position: " + expectedEndOfStream); - } - source.seek(originOffset); + return false; + } + source.seek(expectedEndOfStream); + skipSpaces(); + boolean endStreamFound = isString(ENDSTREAM); + source.seek(originOffset); + if (!endStreamFound) + { + Log.w("PdfBox-Android", + "The end of the stream doesn't point to the correct offset, using workaround to read the stream, " + + "stream start position: " + originOffset + ", length: " + streamLength + + ", expected end position: " + expectedEndOfStream); + return false; } - return streamLengthIsValid; + return true; } /** @@ -1393,7 +1402,7 @@ private boolean checkXRefStreamOffset(long startXRefOffset) throws IOException { return true; } - // seek to offset-1 + // seek to offset-1 source.seek(startXRefOffset-1); int nextValue = source.read(); // the first character has to be a whitespace, and then a digit @@ -1473,7 +1482,7 @@ private boolean validateXrefOffsets(Map xrefOffset) throws I if (foundObjectKey == null) { Log.d("PdfBox-Android", "Stop checking xref offsets as at least one (" + objectKey - + ") couldn't be dereferenced"); + + ") couldn't be dereferenced"); return false; } else if (foundObjectKey != objectKey) @@ -1579,11 +1588,11 @@ private COSObjectKey findObjectKey(COSObjectKey objectKey, long offset, Map 0 - && Math.abs(offset - existingOffset) < 10) + && Math.abs(offset - existingOffset) < 10) { Log.d("PdfBox-Android", "Found the object " + newObjKey + " instead of " // - + objectKey + " at offset " + offset // - + " - ignoring"); + + objectKey + " at offset " + offset // + + " - ignoring"); return null; } // something seems to be wrong but it's hard to determine what exactly -> simply continue @@ -1596,7 +1605,7 @@ private COSObjectKey findObjectKey(COSObjectKey objectKey, long offset, Map values, long offset) long newDifference = offset - values.get(i); // find the nearest offset if (currentDifference == null - || (Math.abs(currentDifference) > Math.abs(newDifference))) + || (Math.abs(currentDifference) > Math.abs(newDifference))) { currentDifference = newDifference; currentOffsetIndex = i; @@ -1854,27 +1863,21 @@ private boolean bfSearchForTrailer(COSDictionary trailer) throws IOException { trailer.setItem(COSName.ROOT, rootObj); trailer.setItem(COSName.INFO, infoObj); - if (trailerDict.containsKey(COSName.ENCRYPT)) + COSObject encObj = trailerDict.getCOSObject(COSName.ENCRYPT); + if (encObj != null) { - COSObject encObj = trailerDict.getCOSObject(COSName.ENCRYPT); - if (encObj != null) + // check if the dictionary can be dereferenced + // TODO check if the dictionary is an encryption dictionary? + COSDictionary encDict = retrieveCOSDictionary(encObj); + if (encDict != null) { - // check if the dictionary can be dereferenced - // TODO check if the dictionary is an encryption dictionary? - COSDictionary encDict = retrieveCOSDictionary(encObj); - if (encDict != null) - { - trailer.setItem(COSName.ENCRYPT, encObj); - } + trailer.setItem(COSName.ENCRYPT, encObj); } } - if (trailerDict.containsKey(COSName.ID)) + COSBase idObj = trailerDict.getItem(COSName.ID); + if (idObj instanceof COSArray) { - COSBase idObj = trailerDict.getItem(COSName.ID); - if (idObj instanceof COSArray) - { - trailer.setItem(COSName.ID, idObj); - } + trailer.setItem(COSName.ID, idObj); } return true; } @@ -1991,9 +1994,9 @@ private void bfSearchForObjStreams() throws IOException long objNumber = readObjectNumber(); int genNumber = readGenerationNumber(); COSObjectKey streamObjectKey = new COSObjectKey(objNumber, - genNumber); + genNumber); bfSearchObjStreamsOffsets.put(newOffset, - streamObjectKey); + streamObjectKey); } } } @@ -2021,7 +2024,7 @@ private void bfSearchForObjStreams() throws IOException if (bfOffset == null) { Log.w("PdfBox-Android", "Skipped incomplete object stream:" + bfSearchObjStreamsOffsets.get(offset) - + " at " + offset); + + " at " + offset); continue; } // check if the object was overwritten @@ -2050,7 +2053,11 @@ private void bfSearchForObjStreams() throws IOException securityHandler.decryptStream(stream, stmObjNumber, stmGenNumber); } PDFObjectStreamParser strmParser = new PDFObjectStreamParser(stream, document); - objectNumbers = new ArrayList(nrOfObjects); + // don't use "nrOfObjects" to initialize the list as a malformed pdf might + // contain an invalid value which may lead to an out of memory exception + // if the number is to big the following code will most likely fail hitting + // the end of the stream prematurely + objectNumbers = new ArrayList(); for (int i = 0; i < nrOfObjects; i++) { objectNumbers.add(strmParser.readObjectNumber()); @@ -2059,7 +2066,8 @@ private void bfSearchForObjStreams() throws IOException } catch (IOException exception) { - Log.d("PdfBox-Android", "Skipped corrupt stream: (" + stmObjNumber + " 0 at offset " + offset); + Log.d("PdfBox-Android", + "Skipped corrupt stream: (" + stmObjNumber + " 0 at offset " + offset); continue; } finally @@ -2071,7 +2079,8 @@ private void bfSearchForObjStreams() throws IOException } if (objectNumbers.size() < nrOfObjects) { - Log.d("PdfBox-Android", "Skipped corrupt stream: (" + stmObjNumber + " 0 at offset " + offset); + Log.d("PdfBox-Android", + "Skipped corrupt stream: (" + stmObjNumber + " 0 at offset " + offset); continue; } Map xrefOffset = xrefTrailerResolver.getXrefTable(); @@ -2188,7 +2197,7 @@ private void bfSearchForXRefStreams() throws IOException } } Log.d("PdfBox-Android", "Fixed reference for xref stream " + xrefOffset - + " -> " + newOffset); + + " -> " + newOffset); objFound = true; break; } @@ -2311,7 +2320,7 @@ else if (isInfo(dictionary)) } private COSObject compareCOSObjects(COSObject newObject, Long newOffset, - COSObject currentObject, Long currentOffset) + COSObject currentObject, Long currentOffset) { if (currentObject != null) { @@ -2319,8 +2328,8 @@ private COSObject compareCOSObjects(COSObject newObject, Long newOffset, if (currentObject.getObjectNumber() == newObject.getObjectNumber()) { return currentObject.getGenerationNumber() < newObject.getGenerationNumber() - ? newObject - : currentObject; + ? newObject + : currentObject; } // most likely the object with the bigger offset is the newer one return currentOffset != null && newOffset > currentOffset ? newObject : currentObject; @@ -2376,7 +2385,7 @@ private COSDictionary retrieveCOSDictionary(COSObjectKey key, long offset) throw catch (IOException exception) { Log.d("PdfBox-Android", "Skipped object " + key - + ", either it's corrupt or not a dictionary"); + + ", either it's corrupt or not a dictionary"); } } return dictionary; @@ -2471,12 +2480,12 @@ private boolean isInfo(COSDictionary dictionary) return false; } return dictionary.containsKey(COSName.MOD_DATE) || dictionary.containsKey(COSName.TITLE) - || dictionary.containsKey(COSName.AUTHOR) - || dictionary.containsKey(COSName.SUBJECT) - || dictionary.containsKey(COSName.KEYWORDS) - || dictionary.containsKey(COSName.CREATOR) - || dictionary.containsKey(COSName.PRODUCER) - || dictionary.containsKey(COSName.CREATION_DATE); + || dictionary.containsKey(COSName.AUTHOR) + || dictionary.containsKey(COSName.SUBJECT) + || dictionary.containsKey(COSName.KEYWORDS) + || dictionary.containsKey(COSName.CREATOR) + || dictionary.containsKey(COSName.PRODUCER) + || dictionary.containsKey(COSName.CREATION_DATE); } /** @@ -2571,7 +2580,7 @@ private boolean parseTrailer() throws IOException { // warn only the first time Log.w("PdfBox-Android", "Expected trailer object at offset " + trailerOffset - + ", keep trying"); + + ", keep trying"); } readLine(); nextCharacter = source.peek(); @@ -2757,7 +2766,7 @@ protected boolean parseXrefTable(long startByteOffset) throws IOException while(true) { String currentLine = readLine(); - String[] splitString = currentLine.split("\\s"); + String[] splitString = StringUtil.splitOnSpace(currentLine); if (splitString.length != 2) { Log.w("PdfBox-Android", "Unexpected XRefTable Entry: " + currentLine); @@ -2800,7 +2809,7 @@ protected boolean parseXrefTable(long startByteOffset) throws IOException } //Ignore table contents currentLine = readLine(); - splitString = currentLine.split("\\s"); + splitString = StringUtil.splitOnSpace(currentLine); if (splitString.length < 3) { Log.w("PdfBox-Android", "invalid xref line: " + currentLine); @@ -2892,7 +2901,7 @@ public PDEncryption getEncryption() throws IOException if (document == null) { throw new IOException( - "You must parse the document first before calling getEncryption()"); + "You must parse the document first before calling getEncryption()"); } return encryption; } @@ -2909,7 +2918,7 @@ public AccessPermission getAccessPermission() throws IOException if (document == null) { throw new IOException( - "You must parse the document first before calling getAccessPermission()"); + "You must parse the document first before calling getAccessPermission()"); } return accessPermission; } @@ -2983,7 +2992,7 @@ private void prepareDecryption() throws IOException securityHandler = encryption.getSecurityHandler(); securityHandler.prepareForDecryption(encryption, document.getDocumentID(), - decryptionMaterial); + decryptionMaterial); accessPermission = securityHandler.getCurrentAccessPermission(); } catch (IOException e) @@ -2993,7 +3002,7 @@ private void prepareDecryption() throws IOException catch (Exception e) { throw new IOException("Error (" + e.getClass().getSimpleName() - + ") while creating security handler for decryption", e); + + ") while creating security handler for decryption", e); } finally { diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdfparser/InputStreamSource.java b/library/src/main/java/com/tom_roush/pdfbox/pdfparser/InputStreamSource.java index 2b67cbeac..d04c94065 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdfparser/InputStreamSource.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdfparser/InputStreamSource.java @@ -21,7 +21,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.PushbackInputStream; - /** * A SequentialSource backed by an InputStream. */ @@ -29,6 +28,7 @@ final class InputStreamSource implements SequentialSource { private final PushbackInputStream input; private int position; + private boolean isOpen = true; /** * Constructor. @@ -144,5 +144,12 @@ public boolean isEOF() throws IOException public void close() throws IOException { input.close(); + isOpen = false; + } + + @Override + public boolean isClosed() throws IOException + { + return !isOpen; } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdfparser/PDFParser.java b/library/src/main/java/com/tom_roush/pdfbox/pdfparser/PDFParser.java index 16aac5009..ee139b0a2 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdfparser/PDFParser.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdfparser/PDFParser.java @@ -30,9 +30,9 @@ import com.tom_roush.pdfbox.io.ScratchFile; import com.tom_roush.pdfbox.pdmodel.PDDocument; import com.tom_roush.pdfbox.pdmodel.encryption.InvalidPasswordException; - public class PDFParser extends COSParser { + /** * Constructor. * Unrestricted main memory will be used for buffering PDF streams. @@ -81,7 +81,7 @@ public PDFParser(RandomAccessRead source, String decryptionPassword) throws IOEx * @throws IOException If something went wrong. */ public PDFParser(RandomAccessRead source, String decryptionPassword, ScratchFile scratchFile) - throws IOException + throws IOException { this(source, decryptionPassword, null, null, scratchFile); } @@ -92,13 +92,13 @@ public PDFParser(RandomAccessRead source, String decryptionPassword, ScratchFile * * @param source input representing the pdf. * @param decryptionPassword password to be used for decryption. - * @param keyStore key store to be used for decryption when using public key security + * @param keyStore key store to be used for decryption when using public key security * @param alias alias to be used for decryption when using public key security * * @throws IOException If something went wrong. */ public PDFParser(RandomAccessRead source, String decryptionPassword, InputStream keyStore, - String alias) throws IOException + String alias) throws IOException { this(source, decryptionPassword, keyStore, alias, ScratchFile.getMainMemoryOnlyInstance()); } @@ -108,7 +108,7 @@ public PDFParser(RandomAccessRead source, String decryptionPassword, InputStream * * @param source input representing the pdf. * @param decryptionPassword password to be used for decryption. - * @param keyStore key store to be used for decryption when using public key security + * @param keyStore key store to be used for decryption when using public key security * @param alias alias to be used for decryption when using public key security * @param scratchFile buffer handler for temporary storage; it will be closed on * {@link COSDocument#close()} @@ -116,7 +116,7 @@ public PDFParser(RandomAccessRead source, String decryptionPassword, InputStream * @throws IOException If something went wrong. */ public PDFParser(RandomAccessRead source, String decryptionPassword, InputStream keyStore, - String alias, ScratchFile scratchFile) throws IOException + String alias, ScratchFile scratchFile) throws IOException { super(source, decryptionPassword, keyStore, alias); fileLen = source.length(); @@ -135,7 +135,7 @@ private void init(ScratchFile scratchFile) catch (NumberFormatException nfe) { Log.w("PdfBox-Android", "System property " + SYSPROP_EOFLOOKUPRANGE - + " does not contain an integer value, but: '" + eofLookupRangeStr + "'"); + + " does not contain an integer value, but: '" + eofLookupRangeStr + "'"); } } document = new COSDocument(scratchFile); @@ -207,14 +207,21 @@ protected void initialParse() throws IOException */ public void parse() throws IOException { - // set to false if all is processed - boolean exceptionOccurred = true; - try - { + // set to false if all is processed + boolean exceptionOccurred = true; + try + { // PDFBOX-1922 read the version header and rewind if (!parsePDFHeader() && !parseFDFHeader()) { - throw new IOException( "Error: Header doesn't contain versioninfo" ); + if (isLenient()) + { + Log.w("PdfBox-Android", "Error: Header doesn't contain versioninfo"); + } + else + { + throw new IOException("Error: Header doesn't contain versioninfo"); + } } if (!initialParseDone) diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdfparser/PDFStreamParser.java b/library/src/main/java/com/tom_roush/pdfbox/pdfparser/PDFStreamParser.java index 347d4a485..876c9bded 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdfparser/PDFStreamParser.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdfparser/PDFStreamParser.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.regex.Pattern; import com.tom_roush.pdfbox.contentstream.PDContentStream; import com.tom_roush.pdfbox.contentstream.operator.Operator; @@ -35,7 +36,7 @@ import com.tom_roush.pdfbox.cos.COSStream; import com.tom_roush.pdfbox.io.RandomAccessBuffer; import com.tom_roush.pdfbox.pdmodel.common.PDStream; - +import com.tom_roush.pdfbox.util.Charsets; /** * This will parse a PDF byte stream and extract operands and such. * @@ -45,8 +46,13 @@ public class PDFStreamParser extends BaseParser { private final List streamObjects = new ArrayList( 100 ); + // Pattern to match numbers (integers or decimals). Safe from ReDoS: no overlapping quantifiers + // or character classes that cause backtracking. The optional decimal group is explicit and bounded. + private static final Pattern NUMBER_PATTERN = Pattern.compile("^\\d*(\\.\\d*)?$"); private static final int MAX_BIN_CHAR_TEST_LENGTH = 10; private final byte[] binCharTestArr = new byte[MAX_BIN_CHAR_TEST_LENGTH]; + private int inlineImageDepth = 0; + private long inlineOffset = 0; /** * Constructor. @@ -131,9 +137,14 @@ public List getTokens() */ public Object parseNextToken() throws IOException { + if (seqSource.isClosed()) + { + return null; + } skipSpaces(); if (seqSource.isEOF()) { + close(); return null; } char c = (char) seqSource.peek(); @@ -151,7 +162,17 @@ public Object parseNextToken() throws IOException if (c == '<') { - return parseCOSDictionary(); + try + { + return parseCOSDictionary(); + } + catch (IOException exception) + { + Log.w("PdfBox-Android", "Stop reading invalid dictionary from content stream at offset " + + seqSource.getPosition()); + close(); + return null; + } } else { @@ -159,7 +180,17 @@ public Object parseNextToken() throws IOException } case '[': // array - return parseCOSArray(); + try + { + return parseCOSArray(); + } + catch (IOException exception) + { + Log.w("PdfBox-Android", "Stop reading invalid array from content stream at offset " + + seqSource.getPosition()); + close(); + return null; + } case '(': // string return parseCOSString(); @@ -232,12 +263,30 @@ else if( next.equals( "false" ) ) dotNotRead = false; } } - return COSNumber.get(buf.toString()); + String s = buf.toString(); + if ("+".equals(s)) + { + // PDFBOX-5906 + Log.w("PdfBox-Android", "isolated '+' is ignored"); + return COSNull.NULL; + } + return COSNumber.get(s); case 'B': String nextOperator = readString(); Operator beginImageOP = Operator.getOperator(nextOperator); if (nextOperator.equals(OperatorName.BEGIN_INLINE_IMAGE)) { + inlineImageDepth++; + if (inlineImageDepth > 1) + { + // PDFBOX-6038 + throw new IOException("Nested '" + OperatorName.BEGIN_INLINE_IMAGE + + "' operator not allowed at offset " + seqSource.getPosition() + ", first: " + inlineOffset); + } + else + { + inlineOffset = seqSource.getPosition(); + } COSDictionary imageParams = new COSDictionary(); beginImageOP.setImageParameters(imageParams); Object nextToken = null; @@ -247,23 +296,30 @@ else if( next.equals( "false" ) ) if (!(value instanceof COSBase)) { Log.w("PdfBox-Android", "Unexpected token in inline image dictionary at offset " + - seqSource.getPosition()); + (seqSource.isClosed() ? "EOF" : seqSource.getPosition())); break; } imageParams.setItem((COSName) nextToken, (COSBase) value); + inlineImageDepth--; } // final token will be the image data, maybe?? if (nextToken instanceof Operator) { Operator imageData = (Operator) nextToken; if (imageData.getImageData() == null - || imageData.getImageData().length == 0) + || imageData.getImageData().length == 0) { Log.w("PdfBox-Android", "empty inline image at stream offset " - + seqSource.getPosition()); + + (seqSource.isClosed() ? "EOF" : seqSource.getPosition())); } beginImageOP.setImageData(imageData.getImageData()); } + else + { + Log.w("PdfBox-Android", "nextToken " + nextToken + " at position " + + (seqSource.isClosed() ? "EOF" : seqSource.getPosition()) + + ", expected " + OperatorName.BEGIN_INLINE_IMAGE_DATA + "?!"); + } } return beginImageOP; case 'I': @@ -271,13 +327,16 @@ else if( next.equals( "false" ) ) String id = Character.toString((char) seqSource.read()) + (char) seqSource.read(); if (!id.equals(OperatorName.BEGIN_INLINE_IMAGE_DATA)) { + long currentPosition = seqSource.getPosition(); + close(); throw new IOException( "Error: Expected operator 'ID' actual='" + id + - "' at stream offset " + seqSource.getPosition()); + "' at stream offset " + currentPosition); } ByteArrayOutputStream imageData = new ByteArrayOutputStream(); - if( isWhitespace() ) + // skip one line break (CR, LF or CRLF) or any one-byte whitespace + if (!skipLinebreak() && isWhitespace()) { - //pull off the whitespace character + // pull off the whitespace character seqSource.read(); } int lastByte = seqSource.read(); @@ -287,10 +346,10 @@ else if( next.equals( "false" ) ) // until EI. // Be aware not all kind of whitespaces are allowed here. see PDFBOX-1561 while( !(lastByte == 'E' && - currentByte == 'I' && - hasNextSpaceOrReturn() && - hasNoFollowingBinData(seqSource)) && - !seqSource.isEOF() ) + currentByte == 'I' && + hasNextSpaceOrReturn() && + hasNoFollowingBinData(seqSource)) && + !seqSource.isEOF() ) { imageData.write( lastByte ); lastByte = currentByte; @@ -298,7 +357,7 @@ else if( next.equals( "false" ) ) } // the EI operator isn't unread, as it won't be processed anyway Operator beginImageDataOP = Operator - .getOperator(OperatorName.BEGIN_INLINE_IMAGE_DATA); + .getOperator(OperatorName.BEGIN_INLINE_IMAGE_DATA); // save the image data to the operator, so that it can be accessed later beginImageDataOP.setImageData(imageData.toByteArray()); return beginImageDataOP; @@ -334,6 +393,7 @@ private boolean hasNoFollowingBinData(SequentialSource pdfSource) throws IOExcep boolean noBinData = true; int startOpIdx = -1; int endOpIdx = -1; + String s = ""; if (readBytes > 0) { @@ -352,34 +412,38 @@ private boolean hasNoFollowingBinData(SequentialSource pdfSource) throws IOExcep startOpIdx = bIdx; } else if (startOpIdx != -1 && endOpIdx == -1 && - (b == 0 || b == 9 || b == 0x20 || b == 0x0a || b == 0x0d)) + (b == 0 || b == 9 || b == 0x20 || b == 0x0a || b == 0x0d)) { endOpIdx = bIdx; } } // PDFBOX-3742: just assuming that 1-3 non blanks is a PDF operator isn't enough - if (endOpIdx != -1 && startOpIdx != -1) + if (noBinData && endOpIdx != -1 && startOpIdx != -1) { - // usually, the operator here is Q, sometimes EMC (PDFBOX-2376), S (PDFBOX-3784). - String s = new String(binCharTestArr, startOpIdx, endOpIdx - startOpIdx); - if (!"Q".equals(s) && !"EMC".equals(s) && !"S".equals(s)) + // usually, the operator here is Q, sometimes EMC (PDFBOX-2376), S (PDFBOX-3784), + // or a number (PDFBOX-5957) + s = new String(binCharTestArr, startOpIdx, endOpIdx - startOpIdx, Charsets.US_ASCII); + if (!"Q".equals(s) && !"EMC".equals(s) && !"S".equals(s) && + !NUMBER_PATTERN.matcher(s).find()) { + // operator is not Q, not EMC, not S, nor a number -> assume binary data noBinData = false; } } - // only if not close to eof - if (readBytes == MAX_BIN_CHAR_TEST_LENGTH) + // only if not close to EOF + if (noBinData && startOpIdx != -1 && readBytes == MAX_BIN_CHAR_TEST_LENGTH) { - // a PDF operator is 1-3 bytes long - if (startOpIdx != -1 && endOpIdx == -1) + if (endOpIdx == -1) { endOpIdx = MAX_BIN_CHAR_TEST_LENGTH; + s = new String(binCharTestArr, startOpIdx, endOpIdx - startOpIdx, Charsets.US_ASCII); } - if (endOpIdx != -1 && startOpIdx != -1 && endOpIdx - startOpIdx > 3) + // look for token of 3 chars max or a number + if (endOpIdx - startOpIdx > 3 && !NUMBER_PATTERN.matcher(s).find()) { - noBinData = false; + noBinData = false; // "operator" too long, assume binary data } } pdfSource.unread(binCharTestArr, 0, readBytes); @@ -387,7 +451,7 @@ else if (startOpIdx != -1 && endOpIdx == -1 && if (!noBinData) { Log.w("PdfBox-Android", "ignoring 'EI' assumed to be in the middle of inline image at stream offset " + - pdfSource.getPosition()); + pdfSource.getPosition() + ", s = '" + s + "'"); } return noBinData; } @@ -409,14 +473,15 @@ protected String readOperator() throws IOException int nextChar = seqSource.peek(); while( nextChar != -1 && // EOF - !isWhitespace(nextChar) && - !isClosing(nextChar) && - nextChar != '[' && - nextChar != '<' && - nextChar != '(' && - nextChar != '/' && - (nextChar < '0' || - nextChar > '9' ) ) + !isWhitespace(nextChar) && + !isClosing(nextChar) && + nextChar != '[' && + nextChar != '<' && + nextChar != '(' && + nextChar != '/' && + nextChar != '%' && + (nextChar < '0' || + nextChar > '9' ) ) { char currentChar = (char) seqSource.read(); nextChar = seqSource.peek(); @@ -447,4 +512,17 @@ private boolean hasNextSpaceOrReturn() throws IOException { return isSpaceOrReturn( seqSource.peek() ); } + + /** + * Close the underlying resource. + * + * @throws IOException if something went wrong + */ + public void close() throws IOException + { + if (seqSource != null) + { + seqSource.close(); + } + } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdfparser/PDFXrefStreamParser.java b/library/src/main/java/com/tom_roush/pdfbox/pdfparser/PDFXrefStreamParser.java index dea87c8d2..b886cad5a 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdfparser/PDFXrefStreamParser.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdfparser/PDFXrefStreamParser.java @@ -28,7 +28,6 @@ import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.cos.COSObjectKey; import com.tom_roush.pdfbox.cos.COSStream; - /** * This will parse a PDF 1.5 (or better) Xref stream and * extract the xref information from the stream. @@ -51,7 +50,7 @@ public class PDFXrefStreamParser extends BaseParser * @throws IOException If there is an error initializing the stream. */ public PDFXrefStreamParser(COSStream stream, COSDocument document, XrefTrailerResolver resolver) - throws IOException + throws IOException { super(new InputStreamSource(stream.createInputStream())); this.document = document; @@ -77,7 +76,7 @@ private void initParserValues(COSStream stream) throws IOException if (wArray.size() != 3) { throw new IOException( - "Wrong number of values for /W array in XRef: " + Arrays.toString(w)); + "Wrong number of values for /W array in XRef: " + Arrays.toString(w)); } for (int i = 0; i < 3; i++) { @@ -87,6 +86,11 @@ private void initParserValues(COSStream stream) throws IOException { throw new IOException("Incorrect /W array in XRef: " + Arrays.toString(w)); } + if (w[0] + w[1] + w[2] > 20) + { + // PDFBOX-6037 + throw new IOException("Incorrect /W array in XRef: " + Arrays.toString(w)); + } COSArray indexArray = stream.getCOSArray(COSName.INDEX); if (indexArray == null) @@ -99,7 +103,7 @@ private void initParserValues(COSStream stream) throws IOException if (indexArray.size() == 0 || indexArray.size() % 2 == 1) { throw new IOException( - "Wrong number of values for /Index array in XRef: " + Arrays.toString(w)); + "Wrong number of values for /Index array in XRef: " + Arrays.toString(w)); } // create an Iterator for all object numbers using the index array objectNumbers = new ObjectNumbers(indexArray); @@ -171,7 +175,6 @@ private static class ObjectNumbers implements Iterator private int currentRange = 0; private long currentEnd = 0; private long currentNumber = 0; - private long maxValue = 0; private ObjectNumbers(COSArray indexArray) throws IOException { @@ -198,30 +201,34 @@ private ObjectNumbers(COSArray indexArray) throws IOException } long sizeValue = ((COSInteger) base).longValue(); start[counter] = startValue; - end[counter++] = startValue + sizeValue; + end[counter] = startValue + sizeValue; + counter++; } currentNumber = start[0]; currentEnd = end[0]; - maxValue = end[counter - 1]; } @Override public boolean hasNext() { - return currentNumber < maxValue; + if (start.length == 1) + { + return currentNumber < currentEnd; + } + return currentRange < start.length - 1 || currentNumber < currentEnd; } @Override public Long next() { - if (currentNumber >= maxValue) - { - throw new NoSuchElementException(); - } if (currentNumber < currentEnd) { return currentNumber++; } + if (currentRange >= start.length - 1) + { + throw new NoSuchElementException(); + } currentNumber = start[++currentRange]; currentEnd = end[currentRange]; return currentNumber++; diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdfparser/RandomAccessSource.java b/library/src/main/java/com/tom_roush/pdfbox/pdfparser/RandomAccessSource.java index 92d7d2d2f..33e01cf10 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdfparser/RandomAccessSource.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdfparser/RandomAccessSource.java @@ -18,14 +18,15 @@ package com.tom_roush.pdfbox.pdfparser; import java.io.IOException; -import com.tom_roush.pdfbox.io.RandomAccessRead; +import com.tom_roush.pdfbox.io.RandomAccessRead; /** * A SequentialSource backed by a RandomAccessRead. */ final class RandomAccessSource implements SequentialSource { private final RandomAccessRead reader; + private boolean isOpen = true; /** * Constructor. @@ -101,5 +102,12 @@ public boolean isEOF() throws IOException public void close() throws IOException { reader.close(); + isOpen = false; + } + + @Override + public boolean isClosed() throws IOException + { + return !isOpen; } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdfparser/SequentialSource.java b/library/src/main/java/com/tom_roush/pdfbox/pdfparser/SequentialSource.java index e87c53961..d7ea3387b 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdfparser/SequentialSource.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdfparser/SequentialSource.java @@ -19,7 +19,6 @@ import java.io.Closeable; import java.io.IOException; - /** * A SequentialSource provides access to sequential data for parsing. */ @@ -56,7 +55,7 @@ interface SequentialSource extends Closeable /** * Returns offset of next byte to be returned by a read method. * - * @return offset of next byte which will be returned with next {@link #read()} (if no more + * @return offset of next byte which will be returned with next {@link #read()} (if no more * bytes are left it returns a value >= length of source). * @throws IOException If there was an error while reading the data. */ @@ -112,4 +111,11 @@ interface SequentialSource extends Closeable * @throws IOException If there is an error reading the next byte. */ boolean isEOF() throws IOException; + + /** + * Returns true if this source has been closed. + * + * @return true if the source has been closed + */ + boolean isClosed() throws IOException; } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdfwriter/COSWriter.java b/library/src/main/java/com/tom_roush/pdfbox/pdfwriter/COSWriter.java index 397df540a..d49f5108c 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdfwriter/COSWriter.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdfwriter/COSWriter.java @@ -42,6 +42,7 @@ import java.util.Map; import java.util.Set; +import com.tom_roush.pdfbox.android.PDFBoxConfig; import com.tom_roush.pdfbox.cos.COSArray; import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSBoolean; @@ -69,7 +70,6 @@ import com.tom_roush.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface; import com.tom_roush.pdfbox.util.Charsets; import com.tom_roush.pdfbox.util.Hex; - /** * This class acts on a in-memory representation of a PDF document. * @@ -78,6 +78,7 @@ */ public class COSWriter implements ICOSVisitor, Closeable { + /** * The dictionary open token. */ @@ -159,11 +160,11 @@ public class COSWriter implements ICOSVisitor, Closeable public static final byte[] ENDSTREAM = "endstream".getBytes(Charsets.US_ASCII); private final NumberFormat formatXrefOffset = new DecimalFormat("0000000000", - DecimalFormatSymbols.getInstance(Locale.US)); + DecimalFormatSymbols.getInstance(Locale.US)); // the decimal format for the xref object generation number data private final NumberFormat formatXrefGeneration = new DecimalFormat("00000", - DecimalFormatSymbols.getInstance(Locale.US)); + DecimalFormatSymbols.getInstance(Locale.US)); // the stream where we create the pdf output private OutputStream output; @@ -270,10 +271,10 @@ public COSWriter(OutputStream outputStream, RandomAccessRead inputData) throws I * @throws IOException if something went wrong */ public COSWriter(OutputStream outputStream, RandomAccessRead inputData, - Set objectsToWrite) throws IOException + Set objectsToWrite) throws IOException { // Implementation notes / summary of April 2019 comments in PDFBOX-45: - // we allow only COSDictionary in objectsToWrite because other types, + // we allow only COSDictionary in objectsToWrite because other types, // especially COSArray, are written directly. If we'd allow them with the current // COSWriter implementation, they would be written twice, // once directly and once indirectly as orphan. @@ -291,39 +292,39 @@ public COSWriter(OutputStream outputStream, RandomAccessRead inputData, private void prepareIncrement(PDDocument doc) { - try - { - if (doc != null) - { - COSDocument cosDoc = doc.getDocument(); - - Map xrefTable = cosDoc.getXrefTable(); - Set keySet = xrefTable.keySet(); - long highestNumber = doc.getDocument().getHighestXRefObjectNumber(); - for ( COSObjectKey cosObjectKey : keySet ) - { - if (cosObjectKey != null) - { - COSBase object = cosDoc.getObjectFromPool(cosObjectKey).getObject(); - if (object != null && !(object instanceof COSNumber)) - { - objectKeys.put(object, cosObjectKey); - keyObject.put(cosObjectKey, object); - } - long num = cosObjectKey.getNumber(); - if (num > highestNumber) - { - highestNumber = num; - } - } - } - setNumber(highestNumber); - } - } - catch (IOException e) + try + { + if (doc != null) { - Log.e("PdfBox-Android", e.getMessage(),e); + COSDocument cosDoc = doc.getDocument(); + + Map xrefTable = cosDoc.getXrefTable(); + Set keySet = xrefTable.keySet(); + long highestNumber = doc.getDocument().getHighestXRefObjectNumber(); + for ( COSObjectKey cosObjectKey : keySet ) + { + if (cosObjectKey != null) + { + COSBase object = cosDoc.getObjectFromPool(cosObjectKey).getObject(); + if (object != null && !(object instanceof COSNumber)) + { + objectKeys.put(object, cosObjectKey); + keyObject.put(cosObjectKey, object); + } + long num = cosObjectKey.getNumber(); + if (num > highestNumber) + { + highestNumber = num; + } + } + } + setNumber(highestNumber); } + } + catch (IOException e) + { + Log.e("PdfBox-Android", e.getMessage(),e); + } } /** @@ -504,7 +505,7 @@ private void addObjectToWrite( COSBase object ) } if (writtenObjects.contains(object) || objectsToWriteSet.contains(object) - || actualsAdded.contains(actual)) + || actualsAdded.contains(actual)) { return; } @@ -611,17 +612,18 @@ protected void doWriteTrailer(COSDocument doc) throws IOException getStandardOutput().writeEOL(); COSDictionary trailer = doc.getTrailer(); - //sort xref, needed only if object keys not regenerated - Collections.sort(getXRefEntries()); - COSWriterXRefEntry lastEntry = getXRefEntries().get( getXRefEntries().size()-1); - trailer.setLong(COSName.SIZE, lastEntry.getKey().getNumber()+1); // Only need to stay, if an incremental update will be performed if (!incrementalUpdate) { - trailer.removeItem( COSName.PREV ); + // sort xref, needed only if object keys not regenerated + Collections.sort(getXRefEntries()); + COSWriterXRefEntry lastEntry = getXRefEntries().get(getXRefEntries().size() - 1); + trailer.setLong(COSName.SIZE, lastEntry.getKey().getNumber() + 1); + trailer.removeItem(COSName.PREV); } if (!doc.isXRefStream()) { + trailer.setLong(COSName.SIZE, number + 1); trailer.removeItem( COSName.XREF_STM ); } // Remove a checksum if present @@ -734,7 +736,9 @@ private void doWriteXRefTable() throws IOException private void doWriteIncrement() throws IOException { // write existing PDF - IOUtils.copy(new RandomAccessInputStream(incrementalInput), incrementalOutput); + RandomAccessInputStream input = new RandomAccessInputStream(incrementalInput); + IOUtils.copy(input, incrementalOutput); + input.close(); // write the actual incremental update incrementalOutput.write(((ByteArrayOutputStream) output).toByteArray()); } @@ -758,8 +762,10 @@ private void doWriteSignature() throws IOException if (byteRange.length() > byteRangeLength) { throw new IOException("Can't write new byteRange '" + byteRange + - "' not enough space: byteRange.length(): " + byteRange.length() + - ", byteRangeLength: " + byteRangeLength); + "' not enough space: byteRange.length(): " + byteRange.length() + + ", byteRangeLength: " + byteRangeLength + + ", byteRangeOffset: " + byteRangeOffset + + ", inLength: " + inLength); } // copy the new incremental data into a buffer (e.g. signature dict, trailer) @@ -815,11 +821,11 @@ public InputStream getDataToSign() throws IOException int incPartSigOffset = (int) (signatureOffset - incrementalInput.length()); int afterSigOffset = incPartSigOffset + (int) signatureLength; int[] range = {0, incPartSigOffset, - afterSigOffset, incrementPart.length - afterSigOffset}; + afterSigOffset, incrementPart.length - afterSigOffset}; return new SequenceInputStream( - new RandomAccessInputStream(incrementalInput), - new COSFilterInputStream(incrementPart, range)); + new RandomAccessInputStream(incrementalInput), + new COSFilterInputStream(incrementPart, range)); } /** @@ -841,7 +847,7 @@ public void writeExternalSignature(byte[] cmsSignature) throws IOException { if (signatureBytes.length > signatureLength - 2) { throw new IOException("Can't write signature, not enough space; " - + "adjust it with SignatureOptions.setPreferredSignatureSize"); + + "adjust it with SignatureOptions.setPreferredSignatureSize"); } // overwrite the signature Contents in the buffer @@ -887,7 +893,7 @@ private void writeXrefEntry(COSWriterXRefEntry entry) throws IOException *

      * 0 3 5 4 10 1 *

      - * this mean that the element 0 is followed by two other related numbers + * this mean that the element 0 is followed by two other related numbers * that represent a cluster of the size 3. 5 is follow by three other * related numbers and create a cluster of size 4. etc. * @@ -1036,14 +1042,7 @@ public Object visitFromBoolean(COSBoolean obj) throws IOException @Override public Object visitFromDictionary(COSDictionary obj) throws IOException { - if (!reachedSignature) - { - COSBase itemType = obj.getItem(COSName.TYPE); - if (COSName.SIG.equals(itemType) || COSName.DOC_TIME_STAMP.equals(itemType)) - { - reachedSignature = true; - } - } + detectPossibleSignature(obj); getStandardOutput().write(DICT_OPEN); getStandardOutput().writeEOL(); for (Map.Entry entry : obj.entrySet()) @@ -1141,6 +1140,36 @@ else if(reachedSignature && COSName.BYTERANGE.equals(entry.getKey())) return null; } + private void detectPossibleSignature(COSDictionary obj) throws IOException + { + if (!reachedSignature && incrementalUpdate) + { + COSBase itemType = obj.getItem(COSName.TYPE); + if (COSName.SIG.equals(itemType) || COSName.DOC_TIME_STAMP.equals(itemType)) + { + COSArray byteRange = obj.getCOSArray(COSName.BYTERANGE); + if (byteRange != null && byteRange.size() == 4) + { + COSBase base2 = byteRange.get(2); + if (base2 instanceof COSInteger) + { + // PDFBOX-5521 avoid hitting "old" signatures + long br2 = ((COSInteger) base2).longValue(); + if (br2 > incrementalInput.length()) + { + if (PDFBoxConfig.isDebugEnabled()) + { + Log.d("PdfBox-Android", "reachedSignature at offset " + getStandardOutput().getPos() + + ", byteRange: " + byteRange + ", input length: " + incrementalInput.length()); + } + reachedSignature = true; + } + } + } + } + } + } + @Override public Object visitFromDocument(COSDocument doc) throws IOException { @@ -1286,9 +1315,9 @@ public Object visitFromString(COSString obj) throws IOException if (willEncrypt) { pdDocument.getEncryption().getSecurityHandler().encryptString( - obj, - currentObjectKey.getNumber(), - currentObjectKey.getGeneration()); + obj, + currentObjectKey.getNumber(), + currentObjectKey.getGeneration()); } COSWriter.writeString(obj, getStandardOutput()); return null; @@ -1336,7 +1365,7 @@ public void write(PDDocument doc) throws IOException public void write(PDDocument doc, SignatureInterface signInterface) throws IOException { Long idTime = doc.getDocumentId() == null ? System.currentTimeMillis() : - doc.getDocumentId(); + doc.getDocumentId(); pdDocument = doc; signatureInterface = signInterface; @@ -1350,7 +1379,7 @@ public void write(PDDocument doc, SignatureInterface signInterface) throws IOExc if(doc.isAllSecurityToBeRemoved()) { willEncrypt = false; - // also need to get rid of the "Encrypt" in the trailer so readers + // also need to get rid of the "Encrypt" in the trailer so readers // don't try to decrypt a document which is not encrypted COSDocument cosDoc = doc.getDocument(); COSDictionary trailer = cosDoc.getTrailer(); @@ -1366,7 +1395,7 @@ public void write(PDDocument doc, SignatureInterface signInterface) throws IOExc if (!securityHandler.hasProtectionPolicy()) { throw new IllegalStateException("PDF contains an encryption dictionary, please remove it with " - + "setAllSecurityToBeRemoved() or set a protection policy with protect()"); + + "setAllSecurityToBeRemoved() or set a protection policy with protect()"); } securityHandler.prepareDocumentForEncryption(pdDocument); } @@ -1398,10 +1427,10 @@ public void write(PDDocument doc, SignatureInterface signInterface) throws IOExc } if( missingID || incrementalUpdate) { - MessageDigest md5; + MessageDigest sha256; try { - md5 = MessageDigest.getInstance("MD5"); + sha256 = MessageDigest.getInstance("SHA-256"); } catch (NoSuchAlgorithmException e) { @@ -1411,20 +1440,20 @@ public void write(PDDocument doc, SignatureInterface signInterface) throws IOExc // algorithm says to use time/path/size/values in doc to generate the id. // we don't have path or size, so do the best we can - md5.update( Long.toString(idTime).getBytes(Charsets.ISO_8859_1) ); + sha256.update(Long.toString(idTime).getBytes(Charsets.ISO_8859_1)); COSDictionary info = trailer.getCOSDictionary(COSName.INFO); if( info != null ) { for (COSBase cosBase : info.getValues()) { - md5.update(cosBase.toString().getBytes(Charsets.ISO_8859_1)); + sha256.update(cosBase.toString().getBytes(Charsets.ISO_8859_1)); } } // reuse origin documentID if available as first value - COSString firstID = missingID ? new COSString( md5.digest() ) : (COSString)idArray.get(0); + COSString firstID = missingID ? new COSString(sha256.digest()) : (COSString) idArray.get(0); // it's ok to use the same ID for the second part if the ID is created for the first time - COSString secondID = missingID ? firstID : new COSString( md5.digest() ); + COSString secondID = missingID ? firstID : new COSString(sha256.digest() ); idArray = new COSArray(); idArray.add( firstID ); idArray.add( secondID ); @@ -1478,7 +1507,7 @@ public static void writeString(byte[] bytes, OutputStream output) throws IOExcep * @throws IOException If there is an error writing to the stream. */ private static void writeString(byte[] bytes, boolean forceHex, OutputStream output) - throws IOException + throws IOException { // check for non-ASCII characters boolean isASCII = true; diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdfwriter/ContentStreamWriter.java b/library/src/main/java/com/tom_roush/pdfbox/pdfwriter/ContentStreamWriter.java index 1c77fcbda..9c6ceea4c 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdfwriter/ContentStreamWriter.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdfwriter/ContentStreamWriter.java @@ -20,6 +20,7 @@ import java.io.OutputStream; import java.util.List; import java.util.Map; + import com.tom_roush.pdfbox.contentstream.operator.Operator; import com.tom_roush.pdfbox.contentstream.operator.OperatorName; import com.tom_roush.pdfbox.cos.COSArray; @@ -32,7 +33,6 @@ import com.tom_roush.pdfbox.cos.COSNull; import com.tom_roush.pdfbox.cos.COSString; import com.tom_roush.pdfbox.util.Charsets; - /** * A class that will take a list of tokens and write out a stream with them. * @@ -112,7 +112,52 @@ public void writeTokens( List tokens ) throws IOException } } - private void writeObject( Object o ) throws IOException + private void writeObject(Object o) throws IOException + { + if (o instanceof COSBase) + { + writeObject((COSBase) o); + } + else if (o instanceof Operator) + { + writeObject((Operator) o); + } + else + { + throw new IOException("Error:Unknown type in content stream:" + o); + } + } + + private void writeObject(Operator op) throws IOException + { + if (op.getName().equals(OperatorName.BEGIN_INLINE_IMAGE)) + { + output.write(OperatorName.BEGIN_INLINE_IMAGE.getBytes(Charsets.ISO_8859_1)); + output.write(EOL); + COSDictionary dic = op.getImageParameters(); + for (COSName key : dic.keySet()) + { + COSBase value = dic.getDictionaryObject(key); + key.writePDF(output); + output.write(SPACE); + writeObject(value); + output.write(EOL); + } + output.write(OperatorName.BEGIN_INLINE_IMAGE_DATA.getBytes(Charsets.ISO_8859_1)); + output.write(EOL); + output.write(op.getImageData()); + output.write(EOL); + output.write(OperatorName.END_INLINE_IMAGE.getBytes(Charsets.ISO_8859_1)); + output.write(EOL); + } + else + { + output.write(op.getName().getBytes(Charsets.ISO_8859_1)); + output.write(EOL); + } + } + + private void writeObject( COSBase o ) throws IOException { if( o instanceof COSString ) { @@ -165,35 +210,6 @@ else if( o instanceof COSDictionary ) output.write( COSWriter.DICT_CLOSE ); output.write( SPACE ); } - else if( o instanceof Operator) - { - Operator op = (Operator)o; - if (op.getName().equals(OperatorName.BEGIN_INLINE_IMAGE)) - { - output.write(OperatorName.BEGIN_INLINE_IMAGE.getBytes(Charsets.ISO_8859_1)); - output.write(EOL); - COSDictionary dic = op.getImageParameters(); - for( COSName key : dic.keySet() ) - { - Object value = dic.getDictionaryObject( key ); - key.writePDF( output ); - output.write( SPACE ); - writeObject( value ); - output.write( EOL ); - } - output.write(OperatorName.BEGIN_INLINE_IMAGE_DATA.getBytes(Charsets.ISO_8859_1)); - output.write( EOL ); - output.write( op.getImageData() ); - output.write( EOL ); - output.write(OperatorName.END_INLINE_IMAGE.getBytes(Charsets.ISO_8859_1)); - output.write( EOL ); - } - else - { - output.write( op.getName().getBytes(Charsets.ISO_8859_1) ); - output.write( EOL ); - } - } else if (o instanceof COSNull) { output.write("null".getBytes(Charsets.ISO_8859_1)); diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDAbstractContentStream.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDAbstractContentStream.java index e53d1aa01..8feeca343 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDAbstractContentStream.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDAbstractContentStream.java @@ -31,6 +31,7 @@ import com.tom_roush.pdfbox.contentstream.operator.OperatorName; import com.tom_roush.pdfbox.cos.COSArray; import com.tom_roush.pdfbox.cos.COSBase; +import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.cos.COSNumber; import com.tom_roush.pdfbox.pdfwriter.COSWriter; @@ -51,7 +52,6 @@ import com.tom_roush.pdfbox.util.Charsets; import com.tom_roush.pdfbox.util.Matrix; import com.tom_roush.pdfbox.util.NumberFormatUtil; - /** * Provides the ability to write to a content stream. * @@ -59,6 +59,7 @@ */ abstract class PDAbstractContentStream implements Closeable { + protected final PDDocument document; // may be null protected final OutputStream outputStream; @@ -165,7 +166,7 @@ public void setFont(PDFont font, float fontSize) throws IOException else { Log.w("PdfBox-Android", "Using the subsetted font '" + font.getName() + - "' without a PDDocument context; call subset() before saving"); + "' without a PDDocument context; call subset() before saving"); } } @@ -450,20 +451,20 @@ public void drawImage(PDInlineImage inlineImage, float x, float y, float width, sb.append(inlineImage.getHeight()); sb.append("\n /CS "); - sb.append("/"); + sb.append('/'); sb.append(inlineImage.getColorSpace().getName()); COSArray decodeArray = inlineImage.getDecode(); if (decodeArray != null && decodeArray.size() > 0) { sb.append("\n /D "); - sb.append("["); + sb.append('['); for (COSBase base : decodeArray) { sb.append(((COSNumber) base).intValue()); - sb.append(" "); + sb.append(' '); } - sb.append("]"); + sb.append(']'); } if (inlineImage.isStencil()) @@ -599,12 +600,13 @@ protected COSName getName(PDColorSpace colorSpace) */ public void setStrokingColor(PDColor color) throws IOException { + PDColorSpace colorSpace = color.getColorSpace(); if (strokingColorSpaceStack.isEmpty() || - strokingColorSpaceStack.peek() != color.getColorSpace()) + strokingColorSpaceStack.peek() != colorSpace) { - writeOperand(getName(color.getColorSpace())); + writeOperand(getName(colorSpace)); writeOperator(OperatorName.STROKING_COLORSPACE); - setStrokingColorSpaceStack(color.getColorSpace()); + setStrokingColorSpaceStack(colorSpace); } for (float value : color.getComponents()) @@ -612,16 +614,16 @@ public void setStrokingColor(PDColor color) throws IOException writeOperand(value); } -// if (color.getColorSpace() instanceof PDPattern) +// if (colorSpace instanceof PDPattern) // { // writeOperand(color.getPatternName()); // } if ( -// color.getColorSpace() instanceof PDPattern || -// color.getColorSpace() instanceof PDSeparation || -// color.getColorSpace() instanceof PDDeviceN || - color.getColorSpace() instanceof PDICCBased) +// colorSpace instanceof PDPattern || +// colorSpace instanceof PDSeparation || +// colorSpace instanceof PDDeviceN || + colorSpace instanceof PDICCBased) { writeOperator(OperatorName.STROKING_COLOR_N); } @@ -640,7 +642,7 @@ public void setStrokingColor(PDColor color) throws IOException public void setStrokingColor(AWTColor color) throws IOException { float[] components = new float[] { - color.getRed() / 255f, color.getGreen() / 255f, color.getBlue() / 255f }; + color.getRed() / 255f, color.getGreen() / 255f, color.getBlue() / 255f }; PDColor pdColor = new PDColor(components, PDDeviceRGB.INSTANCE); setStrokingColor(pdColor); } @@ -659,7 +661,7 @@ public void setStrokingColor(float r, float g, float b) throws IOException if (isOutsideOneInterval(r) || isOutsideOneInterval(g) || isOutsideOneInterval(b)) { throw new IllegalArgumentException("Parameters must be within 0..1, but are " - + String.format("(%.2f,%.2f,%.2f)", r, g, b)); + + String.format("(%.2f,%.2f,%.2f)", r, g, b)); } writeOperand(r); writeOperand(g); @@ -685,7 +687,7 @@ public void setStrokingColor(int r, int g, int b) throws IOException if (isOutside255Interval(r) || isOutside255Interval(g) || isOutside255Interval(b)) { throw new IllegalArgumentException("Parameters must be within 0..255, but are " - + String.format("(%d,%d,%d)", r, g, b)); + + String.format("(%d,%d,%d)", r, g, b)); } setStrokingColor(r / 255f, g / 255f, b / 255f); } @@ -705,7 +707,7 @@ public void setStrokingColor(float c, float m, float y, float k) throws IOExcept if (isOutsideOneInterval(c) || isOutsideOneInterval(m) || isOutsideOneInterval(y) || isOutsideOneInterval(k)) { throw new IllegalArgumentException("Parameters must be within 0..1, but are " - + String.format("(%.2f,%.2f,%.2f,%.2f)", c, m, y, k)); + + String.format("(%.2f,%.2f,%.2f,%.2f)", c, m, y, k)); } writeOperand(c); writeOperand(m); @@ -741,12 +743,13 @@ public void setStrokingColor(float g) throws IOException */ public void setNonStrokingColor(PDColor color) throws IOException { + PDColorSpace colorSpace = color.getColorSpace(); if (nonStrokingColorSpaceStack.isEmpty() || - nonStrokingColorSpaceStack.peek() != color.getColorSpace()) + nonStrokingColorSpaceStack.peek() != colorSpace) { - writeOperand(getName(color.getColorSpace())); + writeOperand(getName(colorSpace)); writeOperator(OperatorName.NON_STROKING_COLORSPACE); - setNonStrokingColorSpaceStack(color.getColorSpace()); + setNonStrokingColorSpaceStack(colorSpace); } for (float value : color.getComponents()) @@ -754,16 +757,16 @@ public void setNonStrokingColor(PDColor color) throws IOException writeOperand(value); } -// if (color.getColorSpace() instanceof PDPattern) +// if (colorSpace instanceof PDPattern) // { // writeOperand(color.getPatternName()); // } if ( -// color.getColorSpace() instanceof PDPattern || -// color.getColorSpace() instanceof PDSeparation || -// color.getColorSpace() instanceof PDDeviceN || - color.getColorSpace() instanceof PDICCBased) +// colorSpace instanceof PDPattern || +// colorSpace instanceof PDSeparation || +// colorSpace instanceof PDDeviceN || + colorSpace instanceof PDICCBased) { writeOperator(OperatorName.NON_STROKING_COLOR_N); } @@ -782,7 +785,7 @@ public void setNonStrokingColor(PDColor color) throws IOException public void setNonStrokingColor(AWTColor color) throws IOException { float[] components = new float[] { - color.getRed() / 255f, color.getGreen() / 255f, color.getBlue() / 255f }; + color.getRed() / 255f, color.getGreen() / 255f, color.getBlue() / 255f }; PDColor pdColor = new PDColor(components, PDDeviceRGB.INSTANCE); setNonStrokingColor(pdColor); } @@ -801,7 +804,7 @@ public void setNonStrokingColor(float r, float g, float b) throws IOException if (isOutsideOneInterval(r) || isOutsideOneInterval(g) || isOutsideOneInterval(b)) { throw new IllegalArgumentException("Parameters must be within 0..1, but are " - + String.format("(%.2f,%.2f,%.2f)", r, g, b)); + + String.format("(%.2f,%.2f,%.2f)", r, g, b)); } writeOperand(r); writeOperand(g); @@ -827,7 +830,7 @@ public void setNonStrokingColor(int r, int g, int b) throws IOException if (isOutside255Interval(r) || isOutside255Interval(g) || isOutside255Interval(b)) { throw new IllegalArgumentException("Parameters must be within 0..255, but are " - + String.format("(%d,%d,%d)", r, g, b)); + + String.format("(%d,%d,%d)", r, g, b)); } setNonStrokingColor(r / 255f, g / 255f, b / 255f); } @@ -849,7 +852,7 @@ public void setNonStrokingColor(int c, int m, int y, int k) throws IOException if (isOutside255Interval(c) || isOutside255Interval(m) || isOutside255Interval(y) || isOutside255Interval(k)) { throw new IllegalArgumentException("Parameters must be within 0..255, but are " - + String.format("(%d,%d,%d,%d)", c, m, y, k)); + + String.format("(%d,%d,%d,%d)", c, m, y, k)); } setNonStrokingColor(c / 255f, m / 255f, y / 255f, k / 255f); } @@ -868,7 +871,7 @@ public void setNonStrokingColor(float c, float m, float y, float k) throws IOExc if (isOutsideOneInterval(c) || isOutsideOneInterval(m) || isOutsideOneInterval(y) || isOutsideOneInterval(k)) { throw new IllegalArgumentException("Parameters must be within 0..1, but are " - + String.format("(%.2f,%.2f,%.2f,%.2f)", c, m, y, k)); + + String.format("(%.2f,%.2f,%.2f,%.2f)", c, m, y, k)); } writeOperand(c); writeOperand(m); @@ -1347,6 +1350,24 @@ public void beginMarkedContent(COSName tag) throws IOException writeOperator(OperatorName.BEGIN_MARKED_CONTENT); } + /** + * Begin a marked content sequence with a reference to the marked content identifier (MCID). + * + * @param tag the tag to be added to the content stream + * @param mcid the marked content identifier (MCID) + * @throws IOException If the content stream could not be written + */ + public void beginMarkedContent(COSName tag, int mcid) throws IOException + { + if (mcid < 0) + { + throw new IllegalArgumentException("mcid should not be negative"); + } + writeOperand(tag); + write("<> "); + writeOperator(OperatorName.BEGIN_MARKED_CONTENT_SEQ); + } + /** * Begin a marked content sequence with a reference to an entry in the page resources' * Properties dictionary. @@ -1358,7 +1379,18 @@ public void beginMarkedContent(COSName tag) throws IOException public void beginMarkedContent(COSName tag, PDPropertyList propertyList) throws IOException { writeOperand(tag); - writeOperand(resources.add(propertyList)); + + COSDictionary dict = propertyList.getCOSObject(); + if (dict.getInt(COSName.MCID) > -1 && dict.size() == 1) + { + // PDFBOX-5890: use simplified notation if there's only an MCID + write("<> "); + } + else + { + writeOperand(resources.add(propertyList)); + } + writeOperator(OperatorName.BEGIN_MARKED_CONTENT_SEQ); } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDAppearanceContentStream.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDAppearanceContentStream.java index a940a49e9..56b1fd472 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDAppearanceContentStream.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDAppearanceContentStream.java @@ -22,12 +22,10 @@ import com.tom_roush.pdfbox.contentstream.operator.OperatorName; import com.tom_roush.pdfbox.cos.COSArray; - import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColor; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDBorderStyleDictionary; - /** * Provides the ability to write to an appearance content stream. * @@ -35,253 +33,253 @@ */ public final class PDAppearanceContentStream extends PDAbstractContentStream implements Closeable { - /** - * Create a new appearance stream. - * - * @param appearance - * The appearance stream to write to. - * @throws IOException If there is an error writing to the content stream. - */ - public PDAppearanceContentStream(PDAppearanceStream appearance) throws IOException - { - this(appearance, appearance.getStream().createOutputStream()); - } + /** + * Create a new appearance stream. + * + * @param appearance + * The appearance stream to write to. + * @throws IOException If there is an error writing to the content stream. + */ + public PDAppearanceContentStream(PDAppearanceStream appearance) throws IOException + { + this(appearance, appearance.getStream().createOutputStream()); + } - /** - * Create a new appearance stream. - * - * @param appearance The appearance stream to write to. - * @param compress whether the content stream is to be compressed. Set this to true when - * creating long content streams. - * @throws IOException If there is an error writing to the content stream. - */ - public PDAppearanceContentStream(PDAppearanceStream appearance, boolean compress) throws IOException - { - this(appearance, appearance.getStream().createOutputStream(compress ? COSName.FLATE_DECODE : null)); - } + /** + * Create a new appearance stream. + * + * @param appearance The appearance stream to write to. + * @param compress whether the content stream is to be compressed. Set this to true when + * creating long content streams. + * @throws IOException If there is an error writing to the content stream. + */ + public PDAppearanceContentStream(PDAppearanceStream appearance, boolean compress) throws IOException + { + this(appearance, appearance.getStream().createOutputStream(compress ? COSName.FLATE_DECODE : null)); + } - /** - * Create a new appearance stream. - * - * @param appearance - * The appearance stream to add to. - * @param outputStream - * The appearances output stream to write to. - */ - public PDAppearanceContentStream(PDAppearanceStream appearance, OutputStream outputStream) - { - super(null, outputStream, appearance.getResources()); - } + /** + * Create a new appearance stream. + * + * @param appearance + * The appearance stream to add to. + * @param outputStream + * The appearances output stream to write to. + */ + public PDAppearanceContentStream(PDAppearanceStream appearance, OutputStream outputStream) + { + super(null, outputStream, appearance.getResources()); + } - /** - * Set the stroking color. - * - *

      - * The command is only emitted if the color is not null and the number of - * components is > 0. - * - * @param color The colorspace to write. - * @throws IOException If there is an error writing to the content stream. - * @see PDAbstractContentStream#setStrokingColor(PDColor) - */ - public boolean setStrokingColorOnDemand(PDColor color) throws IOException - { - if (color != null) - { - float[] components = color.getComponents(); - if (components.length > 0) - { - setStrokingColor(components); - return true; - } - } - return false; - } + /** + * Set the stroking color. + * + *

      + * The command is only emitted if the color is not null and the number of + * components is > 0. + * + * @param color The colorspace to write. + * @throws IOException If there is an error writing to the content stream. + * @see PDAbstractContentStream#setStrokingColor(PDColor) + */ + public boolean setStrokingColorOnDemand(PDColor color) throws IOException + { + if (color != null) + { + float[] components = color.getComponents(); + if (components.length > 0) + { + setStrokingColor(components); + return true; + } + } + return false; + } - /** - * Set the stroking color. - * - * @see PDAbstractContentStream#setStrokingColor(com.tom_roush.harmony.awt.AWTColor) - * @param components - * the color components dependent on the color space being used. - * @throws IOException If there is an error writing to the content stream. - */ - public void setStrokingColor(float[] components) throws IOException - { - for (float value : components) - { - writeOperand(value); - } + /** + * Set the stroking color. + * + * @see PDAbstractContentStream#setStrokingColor(com.tom_roush.harmony.awt.AWTColor) + * @param components + * the color components dependent on the color space being used. + * @throws IOException If there is an error writing to the content stream. + */ + public void setStrokingColor(float[] components) throws IOException + { + for (float value : components) + { + writeOperand(value); + } - int numComponents = components.length; - switch (numComponents) - { - case 1: + int numComponents = components.length; + switch (numComponents) + { + case 1: writeOperator(OperatorName.STROKING_COLOR_GRAY); break; - case 3: + case 3: writeOperator(OperatorName.STROKING_COLOR_RGB); break; - case 4: + case 4: writeOperator(OperatorName.STROKING_COLOR_CMYK); break; - default: + default: break; - } - //TODO shouldn't we set the stack? - //Or call the appropriate setStrokingColor() method from the base class? - } + } + //TODO shouldn't we set the stack? + //Or call the appropriate setStrokingColor() method from the base class? + } - /** - * Set the non stroking color. - * - *

      - * The command is only emitted if the color is not null and the number of - * components is > 0. - * - * @param color The colorspace to write. - * @throws IOException If there is an error writing to the content stream. - * @see PDAbstractContentStream#setNonStrokingColor(PDColor) - */ - public boolean setNonStrokingColorOnDemand(PDColor color) throws IOException - { - if (color != null) - { - float[] components = color.getComponents(); - if (components.length > 0) - { - setNonStrokingColor(components); - return true; - } - } - return false; - } + /** + * Set the non stroking color. + * + *

      + * The command is only emitted if the color is not null and the number of + * components is > 0. + * + * @param color The colorspace to write. + * @throws IOException If there is an error writing to the content stream. + * @see PDAbstractContentStream#setNonStrokingColor(PDColor) + */ + public boolean setNonStrokingColorOnDemand(PDColor color) throws IOException + { + if (color != null) + { + float[] components = color.getComponents(); + if (components.length > 0) + { + setNonStrokingColor(components); + return true; + } + } + return false; + } - /** - * Set the non stroking color. - * - * @see PDAbstractContentStream#setNonStrokingColor(com.tom_roush.harmony.awt.AWTColor) - * @param components - * the color components dependent on the color space being used. - * @throws IOException If there is an error writing to the content stream. - */ - public void setNonStrokingColor(float[] components) throws IOException - { - for (float value : components) - { - writeOperand(value); - } + /** + * Set the non stroking color. + * + * @see PDAbstractContentStream#setNonStrokingColor(com.tom_roush.harmony.awt.AWTColor) + * @param components + * the color components dependent on the color space being used. + * @throws IOException If there is an error writing to the content stream. + */ + public void setNonStrokingColor(float[] components) throws IOException + { + for (float value : components) + { + writeOperand(value); + } - int numComponents = components.length; - switch (numComponents) - { - case 1: + int numComponents = components.length; + switch (numComponents) + { + case 1: writeOperator(OperatorName.NON_STROKING_GRAY); break; - case 3: + case 3: writeOperator(OperatorName.NON_STROKING_RGB); break; - case 4: + case 4: writeOperator(OperatorName.NON_STROKING_CMYK); break; - default: + default: break; - } - //TODO shouldn't we set the stack? - //Or call the appropriate setNonStrokingColor() method from the base class? - } + } + //TODO shouldn't we set the stack? + //Or call the appropriate setNonStrokingColor() method from the base class? + } - /** - * Convenience method for annotations: sets the line with and dash style. - * - * @param lineWidth The line width. - * @param bs The border style, may be null. - * @param border The border array, must have at least three entries. This is - * only used if the border style is null. - * - * @throws IOException If there is an error writing to the content stream. - */ - public void setBorderLine(float lineWidth, PDBorderStyleDictionary bs, - COSArray border) throws IOException - { - // Can't use PDBorderStyleDictionary.getDashStyle() as - // this will return a default dash style if non is existing - if (bs != null && bs.getCOSObject().containsKey(COSName.D) && - bs.getStyle().equals(PDBorderStyleDictionary.STYLE_DASHED)) - { - setLineDashPattern(bs.getDashStyle().getDashArray(), 0); - } - else if (bs == null && border.size() > 3) - { - if (border.getObject(3) instanceof COSArray) - { - setLineDashPattern(((COSArray) border.getObject(3)).toFloatArray(), 0); - } - else - { - // PDFBOX-5266: invalid dash array, be invisible - setLineDashPattern(new float[1], 0); - } - } - setLineWidthOnDemand(lineWidth); - } + /** + * Convenience method for annotations: sets the line with and dash style. + * + * @param lineWidth The line width. + * @param bs The border style, may be null. + * @param border The border array, must have at least three entries. This is + * only used if the border style is null. + * + * @throws IOException If there is an error writing to the content stream. + */ + public void setBorderLine(float lineWidth, PDBorderStyleDictionary bs, + COSArray border) throws IOException + { + // Can't use PDBorderStyleDictionary.getDashStyle() as + // this will return a default dash style if non is existing + if (bs != null && bs.getCOSObject().containsKey(COSName.D) && + bs.getStyle().equals(PDBorderStyleDictionary.STYLE_DASHED)) + { + setLineDashPattern(bs.getDashStyle().getDashArray(), 0); + } + else if (bs == null && border.size() > 3) + { + if (border.getObject(3) instanceof COSArray) + { + setLineDashPattern(((COSArray) border.getObject(3)).toFloatArray(), 0); + } + else + { + // PDFBOX-5266: invalid dash array, be invisible + setLineDashPattern(new float[1], 0); + } + } + setLineWidthOnDemand(lineWidth); + } - /** - * Sets the line width. The command is only emitted if the lineWidth is - * different to 1. - * - * @param lineWidth the line width of the path. - * @throws IOException If there is an error writing to the content stream. - * @see PDAbstractContentStream#setLineWidth(float) - */ - public void setLineWidthOnDemand(float lineWidth) throws IOException - { - // Acrobat doesn't write a line width command - // for a line width of 1 as this is default. - // Will do the same. - if (!(Math.abs(lineWidth - 1) < 1e-6)) - { - setLineWidth(lineWidth); - } - } + /** + * Sets the line width. The command is only emitted if the lineWidth is + * different to 1. + * + * @param lineWidth the line width of the path. + * @throws IOException If there is an error writing to the content stream. + * @see PDAbstractContentStream#setLineWidth(float) + */ + public void setLineWidthOnDemand(float lineWidth) throws IOException + { + // Acrobat doesn't write a line width command + // for a line width of 1 as this is default. + // Will do the same. + if (Math.abs(lineWidth - 1) >= 1e-6) + { + setLineWidth(lineWidth); + } + } - /** - * Draw a shape. - * - *

      - * Dependent on the lineWidth and whether or not there is a background to be generated there are - * different commands to be used for draw a shape. - * - * @param lineWidth the line width of the path. - * @param hasStroke shall there be a stroking color. - * @param hasFill shall there be a fill color. - * @throws IOException If there is an error writing to the content stream. - */ - public void drawShape(float lineWidth, boolean hasStroke, boolean hasFill) throws IOException - { - // initial setting if stroking shall be done - boolean resolvedHasStroke = hasStroke; + /** + * Draw a shape. + * + *

      + * Dependent on the lineWidth and whether or not there is a background to be generated there are + * different commands to be used for draw a shape. + * + * @param lineWidth the line width of the path. + * @param hasStroke shall there be a stroking color. + * @param hasFill shall there be a fill color. + * @throws IOException If there is an error writing to the content stream. + */ + public void drawShape(float lineWidth, boolean hasStroke, boolean hasFill) throws IOException + { + // initial setting if stroking shall be done + boolean resolvedHasStroke = hasStroke; - // no stroking for very small lines - if (lineWidth < 1e-6) - { - resolvedHasStroke = false; - } - if (hasFill && resolvedHasStroke) - { - fillAndStroke(); - } - else if (resolvedHasStroke) - { - stroke(); - } - else if (hasFill) - { - fill(); - } - else - { - writeOperator(OperatorName.ENDPATH); - } - } + // no stroking for very small lines + if (lineWidth < 1e-6) + { + resolvedHasStroke = false; + } + if (hasFill && resolvedHasStroke) + { + fillAndStroke(); + } + else if (resolvedHasStroke) + { + stroke(); + } + else if (hasFill) + { + fill(); + } + else + { + writeOperator(OperatorName.ENDPATH); + } + } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDDestinationNameTreeNode.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDDestinationNameTreeNode.java index 8dc2bd464..e06916fb4 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDDestinationNameTreeNode.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDDestinationNameTreeNode.java @@ -17,13 +17,13 @@ package com.tom_roush.pdfbox.pdmodel; import java.io.IOException; + import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.common.PDNameTreeNode; import com.tom_roush.pdfbox.pdmodel.interactive.documentnavigation.destination.PDDestination; import com.tom_roush.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageDestination; - /** * This class holds all of the name trees that are available at the document level. * @@ -36,7 +36,6 @@ public class PDDestinationNameTreeNode extends PDNameTreeNode */ public PDDestinationNameTreeNode() { - super(); } /** @@ -60,7 +59,13 @@ protected PDPageDestination convertCOSToPD( COSBase base ) throws IOException //it for now destination = ((COSDictionary)base).getDictionaryObject( COSName.D ); } - return (PDPageDestination)PDDestination.create( destination ); + PDDestination dest = PDDestination.create(destination); + if (dest instanceof PDPageDestination) + { + return (PDPageDestination) dest; + } + // PDFBOX-5975: invalid tree entry + return null; } @Override diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDDocument.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDDocument.java index daaf4c79c..5b17edc8e 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDDocument.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDDocument.java @@ -73,7 +73,6 @@ import com.tom_roush.pdfbox.pdmodel.interactive.form.PDAcroForm; import com.tom_roush.pdfbox.pdmodel.interactive.form.PDField; import com.tom_roush.pdfbox.pdmodel.interactive.form.PDSignatureField; - /** * This is the in-memory representation of the PDF document. * The #close() method must be called once the document is no longer needed. @@ -92,7 +91,8 @@ public class PDDocument implements Closeable */ private static final int[] RESERVE_BYTE_RANGE = new int[] { 0, 1000000000, 1000000000, 1000000000 }; - /** + + /* * avoid concurrency issues with PDDeviceRGB and deadlock in COSNumber/COSInteger */ static @@ -164,7 +164,7 @@ public PDDocument() * Creates an empty PDF document. * You need to add at least one page for the document to be valid. * - * @param memUsageSetting defines how memory is used for buffering PDF streams + * @param memUsageSetting defines how memory is used for buffering PDF streams */ public PDDocument(MemoryUsageSetting memUsageSetting) { @@ -176,7 +176,7 @@ public PDDocument(MemoryUsageSetting memUsageSetting) catch (IOException ioe) { Log.w("PdfBox-Android", "Error initializing scratch file: " + ioe.getMessage() + - ". Fall back to main memory usage only."); + ". Fall back to main memory usage only."); try { scratchFile = new ScratchFile(MemoryUsageSetting.setupMainMemoryOnly()); @@ -232,7 +232,7 @@ public PDDocument(COSDocument doc, RandomAccessRead source) * * @param doc The COSDocument that this document wraps. * @param source the parser which is used to read the pdf - * @param permission he access permissions of the pdf + * @param permission the access permissions of the pdf * */ public PDDocument(COSDocument doc, RandomAccessRead source, AccessPermission permission) @@ -325,7 +325,7 @@ public void addSignature(PDSignature sigObject, SignatureInterface signatureInte * fields. */ public void addSignature(PDSignature sigObject, SignatureInterface signatureInterface, - SignatureOptions options) throws IOException + SignatureOptions options) throws IOException { if (signatureAdded) { @@ -361,9 +361,6 @@ public void addSignature(PDSignature sigObject, SignatureInterface signatureInte throw new IllegalStateException("Cannot sign an empty document"); } - int startIndex = Math.min(Math.max(options.getPage(), 0), pageCount - 1); - PDPage page = pageTree.get(startIndex); - // Get the AcroForm from the Root-Dictionary and append the annotation PDDocumentCatalog catalog = getDocumentCatalog(); PDAcroForm acroForm = catalog.getAcroForm(null); @@ -380,10 +377,9 @@ public void addSignature(PDSignature sigObject, SignatureInterface signatureInte } PDSignatureField signatureField = null; - COSBase cosFieldBase = acroForm.getCOSObject().getDictionaryObject(COSName.FIELDS); - if (cosFieldBase instanceof COSArray) + COSArray fieldArray = acroForm.getCOSObject().getCOSArray(COSName.FIELDS); + if (fieldArray != null) { - COSArray fieldArray = (COSArray) cosFieldBase; fieldArray.setNeedToBeUpdated(true); signatureField = findSignatureField(acroForm.getFieldIterator(), sigObject); } @@ -392,12 +388,15 @@ public void addSignature(PDSignature sigObject, SignatureInterface signatureInte acroForm.getCOSObject().setItem(COSName.FIELDS, new COSArray()); } PDAnnotationWidget firstWidget; + PDPage page; if (signatureField == null) { signatureField = new PDSignatureField(acroForm); // append the signature object signatureField.setValue(sigObject); firstWidget = signatureField.getWidgets().get(0); + int startIndex = Math.min(Math.max(options.getPage(), 0), pageCount - 1); + page = pageTree.get(startIndex); // backward linking firstWidget.setPage(page); } @@ -405,13 +404,14 @@ public void addSignature(PDSignature sigObject, SignatureInterface signatureInte { firstWidget = signatureField.getWidgets().get(0); sigObject.getCOSObject().setNeedToBeUpdated(true); + page = null; } // TODO This "overwrites" the settings of the original signature field which might not be intended by the user // better make it configurable (not all users need/want PDF/A but their own setting): // to conform PDF/A-1 requirement: - // The /F key's Print flag bit shall be set to 1 and + // The /F key's Print flag bit shall be set to 1 and // its Hidden, Invisible and NoView flag bits shall be set to 0 firstWidget.setPrinted(true); // This may be troublesome if several form fields are signed, @@ -442,38 +442,42 @@ public void addSignature(PDSignature sigObject, SignatureInterface signatureInte if (visualSignature == null) { prepareNonVisibleSignature(firstWidget); - return; + } + else + { + prepareVisibleSignature(firstWidget, acroForm, visualSignature); } - prepareVisibleSignature(firstWidget, acroForm, visualSignature); - - // Create Annotation / Field for signature - List annotations = page.getAnnotations(); + if (page != null) + { + // Create Annotation / Field for signature + List annotations = page.getAnnotations(); - // Make /Annots a direct object to avoid problem if it is an existing indirect object: - // it would not be updated in incremental save, and if we'd set the /Annots array "to be updated" - // while keeping it indirect, Adobe Reader would claim that the document had been modified. - page.setAnnotations(annotations); + // Make /Annots a direct object to avoid problem if it is an existing indirect object: + // it would not be updated in incremental save, and if we'd set the /Annots array "to be updated" + // while keeping it indirect, Adobe Reader would claim that the document had been modified. + page.setAnnotations(annotations); - // Get the annotations of the page and append the signature-annotation to it - // take care that page and acroforms do not share the same array (if so, we don't need to add it twice) - if (!(checkFields && - annotations instanceof COSArrayList && - acroFormFields instanceof COSArrayList && - ((COSArrayList) annotations).toList(). - equals(((COSArrayList) acroFormFields).toList()))) - { - // use check to prevent the annotation widget from appearing twice - if (checkSignatureAnnotation(annotations, firstWidget)) - { - firstWidget.getCOSObject().setNeedToBeUpdated(true); - } - else + // Get the annotations of the page and append the signature-annotation to it + // take care that page and acroforms do not share the same array (if so, we don't need to add it twice) + if (!(checkFields && + annotations instanceof COSArrayList && + acroFormFields instanceof COSArrayList && + ((COSArrayList) annotations).toList(). + equals(((COSArrayList) acroFormFields).toList()))) { - annotations.add(firstWidget); + // use check to prevent the annotation widget from appearing twice + if (checkSignatureAnnotation(annotations, firstWidget)) + { + firstWidget.getCOSObject().setNeedToBeUpdated(true); + } + else + { + annotations.add(firstWidget); + } } + page.getCOSObject().setNeedToBeUpdated(true); } - page.getCOSObject().setNeedToBeUpdated(true); } /** @@ -515,7 +519,7 @@ private boolean checkSignatureField(Iterator fieldIterator, PDSignature { PDField field = fieldIterator.next(); if (field instanceof PDSignatureField - && field.getCOSObject().equals(signatureField.getCOSObject())) + && field.getCOSObject().equals(signatureField.getCOSObject())) { return true; } @@ -542,8 +546,23 @@ private boolean checkSignatureAnnotation(List annotations, PDAnnot return false; } + private void prepareNonVisibleSignature(PDAnnotationWidget firstWidget) + { + // "Signature fields that are not intended to be visible shall + // have an annotation rectangle that has zero height and width." + // Set rectangle for non-visual signature to rectangle array [ 0 0 0 0 ] + firstWidget.setRectangle(new PDRectangle()); + + // The visual appearance must also exist for an invisible signature but may be empty. + PDAppearanceDictionary appearanceDictionary = new PDAppearanceDictionary(); + PDAppearanceStream appearanceStream = new PDAppearanceStream(this); + appearanceStream.setBBox(new PDRectangle()); + appearanceDictionary.setNormalAppearance(appearanceStream); + firstWidget.setAppearance(appearanceDictionary); + } + private void prepareVisibleSignature(PDAnnotationWidget firstWidget, PDAcroForm acroForm, - COSDocument visualSignature) + COSDocument visualSignature) { // Obtain visual signature object boolean annotNotFound = true; @@ -594,7 +613,7 @@ private void assignSignatureRectangle(PDAnnotationWidget firstWidget, COSDiction //in case of an existing field keep the original rect if (existingRectangle == null || existingRectangle.getCOSArray().size() != 4) { - COSArray rectArray = (COSArray) annotDict.getDictionaryObject(COSName.RECT); + COSArray rectArray = annotDict.getCOSArray(COSName.RECT); PDRectangle rect = new PDRectangle(rectArray); firstWidget.setRectangle(rect); } @@ -611,10 +630,9 @@ private void assignAppearanceDictionary(PDAnnotationWidget firstWidget, COSDicti private void assignAcroFormDefaultResource(PDAcroForm acroForm, COSDictionary newDict) { // read and set/update AcroForm default resource dictionary /DR if available - COSBase newBase = newDict.getDictionaryObject(COSName.DR); - if (newBase instanceof COSDictionary) + COSDictionary newDR = newDict.getCOSDictionary(COSName.DR); + if (newDR != null) { - COSDictionary newDR = (COSDictionary) newBase; PDResources defaultResources = acroForm.getDefaultResources(); if (defaultResources == null) { @@ -637,21 +655,6 @@ private void assignAcroFormDefaultResource(PDAcroForm acroForm, COSDictionary ne } } - private void prepareNonVisibleSignature(PDAnnotationWidget firstWidget) - { - // "Signature fields that are not intended to be visible shall - // have an annotation rectangle that has zero height and width." - // Set rectangle for non-visual signature to rectangle array [ 0 0 0 0 ] - firstWidget.setRectangle(new PDRectangle()); - - // The visual appearance must also exist for an invisible signature but may be empty. - PDAppearanceDictionary appearanceDictionary = new PDAppearanceDictionary(); - PDAppearanceStream appearanceStream = new PDAppearanceStream(this); - appearanceStream.setBBox(new PDRectangle()); - appearanceDictionary.setNormalAppearance(appearanceStream); - firstWidget.setAppearance(appearanceDictionary); - } - /** * This will add a list of signature fields to the document. * @@ -665,7 +668,7 @@ private void prepareNonVisibleSignature(PDAnnotationWidget firstWidget) */ @Deprecated public void addSignatureField(List sigFields, SignatureInterface signatureInterface, - SignatureOptions options) throws IOException + SignatureOptions options) throws IOException { PDDocumentCatalog catalog = getDocumentCatalog(); catalog.getCOSObject().setNeedToBeUpdated(true); @@ -716,7 +719,9 @@ public void addSignatureField(List sigFields, SignatureInterfa } /** - * Remove the page from the document. + * Remove the page from the document. Do not use this method if other pages link to this one or + * if your document has a structure tree for accessibility unless you are able to fix these as + * well. In such cases it is better to use the splitter() class which will do these fixes. * * @param page The page to remove from the document. */ @@ -726,7 +731,9 @@ public void removePage(PDPage page) } /** - * Remove the page from the document. + * Remove the page from the document. Do not use this method if other pages link to this one or + * if your document has a structure tree for accessibility unless you are able to fix these as + * well. In such cases it is better to use the splitter() class which will do these fixes. * * @param pageNumber 0 based index to page number. */ @@ -838,10 +845,10 @@ public PDDocumentCatalog getDocumentCatalog() if (documentCatalog == null) { COSDictionary trailer = document.getTrailer(); - COSBase dictionary = trailer.getDictionaryObject(COSName.ROOT); - if (dictionary instanceof COSDictionary) + COSDictionary dictionary = trailer.getCOSDictionary(COSName.ROOT); + if (dictionary != null) { - documentCatalog = new PDDocumentCatalog(this, (COSDictionary) dictionary); + documentCatalog = new PDDocumentCatalog(this, dictionary); } else { @@ -942,10 +949,10 @@ public List getSignatureDictionaries() throws IOException List signatures = new ArrayList(); for (PDSignatureField field : getSignatureFields()) { - COSBase value = field.getCOSObject().getDictionaryObject(COSName.V); + COSDictionary value = field.getCOSObject().getCOSDictionary(COSName.V); if (value != null) { - signatures.add(new PDSignature((COSDictionary)value)); + signatures.add(new PDSignature(value)); } } return signatures; @@ -990,7 +997,7 @@ public static PDDocument load(File file) throws IOException * Parses a PDF. * * @param file file to be loaded - * @param memUsageSetting defines how memory is used for buffering PDF streams + * @param memUsageSetting defines how memory is used for buffering PDF streams * * @return loaded document * @@ -998,7 +1005,7 @@ public static PDDocument load(File file) throws IOException * @throws IOException in case of a file reading or parsing error */ public static PDDocument load(File file, MemoryUsageSetting memUsageSetting) - throws IOException + throws IOException { return load(file, "", null, null, memUsageSetting); } @@ -1015,7 +1022,7 @@ public static PDDocument load(File file, MemoryUsageSetting memUsageSetting) * @throws IOException in case of a file reading or parsing error */ public static PDDocument load(File file, String password) - throws IOException + throws IOException { return load(file, password, null, null, MemoryUsageSetting.setupMainMemoryOnly()); } @@ -1025,7 +1032,7 @@ public static PDDocument load(File file, String password) * * @param file file to be loaded * @param password password to be used for decryption - * @param memUsageSetting defines how memory is used for buffering PDF streams + * @param memUsageSetting defines how memory is used for buffering PDF streams * * @return loaded document * @@ -1033,7 +1040,7 @@ public static PDDocument load(File file, String password) * @throws IOException in case of a file reading or parsing error */ public static PDDocument load(File file, String password, MemoryUsageSetting memUsageSetting) - throws IOException + throws IOException { return load(file, password, null, null, memUsageSetting); } @@ -1043,7 +1050,7 @@ public static PDDocument load(File file, String password, MemoryUsageSetting mem * * @param file file to be loaded * @param password password to be used for decryption - * @param keyStore key store to be used for decryption when using public key security + * @param keyStore key store to be used for decryption when using public key security * @param alias alias to be used for decryption when using public key security * * @return loaded document @@ -1051,7 +1058,7 @@ public static PDDocument load(File file, String password, MemoryUsageSetting mem * @throws IOException in case of a file reading or parsing error */ public static PDDocument load(File file, String password, InputStream keyStore, String alias) - throws IOException + throws IOException { return load(file, password, keyStore, alias, MemoryUsageSetting.setupMainMemoryOnly()); } @@ -1061,16 +1068,16 @@ public static PDDocument load(File file, String password, InputStream keyStore, * * @param file file to be loaded * @param password password to be used for decryption - * @param keyStore key store to be used for decryption when using public key security + * @param keyStore key store to be used for decryption when using public key security * @param alias alias to be used for decryption when using public key security - * @param memUsageSetting defines how memory is used for buffering PDF streams + * @param memUsageSetting defines how memory is used for buffering PDF streams * * @return loaded document * * @throws IOException in case of a file reading or parsing error */ public static PDDocument load(File file, String password, InputStream keyStore, String alias, - MemoryUsageSetting memUsageSetting) throws IOException + MemoryUsageSetting memUsageSetting) throws IOException { @SuppressWarnings({"squid:S2095"}) // raFile not closed here, may be needed for signing RandomAccessBufferedFileInputStream raFile = new RandomAccessBufferedFileInputStream(file); @@ -1086,8 +1093,8 @@ public static PDDocument load(File file, String password, InputStream keyStore, } private static PDDocument load(RandomAccessBufferedFileInputStream raFile, String password, - InputStream keyStore, String alias, - MemoryUsageSetting memUsageSetting) throws IOException + InputStream keyStore, String alias, + MemoryUsageSetting memUsageSetting) throws IOException { ScratchFile scratchFile = new ScratchFile(memUsageSetting); try @@ -1124,7 +1131,7 @@ public static PDDocument load(InputStream input) throws IOException * copied to main memory or to a temporary file to enable random access to the pdf. * * @param input stream that contains the document. Don't forget to close it after loading. - * @param memUsageSetting defines how memory is used for buffering input stream and PDF streams + * @param memUsageSetting defines how memory is used for buffering input stream and PDF streams * * @return loaded document * @@ -1132,7 +1139,7 @@ public static PDDocument load(InputStream input) throws IOException * @throws IOException In case of a reading or parsing error. */ public static PDDocument load(InputStream input, MemoryUsageSetting memUsageSetting) - throws IOException + throws IOException { return load(input, "", null, null, memUsageSetting); } @@ -1150,7 +1157,7 @@ public static PDDocument load(InputStream input, MemoryUsageSetting memUsageSett * @throws IOException In case of a reading or parsing error. */ public static PDDocument load(InputStream input, String password) - throws IOException + throws IOException { return load(input, password, null, null, MemoryUsageSetting.setupMainMemoryOnly()); } @@ -1161,7 +1168,7 @@ public static PDDocument load(InputStream input, String password) * * @param input stream that contains the document. Don't forget to close it after loading. * @param password password to be used for decryption - * @param keyStore key store to be used for decryption when using public key security + * @param keyStore key store to be used for decryption when using public key security * @param alias alias to be used for decryption when using public key security * * @return loaded document @@ -1169,7 +1176,7 @@ public static PDDocument load(InputStream input, String password) * @throws IOException In case of a reading or parsing error. */ public static PDDocument load(InputStream input, String password, InputStream keyStore, String alias) - throws IOException + throws IOException { return load(input, password, keyStore, alias, MemoryUsageSetting.setupMainMemoryOnly()); } @@ -1180,7 +1187,7 @@ public static PDDocument load(InputStream input, String password, InputStream ke * * @param input stream that contains the document. Don't forget to close it after loading. * @param password password to be used for decryption - * @param memUsageSetting defines how memory is used for buffering input stream and PDF streams + * @param memUsageSetting defines how memory is used for buffering input stream and PDF streams * * @return loaded document * @@ -1188,7 +1195,7 @@ public static PDDocument load(InputStream input, String password, InputStream ke * @throws IOException In case of a reading or parsing error. */ public static PDDocument load(InputStream input, String password, MemoryUsageSetting memUsageSetting) - throws IOException + throws IOException { return load(input, password, null, null, memUsageSetting); } @@ -1199,9 +1206,9 @@ public static PDDocument load(InputStream input, String password, MemoryUsageSet * * @param input stream that contains the document. Don't forget to close it after loading. * @param password password to be used for decryption - * @param keyStore key store to be used for decryption when using public key security + * @param keyStore key store to be used for decryption when using public key security * @param alias alias to be used for decryption when using public key security - * @param memUsageSetting defines how memory is used for buffering input stream and PDF streams + * @param memUsageSetting defines how memory is used for buffering input stream and PDF streams * * @return loaded document * @@ -1209,7 +1216,7 @@ public static PDDocument load(InputStream input, String password, MemoryUsageSet * @throws IOException In case of a reading or parsing error. */ public static PDDocument load(InputStream input, String password, InputStream keyStore, - String alias, MemoryUsageSetting memUsageSetting) throws IOException + String alias, MemoryUsageSetting memUsageSetting) throws IOException { ScratchFile scratchFile = new ScratchFile(memUsageSetting); try @@ -1253,7 +1260,7 @@ public static PDDocument load(byte[] input) throws IOException * @throws IOException In case of a reading or parsing error. */ public static PDDocument load(byte[] input, String password) - throws IOException + throws IOException { return load(input, password, null, null); } @@ -1263,7 +1270,7 @@ public static PDDocument load(byte[] input, String password) * * @param input byte array that contains the document. * @param password password to be used for decryption - * @param keyStore key store to be used for decryption when using public key security + * @param keyStore key store to be used for decryption when using public key security * @param alias alias to be used for decryption when using public key security * * @return loaded document @@ -1272,7 +1279,7 @@ public static PDDocument load(byte[] input, String password) * @throws IOException In case of a reading or parsing error. */ public static PDDocument load(byte[] input, String password, InputStream keyStore, - String alias) throws IOException + String alias) throws IOException { return load(input, password, keyStore, alias, MemoryUsageSetting.setupMainMemoryOnly()); } @@ -1282,9 +1289,9 @@ public static PDDocument load(byte[] input, String password, InputStream keyStor * * @param input byte array that contains the document. * @param password password to be used for decryption - * @param keyStore key store to be used for decryption when using public key security + * @param keyStore key store to be used for decryption when using public key security * @param alias alias to be used for decryption when using public key security - * @param memUsageSetting defines how memory is used for buffering input stream and PDF streams + * @param memUsageSetting defines how memory is used for buffering input stream and PDF streams * * @return loaded document * @@ -1292,7 +1299,7 @@ public static PDDocument load(byte[] input, String password, InputStream keyStor * @throws IOException In case of a reading or parsing error. */ public static PDDocument load(byte[] input, String password, InputStream keyStore, - String alias, MemoryUsageSetting memUsageSetting) throws IOException + String alias, MemoryUsageSetting memUsageSetting) throws IOException { ScratchFile scratchFile = new ScratchFile(memUsageSetting); RandomAccessRead source = new RandomAccessBuffer(input); @@ -1307,6 +1314,8 @@ public static PDDocument load(byte[] input, String password, InputStream keyStor * If encryption has been activated (with * {@link #protect(com.tom_roush.pdfbox.pdmodel.encryption.ProtectionPolicy) protect(ProtectionPolicy)}), * do not use the document after saving because the contents are now encrypted. + * The same applies if your file was created from parts of another file and that + * one is to be used after saving. * * @param fileName The file to save as. * @@ -1323,6 +1332,8 @@ public void save(String fileName) throws IOException * If encryption has been activated (with * {@link #protect(com.tom_roush.pdfbox.pdmodel.encryption.ProtectionPolicy) protect(ProtectionPolicy)}), * do not use the document after saving because the contents are now encrypted. + * The same applies if your file was created from parts of another file and that + * one is to be used after saving. * * @param file The file to save as. * @@ -1339,6 +1350,8 @@ public void save(File file) throws IOException * If encryption has been activated (with * {@link #protect(com.tom_roush.pdfbox.pdmodel.encryption.ProtectionPolicy) protect(ProtectionPolicy)}), * do not use the document after saving because the contents are now encrypted. + * The same applies if your file was created from parts of another file and that + * one is to be used after saving. * * @param output The stream to write to. It will be closed when done. It is recommended to wrap * it in a {@link java.io.BufferedOutputStream}, unless it is already buffered. @@ -1352,12 +1365,7 @@ public void save(OutputStream output) throws IOException throw new IOException("Cannot save a document which has been closed"); } - // subset designated fonts - for (PDFont font : fontsToSubset) - { - font.subset(); - } - fontsToSubset.clear(); + subsetDesignatedFonts(); // save PDF COSWriter writer = new COSWriter(output); @@ -1371,6 +1379,15 @@ public void save(OutputStream output) throws IOException } } + private void subsetDesignatedFonts() throws IOException + { + for (PDFont font : fontsToSubset) + { + font.subset(); + } + fontsToSubset.clear(); + } + /** * Save the PDF as an incremental update. This is only possible if the PDF was loaded from a * file or a stream, not if the document was created in PDFBox itself. There must be a path of @@ -1380,6 +1397,24 @@ public void save(OutputStream output) throws IOException * Other usages of this method are for experienced users only. You will usually never need it. * It is useful only if you are required to keep the current revision and append the changes. A * typical use case is changing a signed file without invalidating the signature. + *

      + * If your modification includes annotations, make sure these link back to their page by calling + * {@link PDAnnotation#setPage(PDPage)}. Although this is optional, + * not doing it + * can cause trouble when PDFs get + * signed. (PDFBox already does this for signature widget annotations) + *

      + * Another problem with page-based modifications can occur if the page tree isn't flat: there + * won't be a closed update path from the catalog to the page. To fix this, add code like this: + *

      {@code
      +     * COSDictionary parent = page.getCOSObject().getCOSDictionary(COSName.PARENT);
      +     * while (parent != null)
      +     * {
      +     *     parent.setNeedToBeUpdated(true);
      +     *     parent = parent.getCOSDictionary(COSName.PARENT);
      +     * }
      +     * }
      + * Don't use the input file as target as this will produce a corrupted file. * * @param output stream to write to. It will be closed when done. It * must never point to the source file or that one will be @@ -1390,6 +1425,7 @@ public void save(OutputStream output) throws IOException public void saveIncremental(OutputStream output) throws IOException { + subsetDesignatedFonts(); COSWriter writer = null; try { @@ -1423,6 +1459,12 @@ public void saveIncremental(OutputStream output) throws IOException * getting changed, you need to have some understanding of the PDF specification, and look at * the saved file with an editor to verify that you are updating the correct objects. You should * also inspect the page and document structures of the file with PDFDebugger. + *

      + * If your modification includes annotations, make sure these link back to their page by calling + * {@link PDAnnotation#setPage(PDPage)}. Although this is optional, + * not doing it + * can cause trouble when PDFs get + * signed. (PDFBox already does this for signature widget annotations) * * @param output stream to write to. It will be closed when done. It * must never point to the source file or that one will be harmed! @@ -1432,6 +1474,7 @@ public void saveIncremental(OutputStream output) throws IOException */ public void saveIncremental(OutputStream output, Set objectsToWrite) throws IOException { + subsetDesignatedFonts(); if (pdfSource == null) { throw new IllegalStateException("document was not loaded from a file or a stream"); @@ -1452,9 +1495,6 @@ public void saveIncremental(OutputStream output, Set objectsToWri } /** - *

      - * (This is a new feature for 2.0.3. The API for external signing might change based on feedback after release!) - *

      * Save PDF incrementally without closing for external signature creation scenario. The general * sequence is: *

      @@ -1492,6 +1532,7 @@ public void saveIncremental(OutputStream output, Set objectsToWri
            */
           public ExternalSigningSupport saveIncrementalForExternalSigning(OutputStream output) throws IOException
           {
      +        subsetDesignatedFonts();
               if (pdfSource == null)
               {
                   throw new IllegalStateException("document was not loaded from a file or a stream");
      @@ -1511,7 +1552,7 @@ public ExternalSigningSupport saveIncrementalForExternalSigning(OutputStream out
               if (!Arrays.equals(byteRange, RESERVE_BYTE_RANGE))
               {
                   throw new IllegalStateException("signature reserve byte range has been changed "
      -                + "after addSignature(), please set the byte range that existed after addSignature()");
      +                    + "after addSignature(), please set the byte range that existed after addSignature()");
               }
               COSWriter writer = new COSWriter(output, pdfSource);
               writer.write(this);
      @@ -1528,6 +1569,10 @@ public ExternalSigningSupport saveIncrementalForExternalSigning(OutputStream out
            *
            * @param pageIndex the 0-based page index
            * @return the page at the given index.
      +     *
      +     * @throws IllegalStateException if the requested index isn't found or doesn't point to a valid
      +     * page dictionary.
      +     * @throws IndexOutOfBoundsException if the requested index is higher than the page count.
            */
           public PDPage getPage(int pageIndex) // todo: REPLACE most calls to this method with BELOW method
           {
      @@ -1607,6 +1652,8 @@ public void close() throws IOException
            * previously and logs a warning.
            * 

      * Do not use the document after saving, because the structures are encrypted. + * The same applies if your file was created from parts of another file and that + * one is to be used after saving. * * @see com.tom_roush.pdfbox.pdmodel.encryption.StandardProtectionPolicy * @see com.tom_roush.pdfbox.pdmodel.encryption.PublicKeyProtectionPolicy @@ -1619,7 +1666,7 @@ public void protect(ProtectionPolicy policy) throws IOException if (isAllSecurityToBeRemoved()) { Log.w("PdfBox-Android", "do not call setAllSecurityToBeRemoved(true) before calling protect(), " - + "as protect() implies setAllSecurityToBeRemoved(false)"); + + "as protect() implies setAllSecurityToBeRemoved(false)"); setAllSecurityToBeRemoved(false); } @@ -1675,7 +1722,10 @@ public void setAllSecurityToBeRemoved(boolean removeAllSecurity) } /** - * Provides the document ID. + * Provides the document ID. This is not the trailer document ID but the time used to create it. + * Use {@link COSDocument#getDocumentID()} for the trailer document ID. Read + * PDFBOX-1613 for more details + * about the purpose. * * @return the document ID */ @@ -1685,7 +1735,10 @@ public Long getDocumentId() } /** - * Sets the document ID to the given value. + * Sets the document ID to the given value. This is not the trailer document ID but the time + * used to create it. Use {@link COSDocument#setDocumentID(COSArray)} for the trailer document ID. Read + * PDFBOX-1613 for more details + * about the purpose. * * @param docId the new document ID */ diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDDocumentCatalog.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDDocumentCatalog.java index 6dded9a27..a5387b08e 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDDocumentCatalog.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDDocumentCatalog.java @@ -49,7 +49,6 @@ import com.tom_roush.pdfbox.pdmodel.interactive.form.PDAcroForm; import com.tom_roush.pdfbox.pdmodel.interactive.pagenavigation.PDThread; import com.tom_roush.pdfbox.pdmodel.interactive.viewerpreferences.PDViewerPreferences; - /** * The Document Catalog of a PDF. * @@ -57,6 +56,7 @@ */ public class PDDocumentCatalog implements COSObjectable { + private final COSDictionary root; private final PDDocument document; private PDDocumentFixup acroFormFixupApplied; @@ -162,7 +162,7 @@ public void setAcroForm(PDAcroForm acroForm) public PDPageTree getPages() { // todo: cache me? - return new PDPageTree((COSDictionary)root.getDictionaryObject(COSName.PAGES), document); + return new PDPageTree(root.getCOSDictionary(COSName.PAGES), document); } /** @@ -214,7 +214,7 @@ public void setDocumentOutline(PDDocumentOutline outlines) */ public List getThreads() { - COSArray array = (COSArray)root.getDictionaryObject(COSName.THREADS); + COSArray array = root.getCOSArray(COSName.THREADS); if (array == null) { array = new COSArray(); @@ -301,7 +301,7 @@ else if (openAction instanceof COSArray) */ public PDDocumentCatalogAdditionalActions getActions() { - COSDictionary addAction = (COSDictionary) root.getDictionaryObject(COSName.AA); + COSDictionary addAction = root.getCOSDictionary(COSName.AA); if (addAction == null) { addAction = new COSDictionary(); @@ -325,7 +325,7 @@ public void setActions(PDDocumentCatalogAdditionalActions actions) */ public PDDocumentNameDictionary getNames() { - COSDictionary names = (COSDictionary) root.getDictionaryObject(COSName.NAMES); + COSDictionary names = root.getCOSDictionary(COSName.NAMES); return names == null ? null : new PDDocumentNameDictionary(this, names); } @@ -335,7 +335,7 @@ public PDDocumentNameDictionary getNames() public PDDocumentNameDestinationDictionary getDests() { PDDocumentNameDestinationDictionary nameDic = null; - COSDictionary dests = (COSDictionary) root.getDictionaryObject(COSName.DESTS); + COSDictionary dests = root.getCOSDictionary(COSName.DESTS); if (dests != null) { nameDic = new PDDocumentNameDestinationDictionary(dests); @@ -350,7 +350,7 @@ public PDDocumentNameDestinationDictionary getDests() * @throws IOException if there is an error creating the PDPageDestination object. */ public PDPageDestination findNamedDestinationPage(PDNamedDestination namedDest) - throws IOException + throws IOException { PDPageDestination pageDestination = null; PDDocumentNameDictionary namesDict = getNames(); @@ -393,7 +393,7 @@ public void setNames(PDDocumentNameDictionary names) */ public PDMarkInfo getMarkInfo() { - COSDictionary dic = (COSDictionary)root.getDictionaryObject(COSName.MARK_INFO); + COSDictionary dic = root.getCOSDictionary(COSName.MARK_INFO); return dic == null ? null : new PDMarkInfo(dic); } @@ -410,14 +410,15 @@ public void setMarkInfo(PDMarkInfo markInfo) /** * Get the list of OutputIntents defined in the document. * - * @return The list of PDOutputIntent + * @return The list of PDOutputIntent, never null. */ public List getOutputIntents() { - List retval = new ArrayList(); - COSArray array = (COSArray)root.getDictionaryObject(COSName.OUTPUT_INTENTS); + List retval; + COSArray array = root.getCOSArray(COSName.OUTPUT_INTENTS); if (array != null) { + retval = new ArrayList(array.size()); for (COSBase cosBase : array) { if (cosBase instanceof COSObject) @@ -428,6 +429,10 @@ public List getOutputIntents() retval.add(oi); } } + else + { + retval = new ArrayList(); + } return retval; } @@ -439,7 +444,7 @@ public List getOutputIntents() */ public void addOutputIntent(PDOutputIntent outputIntent) { - COSArray array = (COSArray)root.getDictionaryObject(COSName.OUTPUT_INTENTS); + COSArray array = root.getCOSArray(COSName.OUTPUT_INTENTS); if (array == null) { array = new COSArray(); @@ -538,7 +543,7 @@ public void setPageLayout(PageLayout layout) */ public PDURIDictionary getURI() { - COSDictionary uri = (COSDictionary)root.getDictionaryObject(COSName.URI); + COSDictionary uri = root.getCOSDictionary(COSName.URI); return uri == null ? null : new PDURIDictionary(uri); } @@ -621,7 +626,7 @@ public void setVersion(String version) */ public PDPageLabels getPageLabels() throws IOException { - COSDictionary dict = (COSDictionary) root.getDictionaryObject(COSName.PAGE_LABELS); + COSDictionary dict = root.getCOSDictionary(COSName.PAGE_LABELS); return dict == null ? null : new PDPageLabels(document, dict); } @@ -642,7 +647,7 @@ public void setPageLabels(PDPageLabels labels) */ public PDOptionalContentProperties getOCProperties() { - COSDictionary dict = (COSDictionary)root.getDictionaryObject(COSName.OCPROPERTIES); + COSDictionary dict = root.getCOSDictionary(COSName.OCPROPERTIES); return dict == null ? null : new PDOptionalContentProperties(dict); } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDDocumentNameDictionary.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDDocumentNameDictionary.java index a59e8142e..0c75206e9 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDDocumentNameDictionary.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDDocumentNameDictionary.java @@ -20,7 +20,6 @@ import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.common.COSObjectable; - /** * This class holds all of the name trees that are available at the document level. * @@ -84,13 +83,13 @@ public PDDestinationNameTreeNode getDests() { PDDestinationNameTreeNode dests = null; - COSDictionary dic = (COSDictionary)nameDictionary.getDictionaryObject( COSName.DESTS ); + COSDictionary dic = nameDictionary.getCOSDictionary(COSName.DESTS ); //The document catalog also contains the Dests entry sometimes //so check there as well. if( dic == null ) { - dic = (COSDictionary)catalog.getCOSObject().getDictionaryObject( COSName.DESTS ); + dic = catalog.getCOSObject().getCOSDictionary( COSName.DESTS ); } if( dic != null ) @@ -127,7 +126,7 @@ public PDEmbeddedFilesNameTreeNode getEmbeddedFiles() { PDEmbeddedFilesNameTreeNode retval = null; - COSDictionary dic = (COSDictionary)nameDictionary.getDictionaryObject( COSName.EMBEDDED_FILES ); + COSDictionary dic = nameDictionary.getCOSDictionary(COSName.EMBEDDED_FILES); if( dic != null ) { @@ -158,7 +157,7 @@ public PDJavascriptNameTreeNode getJavaScript() { PDJavascriptNameTreeNode retval = null; - COSDictionary dic = (COSDictionary) nameDictionary.getDictionaryObject(COSName.JAVA_SCRIPT); + COSDictionary dic = nameDictionary.getCOSDictionary(COSName.JAVA_SCRIPT); if( dic != null ) { diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDEmbeddedFilesNameTreeNode.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDEmbeddedFilesNameTreeNode.java index 7f35d9a52..c42a122ee 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDEmbeddedFilesNameTreeNode.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDEmbeddedFilesNameTreeNode.java @@ -17,11 +17,11 @@ package com.tom_roush.pdfbox.pdmodel; import java.io.IOException; + import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.pdmodel.common.PDNameTreeNode; import com.tom_roush.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification; - /** * This class holds all of the name trees that are available at the document level. * @@ -34,7 +34,6 @@ public class PDEmbeddedFilesNameTreeNode extends PDNameTreeNode */ public PDJavascriptNameTreeNode() { - super(); } /** diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDPage.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDPage.java index 77e43eca1..c20ac9252 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDPage.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDPage.java @@ -47,7 +47,6 @@ import com.tom_roush.pdfbox.pdmodel.interactive.pagenavigation.PDThreadBead; import com.tom_roush.pdfbox.pdmodel.interactive.pagenavigation.PDTransition; import com.tom_roush.pdfbox.util.Matrix; - /** * A page in a PDF document. * @@ -528,7 +527,7 @@ public void setContents(List contents) */ public List getThreadBeads() { - COSArray beads = (COSArray) page.getDictionaryObject(COSName.B); + COSArray beads = page.getCOSArray(COSName.B); if (beads == null) { beads = new COSArray(); @@ -692,17 +691,28 @@ public List getAnnotations(AnnotationFilter annotationFilter) thro { continue; } - PDAnnotation createdAnnotation = PDAnnotation.createAnnotation(item); - if (annotationFilter.accept(createdAnnotation)) + try + { + PDAnnotation createdAnnotation = PDAnnotation.createAnnotation(item); + if (annotationFilter.accept(createdAnnotation)) + { + actuals.add(createdAnnotation); + } + } + catch (IOException ex) { - actuals.add(createdAnnotation); + Log.e("PdfBox-Android", ex.getMessage(), ex); } } return new COSArrayList(actuals, annots); } /** - * This will set the list of annotations. + * This will set the list of annotations. Although this is optional, you should take care that + * any newly created annotations link back to this page by calling + * {@link PDAnnotation#setPage(com.tom_roush.pdfbox.pdmodel.PDPage)}. Not doing it + * can cause trouble when PDFs get + * signed. * * @param annotations The new list of annotations. */ @@ -746,7 +756,7 @@ public List getViewports() return null; } COSArray array = (COSArray) base; - List viewports = new ArrayList(); + List viewports = new ArrayList(array.size()); for (int i = 0; i < array.size(); ++i) { COSBase base2 = array.getObject(i); diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDPageContentStream.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDPageContentStream.java index 4da9ee658..44ecbd5ab 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDPageContentStream.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDPageContentStream.java @@ -31,6 +31,7 @@ import com.tom_roush.pdfbox.contentstream.operator.OperatorName; import com.tom_roush.pdfbox.cos.COSArray; import com.tom_roush.pdfbox.cos.COSBase; +import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.cos.COSNumber; import com.tom_roush.pdfbox.pdfwriter.COSWriter; @@ -55,7 +56,6 @@ import com.tom_roush.pdfbox.util.Charsets; import com.tom_roush.pdfbox.util.Matrix; import com.tom_roush.pdfbox.util.NumberFormatUtil; - /** * Provides the ability to write to a page content stream. * @@ -93,6 +93,7 @@ public boolean isPrepend() } } + private final PDDocument document; private OutputStream output; private PDResources resources; @@ -139,7 +140,7 @@ public PDPageContentStream(PDDocument document, PDPage sourcePage) throws IOExce */ @Deprecated public PDPageContentStream(PDDocument document, PDPage sourcePage, boolean appendContent, - boolean compress) throws IOException + boolean compress) throws IOException { this(document, sourcePage, appendContent, compress, false); } @@ -157,7 +158,7 @@ public PDPageContentStream(PDDocument document, PDPage sourcePage, boolean appen * @throws IOException If there is an error writing to the page contents. */ public PDPageContentStream(PDDocument document, PDPage sourcePage, AppendMode appendContent, - boolean compress) throws IOException + boolean compress) throws IOException { this(document, sourcePage, appendContent, compress, false); } @@ -178,9 +179,9 @@ public PDPageContentStream(PDDocument document, PDPage sourcePage, AppendMode ap */ @Deprecated public PDPageContentStream(PDDocument document, PDPage sourcePage, boolean appendContent, - boolean compress, boolean resetContext) throws IOException + boolean compress, boolean resetContext) throws IOException { - this (document, sourcePage, appendContent ? AppendMode.APPEND : AppendMode.OVERWRITE, compress, resetContext); + this (document, sourcePage, appendContent ? AppendMode.APPEND : AppendMode.OVERWRITE, compress, resetContext); } /** @@ -197,7 +198,7 @@ public PDPageContentStream(PDDocument document, PDPage sourcePage, boolean appen * @throws IOException If there is an error writing to the page contents. */ public PDPageContentStream(PDDocument document, PDPage sourcePage, AppendMode appendContent, - boolean compress, boolean resetContext) throws IOException + boolean compress, boolean resetContext) throws IOException { this.document = document; COSName filter = compress ? COSName.FLATE_DECODE : null; @@ -299,7 +300,7 @@ public PDPageContentStream(PDDocument doc, PDAppearanceStream appearance) throws * @throws IOException If there is an error writing to the page contents. */ public PDPageContentStream(PDDocument doc, PDAppearanceStream appearance, OutputStream outputStream) - throws IOException + throws IOException { this.document = doc; @@ -319,7 +320,7 @@ public PDPageContentStream(PDDocument doc, PDAppearanceStream appearance, Output * @throws IOException If there is an error writing to the page contents. */ public PDPageContentStream(PDDocument doc, PDFormXObject form, OutputStream outputStream) - throws IOException + throws IOException { this.document = doc; @@ -339,7 +340,7 @@ public PDPageContentStream(PDDocument doc, PDFormXObject form, OutputStream outp * @throws IOException If there is an error writing to the page contents. */ public PDPageContentStream(PDDocument doc, PDTilingPattern pattern, OutputStream outputStream) - throws IOException + throws IOException { this.document = doc; @@ -830,20 +831,20 @@ public void drawImage(PDInlineImage inlineImage, float x, float y, float width, sb.append(inlineImage.getHeight()); sb.append("\n /CS "); - sb.append("/"); + sb.append('/'); sb.append(inlineImage.getColorSpace().getName()); COSArray decodeArray = inlineImage.getDecode(); if (decodeArray != null && decodeArray.size() > 0) { sb.append("\n /D "); - sb.append("["); + sb.append('['); for (COSBase base : decodeArray) { sb.append(((COSNumber) base).intValue()); - sb.append(" "); + sb.append(' '); } - sb.append("]"); + sb.append(']'); } if (inlineImage.isStencil()) @@ -1141,7 +1142,7 @@ public void setStrokingColor(PDColor color) throws IOException public void setStrokingColor(AWTColor color) throws IOException { float[] components = new float[] { - color.getRed() / 255f, color.getGreen() / 255f, color.getBlue() / 255f }; + color.getRed() / 255f, color.getGreen() / 255f, color.getBlue() / 255f }; PDColor pdColor = new PDColor(components, PDDeviceRGB.INSTANCE); setStrokingColor(pdColor); } @@ -1195,7 +1196,7 @@ public void setStrokingColor(float r, float g, float b) throws IOException if (isOutsideOneInterval(r) || isOutsideOneInterval(g) || isOutsideOneInterval(b)) { throw new IllegalArgumentException("Parameters must be within 0..1, but are " - + String.format("(%.2f,%.2f,%.2f)", r, g, b)); + + String.format("(%.2f,%.2f,%.2f)", r, g, b)); } writeOperand(r); writeOperand(g); @@ -1221,7 +1222,7 @@ public void setStrokingColor(int r, int g, int b) throws IOException if (isOutside255Interval(r) || isOutside255Interval(g) || isOutside255Interval(b)) { throw new IllegalArgumentException("Parameters must be within 0..255, but are " - + String.format("(%d,%d,%d)", r, g, b)); + + String.format("(%d,%d,%d)", r, g, b)); } setStrokingColor(r / 255f, g / 255f, b / 255f); } @@ -1243,7 +1244,7 @@ public void setStrokingColor(int c, int m, int y, int k) throws IOException if (isOutside255Interval(c) || isOutside255Interval(m) || isOutside255Interval(y) || isOutside255Interval(k)) { throw new IllegalArgumentException("Parameters must be within 0..255, but are " - + String.format("(%d,%d,%d,%d)", c, m, y, k)); + + String.format("(%d,%d,%d,%d)", c, m, y, k)); } setStrokingColor(c / 255f, m / 255f, y / 255f, k / 255f); } @@ -1263,7 +1264,7 @@ public void setStrokingColor(float c, float m, float y, float k) throws IOExcept if (isOutsideOneInterval(c) || isOutsideOneInterval(m) || isOutsideOneInterval(y) || isOutsideOneInterval(k)) { throw new IllegalArgumentException("Parameters must be within 0..1, but are " - + String.format("(%.2f,%.2f,%.2f,%.2f)", c, m, y, k)); + + String.format("(%.2f,%.2f,%.2f,%.2f)", c, m, y, k)); } writeOperand(c); writeOperand(m); @@ -1372,7 +1373,7 @@ public void setNonStrokingColor(PDColor color) throws IOException public void setNonStrokingColor(AWTColor color) throws IOException { float[] components = new float[] { - color.getRed() / 255f, color.getGreen() / 255f, color.getBlue() / 255f }; + color.getRed() / 255f, color.getGreen() / 255f, color.getBlue() / 255f }; PDColor pdColor = new PDColor(components, PDDeviceRGB.INSTANCE); setNonStrokingColor(pdColor); } @@ -1426,7 +1427,7 @@ public void setNonStrokingColor(float r, float g, float b) throws IOException if (isOutsideOneInterval(r) || isOutsideOneInterval(g) || isOutsideOneInterval(b)) { throw new IllegalArgumentException("Parameters must be within 0..1, but are " - + String.format("(%.2f,%.2f,%.2f)", r, g, b)); + + String.format("(%.2f,%.2f,%.2f)", r, g, b)); } writeOperand(r); writeOperand(g); @@ -1452,7 +1453,7 @@ public void setNonStrokingColor(int r, int g, int b) throws IOException if (isOutside255Interval(r) || isOutside255Interval(g) || isOutside255Interval(b)) { throw new IllegalArgumentException("Parameters must be within 0..255, but are " - + String.format("(%d,%d,%d)", r, g, b)); + + String.format("(%d,%d,%d)", r, g, b)); } setNonStrokingColor(r / 255f, g / 255f, b / 255f); } @@ -1472,7 +1473,7 @@ public void setNonStrokingColor(int c, int m, int y, int k) throws IOException if (isOutside255Interval(c) || isOutside255Interval(m) || isOutside255Interval(y) || isOutside255Interval(k)) { throw new IllegalArgumentException("Parameters must be within 0..255, but are " - + String.format("(%d,%d,%d,%d)", c, m, y, k)); + + String.format("(%d,%d,%d,%d)", c, m, y, k)); } setNonStrokingColor(c / 255f, m / 255f, y / 255f, k / 255f); } @@ -1508,7 +1509,7 @@ public void setNonStrokingColor(float c, float m, float y, float k) throws IOExc if (isOutsideOneInterval(c) || isOutsideOneInterval(m) || isOutsideOneInterval(y) || isOutsideOneInterval(k)) { throw new IllegalArgumentException("Parameters must be within 0..1, but are " - + String.format("(%.2f,%.2f,%.2f,%.2f)", c, m, y, k)); + + String.format("(%.2f,%.2f,%.2f,%.2f)", c, m, y, k)); } writeOperand(c); writeOperand(m); @@ -2291,6 +2292,24 @@ public void beginMarkedContentSequence(COSName tag, COSName propsName) throws IO writeOperator(OperatorName.BEGIN_MARKED_CONTENT_SEQ); } + /** + * Begin a marked content sequence with a reference to the marked content identifier (MCID). + * + * @param tag the tag to be added to the content stream + * @param mcid the marked content identifier (MCID) + * @throws IOException If the content stream could not be written + */ + public void beginMarkedContent(COSName tag, int mcid) throws IOException + { + if (mcid < 0) + { + throw new IllegalArgumentException("mcid should not be negative"); + } + writeOperand(tag); + write("<> "); + writeOperator(OperatorName.BEGIN_MARKED_CONTENT_SEQ); + } + /** * Begin a marked content sequence with a reference to an entry in the page resources' * Properties dictionary. @@ -2302,7 +2321,18 @@ public void beginMarkedContentSequence(COSName tag, COSName propsName) throws IO public void beginMarkedContent(COSName tag, PDPropertyList propertyList) throws IOException { writeOperand(tag); - writeOperand(resources.add(propertyList)); + + COSDictionary dict = propertyList.getCOSObject(); + if (dict.getInt(COSName.MCID) > -1 && dict.size() == 1) + { + // PDFBOX-5890: use simplified notation if there's only an MCID + write("<> "); + } + else + { + writeOperand(resources.add(propertyList)); + } + writeOperator(OperatorName.BEGIN_MARKED_CONTENT_SEQ); } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDPageTree.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDPageTree.java index 2a55192f7..ac04ed704 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDPageTree.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDPageTree.java @@ -20,6 +20,7 @@ import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -33,7 +34,6 @@ import com.tom_roush.pdfbox.cos.COSInteger; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.common.COSObjectable; - /** * The page tree, which defines the ordering of pages in the document in an efficient manner. * @@ -105,6 +105,17 @@ public PDPageTree(COSDictionary root) */ public static COSBase getInheritableAttribute(COSDictionary node, COSName key) { + return getInheritableAttribute(node, key, new HashSet()); + } + + private static COSBase getInheritableAttribute(COSDictionary node, COSName key, Set visited) + { + if (visited.contains(node)) + { + return null; + } + visited.add(node); + COSBase value = node.getDictionaryObject(key); if (value != null) { @@ -117,7 +128,7 @@ public static COSBase getInheritableAttribute(COSDictionary node, COSName key) COSDictionary parent = (COSDictionary) base; if (COSName.PAGES.equals(parent.getDictionaryObject(COSName.TYPE))) { - return getInheritableAttribute(parent, key); + return getInheritableAttribute(parent, key, visited); } } @@ -140,16 +151,17 @@ public Iterator iterator() */ private List getKids(COSDictionary node) { - List result = new ArrayList(); - COSArray kids = node.getCOSArray(COSName.KIDS); if (kids == null) { // probably a malformed PDF - return result; + return Collections.emptyList(); } - for (int i = 0, size = kids.size(); i < size; i++) + int size = kids.size(); + List result = new ArrayList(size); + + for (int i = 0; i < size; i++) { COSBase base = kids.getObject(i); if (base instanceof COSDictionary) @@ -158,8 +170,17 @@ private List getKids(COSDictionary node) } else { + if (base == null) + { + Log.w("PdfBox-Android", "replaced null entry with an empty page"); + COSDictionary emptyPage = new COSDictionary(); + emptyPage.setItem(COSName.TYPE, COSName.PAGE); + kids.set(i, emptyPage); + result.add(emptyPage); + } + else Log.w("PdfBox-Android", "COSDictionary expected, but got " + - (base == null ? "null" : base.getClass().getSimpleName())); + (base == null ? "null" : base.getClass().getSimpleName())); } } @@ -202,14 +223,14 @@ else if (kid.containsKey(COSName.KIDS)) } else { - if (COSName.PAGE.equals(node.getCOSName(COSName.TYPE))) + if (node != null && COSName.PAGE.equals(node.getCOSName(COSName.TYPE))) { queue.add(node); } else { Log.e("PdfBox-Android", "Page skipped due to an invalid or missing type " - + node.getCOSName(COSName.TYPE)); + + (node == null ? "(null)" : node.getCOSName(COSName.TYPE))); } } } @@ -295,7 +316,7 @@ private COSDictionary get(int pageNum, COSDictionary node, int encountered) { pageSet.clear(); throw new IllegalStateException( - "Possible recursion found when searching for page " + pageNum); + "Possible recursion found when searching for page " + pageNum); } else { @@ -366,7 +387,7 @@ private boolean isPageTreeNode(COSDictionary node ) // some files such as PDFBOX-2250-229205.pdf don't have Pages set as the Type, so we have // to check for the presence of Kids too return node != null && - (node.getCOSName(COSName.TYPE) == COSName.PAGES || node.containsKey(COSName.KIDS)); + (node.getCOSName(COSName.TYPE) == COSName.PAGES || node.containsKey(COSName.KIDS)); } /** @@ -466,7 +487,7 @@ private void remove(COSDictionary node) { // remove from parent's kids COSDictionary parent = (COSDictionary) node.getDictionaryObject(COSName.PARENT, COSName.P); - COSArray kids = (COSArray)parent.getDictionaryObject(COSName.KIDS); + COSArray kids = parent.getCOSArray(COSName.KIDS); if (kids.removeObject(node)) { // update ancestor counts @@ -496,7 +517,7 @@ public void add(PDPage page) // todo: re-balance tree? (or at least group new pages into tree nodes of e.g. 20) // add to parent's kids - COSArray kids = (COSArray)root.getDictionaryObject(COSName.KIDS); + COSArray kids = root.getCOSArray(COSName.KIDS); kids.add(node); // update ancestor counts @@ -522,8 +543,8 @@ public void add(PDPage page) public void insertBefore(PDPage newPage, PDPage nextPage) { COSDictionary nextPageDict = nextPage.getCOSObject(); - COSDictionary parentDict = (COSDictionary) nextPageDict.getDictionaryObject(COSName.PARENT); - COSArray kids = (COSArray) parentDict.getDictionaryObject(COSName.KIDS); + COSDictionary parentDict = nextPageDict.getCOSDictionary(COSName.PARENT); + COSArray kids = parentDict.getCOSArray(COSName.KIDS); boolean found = false; for (int i = 0; i < kids.size(); ++i) { @@ -554,8 +575,8 @@ public void insertBefore(PDPage newPage, PDPage nextPage) public void insertAfter(PDPage newPage, PDPage prevPage) { COSDictionary prevPageDict = prevPage.getCOSObject(); - COSDictionary parentDict = (COSDictionary) prevPageDict.getDictionaryObject(COSName.PARENT); - COSArray kids = (COSArray) parentDict.getDictionaryObject(COSName.KIDS); + COSDictionary parentDict = prevPageDict.getCOSDictionary(COSName.PARENT); + COSArray kids = parentDict.getCOSArray(COSName.KIDS); boolean found = false; for (int i = 0; i < kids.size(); ++i) { @@ -581,7 +602,7 @@ private void increaseParents(COSDictionary parentDict) { int cnt = parentDict.getInt(COSName.COUNT); parentDict.setInt(COSName.COUNT, cnt + 1); - parentDict = (COSDictionary) parentDict.getDictionaryObject(COSName.PARENT); + parentDict = parentDict.getCOSDictionary(COSName.PARENT); } while (parentDict != null); } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDResources.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDResources.java index 5e2c65955..8388766e6 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDResources.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDResources.java @@ -31,15 +31,14 @@ import com.tom_roush.pdfbox.pdmodel.documentinterchange.markedcontent.PDPropertyList; import com.tom_roush.pdfbox.pdmodel.font.PDFont; import com.tom_roush.pdfbox.pdmodel.font.PDFontFactory; +import com.tom_roush.pdfbox.pdmodel.graphics.PDXObject; +import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColorSpace; import com.tom_roush.pdfbox.pdmodel.graphics.form.PDFormXObject; +import com.tom_roush.pdfbox.pdmodel.graphics.image.PDImageXObject; import com.tom_roush.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentGroup; -import com.tom_roush.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState; -import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColorSpace; import com.tom_roush.pdfbox.pdmodel.graphics.pattern.PDAbstractPattern; import com.tom_roush.pdfbox.pdmodel.graphics.shading.PDShading; -import com.tom_roush.pdfbox.pdmodel.graphics.image.PDImageXObject; -import com.tom_roush.pdfbox.pdmodel.graphics.PDXObject; - +import com.tom_roush.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState; /** * A set of resources available at the page/pages/stream level. * @@ -53,8 +52,7 @@ public final class PDResources implements COSObjectable // PDFBOX-3442 cache fonts that are not indirect objects, as these aren't cached in ResourceCache // and this would result in huge memory footprint in text extraction - private final Map > directFontCache = - new HashMap>(); + private final Map> directFontCache; /** * Constructor for embedding. @@ -63,6 +61,7 @@ public PDResources() { resources = new COSDictionary(); cache = null; + directFontCache = new HashMap>(); } /** @@ -78,6 +77,7 @@ public PDResources(COSDictionary resourceDictionary) } resources = resourceDictionary; cache = null; + directFontCache = new HashMap>(); } /** @@ -94,6 +94,30 @@ public PDResources(COSDictionary resourceDictionary, ResourceCache resourceCache } resources = resourceDictionary; cache = resourceCache; + directFontCache = new HashMap>(); + } + + /** + * Constructor for reading. + * + * @param resourceDictionary The cos dictionary for this resource. + * @param resourceCache The document's resource cache, may be null. + * @param directFontCache The document's direct font cache. Must be mutable + */ + public PDResources(COSDictionary resourceDictionary, ResourceCache resourceCache, + Map> directFontCache) + { + if (resourceDictionary == null) + { + throw new IllegalArgumentException("resourceDictionary is null"); + } + if (directFontCache == null) + { + throw new IllegalArgumentException("directFontCache is null"); + } + resources = resourceDictionary; + cache = resourceCache; + this.directFontCache = directFontCache; } /** @@ -246,7 +270,7 @@ public PDExtendedGraphicsState getExtGState(COSName name) COSBase base = get(COSName.EXT_G_STATE, name); if (base instanceof COSDictionary) { - extGState = new PDExtendedGraphicsState((COSDictionary) base); + extGState = new PDExtendedGraphicsState((COSDictionary) base, getResourceCache()); } if (cache != null && indirect != null) @@ -706,7 +730,7 @@ private COSName add(COSName kind, String prefix, COSObjectable object) return dict.getKeyForValue(object.getCOSObject()); } - // PDFBOX-4509: It could exist as an indirect object, happens when a font is taken from the + // PDFBOX-4509: It could exist as an indirect object, happens when a font is taken from the // AcroForm default resources of a loaded PDF. if (dict != null && COSName.FONT.equals(kind)) { diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDStructureElementNameTreeNode.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDStructureElementNameTreeNode.java index 875b7c026..e31387240 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDStructureElementNameTreeNode.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/PDStructureElementNameTreeNode.java @@ -18,11 +18,11 @@ package com.tom_roush.pdfbox.pdmodel; import java.io.IOException; + import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.pdmodel.common.PDNameTreeNode; import com.tom_roush.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureElement; - /** * todo: JavaDoc * @@ -51,6 +51,10 @@ public PDStructureElementNameTreeNode( COSDictionary dic ) @Override protected PDStructureElement convertCOSToPD( COSBase base ) throws IOException { + if (base != null && !(base instanceof COSDictionary)) + { + throw new IOException("dictionary expected here, but got " + base); + } return new PDStructureElement((COSDictionary)base); } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/COSArrayList.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/COSArrayList.java index 8a6fbfb3d..9b261b547 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/COSArrayList.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/COSArrayList.java @@ -32,7 +32,6 @@ import com.tom_roush.pdfbox.cos.COSNumber; import com.tom_roush.pdfbox.cos.COSObject; import com.tom_roush.pdfbox.cos.COSString; - /** * This is an implementation of a List that will sync its contents to a COSArray. * @@ -44,7 +43,7 @@ public class COSArrayList implements List private final List actual; // indicates that the list has been filtered - // i.e. the number of entries in array and actual differ + // i.e. the number of entries in array and actual differ private boolean isFiltered = false; private COSDictionary parentDict; @@ -79,7 +78,7 @@ public COSArrayList( List actualList, COSArray cosArray ) array = cosArray; // if the number of entries differs this may come from a filter being - // applied at the PDModel level + // applied at the PDModel level if (actual.size() != array.size()) { isFiltered = true; } @@ -120,7 +119,7 @@ public COSArrayList( E actualObject, COSBase item, COSDictionary dictionary, COS { array = new COSArray(); array.add( item ); - actual = new ArrayList(); + actual = new ArrayList(1); actual.add( actualObject ); parentDict = dictionary; @@ -487,7 +486,7 @@ else if( next == null ) else { throw new IllegalArgumentException( "Error: Don't know how to convert type to COSBase '" + - next.getClass().getName() + "'" ); + next.getClass().getName() + "'" ); } } } @@ -623,11 +622,12 @@ public E set(int index, E element) } else { + COSBase cosObject = ((COSObjectable) element).getCOSObject(); if( parentDict != null && index == 0 ) { - parentDict.setItem( dictKey, ((COSObjectable)element).getCOSObject() ); + parentDict.setItem(dictKey, cosObject); } - array.set( index, ((COSObjectable)element).getCOSObject() ); + array.set(index, cosObject); } return actual.set( index, element ); } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/PDMetadata.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/PDMetadata.java index 4ef1ea563..9c2505a58 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/PDMetadata.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/PDMetadata.java @@ -19,13 +19,10 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import com.tom_roush.pdfbox.cos.COSName; - +import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.cos.COSStream; - import com.tom_roush.pdfbox.pdmodel.PDDocument; - /** * This class represents metadata for various objects in a PDF document. * @@ -35,7 +32,8 @@ public class PDMetadata extends PDStream { /** - * This will create a new PDMetadata object. + * This will create a new PDMetadata object with the /Type and /Subtype entries for a document + * level metadata object. * * @param document The document that the stream will be part of. */ @@ -47,8 +45,9 @@ public PDMetadata( PDDocument document ) } /** - * Constructor. Reads all data from the input stream and embeds it into the - * document, this will close the InputStream. + * Constructor. Reads all data from the input stream and embeds it into the document, this will + * close the InputStream. The /Type and /Subtype entries for a document level metadata object + * will be created. * * @param doc The document that will hold the stream. * @param str The stream parameter. @@ -62,7 +61,7 @@ public PDMetadata( PDDocument doc, InputStream str) throws IOException } /** - * Constructor. + * Constructor. This will not set up the /Type and /Subtype entries. * * @param str The stream parameter. */ diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/PDNameTreeNode.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/PDNameTreeNode.java index 66b4eba9c..294c983e8 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/PDNameTreeNode.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/PDNameTreeNode.java @@ -31,7 +31,6 @@ import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.cos.COSString; - /** * This class represents a node in a name tree. * @@ -41,6 +40,7 @@ */ public abstract class PDNameTreeNode implements COSObjectable { + private final COSDictionary node; private PDNameTreeNode parent; @@ -118,7 +118,18 @@ public List> getKids() List> pdObjects = new ArrayList>(kids.size()); for( int i=0; i childNode; + if (base instanceof COSDictionary) + { + childNode = createChildNode((COSDictionary) base); + } + else + { + Log.w("PdfBox-Android", "Bad child node at position " + i); + childNode = createChildNode(new COSDictionary()); + } + pdObjects.add(childNode); } retval = new COSArrayList>(pdObjects, kids); } @@ -152,7 +163,7 @@ public void setKids( List> kids ) { // remove kids node.setItem(COSName.KIDS, null); - // remove Limits + // remove Limits node.setItem(COSName.LIMITS, null); } calculateLimits(); @@ -256,12 +267,13 @@ public Map getNames() throws IOException COSArray namesArray = node.getCOSArray(COSName.NAMES); if( namesArray != null ) { - Map names = new LinkedHashMap(); + int size = namesArray.size(); + Map names = new LinkedHashMap(size); if (namesArray.size() % 2 != 0) { - Log.w("PdfBox-Android", "Names array has odd size: " + namesArray.size()); + Log.w("PdfBox-Android", "Names array has odd size: " + size); } - for (int i = 0; i + 1 < namesArray.size(); i += 2) + for (int i = 0; i + 1 < size; i += 2) { COSBase base = namesArray.getObject(i); if (!(base instanceof COSString)) diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/PDNumberTreeNode.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/PDNumberTreeNode.java index 7e2da80fb..edff31a52 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/PDNumberTreeNode.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/PDNumberTreeNode.java @@ -31,7 +31,6 @@ import com.tom_roush.pdfbox.cos.COSInteger; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.cos.COSNull; - /** * This class represents a PDF Number tree. See the PDF Reference 1.7 section * 7.9.7 for more details. @@ -41,6 +40,7 @@ */ public class PDNumberTreeNode implements COSObjectable { + private final COSDictionary node; private Class valueType = null; @@ -86,13 +86,24 @@ public COSDictionary getCOSObject() public List getKids() { List retval = null; - COSArray kids = (COSArray)node.getDictionaryObject( COSName.KIDS ); + COSArray kids = node.getCOSArray(COSName.KIDS ); if( kids != null ) { - List pdObjects = new ArrayList(); + List pdObjects = new ArrayList(kids.size()); for( int i=0; i(pdObjects,kids); } @@ -164,8 +175,8 @@ public Object getValue(Integer index) throws IOException } /** - * This will return a map of numbers. The key will be a java.lang.Integer, the value will - * depend on where this class is being used. + * This will return a map of numbers on this level. The key will be a java.lang.Integer, the + * value will depend on where this class is being used. * * @return A map of COS objects. * @@ -178,12 +189,13 @@ public Map getNumbers() throws IOException if (numBase instanceof COSArray) { COSArray numbersArray = (COSArray) numBase; + int size = numbersArray.size(); indices = new HashMap(); - if (numbersArray.size() % 2 != 0) + if (size % 2 != 0) { - Log.w("PdfBox-Android", "Numbers array has odd size: " + numbersArray.size()); + Log.w("PdfBox-Android", "Numbers array has odd size: " + size); } - for (int i = 0; i + 1 < numbersArray.size(); i += 2) + for (int i = 0; i + 1 < size; i += 2) { COSBase base = numbersArray.getObject(i); if (!(base instanceof COSInteger)) @@ -273,12 +285,12 @@ public void setNumbers( Map numbers ) /** * Get the highest value for a key in the number map. * - * @return The highest value for a key in the map. + * @return The highest value for a key in the map or null if missing. */ public Integer getUpperLimit() { Integer retval = null; - COSArray arr = (COSArray)node.getDictionaryObject( COSName.LIMITS ); + COSArray arr = node.getCOSArray(COSName.LIMITS); if( arr != null && arr.get(1) != null ) { retval = arr.getInt( 1 ); @@ -293,7 +305,7 @@ public Integer getUpperLimit() */ private void setUpperLimit( Integer upper ) { - COSArray arr = (COSArray)node.getDictionaryObject( COSName.LIMITS ); + COSArray arr = node.getCOSArray(COSName.LIMITS); if( arr == null ) { arr = new COSArray(); @@ -314,12 +326,12 @@ private void setUpperLimit( Integer upper ) /** * Get the lowest value for a key in the number map. * - * @return The lowest value for a key in the map. + * @return The lowest value for a key in the map or null if missing. */ public Integer getLowerLimit() { Integer retval = null; - COSArray arr = (COSArray)node.getDictionaryObject( COSName.LIMITS ); + COSArray arr = node.getCOSArray(COSName.LIMITS); if( arr != null && arr.get(0) != null ) { retval = arr.getInt( 0 ); @@ -334,7 +346,7 @@ public Integer getLowerLimit() */ private void setLowerLimit( Integer lower ) { - COSArray arr = (COSArray)node.getDictionaryObject( COSName.LIMITS ); + COSArray arr = node.getCOSArray(COSName.LIMITS); if( arr == null ) { arr = new COSArray(); diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/PDObjectStream.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/PDObjectStream.java index 6ab034e9a..f94154d6c 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/PDObjectStream.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/PDObjectStream.java @@ -18,11 +18,7 @@ import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.cos.COSStream; - import com.tom_roush.pdfbox.pdmodel.PDDocument; - - - /** * A PDStream represents a stream in a PDF document. Streams are tied to a single * PDF document. @@ -115,7 +111,7 @@ public void setFirstByteOffset( int n ) public PDObjectStream getExtends() { PDObjectStream retval = null; - COSStream stream = (COSStream)getCOSObject().getDictionaryObject( COSName.EXTENDS ); + COSStream stream = getCOSObject().getCOSStream(COSName.EXTENDS); if( stream != null ) { retval = new PDObjectStream( stream ); diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/PDPageLabels.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/PDPageLabels.java index d9c972aae..17ccce3d5 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/PDPageLabels.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/PDPageLabels.java @@ -20,8 +20,8 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.Map.Entry; +import java.util.Map; import java.util.NavigableSet; import java.util.NoSuchElementException; import java.util.TreeMap; @@ -33,7 +33,6 @@ import com.tom_roush.pdfbox.cos.COSInteger; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.PDDocument; - /** * Represents the page label dictionary of a document. * @@ -100,7 +99,7 @@ public PDPageLabels(PDDocument document, COSDictionary dict) throws IOException private void findLabels(PDNumberTreeNode node) throws IOException { List kids = node.getKids(); - if (node.getKids() != null) + if (kids != null) { for (PDNumberTreeNode kid : kids) { @@ -208,7 +207,7 @@ public COSBase getCOSObject() public Map getPageIndicesByLabels() { int numberOfPages = doc.getNumberOfPages(); - final Map labelMap = new HashMap(numberOfPages); + final Map labelMap = new HashMap(); computeLabels(new LabelHandler() { @Override @@ -280,7 +279,7 @@ private void computeLabels(LabelHandler handler, int numberOfPages) Entry entry = iterator.next(); int numPages = entry.getKey() - lastEntry.getKey(); LabelGenerator gen = new LabelGenerator(lastEntry.getValue(), - numPages); + numPages); while (gen.hasNext()) { handler.newLabel(pageIndex, gen.next()); @@ -289,7 +288,7 @@ private void computeLabels(LabelHandler handler, int numberOfPages) lastEntry = entry; } LabelGenerator gen = new LabelGenerator(lastEntry.getValue(), - numberOfPages - lastEntry.getKey()); + numberOfPages - lastEntry.getKey()); while (gen.hasNext()) { handler.newLabel(pageIndex, gen.next()); diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/PDRectangle.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/PDRectangle.java index 0096325f5..ba92669c3 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/PDRectangle.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/PDRectangle.java @@ -18,6 +18,7 @@ import android.graphics.Path; import android.graphics.PointF; +import android.graphics.RectF; import java.util.Arrays; @@ -27,7 +28,6 @@ import com.tom_roush.pdfbox.cos.COSFloat; import com.tom_roush.pdfbox.cos.COSNumber; import com.tom_roush.pdfbox.util.Matrix; - /** * A rectangle in a PDF document. * @@ -43,10 +43,13 @@ public class PDRectangle implements COSObjectable /** A rectangle the size of U.S. Letter, 8.5" x 11". */ public static final PDRectangle LETTER = new PDRectangle(8.5f * POINTS_PER_INCH, - 11f *POINTS_PER_INCH); + 11f *POINTS_PER_INCH); + /** A rectangle the size of U.S. Tabloid, 11" x 17". */ + public static final PDRectangle TABLOID = new PDRectangle(11f * POINTS_PER_INCH, + 17f * POINTS_PER_INCH); /** A rectangle the size of U.S. Legal, 8.5" x 14". */ public static final PDRectangle LEGAL = new PDRectangle(8.5f * POINTS_PER_INCH, - 14f * POINTS_PER_INCH); + 14f * POINTS_PER_INCH); /** A rectangle the size of A0 Paper. */ public static final PDRectangle A0 = new PDRectangle(841 * POINTS_PER_MM, 1189 * POINTS_PER_MM); @@ -130,6 +133,14 @@ public PDRectangle( BoundingBox box ) public PDRectangle( COSArray array ) { float[] values = Arrays.copyOf(array.toFloatArray(), 4); + // replace huge values, most likely those are invalid due to a malformed pdf + for (int i = 0; i < values.length; i++) + { + if (Math.abs(values[i]) > Integer.MAX_VALUE) + { + values[i] = values[i] > 0 ? Integer.MAX_VALUE : -Integer.MAX_VALUE; + } + } rectArray = new COSArray(); // we have to start with the lower left corner rectArray.add( new COSFloat( Math.min(values[0],values[2] )) ); @@ -151,7 +162,7 @@ public boolean contains( float x, float y ) float lly = getLowerLeftY(); float ury = getUpperRightY(); return x >= llx && x <= urx && - y >= lly && y <= ury; + y >= lly && y <= ury; } /** @@ -353,6 +364,6 @@ public Path toGeneralPath() public String toString() { return "[" + getLowerLeftX() + "," + getLowerLeftY() + "," + - getUpperRightX() + "," + getUpperRightY() +"]"; + getUpperRightX() + "," + getUpperRightY() +"]"; } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/PDStream.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/PDStream.java index e1f3cca4b..ce81b8828 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/PDStream.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/PDStream.java @@ -24,6 +24,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -41,7 +42,6 @@ import com.tom_roush.pdfbox.io.IOUtils; import com.tom_roush.pdfbox.pdmodel.PDDocument; import com.tom_roush.pdfbox.pdmodel.common.filespecification.PDFileSpecification; - /** * A PDStream represents a stream in a PDF document. Streams are tied to a single PDF document. * @@ -49,6 +49,7 @@ */ public class PDStream implements COSObjectable { + private final COSStream stream; /** @@ -252,6 +253,10 @@ public COSInputStream createInputStream(DecodeOptions options) throws IOExceptio */ public InputStream createInputStream(List stopFilters) throws IOException { + if (stopFilters == null) + { + stopFilters = Collections.emptyList(); + } InputStream is = stream.createRawInputStream(); ByteArrayOutputStream os = new ByteArrayOutputStream(); List filters = getFilters(); @@ -260,7 +265,7 @@ public InputStream createInputStream(List stopFilters) throws IOExceptio for (int i = 0; i < filters.size(); i++) { COSName nextFilter = filters.get(i); - if ((stopFilters != null) && stopFilters.contains(nextFilter.getName())) + if (stopFilters.contains(nextFilter.getName())) { break; } @@ -404,7 +409,7 @@ private List internalGetDecodeParams(COSName name1, COSName name2) throw public void setDecodeParms(List decodeParams) { stream.setItem(COSName.DECODE_PARMS, - COSArrayList.converterToCOSArray(decodeParams)); + COSArrayList.converterToCOSArray(decodeParams)); } /** @@ -444,12 +449,12 @@ public List getFileFilters() { COSName name = (COSName) filters; retval = new COSArrayList(name.getName(), name, stream, - COSName.F_FILTER); + COSName.F_FILTER); } else if (filters instanceof COSArray) { retval = COSArrayList - .convertCOSNameCOSArrayToList((COSArray) filters); + .convertCOSNameCOSArrayToList((COSArray) filters); } return retval; } @@ -521,7 +526,7 @@ else if (mdStream instanceof COSNull) else if (mdStream != null) { throw new IllegalStateException("Expected a COSStream but was a " - + mdStream.getClass().getSimpleName()); + + mdStream.getClass().getSimpleName()); } return retval; } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/filespecification/PDComplexFileSpecification.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/filespecification/PDComplexFileSpecification.java index 95361e32b..5ca189688 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/filespecification/PDComplexFileSpecification.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/filespecification/PDComplexFileSpecification.java @@ -20,7 +20,6 @@ import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.cos.COSStream; - /** * This represents a file specification. * @@ -90,11 +89,14 @@ private COSBase getObjectFromEFDictionary(COSName key) } /** - *

      Preferred method for getting the filename. - * It will determinate the recommended file name.

      - *

      First of all we try to get the unicode filename if it exist. - * If it doesn't exist we take a look at the DOS, MAC UNIX filenames. - * If no one exist the required F entry will be returned.

      + *

      + * Preferred method for getting the filename. It will determinate the recommended file name. + *

      + * First of all we try to get the unicode filename if it exists. If it doesn't exist we take a + * look at the DOS, MAC UNIX filenames. If no one exist the required F entry will be returned. + *

      + * Be aware that the filename may contain a directory separator and needs to be sanitized + * (CWE-22). * * @return The preferred file name. */ @@ -122,6 +124,8 @@ public String getFilename() /** * This will get the unicode file name. + * Be aware that the filename may contain a directory separator and needs to be sanitized + * (CWE-22). * * @return The file name. */ @@ -144,6 +148,8 @@ public void setFileUnicode( String file ) /** * This will get the file name. + * Be aware that the filename may contain a directory separator and needs to be sanitized + * (CWE-22). * * @return The file name. */ @@ -168,6 +174,8 @@ public void setFile( String file ) /** * This will get the name representing a Dos file. + * Be aware that the filename may contain a directory separator and needs to be sanitized + * (CWE-22). * * @return The file name. */ @@ -190,6 +198,8 @@ public void setFileDos( String file ) /** * This will get the name representing a Mac file. + * Be aware that the filename may contain a directory separator and needs to be sanitized + * (CWE-22). * * @return The file name. */ @@ -212,6 +222,8 @@ public void setFileMac( String file ) /** * This will get the name representing a Unix file. + * Be aware that the filename may contain a directory separator and needs to be sanitized + * (CWE-22). * * @return The file name. */ diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/filespecification/PDEmbeddedFile.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/filespecification/PDEmbeddedFile.java index 1e7c0a4cc..496797e6a 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/filespecification/PDEmbeddedFile.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/filespecification/PDEmbeddedFile.java @@ -25,7 +25,6 @@ import com.tom_roush.pdfbox.cos.COSStream; import com.tom_roush.pdfbox.pdmodel.PDDocument; import com.tom_roush.pdfbox.pdmodel.common.PDStream; - /** * This represents an embedded file in a file specification. * @@ -182,7 +181,7 @@ public void setCheckSum( String checksum ) public String getMacSubtype() { String retval = null; - COSDictionary params = (COSDictionary)getCOSObject().getDictionaryObject( COSName.PARAMS ); + COSDictionary params = getCOSObject().getCOSDictionary(COSName.PARAMS); if( params != null ) { retval = params.getEmbeddedString( "Mac", "Subtype" ); @@ -197,7 +196,7 @@ public String getMacSubtype() */ public void setMacSubtype( String macSubtype ) { - COSDictionary params = (COSDictionary)getCOSObject().getDictionaryObject( COSName.PARAMS ); + COSDictionary params = getCOSObject().getCOSDictionary(COSName.PARAMS); if( params == null && macSubtype != null ) { params = new COSDictionary(); @@ -217,7 +216,7 @@ public void setMacSubtype( String macSubtype ) public String getMacCreator() { String retval = null; - COSDictionary params = (COSDictionary)getCOSObject().getDictionaryObject( COSName.PARAMS ); + COSDictionary params = getCOSObject().getCOSDictionary(COSName.PARAMS); if( params != null ) { retval = params.getEmbeddedString( "Mac", "Creator" ); @@ -232,7 +231,7 @@ public String getMacCreator() */ public void setMacCreator( String macCreator ) { - COSDictionary params = (COSDictionary)getCOSObject().getDictionaryObject( COSName.PARAMS ); + COSDictionary params = getCOSObject().getCOSDictionary(COSName.PARAMS); if( params == null && macCreator != null ) { params = new COSDictionary(); @@ -252,7 +251,7 @@ public void setMacCreator( String macCreator ) public String getMacResFork() { String retval = null; - COSDictionary params = (COSDictionary)getCOSObject().getDictionaryObject( COSName.PARAMS ); + COSDictionary params = getCOSObject().getCOSDictionary(COSName.PARAMS); if( params != null ) { retval = params.getEmbeddedString( "Mac", "ResFork" ); @@ -267,7 +266,7 @@ public String getMacResFork() */ public void setMacResFork( String macResFork ) { - COSDictionary params = (COSDictionary)getCOSObject().getDictionaryObject( COSName.PARAMS ); + COSDictionary params = getCOSObject().getCOSDictionary(COSName.PARAMS); if( params == null && macResFork != null ) { params = new COSDictionary(); @@ -280,5 +279,4 @@ public void setMacResFork( String macResFork ) } - } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/function/PDFunction.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/function/PDFunction.java index c5c80a7ec..5cc00fda8 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/function/PDFunction.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/function/PDFunction.java @@ -27,7 +27,6 @@ import com.tom_roush.pdfbox.pdmodel.common.COSObjectable; import com.tom_roush.pdfbox.pdmodel.common.PDRange; import com.tom_roush.pdfbox.pdmodel.common.PDStream; - /** * This class represents a function in a PDF document. * @@ -127,7 +126,7 @@ public static PDFunction create( COSBase function ) throws IOException if (!(base instanceof COSDictionary)) { throw new IOException("Error: Function must be a Dictionary, but is " + - (base == null ? "(null)" : base.getClass().getSimpleName())); + (base == null ? "(null)" : base.getClass().getSimpleName())); } COSDictionary functionDictionary = (COSDictionary) base; int functionType = functionDictionary.getInt(COSName.FUNCTION_TYPE); @@ -263,26 +262,26 @@ public COSArray eval(COSArray input) throws IOException * Evaluates the function at the given input. * ReturnValue = f(input) * - * @param input The array of input values for the function. + * @param input The array of input values for the function. * In many cases will be an array of a single value, but not always. * - * @return The of outputs the function returns based on those inputs. + * @return The of outputs the function returns based on those inputs. * In many cases will be an array of a single value, but not always. * - * @throws IOException if something went wrong processing the function. + * @throws IOException if something went wrong processing the function. */ public abstract float[] eval(float[] input) throws IOException; /** * Returns all ranges for the output values as COSArray . * Required for type 0 and type 4 functions - * @return the ranges array. + * @return the ranges array. */ protected COSArray getRangeValues() { if (range == null) { - range = (COSArray) getCOSObject().getDictionaryObject(COSName.RANGE); + range = getCOSObject().getCOSArray(COSName.RANGE); } return range; } @@ -290,13 +289,13 @@ protected COSArray getRangeValues() /** * Returns all domains for the input values as COSArray. * Required for all function types. - * @return the domains array. + * @return the domains array. */ private COSArray getDomainValues() { if (domain == null) { - domain = (COSArray) getCOSObject().getDictionaryObject(COSName.DOMAIN); + domain = getCOSObject().getCOSArray(COSName.DOMAIN); } return domain; } @@ -352,8 +351,8 @@ else if (x > rangeMax) } /** - * For a given value of x, interpolate calculates the y value - * on the line defined by the two points (xRangeMin , xRangeMax ) + * For a given value of x, interpolate calculates the y value + * on the line defined by the two points (xRangeMin , xRangeMax ) * and (yRangeMin , yRangeMax ). * * @param x the to be interpolated value. @@ -365,6 +364,11 @@ else if (x > rangeMax) */ protected float interpolate(float x, float xRangeMin, float xRangeMax, float yRangeMin, float yRangeMax) { + if (xRangeMax == xRangeMin) + { + // PDFBOX-5593 / PR #162 + return yRangeMin; + } return yRangeMin + ((x - xRangeMin) * (yRangeMax - yRangeMin)/(xRangeMax - xRangeMin)); } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/function/PDFunctionType0.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/function/PDFunctionType0.java index 622c0f345..ddcadc902 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/function/PDFunctionType0.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/function/PDFunctionType0.java @@ -28,7 +28,6 @@ import com.tom_roush.pdfbox.cos.COSInteger; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.common.PDRange; - /** * This class represents a type 0 function in a PDF document. * @@ -40,19 +39,19 @@ public class PDFunctionType0 extends PDFunction { /** - * An array of 2 x m numbers specifying the linear mapping of input values + * An array of 2 x m numbers specifying the linear mapping of input values * into the domain of the function's sample table. Default value: [ 0 (Size0 * - 1) 0 (Size1 - 1) ...]. */ private COSArray encode = null; /** - * An array of 2 x n numbers specifying the linear mapping of sample values + * An array of 2 x n numbers specifying the linear mapping of sample values * into the range appropriate for the function's output values. Default * value: same as the value of Range */ private COSArray decode = null; /** - * An array of m positive integers specifying the number of samples in each + * An array of m positive integers specifying the number of samples in each * input dimension of the sample table. */ private COSArray size = null; @@ -90,13 +89,13 @@ public COSArray getSize() { if (size == null) { - size = (COSArray) getCOSObject().getDictionaryObject(COSName.SIZE); + size = getCOSObject().getCOSArray(COSName.SIZE); } return size; } /** - * Get the number of bits that the output value will take up. + * Get the number of bits that the output value will take up. * * Valid values are 1,2,4,8,12,16,24,32. * @@ -133,13 +132,13 @@ public void setBitsPerSample(int bps) /** * Returns all encode values as COSArray. * - * @return the encode array. + * @return the encode array. */ private COSArray getEncodeValues() { if (encode == null) { - encode = (COSArray) getCOSObject().getDictionaryObject(COSName.ENCODE); + encode = getCOSObject().getCOSArray(COSName.ENCODE); // the default value is [0 (size[0]-1) 0 (size[1]-1) ...] if (encode == null) { @@ -159,13 +158,13 @@ private COSArray getEncodeValues() /** * Returns all decode values as COSArray. * - * @return the decode array. + * @return the decode array. */ private COSArray getDecodeValues() { if (decode == null) { - decode = (COSArray) getCOSObject().getDictionaryObject(COSName.DECODE); + decode = getCOSObject().getCOSArray(COSName.DECODE); // if decode is null, the default values are the range values if (decode == null) { @@ -388,7 +387,7 @@ private int[][] getSamples() try { // PDF spec 1.7 p.171: - // Each sample value is represented as a sequence of BitsPerSample bits. + // Each sample value is represented as a sequence of BitsPerSample bits. // Successive values are adjacent in the bit stream; there is no padding at byte boundaries. InputStream inputStream = getPDStream().createInputStream(); ImageInputStream mciis = new MemoryCacheImageInputStream(inputStream); @@ -414,8 +413,8 @@ private int[][] getSamples() } /** - * {@inheritDoc} - */ + * {@inheritDoc} + */ @Override public float[] eval(float[] input) throws IOException { @@ -436,9 +435,10 @@ public float[] eval(float[] input) throws IOException { PDRange domain = getDomainForInput(i); PDRange encodeValues = getEncodeForParameter(i); - input[i] = clipToRange(input[i], domain.getMin(), domain.getMax()); - input[i] = interpolate(input[i], domain.getMin(), domain.getMax(), - encodeValues.getMin(), encodeValues.getMax()); + float min = domain.getMin(); + float max = domain.getMax(); + input[i] = clipToRange(input[i], min, max); + input[i] = interpolate(input[i], min, max, encodeValues.getMin(), encodeValues.getMax()); input[i] = clipToRange(input[i], 0, sizeValues[i] - 1); inputPrev[i] = (int) Math.floor(input[i]); inputNext[i] = (int) Math.ceil(input[i]); diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/function/PDFunctionType2.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/function/PDFunctionType2.java index 4207a0eb3..4b91c7c50 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/function/PDFunctionType2.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/function/PDFunctionType2.java @@ -20,10 +20,10 @@ import com.tom_roush.pdfbox.cos.COSArray; import com.tom_roush.pdfbox.cos.COSBase; +import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSFloat; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.cos.COSNumber; - /** * This class represents a Type 2 (exponential interpolation) function in a PDF * document. @@ -55,7 +55,8 @@ public PDFunctionType2(COSBase function) { super(function); - COSArray cosArray0 = getCOSObject().getCOSArray(COSName.C0); + COSDictionary cosObject = getCOSObject(); + COSArray cosArray0 = cosObject.getCOSArray(COSName.C0); if (cosArray0 != null) { c0 = cosArray0; @@ -69,7 +70,7 @@ public PDFunctionType2(COSBase function) c0.add(new COSFloat(0)); } - COSArray cosArray1 = getCOSObject().getCOSArray(COSName.C1); + COSArray cosArray1 = cosObject.getCOSArray(COSName.C1); if (cosArray1 != null) { c1 = cosArray1; @@ -154,8 +155,8 @@ public float getN() public String toString() { return "FunctionType2{" - + "C0: " + getC0() + " " - + "C1: " + getC1() + " " - + "N: " + getN() + "}"; + + "C0: " + getC0() + " " + + "C1: " + getC1() + " " + + "N: " + getN() + "}"; } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/function/PDFunctionType3.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/function/PDFunctionType3.java index a98eea6dc..011973ff0 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/function/PDFunctionType3.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/function/PDFunctionType3.java @@ -22,7 +22,6 @@ import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.common.PDRange; - /** * This class represents a Type 3 (stitching) function in a PDF document. * @@ -56,8 +55,8 @@ public int getFunctionType() } /** - * {@inheritDoc} - */ + * {@inheritDoc} + */ @Override public float[] eval(float[] input) throws IOException { @@ -101,11 +100,11 @@ public float[] eval(float[] input) throws IOException partitionValues[0] = domain.getMin(); partitionValues[partitionValuesSize-1] = domain.getMax(); System.arraycopy(boundsValues, 0, partitionValues, 1, boundsSize); - // find the partition + // find the partition for (int i=0; i < partitionValuesSize-1; i++) { if ( x >= partitionValues[i] && - (x < partitionValues[i+1] || (i == partitionValuesSize - 2 && x == partitionValues[i+1]))) + (x < partitionValues[i+1] || (i == partitionValuesSize - 2 && x == partitionValues[i+1]))) { function = functionsArray[i]; PDRange encRange = getEncodeForParameter(i); @@ -128,13 +127,13 @@ public float[] eval(float[] input) throws IOException /** * Returns all functions values as COSArray. * - * @return the functions array. + * @return the functions array. */ public COSArray getFunctions() { if (functions == null) { - functions = (COSArray)(getCOSObject().getDictionaryObject( COSName.FUNCTIONS )); + functions = getCOSObject().getCOSArray(COSName.FUNCTIONS); } return functions; } @@ -142,13 +141,13 @@ public COSArray getFunctions() /** * Returns all bounds values as COSArray. * - * @return the bounds array. + * @return the bounds array. */ public COSArray getBounds() { if (bounds == null) { - bounds = (COSArray)(getCOSObject().getDictionaryObject( COSName.BOUNDS )); + bounds = getCOSObject().getCOSArray(COSName.BOUNDS); } return bounds; } @@ -156,13 +155,13 @@ public COSArray getBounds() /** * Returns all encode values as COSArray. * - * @return the encode array. + * @return the encode array. */ public COSArray getEncode() { if (encode == null) { - encode = (COSArray)(getCOSObject().getDictionaryObject( COSName.ENCODE )); + encode = getCOSObject().getCOSArray(COSName.ENCODE); } return encode; } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/function/type4/InstructionSequenceBuilder.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/function/type4/InstructionSequenceBuilder.java index 56ac8482a..f8da5b2ce 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/function/type4/InstructionSequenceBuilder.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/function/type4/InstructionSequenceBuilder.java @@ -19,7 +19,6 @@ import java.util.Stack; import java.util.regex.Matcher; import java.util.regex.Pattern; - /** * Basic parser for Type 4 functions which is used to build up instruction sequences. * @@ -27,7 +26,7 @@ public final class InstructionSequenceBuilder extends Parser.AbstractSyntaxHandler { private static final Pattern INTEGER_PATTERN = Pattern.compile("[\\+\\-]?\\d+"); - private static final Pattern REAL_PATTERN = Pattern.compile("[\\-]?\\d*\\.\\d*([Ee]\\-?\\d+)?"); + private static final Pattern REAL_PATTERN = Pattern.compile("\\-?\\d*\\.\\d*([Ee]\\-?\\d+)?"); private final InstructionSequence mainSequence = new InstructionSequence(); private final Stack seqStack = new Stack(); diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/function/type4/Operators.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/function/type4/Operators.java index 96ca30f7e..ace5677f5 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/function/type4/Operators.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/common/function/type4/Operators.java @@ -17,7 +17,6 @@ package com.tom_roush.pdfbox.pdmodel.common.function.type4; import java.util.Map; - /** * This class provides all the supported operators. */ @@ -74,7 +73,7 @@ public class Operators private static final Operator POP = new StackOperators.Pop(); private static final Operator ROLL = new StackOperators.Roll(); - private final Map operators = new java.util.HashMap(); + private final Map operators = new java.util.HashMap(42); /** * Creates a new Operators object with the default set of operators. diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/logicalstructure/PDMarkedContentReference.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/logicalstructure/PDMarkedContentReference.java index 00f62f97a..0868ba097 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/logicalstructure/PDMarkedContentReference.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/logicalstructure/PDMarkedContentReference.java @@ -20,7 +20,6 @@ import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.PDPage; import com.tom_roush.pdfbox.pdmodel.common.COSObjectable; - /** * A marked-content reference. * @@ -67,7 +66,7 @@ public COSDictionary getCOSObject() */ public PDPage getPage() { - COSDictionary pg = (COSDictionary) this.getCOSObject().getDictionaryObject(COSName.PG); + COSDictionary pg = this.getCOSObject().getCOSDictionary(COSName.PG); if (pg != null) { return new PDPage(pg); @@ -102,6 +101,10 @@ public int getMCID() */ public void setMCID(int mcid) { + if (mcid < 0) + { + throw new IllegalArgumentException("MCID is negative"); + } this.getCOSObject().setInt(COSName.MCID, mcid); } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/logicalstructure/PDObjectReference.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/logicalstructure/PDObjectReference.java index 7ce8bee04..1da22d44c 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/logicalstructure/PDObjectReference.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/logicalstructure/PDObjectReference.java @@ -22,11 +22,11 @@ import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.cos.COSStream; +import com.tom_roush.pdfbox.pdmodel.PDPage; import com.tom_roush.pdfbox.pdmodel.common.COSObjectable; import com.tom_roush.pdfbox.pdmodel.graphics.PDXObject; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotation; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationUnknown; - /** * An object reference. *

      @@ -108,7 +108,7 @@ public COSObjectable getReferencedObject() * what else can be the target of the object reference? */ if (!(annotation instanceof PDAnnotationUnknown) - || COSName.ANNOT.equals(objDictionary.getDictionaryObject(COSName.TYPE))) + || COSName.ANNOT.equals(objDictionary.getDictionaryObject(COSName.TYPE))) { return annotation; } @@ -140,4 +140,30 @@ public void setReferencedObject(PDXObject xobject) this.getCOSObject().setItem(COSName.OBJ, xobject); } + /** + * Get the page on which the object shall be rendered. + * + * @return the referenced page or null. + */ + public PDPage getPage() + { + COSDictionary pageDict = this.getCOSObject().getCOSDictionary(COSName.PG); + if (pageDict != null) + { + return new PDPage(pageDict); + } + return null; + } + + /** + * Sets the page on which the object shall be rendered. This is optional and overrides the /PG + * entry in the structure element containing the object reference; shall be used if the + * structure element contained no such entry. + * + * @param page + */ + public void setPage(PDPage page) + { + this.getCOSObject().setItem(COSName.PG, page); + } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/logicalstructure/PDParentTreeValue.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/logicalstructure/PDParentTreeValue.java index e867e16b6..683d3e3c6 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/logicalstructure/PDParentTreeValue.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/logicalstructure/PDParentTreeValue.java @@ -20,10 +20,12 @@ import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.pdmodel.common.COSObjectable; - /** - * Described in the PDF specification in the ParentTree segment of the table "Entries in the - * structure tree root". This is either a dictionary or an array. + * This is described in the PDF specification in the ParentTree segment of the table "Entries + * in the structure tree root". This is either a dictionary or an array. It's a dictionary for + * individual objects like an annotation or an XObject, and an array for a page object or a content + * stream containing marked-content sequences identified by an MCID. The index in the array is + * identical to the MCID. You can pass null if the MCIDs are not continous. * * @author Tilman Hausherr */ diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/logicalstructure/PDStructureElement.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/logicalstructure/PDStructureElement.java index 89a4f8741..50aa6eec0 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/logicalstructure/PDStructureElement.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/logicalstructure/PDStructureElement.java @@ -16,7 +16,6 @@ */ package com.tom_roush.pdfbox.pdmodel.documentinterchange.logicalstructure; -import java.util.Iterator; import java.util.Map; import com.tom_roush.pdfbox.cos.COSArray; @@ -28,7 +27,7 @@ import com.tom_roush.pdfbox.cos.COSObject; import com.tom_roush.pdfbox.pdmodel.PDPage; import com.tom_roush.pdfbox.pdmodel.documentinterchange.markedcontent.PDMarkedContent; - +import com.tom_roush.pdfbox.pdmodel.documentinterchange.taggedpdf.StandardStructureTypes; /** * A structure element. * @@ -43,8 +42,9 @@ public class PDStructureElement extends PDStructureNode /** * Constructor with required values. * - * @param structureType the structure type - * @param parent the parent structure node + * @param structureType The structure type. Constants are available in the + * {@link StandardStructureTypes} class. + * @param parent The parent structure node. */ public PDStructureElement(String structureType, PDStructureNode parent) { @@ -168,16 +168,11 @@ public Revisions getAttributes() COSBase a = this.getCOSObject().getDictionaryObject(COSName.A); if (a instanceof COSArray) { - COSArray aa = (COSArray) a; - Iterator it = aa.iterator(); + COSArray array = (COSArray) a; PDAttributeObject ao = null; - while (it.hasNext()) + for (int i = 0; i < array.size(); ++i) { - COSBase item = it.next(); - if (item instanceof COSObject) - { - item = ((COSObject) item).getObject(); - } + COSBase item = array.getObject(i); if (item instanceof COSDictionary) { ao = PDAttributeObject.create((COSDictionary) item); @@ -186,6 +181,8 @@ public Revisions getAttributes() } else if (item instanceof COSInteger) { + // Read "14.7.5.3 Attribute Revision Numbers" + // This is additional to the /R entry attributes.setRevisionNumber(ao, ((COSNumber) item).intValue()); } } @@ -343,15 +340,10 @@ public Revisions getClassNames() if (c instanceof COSArray) { COSArray array = (COSArray) c; - Iterator it = array.iterator(); String className = null; - while (it.hasNext()) + for (int i = 0; i < array.size(); ++i) { - COSBase item = it.next(); - if (item instanceof COSObject) - { - item = ((COSObject) item).getObject(); - } + COSBase item = array.getObject(i); if (item instanceof COSName) { className = ((COSName) item).getName(); @@ -359,7 +351,9 @@ public Revisions getClassNames() } else if (item instanceof COSInteger) { - classNames.setRevisionNumber(className, ((COSInteger) item).intValue()); + // Read "14.7.5.3 Attribute Revision Numbers" + // This is additional to the /R entry + classNames.setRevisionNumber(className, ((COSNumber) item).intValue()); } } } @@ -609,14 +603,10 @@ public void setActualText(String actualText) public String getStandardStructureType() { String type = this.getStructureType(); - Map roleMap = getRoleMap(); - if (roleMap.containsKey(type)) + Object mappedValue = getRoleMap().get(type); + if (mappedValue instanceof String) { - Object mappedValue = getRoleMap().get(type); - if (mappedValue instanceof String) - { - type = (String)mappedValue; - } + type = (String) mappedValue; } return type; } @@ -624,7 +614,23 @@ public String getStandardStructureType() /** * Appends a marked-content sequence kid. * - * @param markedContent the marked-content sequence + * @param mcid the marked-content id (MCID). + * @throws IllegalArgumentException if the mcid is negative. + */ + public void appendKid(int mcid) + { + if (mcid < 0) + { + throw new IllegalArgumentException("MCID should not be negative"); + } + this.appendKid(COSInteger.get(mcid)); + } + + /** + * Appends a marked-content sequence kid. + * + * @param markedContent the marked-content sequence with the MCID. + * @throws IllegalArgumentException if the mcid is negative or doesn't exist. */ public void appendKid(PDMarkedContent markedContent) { @@ -632,7 +638,12 @@ public void appendKid(PDMarkedContent markedContent) { return; } - this.appendKid(COSInteger.get(markedContent.getMCID())); + int mcid = markedContent.getMCID(); + if (mcid < 0) + { + throw new IllegalArgumentException("MCID is negative or doesn't exist"); + } + this.appendKid(COSInteger.get(mcid)); } /** diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/logicalstructure/PDStructureNode.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/logicalstructure/PDStructureNode.java index 382b4b668..f7ea30dfb 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/logicalstructure/PDStructureNode.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/logicalstructure/PDStructureNode.java @@ -27,7 +27,6 @@ import com.tom_roush.pdfbox.cos.COSObject; import com.tom_roush.pdfbox.pdmodel.common.COSArrayList; import com.tom_roush.pdfbox.pdmodel.common.COSObjectable; - /** * A node in the structure tree. * @@ -142,7 +141,7 @@ public void setKids(List kids) } /** - * Appends a structure element kid. + * Appends a structure element kid and sets the parent property (/P) to this. * * @param structureElement the structure element */ @@ -272,10 +271,10 @@ protected void insertBefore(COSBase newKid, Object refKid) } /** - * Removes a structure element kid. + * Removes a structure element kid and if successful also removes the parent property (/P). * * @param structureElement the structure element - * @return true if the kid was removed, false otherwise + * @return true if the kid was removed, false otherwise. */ public boolean removeKid(PDStructureElement structureElement) { diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/logicalstructure/PDStructureTreeRoot.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/logicalstructure/PDStructureTreeRoot.java index 6a37a0803..bf4207700 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/logicalstructure/PDStructureTreeRoot.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/logicalstructure/PDStructureTreeRoot.java @@ -19,18 +19,20 @@ import android.util.Log; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import com.tom_roush.pdfbox.cos.COSArray; import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; +import com.tom_roush.pdfbox.cos.COSObject; import com.tom_roush.pdfbox.pdmodel.PDStructureElementNameTreeNode; import com.tom_roush.pdfbox.pdmodel.common.COSDictionaryMap; import com.tom_roush.pdfbox.pdmodel.common.PDNameTreeNode; import com.tom_roush.pdfbox.pdmodel.common.PDNumberTreeNode; - /** * A root of a structure tree. * @@ -40,7 +42,6 @@ */ public class PDStructureTreeRoot extends PDStructureNode { - private static final String TYPE = "StructTreeRoot"; /** @@ -94,7 +95,7 @@ else if (k instanceof COSArray) /** * Returns the K entry. This can be a dictionary representing a structure element, or an array - * of them. + * of them. To get it as a list of PDStructureElement objects, use {@link #getKids()} instead. * * @return the K entry. */ @@ -139,9 +140,15 @@ public void setIDTree(PDNameTreeNode idTree) } /** - * Returns the parent tree. + * Returns the parent tree.

      + * The keys correspond to a single page of the document or to an individual object, e.g. an + * annotation or an XObject, which have a /StructParent or /StructParents + * entry.

      + * The values of type {@link PDParentTreeValue} are either a dictionary or an array. It's a + * dictionary for individual objects like an annotation or an XObject, and an array for a page + * object or a content stream containing marked-content sequences identified by an MCID. * - * @return the parent tree + * @return the parent tree. */ public PDNumberTreeNode getParentTree() { @@ -154,7 +161,15 @@ public PDNumberTreeNode getParentTree() } /** - * Sets the parent tree. + * Sets the parent tree.

      + * The keys correspond to a single page of the document or to an individual object, e.g. an + * annotation or an XObject, which have a /StructParent or /StructParents + * entry.

      + * The values of type {@link PDParentTreeValue} are either a dictionary or an array. It's a + * dictionary for individual objects like an annotation or an XObject, and an array for a page + * object or a content stream containing marked-content sequences identified by an MCID. + *

      + * To create an empty parent tree, call {@code new PDNumberTreeNode(PDParentTreeValue.class)}. * * @param parentTree the parent tree */ @@ -164,9 +179,10 @@ public void setParentTree(PDNumberTreeNode parentTree) } /** - * Returns the next key in the parent tree. + * Returns The next key for the parent tree. This is a number greater than any existing key, and + * which shall be used for the next entry to be added to the tree. * - * @return the next key in the parent tree + * @return The next key for the parent tree */ public int getParentTreeNextKey() { @@ -174,9 +190,10 @@ public int getParentTreeNextKey() } /** - * Sets the next key in the parent tree. + * Sets the next key in the parent tree. This is a number greater than any existing key, and + * which shall be used for the next entry to be added to the tree. * - * @param parentTreeNextkey the next key in the parent tree. + * @param parentTreeNextkey The next key in the parent tree. */ public void setParentTreeNextKey(int parentTreeNextkey) { @@ -220,4 +237,83 @@ public void setRoleMap(Map roleMap) this.getCOSObject().setItem(COSName.ROLE_MAP, rmDic); } + /** + * Sets the ClassMap. + * + * @return the ClassMap, never null. The elements are either {@link PDAttributeObject} or lists + * of it. + */ + public Map getClassMap() + { + Map classMap = new HashMap(); + COSDictionary classMapDictionary = this.getCOSObject().getCOSDictionary(COSName.CLASS_MAP); + if (classMapDictionary == null) + { + return classMap; + } + for (Map.Entry entry : classMapDictionary.entrySet()) + { + COSName name = entry.getKey(); + COSBase base = entry.getValue(); + if (base instanceof COSObject) + { + base = ((COSObject) base).getObject(); + } + if (base instanceof COSDictionary) + { + classMap.put(name.getName(), PDAttributeObject.create((COSDictionary) base)); + } + else if (base instanceof COSArray) + { + COSArray array = (COSArray) base; + List list = new ArrayList(); + for (int i = 0; i < array.size(); ++i) + { + COSBase base2 = array.getObject(i); + if (base2 instanceof COSDictionary) + { + list.add(PDAttributeObject.create((COSDictionary) base2)); + } + } + classMap.put(name.getName(), list); + } + } + return classMap; + } + + /** + * Sets the ClassMap. + * + * @param classMap null, or a map whose elements are either {@link PDAttributeObject} or lists + * of it. + */ + public void setClassMap(Map classMap) + { + if (classMap == null || classMap.isEmpty()) + { + this.getCOSObject().removeItem(COSName.CLASS_MAP); + return; + } + COSDictionary classMapDictionary = new COSDictionary(); + for (Map.Entry entry : classMap.entrySet()) + { + String name = entry.getKey(); + Object object = entry.getValue(); + if (object instanceof PDAttributeObject) + { + classMapDictionary.setItem(name, ((PDAttributeObject) object).getCOSObject()); + } + else if (object instanceof List) + { + List list = (List) object; + COSArray array = new COSArray(); + for (PDAttributeObject attributeObject : list) + { + array.add(attributeObject.getCOSObject()); + } + classMapDictionary.setItem(name, array); + } + } + this.getCOSObject().setItem(COSName.CLASS_MAP, classMapDictionary); + } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/logicalstructure/PDUserAttributeObject.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/logicalstructure/PDUserAttributeObject.java index 5b8b38e05..dccc20a36 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/logicalstructure/PDUserAttributeObject.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/logicalstructure/PDUserAttributeObject.java @@ -22,7 +22,6 @@ import com.tom_roush.pdfbox.cos.COSArray; import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; - /** * A User attribute object. * @@ -95,7 +94,7 @@ public void setUserProperties(List userProperties) */ public void addUserProperty(PDUserProperty userProperty) { - COSArray p = (COSArray) this.getCOSObject().getDictionaryObject(COSName.P); + COSArray p = this.getCOSObject().getCOSArray(COSName.P); p.add(userProperty); this.notifyChanged(); } @@ -111,7 +110,7 @@ public void removeUserProperty(PDUserProperty userProperty) { return; } - COSArray p = (COSArray) this.getCOSObject().getDictionaryObject(COSName.P); + COSArray p = this.getCOSObject().getCOSArray(COSName.P); p.remove(userProperty.getCOSObject()); this.notifyChanged(); } @@ -128,8 +127,8 @@ public void userPropertyChanged(PDUserProperty userProperty) public String toString() { return super.toString() + - ", userProperties=" + - this.getOwnerUserProperties(); + ", userProperties=" + + this.getOwnerUserProperties(); } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/logicalstructure/Revisions.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/logicalstructure/Revisions.java index cb9884a1d..7552c60a1 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/logicalstructure/Revisions.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/logicalstructure/Revisions.java @@ -18,7 +18,6 @@ import java.util.ArrayList; import java.util.List; - /** * * @author Johannes Koch @@ -27,32 +26,16 @@ */ public class Revisions { - private List objects; - private List revisionNumbers; + private final List objects; + private final List revisionNumbers; /** * Constructor. */ public Revisions() { - } - - private List getObjects() - { - if (this.objects == null) - { - this.objects = new ArrayList(); - } - return this.objects; - } - - private List getRevisionNumbers() - { - if (this.revisionNumbers == null) - { - this.revisionNumbers = new ArrayList(); - } - return this.revisionNumbers; + objects = new ArrayList(); + revisionNumbers = new ArrayList(); } /** @@ -64,7 +47,7 @@ private List getRevisionNumbers() */ public T getObject(int index) { - return this.getObjects().get(index); + return objects.get(index); } /** @@ -76,7 +59,7 @@ public T getObject(int index) */ public int getRevisionNumber(int index) { - return this.getRevisionNumbers().get(index); + return revisionNumbers.get(index); } /** @@ -87,8 +70,8 @@ public int getRevisionNumber(int index) */ public void addObject(T object, int revisionNumber) { - this.getObjects().add(object); - this.getRevisionNumbers().add(revisionNumber); + objects.add(object); + revisionNumbers.add(revisionNumber); } /** @@ -99,10 +82,10 @@ public void addObject(T object, int revisionNumber) */ protected void setRevisionNumber(T object, int revisionNumber) { - int index = this.getObjects().indexOf(object); + int index = objects.indexOf(object); if (index > -1) { - this.getRevisionNumbers().set(index, revisionNumber); + revisionNumbers.set(index, revisionNumber); } } @@ -113,7 +96,7 @@ protected void setRevisionNumber(T object, int revisionNumber) */ public int size() { - return this.getObjects().size(); + return objects.size(); } /** @@ -123,14 +106,14 @@ public int size() public String toString() { StringBuilder sb = new StringBuilder(); - for (int i = 0; i < this.getObjects().size(); i++) + for (int i = 0; i < objects.size(); i++) { if (i > 0) { sb.append("; "); } - sb.append("object=").append(this.getObjects().get(i)) - .append(", revisionNumber=").append(this.getRevisionNumber(i)); + sb.append("object=").append(objects.get(i)) + .append(", revisionNumber=").append(getRevisionNumber(i)); } return sb.toString(); } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/prepress/PDBoxStyle.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/prepress/PDBoxStyle.java index 0def61e4e..a98ec8091 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/prepress/PDBoxStyle.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/prepress/PDBoxStyle.java @@ -24,7 +24,6 @@ import com.tom_roush.pdfbox.pdmodel.graphics.PDLineDashPattern; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColor; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDDeviceRGB; - /** * The Box Style specifies visual characteristics for displaying box areas. * @@ -81,7 +80,7 @@ public COSDictionary getCOSObject() */ public PDColor getGuidelineColor() { - COSArray colorValues = (COSArray) dictionary.getDictionaryObject(COSName.C); + COSArray colorValues = dictionary.getCOSArray(COSName.C); if( colorValues == null ) { colorValues = new COSArray(); @@ -163,7 +162,7 @@ public void setGuidelineStyle( String style ) public PDLineDashPattern getLineDashPattern() { PDLineDashPattern pattern; - COSArray d = (COSArray) dictionary.getDictionaryObject(COSName.D); + COSArray d = dictionary.getCOSArray(COSName.D); if( d == null ) { d = new COSArray(); diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/taggedpdf/PDArtifactMarkedContent.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/taggedpdf/PDArtifactMarkedContent.java index d27cb684c..0ab42a22b 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/taggedpdf/PDArtifactMarkedContent.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/taggedpdf/PDArtifactMarkedContent.java @@ -21,7 +21,6 @@ import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.common.PDRectangle; import com.tom_roush.pdfbox.pdmodel.documentinterchange.markedcontent.PDMarkedContent; - /** * An artifact marked content. * @@ -55,8 +54,7 @@ public String getType() public PDRectangle getBBox() { PDRectangle retval = null; - COSArray a = (COSArray) this.getProperties().getDictionaryObject( - COSName.BBOX); + COSArray a = this.getProperties().getCOSArray(COSName.BBOX); if (a != null) { retval = new PDRectangle(a); @@ -128,8 +126,7 @@ public String getSubtype() */ private boolean isAttached(String edge) { - COSArray a = (COSArray) this.getProperties().getDictionaryObject( - COSName.ATTACHED); + COSArray a = this.getProperties().getCOSArray(COSName.ATTACHED); if (a != null) { for (int i = 0; i < a.size(); i++) diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/taggedpdf/PDLayoutAttributeObject.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/taggedpdf/PDLayoutAttributeObject.java index e383e1ee8..70bdf9f91 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/taggedpdf/PDLayoutAttributeObject.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/taggedpdf/PDLayoutAttributeObject.java @@ -19,9 +19,9 @@ import com.tom_roush.pdfbox.cos.COSArray; import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSDictionary; +import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.common.PDRectangle; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDGamma; - /** * A Layout attribute object. * @@ -840,8 +840,7 @@ public void setTextAlign(String textIndent) */ public PDRectangle getBBox() { - COSArray array = - (COSArray) this.getCOSObject().getDictionaryObject(BBOX); + COSArray array = this.getCOSObject().getCOSArray(COSName.BBOX); if (array != null) { return new PDRectangle(array); diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/taggedpdf/PDStandardAttributeObject.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/taggedpdf/PDStandardAttributeObject.java index 171f943fd..eaf93ae22 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/taggedpdf/PDStandardAttributeObject.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/documentinterchange/taggedpdf/PDStandardAttributeObject.java @@ -25,7 +25,6 @@ import com.tom_roush.pdfbox.cos.COSString; import com.tom_roush.pdfbox.pdmodel.documentinterchange.logicalstructure.PDAttributeObject; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDGamma; - /** * A standard attribute object. * @@ -383,10 +382,10 @@ protected void setArrayOfNumber(String name, float[] values) */ protected PDGamma getColor(String name) { - COSArray c = (COSArray) this.getCOSObject().getDictionaryObject(name); - if (c != null) + COSBase base = this.getCOSObject().getDictionaryObject(name); + if (base instanceof COSArray) { - return new PDGamma(c); + return new PDGamma((COSArray) base); } return null; } @@ -399,12 +398,12 @@ protected PDGamma getColor(String name) */ protected Object getColorOrFourColors(String name) { - COSArray array = - (COSArray) this.getCOSObject().getDictionaryObject(name); - if (array == null) + COSBase base = this.getCOSObject().getDictionaryObject(name); + if (!(base instanceof COSArray)) { return null; } + COSArray array = (COSArray) base; if (array.size() == 3) { // only one colour diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/PDCryptFilterDictionary.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/PDCryptFilterDictionary.java index 7132cef68..6d42ceea9 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/PDCryptFilterDictionary.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/PDCryptFilterDictionary.java @@ -22,7 +22,6 @@ import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.common.COSObjectable; - /** * This class is a specialized view of the crypt filter dictionary of a PDF document. * It contains a low level dictionary (COSDictionary) and provides the methods to @@ -98,8 +97,8 @@ public int getLength() return cryptFilterDictionary.getInt( COSName.LENGTH, 40 ); } - /** - * This will set the crypt filter method. + /** + * This will set the crypt filter method. * Allowed values are: NONE, V2, AESV2, AESV3 * * @param cfm name of the crypt filter method. @@ -111,14 +110,14 @@ public void setCryptFilterMethod(COSName cfm) } /** - * This will return the crypt filter method. + * This will return the crypt filter method. * Allowed values are: NONE, V2, AESV2, AESV3 * * @return the name of the crypt filter method. */ public COSName getCryptFilterMethod() { - return (COSName)cryptFilterDictionary.getDictionaryObject( COSName.CFM ); + return cryptFilterDictionary.getCOSName(COSName.CFM); } /** diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/PDEncryption.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/PDEncryption.java index 52fdf1b52..517b34caa 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/PDEncryption.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/PDEncryption.java @@ -18,6 +18,7 @@ package com.tom_roush.pdfbox.pdmodel.encryption; import java.io.IOException; +import java.util.Arrays; import com.tom_roush.pdfbox.cos.COSArray; import com.tom_roush.pdfbox.cos.COSBase; @@ -26,7 +27,6 @@ import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.cos.COSString; import com.tom_roush.pdfbox.pdmodel.common.COSObjectable; - /** * This class is a specialized view of the encryption dictionary of a PDF document. * It contains a low level dictionary (COSDictionary) and provides the methods to @@ -106,6 +106,7 @@ public SecurityHandler getSecurityHandler() throws IOException { if (securityHandler == null) { + // Don't change this text, it's used by Apache Tika (TIKA-4082) throw new IOException("No security handler for filter " + getFilter()); } return securityHandler; @@ -265,7 +266,7 @@ public int getRevision() return dictionary.getInt( COSName.R, DEFAULT_VERSION ); } - /** + /** * This will set the O entry in the standard encryption dictionary. * * @param o A 32 byte array or null if there is no owner key. @@ -291,6 +292,15 @@ public byte[] getOwnerKey() throws IOException if( owner != null ) { o = owner.getBytes(); + int r = getRevision(); + if (r <= 4) + { + o = Arrays.copyOf(o, 32); + } + else if (r == 5 || r == 6) + { + o = Arrays.copyOf(o, 48); + } } return o; } @@ -321,6 +331,15 @@ public byte[] getUserKey() throws IOException if( user != null ) { u = user.getBytes(); + int r = getRevision(); + if (r <= 4) + { + u = Arrays.copyOf(u, 32); + } + else if (r == 5 || r == 6) + { + u = Arrays.copyOf(u, 48); + } } return u; } @@ -350,7 +369,7 @@ public byte[] getOwnerEncryptionKey() throws IOException COSString ownerEncryptionKey = (COSString)dictionary.getDictionaryObject( COSName.OE ); if( ownerEncryptionKey != null ) { - oe = ownerEncryptionKey.getBytes(); + oe = Arrays.copyOf(ownerEncryptionKey.getBytes(), 32); } return oe; } @@ -380,7 +399,7 @@ public byte[] getUserEncryptionKey() throws IOException COSString userEncryptionKey = (COSString)dictionary.getDictionaryObject( COSName.UE ); if( userEncryptionKey != null ) { - ue = userEncryptionKey.getBytes(); + ue = Arrays.copyOf(userEncryptionKey.getBytes(), 32); } return ue; } @@ -557,7 +576,7 @@ public void setDefaultCryptFilterDictionary(PDCryptFilterDictionary defaultFilte */ public COSName getStreamFilterName() { - COSName stmF = (COSName) dictionary.getDictionaryObject( COSName.STM_F ); + COSName stmF = dictionary.getCOSName(COSName.STM_F ); if (stmF == null) { stmF = COSName.IDENTITY; @@ -583,7 +602,7 @@ public void setStreamFilterName(COSName streamFilterName) */ public COSName getStringFilterName() { - COSName strF = (COSName) dictionary.getDictionaryObject( COSName.STR_F ); + COSName strF = dictionary.getCOSName(COSName.STR_F); if (strF == null) { strF = COSName.IDENTITY; diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/PublicKeySecurityHandler.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/PublicKeySecurityHandler.java index 49db98da7..9f17b96b1 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/PublicKeySecurityHandler.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/PublicKeySecurityHandler.java @@ -33,7 +33,6 @@ import java.security.cert.X509Certificate; import java.util.Collection; import java.util.Iterator; - import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; @@ -70,7 +69,6 @@ import org.bouncycastle.cms.RecipientInformation; import org.bouncycastle.cms.jcajce.JceKeyTransEnvelopedRecipient; import org.bouncycastle.util.Arrays; - /** * This class implements the public key security handler described in the PDF specification. * @@ -119,14 +117,14 @@ public PublicKeySecurityHandler(PublicKeyProtectionPolicy publicKeyProtectionPol */ @Override public void prepareForDecryption(PDEncryption encryption, COSArray documentIDArray, - DecryptionMaterial decryptionMaterial) - throws IOException + DecryptionMaterial decryptionMaterial) + throws IOException { if (!(decryptionMaterial instanceof PublicKeyDecryptionMaterial)) { throw new IOException( - "Provided decryption material is not compatible with the document - " - + "did you pass a null keyStore?"); + "Provided decryption material is not compatible with the document - " + + "did you pass a null keyStore?"); } PDCryptFilterDictionary defaultCryptFilterDictionary = encryption.getDefaultCryptFilterDictionary(); @@ -135,10 +133,14 @@ public void prepareForDecryption(PDEncryption encryption, COSArray documentIDArr setKeyLength(defaultCryptFilterDictionary.getLength()); setDecryptMetadata(defaultCryptFilterDictionary.isEncryptMetaData()); } - else if (encryption.getLength() != 0) + else { - setKeyLength(encryption.getLength()); - setDecryptMetadata(encryption.isEncryptMetaData()); + int encryptionLength = encryption.getLength(); + if (encryptionLength != 0) + { + setKeyLength(encryptionLength); + setDecryptMetadata(encryption.isEncryptMetaData()); + } } PublicKeyDecryptionMaterial material = (PublicKeyDecryptionMaterial) decryptionMaterial; @@ -179,7 +181,7 @@ else if (encryption.getLength() != 0) byte[] recipientBytes = recipientFieldString.getBytes(); CMSEnvelopedData data = new CMSEnvelopedData(recipientBytes); Collection recipCertificatesIt = data.getRecipientInfos() - .getRecipients(); + .getRecipients(); int j = 0; for (RecipientInformation ri : recipCertificatesIt) { @@ -213,7 +215,7 @@ else if (encryption.getLength() != 0) if (!foundRecipient || envelopedData == null) { throw new IOException("The certificate matches none of " + array.size() - + " recipient entries" + extraInfo.toString()); + + " recipient entries" + extraInfo.toString()); } if (envelopedData.length != 24) { @@ -241,12 +243,13 @@ else if (encryption.getLength() != 0) for (byte[] recipientFieldsByte : recipientFieldsBytes) { System.arraycopy(recipientFieldsByte, 0, sha1Input, sha1InputOffset, - recipientFieldsByte.length); + recipientFieldsByte.length); sha1InputOffset += recipientFieldsByte.length; } byte[] mdResult; - if (encryption.getVersion() == 4 || encryption.getVersion() == 5) + int encryptionVersion = encryption.getVersion(); + if (encryptionVersion == 4 || encryptionVersion == 5) { if (!isDecryptMetadata()) { @@ -255,7 +258,7 @@ else if (encryption.getLength() != 0) sha1Input = Arrays.copyOf(sha1Input, sha1Input.length + 4); System.arraycopy(new byte[]{ (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff}, 0, sha1Input, sha1Input.length - 4, 4); } - if (encryption.getVersion() == 4) + if (encryptionVersion == 4) { mdResult = MessageDigests.getSHA1().digest(sha1Input); } @@ -264,14 +267,14 @@ else if (encryption.getLength() != 0) mdResult = MessageDigests.getSHA256().digest(sha1Input); } - // detect whether AES encryption is used. This assumes that the encryption algo is + // detect whether AES encryption is used. This assumes that the encryption algo is // stored in the PDCryptFilterDictionary // However, crypt filters are used only when V is 4 or 5. if (defaultCryptFilterDictionary != null) { COSName cryptFilterMethod = defaultCryptFilterDictionary.getCryptFilterMethod(); setAES(COSName.AESV2.equals(cryptFilterMethod) || - COSName.AESV3.equals(cryptFilterMethod)); + COSName.AESV3.equals(cryptFilterMethod)); } } else @@ -298,7 +301,7 @@ else if (encryption.getLength() != 0) } private void appendCertInfo(StringBuilder extraInfo, KeyTransRecipientId ktRid, - X509Certificate certificate, X509CertificateHolder materialCert) + X509Certificate certificate, X509CertificateHolder materialCert) { BigInteger ridSerialNumber = ktRid.getSerialNumber(); if (ridSerialNumber != null) @@ -477,7 +480,7 @@ private byte[][] computeRecipientsField(byte[] seed) throws GeneralSecurityExcep } private ASN1Primitive createDERForRecipient(byte[] in, X509Certificate cert) - throws IOException, GeneralSecurityException + throws IOException, GeneralSecurityException { String algorithm = PKCSObjectIdentifiers.RC2_CBC.getId(); AlgorithmParameterGenerator apg; @@ -494,7 +497,7 @@ private ASN1Primitive createDERForRecipient(byte[] in, X509Certificate cert) { // happens when using the command line app .jar file throw new IOException("Could not find a suitable javax.crypto provider for algorithm " + - algorithm + "; possible reason: using an unsigned .jar file", e); + algorithm + "; possible reason: using an unsigned .jar file", e); } catch (NoSuchPaddingException e) { @@ -519,7 +522,7 @@ private ASN1Primitive createDERForRecipient(byte[] in, X509Certificate cert) AlgorithmIdentifier algorithmId = new AlgorithmIdentifier(new ASN1ObjectIdentifier(algorithm), object); EncryptedContentInfo encryptedInfo = - new EncryptedContentInfo(PKCSObjectIdentifiers.data, algorithmId, new DEROctetString(bytes)); + new EncryptedContentInfo(PKCSObjectIdentifiers.data, algorithmId, new DEROctetString(bytes)); EnvelopedData enveloped = new EnvelopedData(null, set, encryptedInfo, (ASN1Set) null); ContentInfo contentInfo = new ContentInfo(PKCSObjectIdentifiers.envelopedData, enveloped); @@ -528,7 +531,7 @@ private ASN1Primitive createDERForRecipient(byte[] in, X509Certificate cert) private KeyTransRecipientInfo computeRecipientInfo(X509Certificate x509certificate, byte[] abyte0) throws IOException, CertificateEncodingException, InvalidKeyException, - BadPaddingException, IllegalBlockSizeException + BadPaddingException, IllegalBlockSizeException { ASN1InputStream input = new ASN1InputStream(x509certificate.getTBSCertificate()); TBSCertificate certificate = TBSCertificate.getInstance(input.readObject()); @@ -537,14 +540,14 @@ private KeyTransRecipientInfo computeRecipientInfo(X509Certificate x509certifica AlgorithmIdentifier algorithmId = certificate.getSubjectPublicKeyInfo().getAlgorithm(); IssuerAndSerialNumber serial = new IssuerAndSerialNumber( - certificate.getIssuer(), - certificate.getSerialNumber().getValue()); + certificate.getIssuer(), + certificate.getSerialNumber().getValue()); Cipher cipher; try { cipher = Cipher.getInstance(algorithmId.getAlgorithm().getId(), - SecurityProvider.getProvider()); + SecurityProvider.getProvider()); } catch (NoSuchAlgorithmException e) { diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/RC4Cipher.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/RC4Cipher.java index 5b4b0f3ca..b0ab20cf1 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/RC4Cipher.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/RC4Cipher.java @@ -19,7 +19,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; - /** * An implementation of the RC4 stream cipher. * @@ -95,6 +94,15 @@ private static void swap( int[] data, int firstIndex, int secondIndex ) data[ secondIndex ] = tmp; } + private int encrypt(byte aByte) + { + b = (b + 1) % 256; + c = (salt[b] + c) % 256; + swap(salt, b, c); + int saltIndex = (salt[b] + salt[c]) % 256; + return aByte ^ (byte) salt[saltIndex]; + } + /** * This will encrypt and write the next byte. * @@ -102,14 +110,13 @@ private static void swap( int[] data, int firstIndex, int secondIndex ) * @param output The stream to write to. * * @throws IOException If there is an error writing to the output stream. + * + * @deprecated This method will be removed in 4.0. */ + @Deprecated public void write( byte aByte, OutputStream output ) throws IOException { - b = (b + 1) % 256; - c = (salt[b] + c) % 256; - swap( salt, b, c ); - int saltIndex = (salt[b] + salt[c]) % 256; - output.write(aByte ^ (byte)salt[saltIndex]); + output.write(encrypt(aByte)); } /** @@ -122,10 +129,7 @@ public void write( byte aByte, OutputStream output ) throws IOException */ public void write( byte[] data, OutputStream output ) throws IOException { - for (byte aData : data) - { - write(aData, output); - } + write(data, 0, data.length, output, new byte[data.length]); } /** @@ -142,7 +146,7 @@ public void write( InputStream data, OutputStream output ) throws IOException int amountRead; while( (amountRead = data.read( buffer )) != -1 ) { - write( buffer, 0, amountRead, output ); + write(buffer, 0, amountRead, output, buffer); } } @@ -155,12 +159,33 @@ public void write( InputStream data, OutputStream output ) throws IOException * @param output The stream to write to. * * @throws IOException If there is an error writing to the output stream. + * + * @deprecated This method will be removed in 4.0. */ + @Deprecated public void write( byte[] data, int offset, int len, OutputStream output) throws IOException { - for( int i = offset; i < offset + len; i++ ) + write(data, offset, len, output, new byte[len]); + } + + /** + * This will encrypt and write the data. + * + * @param data The data to encrypt, may be overwritten. + * @param offset The offset into the array to start reading data from. + * @param len The number of bytes to attempt to read. + * @param output The stream to write to. + * @param buffer The buffer to use, it can be altered and be identical to the data to encrypt. + * + * @throws IOException If there is an error writing to the output stream. + */ + private void write(byte[] data, int offset, int len, OutputStream output, byte[] buffer) throws IOException + { + for (int i = 0, j = offset; i < len; ++i, ++j) { - write( data[i], output ); + buffer[i] = (byte) encrypt(data[j]); } + + output.write(buffer, 0, len); } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/SaslPrep.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/SaslPrep.java index 3300f1d6b..8037b4540 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/SaslPrep.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/SaslPrep.java @@ -1,9 +1,12 @@ /* - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -16,7 +19,6 @@ import java.nio.CharBuffer; import java.text.Normalizer; - /** * Copied from https://github.com/tombentley/saslprep/blob/master/src/main/java/SaslPrep.java on * 30.5.2019, commit 2e30daa. @@ -26,319 +28,319 @@ class SaslPrep { - private SaslPrep() - { - } + private SaslPrep() + { + } - /** - * Return the {@code SASLPrep}-canonicalised version of the given {@code str} for use as a query - * string. This implements the {@code SASLPrep} algorithm defined in - * RFC 4013. - * - * @param str The string to canonicalise. - * @return The canonicalised string. - * @throws IllegalArgumentException if the string contained prohibited codepoints, or broke the - * requirements for bidirectional character handling. - * @see RFC 3454, Section 7 for - * discussion of what a query string is. - */ - static String saslPrepQuery(String str) - { - return saslPrep(str, true); - } + /** + * Return the {@code SASLPrep}-canonicalised version of the given {@code str} for use as a query + * string. This implements the {@code SASLPrep} algorithm defined in + * RFC 4013. + * + * @param str The string to canonicalise. + * @return The canonicalised string. + * @throws IllegalArgumentException if the string contained prohibited codepoints, or broke the + * requirements for bidirectional character handling. + * @see RFC 3454, Section 7 for + * discussion of what a query string is. + */ + static String saslPrepQuery(String str) + { + return saslPrep(str, true); + } - /** - * Return the {@code SASLPrep}-canonicalised version of the given - * @code str} for use as a stored string. This implements the {@code SASLPrep} algorithm defined - * in - * RFC 4013. - * - * @param str The string to canonicalise. - * @return The canonicalised string. - * @throws IllegalArgumentException if the string contained prohibited codepoints, or broke the - * requirements for bidirectional character handling. - * @see RFC 3454, Section 7 for - * discussion of what a stored string is. - */ - static String saslPrepStored(String str) - { - return saslPrep(str, false); - } + /** + * Return the {@code SASLPrep}-canonicalised version of the given + * @code str} for use as a stored string. This implements the {@code SASLPrep} algorithm defined + * in + * RFC 4013. + * + * @param str The string to canonicalise. + * @return The canonicalised string. + * @throws IllegalArgumentException if the string contained prohibited codepoints, or broke the + * requirements for bidirectional character handling. + * @see RFC 3454, Section 7 for + * discussion of what a stored string is. + */ + static String saslPrepStored(String str) + { + return saslPrep(str, false); + } - private static String saslPrep(String str, boolean allowUnassigned) - { - char[] chars = str.toCharArray(); + private static String saslPrep(String str, boolean allowUnassigned) + { + char[] chars = str.toCharArray(); - // 1. Map - // non-ASCII space chars mapped to space - for (int i = 0; i < str.length(); i++) - { - char ch = str.charAt(i); - if (nonAsciiSpace(ch)) - { - chars[i] = ' '; - } - } + // 1. Map + // non-ASCII space chars mapped to space + for (int i = 0; i < str.length(); i++) + { + char ch = str.charAt(i); + if (nonAsciiSpace(ch)) + { + chars[i] = ' '; + } + } - int length = 0; - for (int i = 0; i < str.length(); i++) - { - char ch = chars[i]; - if (!mappedToNothing(ch)) - { - chars[length++] = ch; - } - } + int length = 0; + for (int i = 0; i < str.length(); i++) + { + char ch = chars[i]; + if (!mappedToNothing(ch)) + { + chars[length++] = ch; + } + } - // 2. Normalize - String normalized = Normalizer.normalize(CharBuffer.wrap(chars, 0, length), Normalizer.Form.NFKC); + // 2. Normalize + String normalized = Normalizer.normalize(CharBuffer.wrap(chars, 0, length), Normalizer.Form.NFKC); - boolean containsRandALCat = false; - boolean containsLCat = false; - boolean initialRandALCat = false; - int i = 0; - while (i < normalized.length()) - { - final int codepoint = normalized.codePointAt(i); - // 3. Prohibit - if (prohibited(codepoint)) - { - throw new IllegalArgumentException("Prohibited character " + - codepoint + " at position " + i); - } + boolean containsRandALCat = false; + boolean containsLCat = false; + boolean initialRandALCat = false; + int i = 0; + while (i < normalized.length()) + { + final int codepoint = normalized.codePointAt(i); + // 3. Prohibit + if (prohibited(codepoint)) + { + throw new IllegalArgumentException("Prohibited character " + + codepoint + " at position " + i); + } - // 4. Check bidi - final byte directionality = Character.getDirectionality(codepoint); - final boolean isRandALcat = directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT - || directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC; - containsRandALCat |= isRandALcat; - containsLCat |= directionality == Character.DIRECTIONALITY_LEFT_TO_RIGHT; + // 4. Check bidi + final byte directionality = Character.getDirectionality(codepoint); + final boolean isRandALcat = directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT + || directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC; + containsRandALCat |= isRandALcat; + containsLCat |= directionality == Character.DIRECTIONALITY_LEFT_TO_RIGHT; - initialRandALCat |= i == 0 && isRandALcat; - if (!allowUnassigned && !Character.isDefined(codepoint)) - { - throw new IllegalArgumentException("Character at position " + i + " is unassigned"); - } + initialRandALCat |= i == 0 && isRandALcat; + if (!allowUnassigned && !Character.isDefined(codepoint)) + { + throw new IllegalArgumentException("Character at position " + i + " is unassigned"); + } - i += Character.charCount(codepoint); + i += Character.charCount(codepoint); - if (initialRandALCat && i >= normalized.length() && !isRandALcat) - { - throw new IllegalArgumentException("First character is RandALCat, but last character is not"); - } - } - if (containsRandALCat && containsLCat) - { - throw new IllegalArgumentException("Contains both RandALCat characters and LCat characters"); - } - return normalized; - } + if (initialRandALCat && i >= normalized.length() && !isRandALcat) + { + throw new IllegalArgumentException("First character is RandALCat, but last character is not"); + } + } + if (containsRandALCat && containsLCat) + { + throw new IllegalArgumentException("Contains both RandALCat characters and LCat characters"); + } + return normalized; + } - /** - * Return true if the given {@code codepoint} is a prohibited character - * as defined by - * RFC 4013, - * Section 2.3. - */ - static boolean prohibited(int codepoint) - { - return nonAsciiSpace((char)codepoint) - || asciiControl((char)codepoint) - || nonAsciiControl(codepoint) - || privateUse(codepoint) - || nonCharacterCodePoint(codepoint) - || surrogateCodePoint(codepoint) - || inappropriateForPlainText(codepoint) - || inappropriateForCanonical(codepoint) - || changeDisplayProperties(codepoint) - || tagging(codepoint); - } + /** + * Return true if the given {@code codepoint} is a prohibited character + * as defined by + * RFC 4013, + * Section 2.3. + */ + static boolean prohibited(int codepoint) + { + return nonAsciiSpace((char)codepoint) + || asciiControl((char)codepoint) + || nonAsciiControl(codepoint) + || privateUse(codepoint) + || nonCharacterCodePoint(codepoint) + || surrogateCodePoint(codepoint) + || inappropriateForPlainText(codepoint) + || inappropriateForCanonical(codepoint) + || changeDisplayProperties(codepoint) + || tagging(codepoint); + } - /** - * Return true if the given {@code codepoint} is a tagging character - * as defined by - * RFC 3454, - * Appendix C.9. - */ - private static boolean tagging(int codepoint) - { - return codepoint == 0xE0001 - || 0xE0020 <= codepoint && codepoint <= 0xE007F; - } + /** + * Return true if the given {@code codepoint} is a tagging character + * as defined by + * RFC 3454, + * Appendix C.9. + */ + private static boolean tagging(int codepoint) + { + return codepoint == 0xE0001 + || 0xE0020 <= codepoint && codepoint <= 0xE007F; + } - /** - * Return true if the given {@code codepoint} is change display properties - * or deprecated characters as defined by - * RFC 3454, - * Appendix C.8. - */ - private static boolean changeDisplayProperties(int codepoint) - { - return codepoint == 0x0340 - || codepoint == 0x0341 - || codepoint == 0x200E - || codepoint == 0x200F - || codepoint == 0x202A - || codepoint == 0x202B - || codepoint == 0x202C - || codepoint == 0x202D - || codepoint == 0x202E - || codepoint == 0x206A - || codepoint == 0x206B - || codepoint == 0x206C - || codepoint == 0x206D - || codepoint == 0x206E - || codepoint == 0x206F - ; - } + /** + * Return true if the given {@code codepoint} is change display properties + * or deprecated characters as defined by + * RFC 3454, + * Appendix C.8. + */ + private static boolean changeDisplayProperties(int codepoint) + { + return codepoint == 0x0340 + || codepoint == 0x0341 + || codepoint == 0x200E + || codepoint == 0x200F + || codepoint == 0x202A + || codepoint == 0x202B + || codepoint == 0x202C + || codepoint == 0x202D + || codepoint == 0x202E + || codepoint == 0x206A + || codepoint == 0x206B + || codepoint == 0x206C + || codepoint == 0x206D + || codepoint == 0x206E + || codepoint == 0x206F + ; + } - /** - * Return true if the given {@code codepoint} is inappropriate for - * canonical representation characters as defined by - * RFC 3454, - * Appendix C.7. - */ - private static boolean inappropriateForCanonical(int codepoint) - { - return 0x2FF0 <= codepoint && codepoint <= 0x2FFB; - } + /** + * Return true if the given {@code codepoint} is inappropriate for + * canonical representation characters as defined by + * RFC 3454, + * Appendix C.7. + */ + private static boolean inappropriateForCanonical(int codepoint) + { + return 0x2FF0 <= codepoint && codepoint <= 0x2FFB; + } - /** - * Return true if the given {@code codepoint} is inappropriate for plain - * text characters as defined by - * RFC 3454, - * Appendix C.6. - */ - private static boolean inappropriateForPlainText(int codepoint) - { - return codepoint == 0xFFF9 - || codepoint == 0xFFFA - || codepoint == 0xFFFB - || codepoint == 0xFFFC - || codepoint == 0xFFFD - ; - } + /** + * Return true if the given {@code codepoint} is inappropriate for plain + * text characters as defined by + * RFC 3454, + * Appendix C.6. + */ + private static boolean inappropriateForPlainText(int codepoint) + { + return codepoint == 0xFFF9 + || codepoint == 0xFFFA + || codepoint == 0xFFFB + || codepoint == 0xFFFC + || codepoint == 0xFFFD + ; + } - /** - * Return true if the given {@code codepoint} is a surrogate - * code point as defined by - * RFC 3454, - * Appendix C.5. - */ - private static boolean surrogateCodePoint(int codepoint) - { - return 0xD800 <= codepoint && codepoint <= 0xDFFF; - } + /** + * Return true if the given {@code codepoint} is a surrogate + * code point as defined by + * RFC 3454, + * Appendix C.5. + */ + private static boolean surrogateCodePoint(int codepoint) + { + return 0xD800 <= codepoint && codepoint <= 0xDFFF; + } - /** - * Return true if the given {@code codepoint} is a non-character - * code point as defined by - * RFC 3454, - * Appendix C.4. - */ - private static boolean nonCharacterCodePoint(int codepoint) - { - return 0xFDD0 <= codepoint && codepoint <= 0xFDEF - || 0xFFFE <= codepoint && codepoint <= 0xFFFF - || 0x1FFFE <= codepoint && codepoint <= 0x1FFFF - || 0x2FFFE <= codepoint && codepoint <= 0x2FFFF - || 0x3FFFE <= codepoint && codepoint <= 0x3FFFF - || 0x4FFFE <= codepoint && codepoint <= 0x4FFFF - || 0x5FFFE <= codepoint && codepoint <= 0x5FFFF - || 0x6FFFE <= codepoint && codepoint <= 0x6FFFF - || 0x7FFFE <= codepoint && codepoint <= 0x7FFFF - || 0x8FFFE <= codepoint && codepoint <= 0x8FFFF - || 0x9FFFE <= codepoint && codepoint <= 0x9FFFF - || 0xAFFFE <= codepoint && codepoint <= 0xAFFFF - || 0xBFFFE <= codepoint && codepoint <= 0xBFFFF - || 0xCFFFE <= codepoint && codepoint <= 0xCFFFF - || 0xDFFFE <= codepoint && codepoint <= 0xDFFFF - || 0xEFFFE <= codepoint && codepoint <= 0xEFFFF - || 0xFFFFE <= codepoint && codepoint <= 0xFFFFF - || 0x10FFFE <= codepoint && codepoint <= 0x10FFFF - ; - } + /** + * Return true if the given {@code codepoint} is a non-character + * code point as defined by + * RFC 3454, + * Appendix C.4. + */ + private static boolean nonCharacterCodePoint(int codepoint) + { + return 0xFDD0 <= codepoint && codepoint <= 0xFDEF + || 0xFFFE <= codepoint && codepoint <= 0xFFFF + || 0x1FFFE <= codepoint && codepoint <= 0x1FFFF + || 0x2FFFE <= codepoint && codepoint <= 0x2FFFF + || 0x3FFFE <= codepoint && codepoint <= 0x3FFFF + || 0x4FFFE <= codepoint && codepoint <= 0x4FFFF + || 0x5FFFE <= codepoint && codepoint <= 0x5FFFF + || 0x6FFFE <= codepoint && codepoint <= 0x6FFFF + || 0x7FFFE <= codepoint && codepoint <= 0x7FFFF + || 0x8FFFE <= codepoint && codepoint <= 0x8FFFF + || 0x9FFFE <= codepoint && codepoint <= 0x9FFFF + || 0xAFFFE <= codepoint && codepoint <= 0xAFFFF + || 0xBFFFE <= codepoint && codepoint <= 0xBFFFF + || 0xCFFFE <= codepoint && codepoint <= 0xCFFFF + || 0xDFFFE <= codepoint && codepoint <= 0xDFFFF + || 0xEFFFE <= codepoint && codepoint <= 0xEFFFF + || 0xFFFFE <= codepoint && codepoint <= 0xFFFFF + || 0x10FFFE <= codepoint && codepoint <= 0x10FFFF + ; + } - /** - * Return true if the given {@code codepoint} is a private use character - * as defined by RFC 3454, - * Appendix C.3. - */ - private static boolean privateUse(int codepoint) - { - return 0xE000 <= codepoint && codepoint <= 0xF8FF - || 0xF0000 <= codepoint && codepoint <= 0xFFFFD - || 0x100000 <= codepoint && codepoint <= 0x10FFFD; - } + /** + * Return true if the given {@code codepoint} is a private use character + * as defined by RFC 3454, + * Appendix C.3. + */ + private static boolean privateUse(int codepoint) + { + return 0xE000 <= codepoint && codepoint <= 0xF8FF + || 0xF0000 <= codepoint && codepoint <= 0xFFFFD + || 0x100000 <= codepoint && codepoint <= 0x10FFFD; + } - /** - * Return true if the given {@code ch} is a non-ASCII control character - * as defined by RFC 3454, - * Appendix C.2.2. - */ - private static boolean nonAsciiControl(int codepoint) - { - return 0x0080 <= codepoint && codepoint <= 0x009F - || codepoint == 0x06DD - || codepoint == 0x070F - || codepoint == 0x180E - || codepoint == 0x200C - || codepoint == 0x200D - || codepoint == 0x2028 - || codepoint == 0x2029 - || codepoint == 0x2060 - || codepoint == 0x2061 - || codepoint == 0x2062 - || codepoint == 0x2063 - || 0x206A <= codepoint && codepoint <= 0x206F - || codepoint == 0xFEFF - || 0xFFF9 <= codepoint && codepoint <= 0xFFFC - || 0x1D173 <= codepoint && codepoint <= 0x1D17A; - } + /** + * Return true if the given {@code ch} is a non-ASCII control character + * as defined by RFC 3454, + * Appendix C.2.2. + */ + private static boolean nonAsciiControl(int codepoint) + { + return 0x0080 <= codepoint && codepoint <= 0x009F + || codepoint == 0x06DD + || codepoint == 0x070F + || codepoint == 0x180E + || codepoint == 0x200C + || codepoint == 0x200D + || codepoint == 0x2028 + || codepoint == 0x2029 + || codepoint == 0x2060 + || codepoint == 0x2061 + || codepoint == 0x2062 + || codepoint == 0x2063 + || 0x206A <= codepoint && codepoint <= 0x206F + || codepoint == 0xFEFF + || 0xFFF9 <= codepoint && codepoint <= 0xFFFC + || 0x1D173 <= codepoint && codepoint <= 0x1D17A; + } - /** - * Return true if the given {@code ch} is an ASCII control character - * as defined by RFC 3454, - * Appendix C.2.1. - */ - private static boolean asciiControl(char ch) - { - return '\u0000' <= ch && ch <= '\u001F' || ch == '\u007F'; - } + /** + * Return true if the given {@code ch} is an ASCII control character + * as defined by RFC 3454, + * Appendix C.2.1. + */ + private static boolean asciiControl(char ch) + { + return '\u0000' <= ch && ch <= '\u001F' || ch == '\u007F'; + } - /** - * Return true if the given {@code ch} is a non-ASCII space character - * as defined by RFC 3454, - * Appendix C.1.2. - */ - private static boolean nonAsciiSpace(char ch) - { - return ch == '\u00A0' - || ch == '\u1680' - || '\u2000' <= ch && ch <= '\u200B' - || ch == '\u202F' - || ch == '\u205F' - || ch == '\u3000'; - } + /** + * Return true if the given {@code ch} is a non-ASCII space character + * as defined by RFC 3454, + * Appendix C.1.2. + */ + private static boolean nonAsciiSpace(char ch) + { + return ch == '\u00A0' + || ch == '\u1680' + || '\u2000' <= ch && ch <= '\u200B' + || ch == '\u202F' + || ch == '\u205F' + || ch == '\u3000'; + } - /** - * Return true if the given {@code ch} is a "commonly mapped to nothing" character - * as defined by RFC 3454, - * Appendix B.1. - */ - private static boolean mappedToNothing(char ch) - { - return ch == '\u00AD' - || ch == '\u034F' - || ch == '\u1806' - || ch == '\u180B' - || ch == '\u180C' - || ch == '\u180D' - || ch == '\u200B' - || ch == '\u200C' - || ch == '\u200D' - || ch == '\u2060' - || '\uFE00' <= ch && ch <= '\uFE0F' - || ch == '\uFEFF'; - } + /** + * Return true if the given {@code ch} is a "commonly mapped to nothing" character + * as defined by RFC 3454, + * Appendix B.1. + */ + private static boolean mappedToNothing(char ch) + { + return ch == '\u00AD' + || ch == '\u034F' + || ch == '\u1806' + || ch == '\u180B' + || ch == '\u180C' + || ch == '\u180D' + || ch == '\u200B' + || ch == '\u200C' + || ch == '\u200D' + || ch == '\u2060' + || '\uFE00' <= ch && ch <= '\uFE0F' + || ch == '\uFEFF'; + } } \ No newline at end of file diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/SecurityHandler.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/SecurityHandler.java index f3642e5b9..36dcb04d7 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/SecurityHandler.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/SecurityHandler.java @@ -33,7 +33,6 @@ import java.util.IdentityHashMap; import java.util.Map; import java.util.Set; - import javax.crypto.Cipher; import javax.crypto.CipherInputStream; import javax.crypto.spec.IvParameterSpec; @@ -48,7 +47,6 @@ import com.tom_roush.pdfbox.io.IOUtils; import com.tom_roush.pdfbox.pdmodel.PDDocument; import com.tom_roush.pdfbox.util.Charsets; - /** * A security handler as described in the PDF specifications. * A security handler is responsible of documents protection. @@ -59,6 +57,7 @@ */ public abstract class SecurityHandler { + private static final short DEFAULT_KEY_LENGTH = 40; // see 7.6.2, page 58, PDF 32000-1:2008 @@ -87,7 +86,7 @@ public abstract class SecurityHandler // This solution keeps all different "equal" objects. // IdentityHashMap solves this problem and is also faster than a HashMap private final Set objects = - Collections.newSetFromMap(new IdentityHashMap()); + Collections.newSetFromMap(new IdentityHashMap()); private boolean useAES; @@ -179,7 +178,7 @@ public void setCustomSecureRandom(SecureRandom customSecureRandom) * @throws IOException If there is an error accessing data. */ public abstract void prepareForDecryption(PDEncryption encryption, COSArray documentIDArray, - DecryptionMaterial decryptionMaterial) throws IOException; + DecryptionMaterial decryptionMaterial) throws IOException; /** * Encrypt or decrypt a set of data. @@ -193,7 +192,7 @@ public abstract void prepareForDecryption(PDEncryption encryption, COSArray docu * @throws IOException If there is an error reading the data. */ private void encryptData(long objectNumber, long genNumber, InputStream data, - OutputStream output, boolean decrypt) throws IOException + OutputStream output, boolean decrypt) throws IOException { // Determine whether we're using Algorithm 1 (for RC4 and AES-128), or 1.A (for AES-256) if (useAES && encryptionKey.length == 32) @@ -261,7 +260,7 @@ private byte[] calcFinalKey(long objectNumber, long genNumber) * @throws IOException If there is an error reading the data. */ protected void encryptDataRC4(byte[] finalKey, InputStream input, OutputStream output) - throws IOException + throws IOException { rc4.setKey(finalKey); rc4.write(input, output); @@ -294,7 +293,7 @@ protected void encryptDataRC4(byte[] finalKey, byte[] input, OutputStream output * @throws IOException If there is an error reading the data. */ private void encryptDataAESother(byte[] finalKey, InputStream data, OutputStream output, boolean decrypt) - throws IOException + throws IOException { byte[] iv = new byte[16]; @@ -396,8 +395,8 @@ private boolean prepareAESInitializationVector(boolean decrypt, byte[] iv, Input if (ivSize != iv.length) { throw new IOException( - "AES initialization vector not fully read: only " - + ivSize + " bytes read instead of " + iv.length); + "AES initialization vector not fully read: only " + + ivSize + " bytes read instead of " + iv.length); } } else @@ -424,6 +423,28 @@ private SecureRandom getSecureRandom() return new SecureRandom(); } + /** + * This will decrypt a string if it is not in set of the objects. + * + * @param string The string to decrypt. + * @param objNum The object number. + * @param genNum The object generation Number. + * + * @return the encrypted/decrypted COS object + */ + private COSBase decryptStringIfAbsent(COSString string, long objNum, long genNum) + { + // PDFBOX-4477: only cache strings and streams, this improves speed and memory footprint + if (objects.contains(string)) + { + return string; + } + // replace the given COSString object with the encrypted/decrypted version + COSBase decryptedString = decryptString(string, objNum, genNum); + objects.add(decryptedString); + return decryptedString; + } + /** * This will dispatch to the correct method. * @@ -433,35 +454,46 @@ private SecureRandom getSecureRandom() * * @throws IOException If there is an error getting the stream data. */ - public void decrypt(COSBase obj, long objNum, long genNum) throws IOException + public COSBase decrypt(COSBase obj, long objNum, long genNum) throws IOException { // PDFBOX-4477: only cache strings and streams, this improves speed and memory footprint if (obj instanceof COSString) { - if (objects.contains(obj)) - { - return; - } - objects.add(obj); - decryptString((COSString) obj, objNum, genNum); + return decryptStringIfAbsent((COSString)obj, objNum, genNum); } else if (obj instanceof COSStream) { - if (objects.contains(obj)) - { - return; - } - objects.add(obj); - decryptStream((COSStream) obj, objNum, genNum); + return decryptStreamIfAbsent((COSStream)obj, objNum, genNum); } else if (obj instanceof COSDictionary) { - decryptDictionary((COSDictionary) obj, objNum, genNum); + return decryptDictionary((COSDictionary) obj, objNum, genNum); } else if (obj instanceof COSArray) { - decryptArray((COSArray) obj, objNum, genNum); + return decryptArray((COSArray) obj, objNum, genNum); } + return obj; + } + + /** + * This will decrypt a stream if it is not in set of the objects. + * + * @param stream The stream to decrypt. + * @param objNum The object number. + * @param genNum The object generation Number. + * + * @return the encrypted/decrypted COS object + */ + private COSBase decryptStreamIfAbsent(COSStream stream, long objNum, long genNum) throws IOException + { + if (!objects.contains(stream)) + { + objects.add(stream); + decryptStream(stream, objNum, genNum); + } + + return stream; } /** @@ -495,7 +527,7 @@ public void decryptStream(COSStream stream, long objNum, long genNum) throws IOE { // PDFBOX-3229 check case where metadata is not encrypted despite /EncryptMetadata missing InputStream is = stream.createRawInputStream(); - byte buf[] = new byte[10]; + byte[] buf = new byte[10]; IOUtils.populateBuffer(is, buf); is.close(); if (Arrays.equals(buf, " entry : dictionary.entrySet()) { if (isSignature && COSName.CONTENTS.equals(entry.getKey())) @@ -582,11 +618,25 @@ private void decryptDictionary(COSDictionary dictionary, long objNum, long genNu } COSBase value = entry.getValue(); // within a dictionary only the following kind of COS objects have to be decrypted - if (value instanceof COSString || value instanceof COSArray || value instanceof COSDictionary) + if (value instanceof COSString) { - decrypt(value, objNum, genNum); + entry.setValue(decryptStringIfAbsent((COSString)value, objNum, genNum)); + } + else if (value instanceof COSArray) + { + entry.setValue(decryptArray((COSArray) value, objNum, genNum)); + } + else if (value instanceof COSStream) + { + entry.setValue(decryptStreamIfAbsent((COSStream)value, objNum, genNum)); + } + else if (value instanceof COSDictionary) + { + entry.setValue(decryptDictionary((COSDictionary)value, objNum, genNum)); } } + + return dictionary; } /** @@ -596,13 +646,15 @@ private void decryptDictionary(COSDictionary dictionary, long objNum, long genNu * @param objNum The object number. * @param genNum The object generation number. * + * @return the decrypted COSString + * */ - private void decryptString(COSString string, long objNum, long genNum) + private COSBase decryptString(COSString string, long objNum, long genNum) { // String encrypted with identity filter if (COSName.IDENTITY.equals(stringFilterName)) { - return; + return string; } ByteArrayInputStream data = new ByteArrayInputStream(string.getBytes()); @@ -611,11 +663,13 @@ private void decryptString(COSString string, long objNum, long genNum) { encryptData(objNum, genNum, data, outputStream, true /* decrypt */); string.setValue(outputStream.toByteArray()); + return string; } catch (IOException ex) { Log.e("PdfBox-Android", "Failed to decrypt COSString of length " + string.getBytes().length + - " in object " + objNum + ": " + ex.getMessage()); + " in object " + objNum + ": " + ex.getMessage()); + return string; } } @@ -643,18 +697,22 @@ public void encryptString(COSString string, long objNum, int genNum) throws IOEx * @param objNum The object number. * @param genNum The object generation number. * + * @return the encrypted/decrypted COS object + * * @throws IOException If there is an error accessing the data. */ - private void decryptArray(COSArray array, long objNum, long genNum) throws IOException + private COSBase decryptArray(COSArray array, long objNum, long genNum) throws IOException { for (int i = 0; i < array.size(); i++) { - decrypt(array.get(i), objNum, genNum); + array.set(i, decrypt(array.get(i), objNum, genNum)); } + + return array; } /** - * Getter of the property keyLength. + * Getter of the property keyLength. * @return Returns the key length in bits. */ public int getKeyLength() @@ -663,7 +721,7 @@ public int getKeyLength() } /** - * Setter of the property keyLength. + * Setter of the property keyLength. * * @param keyLen The key length to set in bits. */ diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/SecurityHandlerFactory.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/SecurityHandlerFactory.java index 6d2f99070..faea814cf 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/SecurityHandlerFactory.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/SecurityHandlerFactory.java @@ -21,7 +21,6 @@ import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.Map; - /** * Manages security handlers for the application. * It follows the singleton pattern. @@ -37,22 +36,22 @@ public final class SecurityHandlerFactory public static final SecurityHandlerFactory INSTANCE = new SecurityHandlerFactory(); private final Map> nameToHandler = - new HashMap>(); + new HashMap>(); private final Map, - Class> policyToHandler = - new HashMap, - Class>(); + Class> policyToHandler = + new HashMap, + Class>(); private SecurityHandlerFactory() { registerHandler(StandardSecurityHandler.FILTER, - StandardSecurityHandler.class, - StandardProtectionPolicy.class); + StandardSecurityHandler.class, + StandardProtectionPolicy.class); registerHandler(PublicKeySecurityHandler.FILTER, - PublicKeySecurityHandler.class, - PublicKeyProtectionPolicy.class); + PublicKeySecurityHandler.class, + PublicKeyProtectionPolicy.class); } /** @@ -67,8 +66,8 @@ private SecurityHandlerFactory() * @param protectionPolicy protection policy class to register */ public void registerHandler(String name, - Class securityHandler, - Class protectionPolicy) + Class securityHandler, + Class protectionPolicy) { if (nameToHandler.containsKey(name)) { @@ -123,12 +122,12 @@ public SecurityHandler newSecurityHandlerForFilter(String name) * @return a new SecurityHandler instance, or null if none is available. */ private SecurityHandler newSecurityHandler(Class handlerClass, - Class[] argsClasses, Object[] args) + Class[] argsClasses, Object[] args) { try { Constructor ctor = - handlerClass.getDeclaredConstructor(argsClasses); + handlerClass.getDeclaredConstructor(argsClasses); return ctor.newInstance(args); } catch(NoSuchMethodException e) diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/SecurityProvider.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/SecurityProvider.java index e2172d667..6060cb5bc 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/SecurityProvider.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/SecurityProvider.java @@ -17,7 +17,6 @@ package com.tom_roush.pdfbox.pdmodel.encryption; import java.io.IOException; -import java.lang.reflect.InvocationTargetException; import java.security.Provider; import java.security.Security; @@ -47,35 +46,10 @@ public static Provider getProvider() throws IOException // TODO synchronize access if (provider == null) { - try - { - // Remove Android-provided BouncyCastle implementation - Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME); - Security.addProvider(new BouncyCastleProvider()); - Class providerClass = (Class) Class - .forName("org.bouncycastle.jce.provider.BouncyCastleProvider"); - provider = providerClass.getDeclaredConstructor().newInstance(); - } - catch (ClassNotFoundException ex) - { - throw new IOException(ex); - } - catch (InstantiationException ex) - { - throw new IOException(ex); - } - catch (IllegalAccessException ex) - { - throw new IOException(ex); - } - catch (NoSuchMethodException ex) - { - throw new IOException(ex); - } - catch (InvocationTargetException ex) - { - throw new IOException(ex); - } + // Remove Android-provided BouncyCastle implementation + Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME); + Security.addProvider(new BouncyCastleProvider()); + provider = new BouncyCastleProvider(); } return provider; } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/StandardSecurityHandler.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/StandardSecurityHandler.java index 90b0e043b..b423aecfc 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/StandardSecurityHandler.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/encryption/StandardSecurityHandler.java @@ -28,7 +28,7 @@ import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Arrays; - +import java.util.Random; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; @@ -38,7 +38,6 @@ import com.tom_roush.pdfbox.cos.COSString; import com.tom_roush.pdfbox.pdmodel.PDDocument; import com.tom_roush.pdfbox.util.Charsets; - /** * The standard security handler. This security handler protects document with password. * @see StandardProtectionPolicy to see how to protect document with this security handler. @@ -56,19 +55,22 @@ public final class StandardSecurityHandler extends SecurityHandler /** Standard padding for encryption. */ private static final byte[] ENCRYPT_PADDING = - { - (byte)0x28, (byte)0xBF, (byte)0x4E, (byte)0x5E, (byte)0x4E, - (byte)0x75, (byte)0x8A, (byte)0x41, (byte)0x64, (byte)0x00, - (byte)0x4E, (byte)0x56, (byte)0xFF, (byte)0xFA, (byte)0x01, - (byte)0x08, (byte)0x2E, (byte)0x2E, (byte)0x00, (byte)0xB6, - (byte)0xD0, (byte)0x68, (byte)0x3E, (byte)0x80, (byte)0x2F, - (byte)0x0C, (byte)0xA9, (byte)0xFE, (byte)0x64, (byte)0x53, - (byte)0x69, (byte)0x7A - }; + { + (byte)0x28, (byte)0xBF, (byte)0x4E, (byte)0x5E, (byte)0x4E, + (byte)0x75, (byte)0x8A, (byte)0x41, (byte)0x64, (byte)0x00, + (byte)0x4E, (byte)0x56, (byte)0xFF, (byte)0xFA, (byte)0x01, + (byte)0x08, (byte)0x2E, (byte)0x2E, (byte)0x00, (byte)0xB6, + (byte)0xD0, (byte)0x68, (byte)0x3E, (byte)0x80, (byte)0x2F, + (byte)0x0C, (byte)0xA9, (byte)0xFE, (byte)0x64, (byte)0x53, + (byte)0x69, (byte)0x7A + }; // hashes used for Algorithm 2.B, depending on remainder from E modulo 3 private static final String[] HASHES_2B = new String[] {"SHA-256", "SHA-384", "SHA-512"}; + // SecureRandom.getInstanceStrong() would be better, but sometimes blocks on Linux + private static final Random RANDOM = new SecureRandom(); + /** * Constructor. */ @@ -134,18 +136,20 @@ private int computeRevisionNumber(int version) */ @Override public void prepareForDecryption(PDEncryption encryption, COSArray documentIDArray, - DecryptionMaterial decryptionMaterial) - throws IOException + DecryptionMaterial decryptionMaterial) + throws IOException { if(!(decryptionMaterial instanceof StandardDecryptionMaterial)) { throw new IOException("Decryption material is not compatible with the document"); } + int encryptionVersion = encryption.getVersion(); // This is only used with security version 4 and 5. - if (encryption.getVersion() >= 4) { - setStreamFilterName(encryption.getStreamFilterName()); - setStringFilterName(encryption.getStringFilterName()); + if (encryptionVersion >= 4) + { + setStreamFilterName(encryption.getStreamFilterName()); + setStringFilterName(encryption.getStringFilterName()); } setDecryptMetadata(encryption.isEncryptMetaData()); StandardDecryptionMaterial material = (StandardDecryptionMaterial)decryptionMaterial; @@ -158,11 +162,11 @@ public void prepareForDecryption(PDEncryption encryption, COSArray documentIDArr int dicPermissions = encryption.getPermissions(); int dicRevision = encryption.getRevision(); - int dicLength = encryption.getVersion() == 1 ? 5 : encryption.getLength() / 8; + int dicLength = encryptionVersion == 1 ? 5 : encryption.getLength() / 8; - if (encryption.getVersion() == 4 || encryption.getVersion() == 5) + if (encryptionVersion == 4 || encryptionVersion == 5) { - // detect whether AES encryption is used. This assumes that the encryption algo is + // detect whether AES encryption is used. This assumes that the encryption algo is // stored in the PDCryptFilterDictionary // However, crypt filters are used only when V is 4 or 5. PDCryptFilterDictionary stdCryptFilterDictionary = encryption.getStdCryptFilterDictionary(); @@ -180,7 +184,7 @@ public void prepareForDecryption(PDEncryption encryption, COSArray documentIDArr if (newLength < dicLength) { Log.w("PdfBox-Android", "Using " + newLength + " bytes key length instead of " + - dicLength + " in AESV2 encryption?!"); + dicLength + " in AESV2 encryption?!"); dicLength = newLength; } } @@ -196,7 +200,7 @@ public void prepareForDecryption(PDEncryption encryption, COSArray documentIDArr if (newLength < dicLength) { Log.w("PdfBox-Android", "Using " + newLength + " bytes key length instead of " + - dicLength + " in AESV3 encryption?!"); + dicLength + " in AESV3 encryption?!"); dicLength = newLength; } } @@ -228,56 +232,50 @@ public void prepareForDecryption(PDEncryption encryption, COSArray documentIDArr AccessPermission currentAccessPermission; - if( isOwnerPassword(password.getBytes(passwordCharset), userKey, ownerKey, - dicPermissions, documentIDBytes, dicRevision, - dicLength, encryptMetadata) ) + byte[] encryptedKey; + byte[] passwordBytes = password.getBytes(passwordCharset); + boolean isOwnerPassword; + if (isOwnerPassword(passwordBytes, userKey, ownerKey, + dicPermissions, documentIDBytes, dicRevision, + dicLength, encryptMetadata) ) { currentAccessPermission = AccessPermission.getOwnerAccessPermission(); setCurrentAccessPermission(currentAccessPermission); - byte[] computedPassword; - if (dicRevision == 6 || dicRevision == 5) + if (dicRevision != 5 && dicRevision != 6) { - computedPassword = password.getBytes(passwordCharset); + passwordBytes = getUserPassword(passwordBytes, + ownerKey, dicRevision, dicLength ); } - else - { - computedPassword = getUserPassword(password.getBytes(passwordCharset), - ownerKey, dicRevision, dicLength ); - } - - setEncryptionKey( - computeEncryptedKey( - computedPassword, - ownerKey, userKey, oe, ue, - dicPermissions, - documentIDBytes, - dicRevision, - dicLength, - encryptMetadata, true)); + isOwnerPassword = true; } - else if( isUserPassword(password.getBytes(passwordCharset), userKey, ownerKey, - dicPermissions, documentIDBytes, dicRevision, - dicLength, encryptMetadata) ) + else if (isUserPassword(passwordBytes, userKey, ownerKey, + dicPermissions, documentIDBytes, dicRevision, + dicLength, encryptMetadata) ) { currentAccessPermission = new AccessPermission(dicPermissions); currentAccessPermission.setReadOnly(); setCurrentAccessPermission(currentAccessPermission); - - setEncryptionKey( - computeEncryptedKey( - password.getBytes(passwordCharset), - ownerKey, userKey, oe, ue, - dicPermissions, - documentIDBytes, - dicRevision, - dicLength, - encryptMetadata, false)); + isOwnerPassword = false; } else { throw new InvalidPasswordException("Cannot decrypt PDF, the password is incorrect"); } + encryptedKey = computeEncryptedKey( + passwordBytes, + ownerKey, userKey, oe, ue, + dicPermissions, + documentIDBytes, + dicRevision, + dicLength, + encryptMetadata, isOwnerPassword); + if (dicRevision == 4 && encryptedKey.length < 16) + { + Log.i("PdfBox-Android", "PDFBOX-5955: padding RC4 key to length 16"); + encryptedKey = Arrays.copyOf(encryptedKey, 16); + } + setEncryptionKey(encryptedKey); if (dicRevision == 6 || dicRevision == 5) { @@ -308,7 +306,7 @@ private void validatePerms(PDEncryption encryption, int dicPermissions, boolean { try { - // "Decrypt the 16-byte Perms string using AES-256 in ECB mode with an + // "Decrypt the 16-byte Perms string using AES-256 in ECB mode with an // initialization vector of zero and the file encryption key as the key." Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(getEncryptionKey(), "AES")); @@ -320,15 +318,15 @@ private void validatePerms(PDEncryption encryption, int dicPermissions, boolean Log.w("PdfBox-Android", "Verification of permissions failed (constant)"); } - // "Bytes 0-3 of the decrypted Perms entry, treated as a little-endian integer, + // "Bytes 0-3 of the decrypted Perms entry, treated as a little-endian integer, // are the user permissions. They should match the value in the P key." int permsP = perms[0] & 0xFF | (perms[1] & 0xFF) << 8 | (perms[2] & 0xFF) << 16 | - (perms[3] & 0xFF) << 24; + (perms[3] & 0xFF) << 24; if (permsP != dicPermissions) { Log.w("PdfBox-Android", "Verification of permissions failed (" + String.format("%08X",permsP) + - " != " + String.format("%08X",dicPermissions) + ")"); + " != " + String.format("%08X",dicPermissions) + ")"); } if (encryptMetadata && perms[8] != 'T' || !encryptMetadata && perms[8] != 'F') @@ -404,7 +402,7 @@ public void prepareDocumentForEncryption(PDDocument document) throws IOException else { prepareEncryptionDictRev2345(ownerPassword, userPassword, encryptionDictionary, permissionInt, - document, revision, length); + document, revision, length); } document.setEncryptionDictionary( encryptionDictionary ); @@ -412,50 +410,49 @@ public void prepareDocumentForEncryption(PDDocument document) throws IOException } private void prepareEncryptionDictRev6(String ownerPassword, String userPassword, - PDEncryption encryptionDictionary, int permissionInt) - throws IOException + PDEncryption encryptionDictionary, int permissionInt) + throws IOException { try { - SecureRandom rnd = new SecureRandom(); Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); // make a random 256-bit file encryption key setEncryptionKey(new byte[32]); - rnd.nextBytes(getEncryptionKey()); + RANDOM.nextBytes(getEncryptionKey()); // Algorithm 8a: Compute U byte[] userPasswordBytes = truncate127(userPassword.getBytes(Charsets.UTF_8)); byte[] userValidationSalt = new byte[8]; byte[] userKeySalt = new byte[8]; - rnd.nextBytes(userValidationSalt); - rnd.nextBytes(userKeySalt); + RANDOM.nextBytes(userValidationSalt); + RANDOM.nextBytes(userKeySalt); byte[] hashU = computeHash2B(concat(userPasswordBytes, userValidationSalt), - userPasswordBytes, null); + userPasswordBytes, null); byte[] u = concat(hashU, userValidationSalt, userKeySalt); // Algorithm 8b: Compute UE byte[] hashUE = computeHash2B(concat(userPasswordBytes, userKeySalt), - userPasswordBytes, null); + userPasswordBytes, null); cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(hashUE, "AES"), - new IvParameterSpec(new byte[16])); + new IvParameterSpec(new byte[16])); byte[] ue = cipher.doFinal(getEncryptionKey()); // Algorithm 9a: Compute O byte[] ownerPasswordBytes = truncate127(ownerPassword.getBytes(Charsets.UTF_8)); byte[] ownerValidationSalt = new byte[8]; byte[] ownerKeySalt = new byte[8]; - rnd.nextBytes(ownerValidationSalt); - rnd.nextBytes(ownerKeySalt); + RANDOM.nextBytes(ownerValidationSalt); + RANDOM.nextBytes(ownerKeySalt); byte[] hashO = computeHash2B(concat(ownerPasswordBytes, ownerValidationSalt, u), - ownerPasswordBytes, u); + ownerPasswordBytes, u); byte[] o = concat(hashO, ownerValidationSalt, ownerKeySalt); // Algorithm 9b: Compute OE byte[] hashOE = computeHash2B(concat(ownerPasswordBytes, ownerKeySalt, u), - ownerPasswordBytes, u); + ownerPasswordBytes, u); cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(hashOE, "AES"), - new IvParameterSpec(new byte[16])); + new IvParameterSpec(new byte[16])); byte[] oe = cipher.doFinal(getEncryptionKey()); // Set keys and other required constants in encryption dictionary @@ -482,11 +479,11 @@ private void prepareEncryptionDictRev6(String ownerPassword, String userPassword perms[11] = 'b'; for (int i = 12; i <= 15; i++) { - perms[i] = (byte) rnd.nextInt(); + perms[i] = (byte) RANDOM.nextInt(); } cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(getEncryptionKey(), "AES"), - new IvParameterSpec(new byte[16])); + new IvParameterSpec(new byte[16])); byte[] permsEnc = cipher.doFinal(perms); @@ -500,20 +497,22 @@ private void prepareEncryptionDictRev6(String ownerPassword, String userPassword } private void prepareEncryptionDictRev2345(String ownerPassword, String userPassword, - PDEncryption encryptionDictionary, int permissionInt, PDDocument document, - int revision, int length) - throws IOException + PDEncryption encryptionDictionary, int permissionInt, PDDocument document, + int revision, int length) + throws IOException { COSArray idArray = document.getDocument().getDocumentID(); //check if the document has an id yet. If it does not then generate one + byte[] userPasswordBytes = userPassword.getBytes(Charsets.ISO_8859_1); + byte[] ownerPasswordBytes = ownerPassword.getBytes(Charsets.ISO_8859_1); if (idArray == null || idArray.size() < 2) { MessageDigest md = MessageDigests.getMD5(); BigInteger time = BigInteger.valueOf(System.currentTimeMillis()); md.update(time.toByteArray()); - md.update(ownerPassword.getBytes(Charsets.ISO_8859_1)); - md.update(userPassword.getBytes(Charsets.ISO_8859_1)); + md.update(ownerPasswordBytes); + md.update(userPasswordBytes); md.update(document.getDocument().toString().getBytes(Charsets.ISO_8859_1)); byte[] id = md.digest(this.toString().getBytes(Charsets.ISO_8859_1)); @@ -528,15 +527,16 @@ private void prepareEncryptionDictRev2345(String ownerPassword, String userPassw COSString id = (COSString) idArray.getObject(0); byte[] ownerBytes = computeOwnerPassword( - ownerPassword.getBytes(Charsets.ISO_8859_1), - userPassword.getBytes(Charsets.ISO_8859_1), revision, length); + ownerPasswordBytes, + userPasswordBytes, revision, length); + byte[] idBytes = id.getBytes(); byte[] userBytes = computeUserPassword( - userPassword.getBytes(Charsets.ISO_8859_1), - ownerBytes, permissionInt, id.getBytes(), revision, length, true); + userPasswordBytes, + ownerBytes, permissionInt, idBytes, revision, length, true); - setEncryptionKey(computeEncryptedKey(userPassword.getBytes(Charsets.ISO_8859_1), ownerBytes, - null, null, null, permissionInt, id.getBytes(), revision, length, true, false)); + setEncryptionKey(computeEncryptedKey(userPasswordBytes, ownerBytes, + null, null, null, permissionInt, idBytes, revision, length, true, false)); encryptionDictionary.setOwnerKey(ownerBytes); encryptionDictionary.setUserKey(userBytes); @@ -575,8 +575,8 @@ private void prepareEncryptionDictAES(PDEncryption encryptionDictionary, COSName * @throws IOException If there is an error accessing data. */ public boolean isOwnerPassword(byte[] ownerPassword, byte[] user, byte[] owner, - int permissions, byte[] id, int encRevision, int keyLengthInBytes, - boolean encryptMetadata) throws IOException + int permissions, byte[] id, int encRevision, int keyLengthInBytes, + boolean encryptMetadata) throws IOException { if (encRevision == 6 || encRevision == 5) { @@ -602,13 +602,13 @@ public boolean isOwnerPassword(byte[] ownerPassword, byte[] user, byte[] owner, hash = computeHash2A(truncatedOwnerPassword, oValidationSalt, user); } - return Arrays.equals(hash, oHash); + return MessageDigest.isEqual(hash, oHash); } else { byte[] userPassword = getUserPassword( ownerPassword, owner, encRevision, keyLengthInBytes ); return isUserPassword( userPassword, user, owner, permissions, id, encRevision, keyLengthInBytes, - encryptMetadata ); + encryptMetadata ); } } @@ -625,7 +625,7 @@ public boolean isOwnerPassword(byte[] ownerPassword, byte[] user, byte[] owner, * @throws IOException If there is an error accessing data while generating the user password. */ public byte[] getUserPassword( byte[] ownerPassword, byte[] owner, int encRevision, - int length ) throws IOException + int length ) throws IOException { ByteArrayOutputStream result = new ByteArrayOutputStream(); byte[] rc4Key = computeRC4key(ownerPassword, encRevision, length); @@ -675,9 +675,9 @@ else if( encRevision == 3 || encRevision == 4) * @throws IOException If there is an error with encryption. */ public byte[] computeEncryptedKey(byte[] password, byte[] o, byte[] u, byte[] oe, byte[] ue, - int permissions, byte[] id, int encRevision, int keyLengthInBytes, - boolean encryptMetadata, boolean isOwnerPassword) - throws IOException + int permissions, byte[] id, int encRevision, int keyLengthInBytes, + boolean encryptMetadata, boolean isOwnerPassword) + throws IOException { if (encRevision == 6 || encRevision == 5) { @@ -690,7 +690,7 @@ public byte[] computeEncryptedKey(byte[] password, byte[] o, byte[] u, byte[] oe } private byte[] computeEncryptedKeyRev234(byte[] password, byte[] o, int permissions, - byte[] id, boolean encryptMetadata, int length, int encRevision) + byte[] id, boolean encryptMetadata, int length, int encRevision) { //Algorithm 2, based on MD5 @@ -733,8 +733,8 @@ private byte[] computeEncryptedKeyRev234(byte[] password, byte[] o, int permissi } private byte[] computeEncryptedKeyRev56(byte[] password, boolean isOwnerPassword, - byte[] o, byte[] u, byte[] oe, byte[] ue, int encRevision) - throws IOException + byte[] o, byte[] u, byte[] oe, byte[] ue, int encRevision) + throws IOException { byte[] hash, fileKeyEnc; @@ -807,12 +807,12 @@ private byte[] computeEncryptedKeyRev56(byte[] password, boolean isOwnerPassword * @throws IOException if the password could not be computed */ public byte[] computeUserPassword(byte[] password, byte[] owner, int permissions, - byte[] id, int encRevision, int keyLengthInBytes, - boolean encryptMetadata) throws IOException + byte[] id, int encRevision, int keyLengthInBytes, + boolean encryptMetadata) throws IOException { ByteArrayOutputStream result = new ByteArrayOutputStream(); byte[] encKey = computeEncryptedKey( password, owner, null, null, null, permissions, - id, encRevision, keyLengthInBytes, encryptMetadata, true ); + id, encRevision, keyLengthInBytes, encryptMetadata, true ); if( encRevision == 2 ) { @@ -861,7 +861,7 @@ else if( encRevision == 3 || encRevision == 4 ) * @throws IOException if the owner password could not be computed */ public byte[] computeOwnerPassword(byte[] ownerPassword, byte[] userPassword, - int encRevision, int length ) throws IOException + int encRevision, int length ) throws IOException { if( encRevision == 2 && length != 5 ) { @@ -894,24 +894,32 @@ public byte[] computeOwnerPassword(byte[] ownerPassword, byte[] userPassword, } // steps (a) to (d) of "Algorithm 3: Computing the encryption dictionary’s O (owner password) value". - private byte[] computeRC4key(byte[] ownerPassword, int encRevision, int length) + private byte[] computeRC4key(byte[] ownerPassword, int encRevision, int length) throws IOException { - MessageDigest md = MessageDigests.getMD5(); - byte[] digest = md.digest(truncateOrPad(ownerPassword)); - if (encRevision == 3 || encRevision == 4) + try { - for (int i = 0; i < 50; i++) + MessageDigest md = MessageDigests.getMD5(); + byte[] digest = md.digest(truncateOrPad(ownerPassword)); + if (encRevision == 3 || encRevision == 4) { - // this deviates from the spec - however, omitting the length - // parameter prevents the file to be opened in Adobe Reader - // with the owner password when the key length is 40 bit (= 5 bytes) - md.update(digest, 0, length); - digest = md.digest(); + for (int i = 0; i < 50; i++) + { + // this deviates from the spec - however, omitting the length + // parameter prevents the file to be opened in Adobe Reader + // with the owner password when the key length is 40 bit (= 5 bytes) + md.update(digest, 0, length); + digest = md.digest(); + } } + byte[] rc4Key = new byte[length]; + System.arraycopy(digest, 0, rc4Key, 0, length); + return rc4Key; + } + catch (IllegalArgumentException ex) + { + // PDFBOX-6115: happens with illegal key length + throw new IOException(ex); } - byte[] rc4Key = new byte[length]; - System.arraycopy(digest, 0, rc4Key, 0, length); - return rc4Key; } @@ -928,7 +936,7 @@ private byte[] truncateOrPad( byte[] password ) int bytesBeforePad = Math.min( password.length, padded.length ); System.arraycopy( password, 0, padded, 0, bytesBeforePad ); System.arraycopy( ENCRYPT_PADDING, 0, padded, bytesBeforePad, - ENCRYPT_PADDING.length-bytesBeforePad ); + ENCRYPT_PADDING.length-bytesBeforePad ); return padded; } @@ -949,8 +957,8 @@ private byte[] truncateOrPad( byte[] password ) * @throws IOException If there is an error accessing data. */ public boolean isUserPassword(byte[] password, byte[] user, byte[] owner, int permissions, - byte[] id, int encRevision, int keyLengthInBytes, boolean encryptMetadata) - throws IOException + byte[] id, int encRevision, int keyLengthInBytes, boolean encryptMetadata) + throws IOException { switch (encRevision) { @@ -958,7 +966,7 @@ public boolean isUserPassword(byte[] password, byte[] user, byte[] owner, int pe case 3: case 4: return isUserPassword234(password, user, owner, permissions, id, encRevision, - keyLengthInBytes, encryptMetadata); + keyLengthInBytes, encryptMetadata); case 5: case 6: return isUserPassword56(password, user, encRevision); @@ -968,19 +976,19 @@ public boolean isUserPassword(byte[] password, byte[] user, byte[] owner, int pe } private boolean isUserPassword234(byte[] password, byte[] user, byte[] owner, int permissions, - byte[] id, int encRevision, int length, boolean encryptMetadata) - throws IOException + byte[] id, int encRevision, int length, boolean encryptMetadata) + throws IOException { byte[] passwordBytes = computeUserPassword(password, owner, permissions, id, encRevision, - length, encryptMetadata); + length, encryptMetadata); if (encRevision == 2) { - return Arrays.equals(user, passwordBytes); + return MessageDigest.isEqual(user, passwordBytes); } else { // compare first 16 bytes only - return Arrays.equals(Arrays.copyOf(user, 16), Arrays.copyOf(passwordBytes, 16)); + return MessageDigest.isEqual(Arrays.copyOf(user, 16), Arrays.copyOf(passwordBytes, 16)); } } @@ -1002,7 +1010,7 @@ private boolean isUserPassword56(byte[] password, byte[] user, int encRevision) hash = computeHash2A(truncatedPassword, uValidationSalt, null); } - return Arrays.equals(hash, uHash); + return MessageDigest.isEqual(hash, uHash); } /** @@ -1022,18 +1030,18 @@ private boolean isUserPassword56(byte[] password, byte[] user, int encRevision) * @throws IOException If there is an error accessing data. */ public boolean isUserPassword(String password, byte[] user, byte[] owner, int permissions, - byte[] id, int encRevision, int keyLengthInBytes, boolean encryptMetadata) - throws IOException + byte[] id, int encRevision, int keyLengthInBytes, boolean encryptMetadata) + throws IOException { if (encRevision == 6 || encRevision == 5) { return isUserPassword(password.getBytes(Charsets.UTF_8), user, owner, permissions, id, - encRevision, keyLengthInBytes, encryptMetadata); + encRevision, keyLengthInBytes, encryptMetadata); } else { return isUserPassword(password.getBytes(Charsets.ISO_8859_1), user, owner, permissions, id, - encRevision, keyLengthInBytes, encryptMetadata); + encRevision, keyLengthInBytes, encryptMetadata); } } @@ -1054,36 +1062,17 @@ public boolean isUserPassword(String password, byte[] user, byte[] owner, int pe * @throws IOException If there is an error accessing data. */ public boolean isOwnerPassword(String password, byte[] user, byte[] owner, int permissions, - byte[] id, int encRevision, int keyLengthInBytes, boolean encryptMetadata) - throws IOException + byte[] id, int encRevision, int keyLengthInBytes, boolean encryptMetadata) + throws IOException { return isOwnerPassword(password.getBytes(Charsets.ISO_8859_1), user,owner,permissions, id, - encRevision, keyLengthInBytes, encryptMetadata); + encRevision, keyLengthInBytes, encryptMetadata); } // Algorithm 2.A from ISO 32000-1 private byte[] computeHash2A(byte[] password, byte[] salt, byte[] u) throws IOException { - byte[] userKey; - if (u == null) - { - userKey = new byte[0]; - } - else if (u.length < 48) - { - throw new IOException("Bad U length"); - } - else if (u.length > 48) - { - // must truncate - userKey = new byte[48]; - System.arraycopy(u, 0, userKey, 0, 48); - } - else - { - userKey = u; - } - + byte[] userKey = adjustUserKey(u); byte[] truncatedPassword = truncate127(password); byte[] input = concat(truncatedPassword, salt, userKey); return computeHash2B(input, truncatedPassword, userKey); @@ -1091,7 +1080,7 @@ else if (u.length > 48) // Algorithm 2.B from ISO 32000-2 private static byte[] computeHash2B(byte[] input, byte[] password, byte[] userKey) - throws IOException + throws IOException { try { @@ -1139,7 +1128,7 @@ private static byte[] computeHash2B(byte[] input, byte[] password, byte[] userKe byte[] eFirst = new byte[16]; System.arraycopy(e, 0, eFirst, 0, 16); BigInteger bi = new BigInteger(1, eFirst); - BigInteger remainder = bi.mod(new BigInteger("3")); + BigInteger remainder = bi.mod(BigInteger.valueOf(3)); String nextHash = HASHES_2B[remainder.intValue()]; md = MessageDigest.getInstance(nextHash); @@ -1164,12 +1153,32 @@ private static byte[] computeHash2B(byte[] input, byte[] password, byte[] userKe } } - private static byte[] computeSHA256(byte[] input, byte[] password, byte[] userKey) + private static byte[] computeSHA256(byte[] input, byte[] password, byte[] userKey) throws IOException { MessageDigest md = MessageDigests.getSHA256(); md.update(input); md.update(password); - return userKey == null ? md.digest() : md.digest(userKey); + return md.digest(adjustUserKey(userKey)); + } + + private static byte[] adjustUserKey(byte[] u) throws IOException + { + if (u == null) + { + return new byte[0]; + } + if (u.length < 48) + { + throw new IOException("Bad U length"); + } + if (u.length > 48) + { + // must truncate + byte[] userKey = new byte[48]; + System.arraycopy(u, 0, userKey, 0, 48); + return userKey; + } + return u; } private static byte[] concat(byte[] a, byte[] b) diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotation.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotation.java index d5f657d4c..85456d453 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotation.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotation.java @@ -39,14 +39,12 @@ import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDBorderEffectDictionary; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDBorderStyleDictionary; import com.tom_roush.pdfbox.util.DateConverter; - import org.w3c.dom.CDATASection; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.Text; - /** * This represents an FDF annotation that is part of the FDF document. * @@ -56,6 +54,7 @@ * */ public abstract class FDFAnnotation implements COSObjectable { + /** * An annotation flag. */ @@ -200,16 +199,8 @@ else if (flagToken.equals("togglenoview")) { throw new IOException("Error: missing attribute 'rect'"); } - String[] rectValues = rect.split(","); - if (rectValues.length != 4) - { - throw new IOException("Error: wrong amount of numbers in attribute 'rect'"); - } - float[] values = new float[4]; - for (int i = 0; i < 4; i++) - { - values[i] = Float.parseFloat(rectValues[i]); - } + float[] values = parseRectangleAttributes( + rect, "Error: wrong amount of numbers in attribute 'rect'"); COSArray array = new COSArray(); array.setFloatArray(values); setRectangle(new PDRectangle(array)); @@ -233,7 +224,10 @@ else if (flagToken.equals("togglenoview")) // not conforming to spec, but qoppa produces it and Adobe accepts it intent = element.getAttribute("IT"); } - setIntent(intent); + if (!intent.isEmpty()) + { + setIntent(intent); + } XPath xpath = XPathFactory.newInstance().newXPath(); try @@ -248,7 +242,7 @@ else if (flagToken.equals("togglenoview")) try { Node richContents = (Node) xpath.evaluate("contents-richtext[1]", element, - XPathConstants.NODE); + XPathConstants.NODE); if (richContents != null) { setRichContents(richContentsToString(richContents, true)); @@ -273,36 +267,36 @@ else if (flagToken.equals("togglenoview")) { if (style.equals("dash")) { - borderStyle.setStyle("D"); + borderStyle.setStyle(PDBorderStyleDictionary.STYLE_DASHED); } else if (style.equals("bevelled")) { - borderStyle.setStyle("B"); + borderStyle.setStyle(PDBorderStyleDictionary.STYLE_BEVELED); } else if (style.equals("inset")) { - borderStyle.setStyle("I"); + borderStyle.setStyle(PDBorderStyleDictionary.STYLE_INSET); } else if (style.equals("underline")) { - borderStyle.setStyle("U"); + borderStyle.setStyle(PDBorderStyleDictionary.STYLE_UNDERLINE); } else if (style.equals("cloudy")) { - borderStyle.setStyle("S"); + borderStyle.setStyle(PDBorderStyleDictionary.STYLE_SOLID); PDBorderEffectDictionary borderEffect = new PDBorderEffectDictionary(); - borderEffect.setStyle("C"); + borderEffect.setStyle(PDBorderEffectDictionary.STYLE_CLOUDY); String intensity = element.getAttribute("intensity"); if (intensity != null && !intensity.isEmpty()) { borderEffect.setIntensity(Float.parseFloat(element - .getAttribute("intensity"))); + .getAttribute("intensity"))); } setBorderEffect(borderEffect); } else { - borderStyle.setStyle("S"); + borderStyle.setStyle(PDBorderStyleDictionary.STYLE_SOLID); } } String dashes = element.getAttribute("dashes"); @@ -316,8 +310,49 @@ else if (style.equals("cloudy")) } borderStyle.setDashStyle(dashPattern); } - setBorderStyle(borderStyle); } + setBorderStyle(borderStyle); + } + + final float[] parseRectangleAttributes(String rect, String errorMessage) throws IOException + { + String[] rectValues = rect.split(","); + if (rectValues.length != 4) + { + throw new IOException(errorMessage); + } + float[] values = new float[4]; + values[0] = Float.parseFloat(rectValues[0]); + values[1] = Float.parseFloat(rectValues[1]); + values[2] = Float.parseFloat(rectValues[2]); + values[3] = Float.parseFloat(rectValues[3]); + return values; + } + + final float[] parseFloats(String[] srcValues) + { + float[] values = new float[srcValues.length]; + for (int i = 0; i < srcValues.length; i++) + { + values[i] = Float.parseFloat(srcValues[i]); + } + return values; + } + + final PDRectangle createRectangleFromAttributes(String rect, String errorMessage) throws IOException + { + String[] rectValues = rect.split(","); + if (rectValues.length != 4) + { + throw new IOException(errorMessage); + } + PDRectangle rectangle = new PDRectangle(); + rectangle.setLowerLeftX(Float.parseFloat(rectValues[0])); + rectangle.setLowerLeftY(Float.parseFloat(rectValues[1])); + rectangle.setUpperRightX(Float.parseFloat(rectValues[2])); + rectangle.setUpperRightY(Float.parseFloat(rectValues[3])); + + return rectangle; } /** @@ -406,7 +441,7 @@ else if (FDFAnnotationUnderline.SUBTYPE.equals(fdfDicName)) else { Log.w("PdfBox-Android", "Unknown or unsupported annotation type '" - + fdfDicName + "'"); + + fdfDicName + "'"); } } return retval; @@ -455,9 +490,14 @@ public final void setPage(int page) * @return The annotation color, or null if there is none. */ public AWTColor getColor() + { + return getColor(COSName.C); + } + + final AWTColor getColor(COSName colorName) { AWTColor retval = null; - COSArray array = (COSArray) annot.getDictionaryObject(COSName.C); + COSArray array = annot.getCOSArray(colorName); if (array != null) { float[] rgb = array.toFloatArray(); @@ -998,16 +1038,16 @@ private String richContentsToString(Node node, boolean root) } else if (child instanceof CDATASection) { - sb.append(""); + sb.append(""); } else if (child instanceof Text) { - String cdata = ((Text) child).getData(); - if (cdata!=null) - { - cdata = cdata.replace("&", "&").replace("<", "<"); - } - sb.append(cdata); + String cdata = ((Text) child).getData(); + if (cdata!=null) + { + cdata = cdata.replace("&", "&").replace("<", "<"); + } + sb.append(cdata); } } if (root) @@ -1023,12 +1063,12 @@ else if (child instanceof Text) String attributeNodeValue = attribute.getNodeValue(); if (attributeNodeValue!=null) { - attributeNodeValue = attributeNodeValue.replace("\"", """); + attributeNodeValue = attributeNodeValue.replace("\"", """); } builder.append(String.format(" %s=\"%s\"", attribute.getNodeName(), - attributeNodeValue)); + attributeNodeValue)); } return String.format("<%s%s>%s", node.getNodeName(), builder.toString(), - sb.toString(), node.getNodeName()); + sb.toString(), node.getNodeName()); } } \ No newline at end of file diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationCaret.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationCaret.java index edd5a1d39..e1be13776 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationCaret.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationCaret.java @@ -23,7 +23,6 @@ import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.common.PDRectangle; import org.w3c.dom.Element; - /** * This represents a Caret FDF annotation. * @@ -81,16 +80,9 @@ private void initFringe(Element element) throws IOException String fringe = element.getAttribute("fringe"); if (fringe != null && !fringe.isEmpty()) { - String[] fringeValues = fringe.split(","); - if (fringeValues.length != 4) - { - throw new IOException("Error: wrong amount of numbers in attribute 'fringe'"); - } - PDRectangle rect = new PDRectangle(); - rect.setLowerLeftX(Float.parseFloat(fringeValues[0])); - rect.setLowerLeftY(Float.parseFloat(fringeValues[1])); - rect.setUpperRightX(Float.parseFloat(fringeValues[2])); - rect.setUpperRightY(Float.parseFloat(fringeValues[3])); + PDRectangle rect = createRectangleFromAttributes( + fringe, "Error: wrong amount of numbers in attribute 'fringe'"); + setFringe(rect); } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationCircle.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationCircle.java index 1542605a0..66423136e 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationCircle.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationCircle.java @@ -23,9 +23,7 @@ import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.common.PDRectangle; - import org.w3c.dom.Element; - /** * This represents a Circle FDF annotation. * @@ -84,16 +82,8 @@ private void initFringe(Element element) throws IOException String fringe = element.getAttribute("fringe"); if (fringe != null && !fringe.isEmpty()) { - String[] fringeValues = fringe.split(","); - if (fringeValues.length != 4) - { - throw new IOException("Error: wrong amount of numbers in attribute 'fringe'"); - } - PDRectangle rect = new PDRectangle(); - rect.setLowerLeftX(Float.parseFloat(fringeValues[0])); - rect.setLowerLeftY(Float.parseFloat(fringeValues[1])); - rect.setUpperRightX(Float.parseFloat(fringeValues[2])); - rect.setUpperRightY(Float.parseFloat(fringeValues[3])); + PDRectangle rect = createRectangleFromAttributes( + fringe, "Error: wrong amount of numbers in attribute 'fringe'"); setFringe(rect); } } @@ -122,17 +112,7 @@ public final void setInteriorColor(AWTColor color) */ public AWTColor getInteriorColor() { - AWTColor retval = null; - COSArray array = (COSArray) annot.getDictionaryObject(COSName.IC); - if (array != null) - { - float[] rgb = array.toFloatArray(); - if (rgb.length >= 3) - { - retval = new AWTColor(rgb[0], rgb[1], rgb[2]); - } - } - return retval; + return getColor(COSName.IC); } /** diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationFreeText.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationFreeText.java index 243ac82d0..97d3ebc9d 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationFreeText.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationFreeText.java @@ -19,7 +19,6 @@ import android.util.Log; import java.io.IOException; - import javax.xml.xpath.XPath; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; @@ -28,9 +27,7 @@ import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.common.PDRectangle; - import org.w3c.dom.Element; - /** * This represents a FreeText FDF annotation. * @@ -38,6 +35,7 @@ */ public class FDFAnnotationFreeText extends FDFAnnotation { + /** * COS Model value for SubType entry. */ @@ -104,16 +102,8 @@ private void initFringe(Element element) throws IOException String fringe = element.getAttribute("fringe"); if (fringe != null && !fringe.isEmpty()) { - String[] fringeValues = fringe.split(","); - if (fringeValues.length != 4) - { - throw new IOException("Error: wrong amount of numbers in attribute 'fringe'"); - } - PDRectangle rect = new PDRectangle(); - rect.setLowerLeftX(Float.parseFloat(fringeValues[0])); - rect.setLowerLeftY(Float.parseFloat(fringeValues[1])); - rect.setUpperRightX(Float.parseFloat(fringeValues[2])); - rect.setUpperRightY(Float.parseFloat(fringeValues[3])); + PDRectangle rect = createRectangleFromAttributes( + fringe, "Error: wrong amount of numbers in attribute 'fringe'"); setFringe(rect); } } @@ -124,11 +114,7 @@ private void initCallout(Element element) if (callout != null && !callout.isEmpty()) { String[] calloutValues = callout.split(","); - float[] values = new float[calloutValues.length]; - for (int i = 0; i < calloutValues.length; i++) - { - values[i] = Float.parseFloat(calloutValues[i]); - } + float[] values = parseFloats(calloutValues); setCallout(values); } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationInk.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationInk.java index 1e9546f0d..3b086003c 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationInk.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationInk.java @@ -21,7 +21,6 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; - import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; @@ -31,11 +30,9 @@ import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; - import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; - /** * This represents a Ink FDF annotation. * @@ -83,7 +80,7 @@ public FDFAnnotationInk(Element element) throws IOException try { NodeList gestures = (NodeList) xpath.evaluate("inklist/gesture", element, - XPathConstants.NODESET); + XPathConstants.NODESET); if (gestures.getLength() == 0) { throw new IOException("Error: missing element 'gesture'"); @@ -95,12 +92,8 @@ public FDFAnnotationInk(Element element) throws IOException if (node instanceof Element) { String gesture = node.getFirstChild().getNodeValue(); - String[] gestureValues = gesture.split(",|;"); - float[] values = new float[gestureValues.length]; - for (int j = 0; j < gestureValues.length; j++) - { - values[j] = Float.parseFloat(gestureValues[j]); - } + String[] gestureValues = gesture.split("[,;]"); + float[] values = parseFloats(gestureValues); inklist.add(values); } } @@ -143,7 +136,7 @@ public List getInkList() COSArray array = (COSArray) annot.getDictionaryObject(COSName.INKLIST); if (array != null) { - List retval = new ArrayList(); + List retval = new ArrayList(array.size()); for (COSBase entry : array) { retval.add(((COSArray) entry).toFloatArray()); diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationLine.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationLine.java index dbdc70594..52b3955a6 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationLine.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationLine.java @@ -24,9 +24,7 @@ import com.tom_roush.pdfbox.cos.COSFloat; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationLine; - import org.w3c.dom.Element; - /** * This represents a Line FDF annotation. * @@ -45,7 +43,6 @@ public class FDFAnnotationLine extends FDFAnnotation */ public FDFAnnotationLine() { - super(); annot.setName(COSName.SUBTYPE, SUBTYPE); } @@ -82,16 +79,8 @@ public FDFAnnotationLine(Element element) throws IOException throw new IOException("Error: missing attribute 'end'"); } String line = startCoords + "," + endCoords; - String[] lineValues = line.split(","); - if (lineValues.length != 4) - { - throw new IOException("Error: wrong amount of line coordinates"); - } - float[] values = new float[4]; - for (int i = 0; i < 4; i++) - { - values[i] = Float.parseFloat(lineValues[i]); - } + float[] values = parseRectangleAttributes( + line, "Error: wrong amount of line coordinates"); setLine(values); String leaderLine = element.getAttribute("leaderLength"); @@ -131,27 +120,27 @@ public FDFAnnotationLine(Element element) throws IOException } String caption = element.getAttribute("caption"); - if (caption != null && !caption.isEmpty()) + if ("yes".equals(caption)) { - setCaption("yes".equals(caption)); - } + setCaption(true); - String captionH = element.getAttribute("caption-offset-h"); - if (captionH != null && !captionH.isEmpty()) - { - setCaptionHorizontalOffset(Float.parseFloat(captionH)); - } + String captionH = element.getAttribute("caption-offset-h"); + if (captionH != null && !captionH.isEmpty()) + { + setCaptionHorizontalOffset(Float.parseFloat(captionH)); + } - String captionV = element.getAttribute("caption-offset-v"); - if (captionV != null && !captionV.isEmpty()) - { - setCaptionVerticalOffset(Float.parseFloat(captionV)); - } + String captionV = element.getAttribute("caption-offset-v"); + if (captionV != null && !captionV.isEmpty()) + { + setCaptionVerticalOffset(Float.parseFloat(captionV)); + } - String captionStyle = element.getAttribute("caption-style"); - if (captionStyle != null && !captionStyle.isEmpty()) - { - setCaptionStyle(captionStyle); + String captionStyle = element.getAttribute("caption-style"); + if (captionStyle != null && !captionStyle.isEmpty()) + { + setCaptionStyle(captionStyle); + } } } @@ -293,17 +282,7 @@ public void setInteriorColor(AWTColor color) */ public AWTColor getInteriorColor() { - AWTColor retval = null; - COSArray array = (COSArray) annot.getDictionaryObject(COSName.IC); - if (array != null) - { - float[] rgb = array.toFloatArray(); - if (rgb.length >= 3) - { - retval = new AWTColor(rgb[0], rgb[1], rgb[2]); - } - } - return retval; + return getColor(COSName.IC); } /** diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationPolygon.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationPolygon.java index 355091d19..6fe51eb53 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationPolygon.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationPolygon.java @@ -19,7 +19,6 @@ import android.util.Log; import java.io.IOException; - import javax.xml.xpath.XPath; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; @@ -28,9 +27,7 @@ import com.tom_roush.pdfbox.cos.COSArray; import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; - import org.w3c.dom.Element; - /** * This represents a Polygon FDF annotation. * @@ -94,12 +91,8 @@ private void initVertices(Element element) throws IOException { throw new IOException("Error: missing element 'vertices'"); } - String[] verticesValues = vertices.split(",|;"); - float[] values = new float[verticesValues.length]; - for (int i = 0; i < verticesValues.length; i++) - { - values[i] = Float.parseFloat(verticesValues[i]); - } + String[] verticesValues = vertices.split("[,;]"); + float[] values = parseFloats(verticesValues); setVertices(values); } catch (XPathExpressionException e) @@ -162,16 +155,6 @@ public final void setInteriorColor(AWTColor color) */ public AWTColor getInteriorColor() { - AWTColor retval = null; - COSArray array = (COSArray) annot.getDictionaryObject(COSName.IC); - if (array != null) - { - float[] rgb = array.toFloatArray(); - if (rgb.length >= 3) - { - retval = new AWTColor(rgb[0], rgb[1], rgb[2]); - } - } - return retval; + return getColor(COSName.IC); } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationPolyline.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationPolyline.java index ce250ad8a..96facae33 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationPolyline.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationPolyline.java @@ -19,7 +19,6 @@ import android.util.Log; import java.io.IOException; - import javax.xml.xpath.XPath; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; @@ -29,9 +28,7 @@ import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationLine; - import org.w3c.dom.Element; - /** * This represents a Polyline FDF annotation. * @@ -90,12 +87,8 @@ private void initVertices(Element element) throws IOException { throw new IOException("Error: missing element 'vertices'"); } - String[] verticesValues = vertices.split(",|;"); - float[] values = new float[verticesValues.length]; - for (int i = 0; i < verticesValues.length; i++) - { - values[i] = Float.parseFloat(verticesValues[i]); - } + String[] verticesValues = vertices.split("[,;]"); + float[] values = parseFloats(verticesValues); setVertices(values); } catch (XPathExpressionException e) @@ -263,16 +256,6 @@ public void setInteriorColor(AWTColor color) */ public AWTColor getInteriorColor() { - AWTColor retval = null; - COSArray array = (COSArray) annot.getDictionaryObject(COSName.IC); - if (array != null) - { - float[] rgb = array.toFloatArray(); - if (rgb.length >= 3) - { - retval = new AWTColor(rgb[0], rgb[1], rgb[2]); - } - } - return retval; + return getColor(COSName.IC); } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationSquare.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationSquare.java index c14882ee1..c50f29c9a 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationSquare.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationSquare.java @@ -23,9 +23,7 @@ import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.common.PDRectangle; - import org.w3c.dom.Element; - /** * This represents a Square FDF annotation. * @@ -85,16 +83,8 @@ private void initFringe(Element element) throws IOException String fringe = element.getAttribute("fringe"); if (fringe != null && !fringe.isEmpty()) { - String[] fringeValues = fringe.split(","); - if (fringeValues.length != 4) - { - throw new IOException("Error: wrong amount of numbers in attribute 'fringe'"); - } - PDRectangle rect = new PDRectangle(); - rect.setLowerLeftX(Float.parseFloat(fringeValues[0])); - rect.setLowerLeftY(Float.parseFloat(fringeValues[1])); - rect.setUpperRightX(Float.parseFloat(fringeValues[2])); - rect.setUpperRightY(Float.parseFloat(fringeValues[3])); + PDRectangle rect = createRectangleFromAttributes( + fringe, "Error: wrong amount of numbers in attribute 'fringe'"); setFringe(rect); } } @@ -123,17 +113,7 @@ public final void setInteriorColor(AWTColor color) */ public AWTColor getInteriorColor() { - AWTColor retval = null; - COSArray array = (COSArray) annot.getDictionaryObject(COSName.IC); - if (array != null) - { - float[] rgb = array.toFloatArray(); - if (rgb.length >= 3) - { - retval = new AWTColor(rgb[0], rgb[1], rgb[2]); - } - } - return retval; + return getColor(COSName.IC); } /** diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationStamp.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationStamp.java index 1acedddd3..d5f4a8d22 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationStamp.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationStamp.java @@ -21,7 +21,6 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.OutputStream; - import javax.xml.xpath.XPath; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; @@ -33,13 +32,12 @@ import com.tom_roush.pdfbox.cos.COSNumber; import com.tom_roush.pdfbox.cos.COSStream; import com.tom_roush.pdfbox.io.IOUtils; +import com.tom_roush.pdfbox.util.Charsets; import com.tom_roush.pdfbox.util.Hex; - import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; - /** * This represents a Stamp FDF annotation. * @@ -48,6 +46,7 @@ */ public class FDFAnnotationStamp extends FDFAnnotation { + /** * COS Model value for SubType entry. */ @@ -113,10 +112,10 @@ public FDFAnnotationStamp(Element element) throws IOException } if (base64EncodedAppearance != null && !base64EncodedAppearance.isEmpty()) { - Log.d("PdfBox-Android", "Decoded XML: " + new String(decodedAppearanceXML)); + Log.d("PdfBox-Android", "Decoded XML: " + new String(decodedAppearanceXML, Charsets.UTF_8)); Document stampAppearance = com.tom_roush.pdfbox.util.XMLUtil - .parse(new ByteArrayInputStream(decodedAppearanceXML)); + .parse(new ByteArrayInputStream(decodedAppearanceXML)); Element appearanceEl = stampAppearance.getDocumentElement(); @@ -124,7 +123,7 @@ public FDFAnnotationStamp(Element element) throws IOException if (!"dict".equalsIgnoreCase(appearanceEl.getNodeName())) { throw new IOException("Error while reading stamp document, " - + "root should be 'dict' and not '" + appearanceEl.getNodeName() + "'"); + + "root should be 'dict' and not '" + appearanceEl.getNodeName() + "'"); } Log.d("PdfBox-Android", "Generate and set the appearance dictionary to the stamp annotation"); annot.setItem(COSName.AP, parseStampAnnotationAppearanceXML(appearanceEl)); @@ -146,14 +145,14 @@ private COSDictionary parseStampAnnotationAppearanceXML(Element appearanceXML) t NodeList nodeList = appearanceXML.getChildNodes(); String parentAttrKey = appearanceXML.getAttribute("KEY"); Log.d("PdfBox-Android", "Appearance Root - tag: " + appearanceXML.getTagName() + ", name: " + - appearanceXML.getNodeName() + ", key: " + parentAttrKey + ", children: " + - nodeList.getLength()); + appearanceXML.getNodeName() + ", key: " + parentAttrKey + ", children: " + + nodeList.getLength()); // Currently only handles Appearance dictionary (AP key on the root) if (!"AP".equals(appearanceXML.getAttribute("KEY"))) { Log.w("PdfBox-Android", parentAttrKey + " => Not handling element: " + appearanceXML.getTagName() + - " with key: " + appearanceXML.getAttribute("KEY")); + " with key: " + appearanceXML.getAttribute("KEY")); return dictionary; } for (int i = 0; i < nodeList.getLength(); i++) @@ -165,9 +164,9 @@ private COSDictionary parseStampAnnotationAppearanceXML(Element appearanceXML) t if ("STREAM".equalsIgnoreCase(child.getTagName())) { Log.d("PdfBox-Android", parentAttrKey + - " => Process " + child.getAttribute("KEY") + - " item in the dictionary after processing the " + - child.getTagName()); + " => Process " + child.getAttribute("KEY") + + " item in the dictionary after processing the " + + child.getTagName()); dictionary.setItem(child.getAttribute("KEY"), parseStreamElement(child)); Log.d("PdfBox-Android", parentAttrKey + " => Set " + child.getAttribute("KEY")); } @@ -197,7 +196,7 @@ private COSStream parseStreamElement(Element streamEl) throws IOException String childAttrKey = child.getAttribute("KEY"); String childAttrVal = child.getAttribute("VAL"); Log.d("PdfBox-Android", parentAttrKey + " => reading child: " + child.getTagName() + - " with key: " + childAttrKey); + " with key: " + childAttrKey); if ("INT".equalsIgnoreCase(child.getTagName())) { if (!"Length".equals(childAttrKey)) @@ -239,7 +238,7 @@ else if ("STREAM".equalsIgnoreCase(child.getTagName())) else if ("DATA".equalsIgnoreCase(child.getTagName())) { Log.d("PdfBox-Android", parentAttrKey + " => Handling DATA with encoding: " + - child.getAttribute("ENCODING")); + child.getAttribute("ENCODING")); if ("HEX".equals(child.getAttribute("ENCODING"))) { OutputStream os = null; @@ -260,8 +259,16 @@ else if ("ASCII".equals(child.getAttribute("ENCODING"))) try { os = stream.createOutputStream(); - // not sure about charset - os.write(child.getTextContent().getBytes()); + String encoding = child.getOwnerDocument().getXmlEncoding(); + if (encoding == null) + { + encoding = child.getOwnerDocument().getInputEncoding(); + } + if (encoding == null) + { + encoding = "UTF-8"; + } + os.write(child.getTextContent().getBytes(encoding)); Log.d("PdfBox-Android", parentAttrKey + " => Data was streamed"); } finally @@ -272,7 +279,7 @@ else if ("ASCII".equals(child.getAttribute("ENCODING"))) else { Log.w("PdfBox-Android", parentAttrKey + " => Not handling element DATA encoding: " + - child.getAttribute("ENCODING")); + child.getAttribute("ENCODING")); } } else @@ -296,12 +303,12 @@ private COSArray parseArrayElement(Element arrayEl) throws IOException if ("BBox".equals(parentAttrKey) && nodeList.getLength() < 4) { throw new IOException("BBox does not have enough coordinates, only has: " + - nodeList.getLength()); + nodeList.getLength()); } else if ("Matrix".equals(parentAttrKey) && nodeList.getLength() < 6) { throw new IOException("Matrix does not have enough coordinates, only has: " + - nodeList.getLength()); + nodeList.getLength()); } for (int i = 0; i < nodeList.getLength(); i++) @@ -313,7 +320,7 @@ else if ("Matrix".equals(parentAttrKey) && nodeList.getLength() < 6) String childAttrKey = child.getAttribute("KEY"); String childAttrVal = child.getAttribute("VAL"); Log.d("PdfBox-Android", parentAttrKey + " => reading child: " + child.getTagName() + - " with key: " + childAttrKey); + " with key: " + childAttrKey); if ("INT".equalsIgnoreCase(child.getTagName()) || "FIXED".equalsIgnoreCase(child.getTagName())) { Log.d("PdfBox-Android", parentAttrKey + " value(" + i + "): " + childAttrVal); diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationText.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationText.java index 18e5a4d50..723d697ae 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationText.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationText.java @@ -22,7 +22,6 @@ import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationText; import org.w3c.dom.Element; - /** * This represents a Text FDF annotation. * @@ -69,7 +68,7 @@ public FDFAnnotationText(Element element) throws IOException String icon = element.getAttribute("icon"); if (icon != null && !icon.isEmpty()) { - setIcon(element.getAttribute("icon")); + setIcon(icon); } String state = element.getAttribute("state"); if (state != null && !state.isEmpty()) @@ -77,8 +76,8 @@ public FDFAnnotationText(Element element) throws IOException String statemodel = element.getAttribute("statemodel"); if (statemodel != null && !statemodel.isEmpty()) { - setState(element.getAttribute("state")); - setStateModel(element.getAttribute("statemodel")); + setState(state); + setStateModel(statemodel); } } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationTextMarkup.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationTextMarkup.java index c949f0bb6..822683c72 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationTextMarkup.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFAnnotationTextMarkup.java @@ -22,7 +22,6 @@ import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; import org.w3c.dom.Element; - /** * This abstract class is used as a superclass for the different FDF annotations with text markup attributes. * @@ -69,18 +68,14 @@ public FDFAnnotationTextMarkup(Element element) throws IOException { throw new IOException("Error: too little numbers in attribute 'coords'"); } - float[] values = new float[coordsValues.length]; - for (int i = 0; i < coordsValues.length; i++) - { - values[i] = Float.parseFloat(coordsValues[i]); - } + float[] values = parseFloats(coordsValues); setCoords(values); } /** * Set the coordinates of individual words or group of words. * - * The quadliterals shall encompasses a word or group of contiguous words in the text underlying the annotation. The + * The quadliterals shall encompass a word or group of contiguous words in the text underlying the annotation. The * coordinates for each quadrilateral shall be given in the order x1 y1 x2 y2 x3 y3 x4 y4. * * @param coords an array of 8 􏰍 n numbers specifying the coordinates of n quadrilaterals. diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFDictionary.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFDictionary.java index e7fdc3a0a..05c9e65a6 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFDictionary.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFDictionary.java @@ -32,11 +32,9 @@ import com.tom_roush.pdfbox.pdmodel.common.COSObjectable; import com.tom_roush.pdfbox.pdmodel.common.filespecification.PDFileSpecification; import com.tom_roush.pdfbox.pdmodel.common.filespecification.PDSimpleFileSpecification; - import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; - /** * This represents an FDF dictionary that is part of the FDF document. * @@ -45,6 +43,7 @@ public class FDFDictionary implements COSObjectable { + private COSDictionary fdf; /** @@ -98,7 +97,7 @@ else if (child.getTagName().equals("ids")) catch (IOException e) { Log.w("PdfBox-Android", "Error parsing ID entry for attribute 'original' [" + original - + "]. ID entry ignored.", e); + + "]. ID entry ignored.", e); } try { @@ -107,7 +106,7 @@ else if (child.getTagName().equals("ids")) catch (IOException e) { Log.w("PdfBox-Android", "Error parsing ID entry for attribute 'modified' [" + modified - + "]. ID entry ignored.", e); + + "]. ID entry ignored.", e); } setID(ids); } @@ -119,16 +118,16 @@ else if (child.getTagName().equals("fields")) { Node currentNode = fields.item(f); if (currentNode instanceof Element - && ((Element) currentNode).getTagName().equals("field")) + && ((Element) currentNode).getTagName().equals("field")) { try { - fieldList.add(new FDFField((Element) fields.item(f))); + fieldList.add(new FDFField((Element) currentNode)); } catch (IOException e) { Log.w("PdfBox-Android", "Error parsing field entry [" + currentNode.getNodeValue() - + "]. Field ignored.", e); + + "]. Field ignored.", e); } } } @@ -220,14 +219,14 @@ else if (annotationName.equals("underline")) else { Log.w("PdfBox-Android", "Unknown or unsupported annotation type '" - + annotationName + "'"); + + annotationName + "'"); } } catch (IOException e) { - Log.w("PdfBox-Android", - "Error parsing annotation information [" - + annot.getNodeValue() + "]. Annotation ignored", e); + Log.w("PdfBox-Android", + "Error parsing annotation information [" + + annot.getNodeValue() + "]. Annotation ignored", e); } } } @@ -333,10 +332,10 @@ public void setID(COSArray id) public List getFields() { List retval = null; - COSArray fieldArray = (COSArray) fdf.getDictionaryObject(COSName.FIELDS); + COSArray fieldArray = fdf.getCOSArray(COSName.FIELDS); if (fieldArray != null) { - List fields = new ArrayList(); + List fields = new ArrayList(fieldArray.size()); for (int i = 0; i < fieldArray.size(); i++) { fields.add(new FDFField((COSDictionary) fieldArray.getObject(i))); @@ -384,10 +383,10 @@ public void setStatus(String status) public List getPages() { List retval = null; - COSArray pageArray = (COSArray) fdf.getDictionaryObject(COSName.PAGES); + COSArray pageArray = fdf.getCOSArray(COSName.PAGES); if (pageArray != null) { - List pages = new ArrayList(); + List pages = new ArrayList(pageArray.size()); for (int i = 0; i < pageArray.size(); i++) { pages.add(new FDFPage((COSDictionary) pageArray.get(i))); @@ -445,10 +444,10 @@ public void setEncoding(String encoding) public List getAnnotations() throws IOException { List retval = null; - COSArray annotArray = (COSArray) fdf.getDictionaryObject(COSName.ANNOTS); + COSArray annotArray = fdf.getCOSArray(COSName.ANNOTS); if (annotArray != null) { - List annots = new ArrayList(); + List annots = new ArrayList(annotArray.size()); for (int i = 0; i < annotArray.size(); i++) { annots.add(FDFAnnotation.create((COSDictionary) annotArray.getObject(i))); @@ -520,10 +519,10 @@ public void setTarget(String target) public List getEmbeddedFDFs() throws IOException { List retval = null; - COSArray embeddedArray = (COSArray) fdf.getDictionaryObject(COSName.EMBEDDED_FDFS); + COSArray embeddedArray = fdf.getCOSArray(COSName.EMBEDDED_FDFS); if (embeddedArray != null) { - List embedded = new ArrayList(); + List embedded = new ArrayList(embeddedArray.size()); for (int i = 0; i < embeddedArray.size(); i++) { embedded.add(PDFileSpecification.createFS(embeddedArray.get(i))); diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFDocument.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFDocument.java index f11b8b2fd..44b7cc00d 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFDocument.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFDocument.java @@ -22,8 +22,8 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; -import java.io.InputStream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; @@ -31,11 +31,11 @@ import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSDocument; import com.tom_roush.pdfbox.cos.COSName; +import com.tom_roush.pdfbox.io.IOUtils; import com.tom_roush.pdfbox.pdfparser.FDFParser; import com.tom_roush.pdfbox.pdfwriter.COSWriter; import org.w3c.dom.Document; import org.w3c.dom.Element; - /** * This is the in-memory representation of the FDF document. You need to call close() on this object when you are done * using it!! @@ -85,7 +85,7 @@ public FDFDocument(Document doc) throws IOException if (!xfdf.getNodeName().equals("xfdf")) { throw new IOException("Error while importing xfdf document, " - + "root should be 'xfdf' and not '" + xfdf.getNodeName() + "'"); + + "root should be 'xfdf' and not '" + xfdf.getNodeName() + "'"); } FDFCatalog cat = new FDFCatalog(xfdf); setCatalog(cat); @@ -210,7 +210,7 @@ public static FDFDocument load(InputStream input) throws IOException */ public static FDFDocument loadXFDF(String filename) throws IOException { - return loadXFDF(new BufferedInputStream(new FileInputStream(filename))); + return loadXFDF(new File(filename)); } /** @@ -224,7 +224,16 @@ public static FDFDocument loadXFDF(String filename) throws IOException */ public static FDFDocument loadXFDF(File file) throws IOException { - return loadXFDF(new BufferedInputStream(new FileInputStream(file))); + InputStream is = null; + try + { + is = new BufferedInputStream(new FileInputStream(file)); + return loadXFDF(is); + } + finally + { + IOUtils.closeQuietly(is); + } } /** @@ -302,7 +311,7 @@ public void save(OutputStream output) throws IOException public void saveXFDF(File fileName) throws IOException { BufferedWriter writer = new BufferedWriter( - new OutputStreamWriter(new FileOutputStream(fileName), "UTF-8")); + new OutputStreamWriter(new FileOutputStream(fileName), "UTF-8")); saveXFDF(writer); writer.close(); } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFField.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFField.java index f143c3270..4f03014b4 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFField.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFField.java @@ -20,6 +20,7 @@ import java.io.Writer; import java.util.ArrayList; import java.util.List; + import com.tom_roush.pdfbox.cos.COSArray; import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSDictionary; @@ -37,7 +38,6 @@ import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; - /** * This represents an FDF field that is part of the FDF document. * @@ -802,30 +802,30 @@ private String escapeXML(String input) char c = input.charAt(i); switch (c) { - case '<': - escapedXML.append("<"); - break; - case '>': - escapedXML.append(">"); - break; - case '\"': - escapedXML.append("""); - break; - case '&': - escapedXML.append("&"); - break; - case '\'': - escapedXML.append("'"); - break; - default: - if (c > 0x7e) - { - escapedXML.append("&#").append((int) c).append(";"); - } - else - { - escapedXML.append(c); - } + case '<': + escapedXML.append("<"); + break; + case '>': + escapedXML.append(">"); + break; + case '\"': + escapedXML.append("""); + break; + case '&': + escapedXML.append("&"); + break; + case '\'': + escapedXML.append("'"); + break; + default: + if (c > 0x7e) + { + escapedXML.append("&#").append((int) c).append(';'); + } + else + { + escapedXML.append(c); + } } } return escapedXML.toString(); diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFIconFit.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFIconFit.java index 45cc47170..1031b66ee 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFIconFit.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFIconFit.java @@ -21,7 +21,6 @@ import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.common.COSObjectable; import com.tom_roush.pdfbox.pdmodel.common.PDRange; - /** * This represents an Icon fit dictionary for an FDF field. * @@ -167,7 +166,7 @@ public PDRange getFractionalSpaceToAllocate() } /** - * This will set frational space to allocate. + * This will set fractional space to allocate. * * @param space The space to allocate. */ diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFPage.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFPage.java index fb638b08e..32bf336b3 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFPage.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFPage.java @@ -24,7 +24,6 @@ import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.common.COSArrayList; import com.tom_roush.pdfbox.pdmodel.common.COSObjectable; - /** * This represents an FDF page that is part of the FDF document. * @@ -64,7 +63,7 @@ public COSDictionary getCOSObject() } /** - * This will get a list of FDFTemplage objects that describe the named pages that serve as templates. + * This will get a list of FDFTemplate objects that describe the named pages that serve as templates. * * @return A list of templates. */ diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFTemplate.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFTemplate.java index 702d32b17..ca9ec4d28 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFTemplate.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fdf/FDFTemplate.java @@ -24,7 +24,6 @@ import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.common.COSArrayList; import com.tom_roush.pdfbox.pdmodel.common.COSObjectable; - /** * This represents an FDF template that is part of the FDF page. * @@ -97,10 +96,10 @@ public void setTemplateReference(FDFNamedPageReference tRef) public List getFields() { List retval = null; - COSArray array = (COSArray) template.getDictionaryObject(COSName.FIELDS); + COSArray array = template.getCOSArray(COSName.FIELDS); if (array != null) { - List fields = new ArrayList(); + List fields = new ArrayList(array.size()); for (int i = 0; i < array.size(); i++) { fields.add(new FDFField((COSDictionary) array.getObject(i))); diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fixup/processor/AcroFormOrphanWidgetsProcessor.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fixup/processor/AcroFormOrphanWidgetsProcessor.java index 0ca27f796..710565c0f 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fixup/processor/AcroFormOrphanWidgetsProcessor.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/fixup/processor/AcroFormOrphanWidgetsProcessor.java @@ -25,6 +25,7 @@ import java.util.Map; import com.tom_roush.fontbox.ttf.TrueTypeFont; +import com.tom_roush.pdfbox.android.PDFBoxConfig; import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.PDDocument; @@ -41,7 +42,6 @@ import com.tom_roush.pdfbox.pdmodel.interactive.form.PDField; import com.tom_roush.pdfbox.pdmodel.interactive.form.PDFieldFactory; import com.tom_roush.pdfbox.pdmodel.interactive.form.PDVariableText; - /** * Generate field entries from page level widget annotations * if there AcroForm /Fields entry is empty. @@ -50,203 +50,213 @@ public class AcroFormOrphanWidgetsProcessor extends AbstractProcessor { - public AcroFormOrphanWidgetsProcessor(PDDocument document) - { - super(document); - } - - @Override - public void process() - { - /* - * Get the AcroForm in it's current state. - * - * Also note: getAcroForm() applies a default fixup which this processor - * is part of. So keep the null parameter otherwise this will end - * in an endless recursive call - */ - PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm(null); - - if (acroForm != null) - { - resolveFieldsFromWidgets(acroForm); - } - } - - private void resolveFieldsFromWidgets(PDAcroForm acroForm) - { - Log.d("PdfBox-Android", "rebuilding fields from widgets"); - - PDResources resources = acroForm.getDefaultResources(); - if (resources == null) - { - // failsafe. Currently resources is never null because defaultfixup is called first. - Log.d("PdfBox-Android", "AcroForm default resources is null"); - return; - } - - List fields = new ArrayList(); - Map nonTerminalFieldsMap = new HashMap(); - for (PDPage page : document.getPages()) - { - try - { - handleAnnotations(acroForm, resources, fields, page.getAnnotations(), nonTerminalFieldsMap); - } - catch (IOException ioe) - { - Log.d("PdfBox-Android", "couldn't read annotations for page " + ioe.getMessage()); - } - } - - acroForm.setFields(fields); - - for (PDField field : acroForm.getFieldTree()) - { - if (field instanceof PDVariableText) - { - ensureFontResources(resources, (PDVariableText) field); - } - } - } - - private void handleAnnotations(PDAcroForm acroForm, PDResources acroFormResources, - List fields, List annotations, - Map nonTerminalFieldsMap) - { - for (PDAnnotation annot : annotations) - { - if (annot instanceof PDAnnotationWidget) - { - addFontFromWidget(acroFormResources, annot); - - COSDictionary parent = annot.getCOSObject().getCOSDictionary(COSName.PARENT); - if (parent != null) + + public AcroFormOrphanWidgetsProcessor(PDDocument document) + { + super(document); + } + + @Override + public void process() + { + /* + * Get the AcroForm in it's current state. + * + * Also note: getAcroForm() applies a default fixup which this processor + * is part of. So keep the null parameter otherwise this will end + * in an endless recursive call + */ + PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm(null); + + if (acroForm != null) + { + resolveFieldsFromWidgets(acroForm); + } + } + + private void resolveFieldsFromWidgets(PDAcroForm acroForm) + { + Log.d("PdfBox-Android", "rebuilding fields from widgets"); + + PDResources resources = acroForm.getDefaultResources(); + if (resources == null) + { + // failsafe. Currently resources is never null because defaultfixup is called first. + Log.d("PdfBox-Android", "AcroForm default resources is null"); + return; + } + + List fields = new ArrayList(); + Map nonTerminalFieldsMap = new HashMap(); + for (PDPage page : document.getPages()) + { + try { - PDField resolvedField = resolveNonRootField(acroForm, parent, nonTerminalFieldsMap); - if (resolvedField != null) - { - fields.add(resolvedField); - } + handleAnnotations(acroForm, resources, fields, page.getAnnotations(), nonTerminalFieldsMap); } - else + catch (IOException ioe) { - fields.add(PDFieldFactory.createField(acroForm, annot.getCOSObject(), null)); + Log.d("PdfBox-Android", "couldn't read annotations for page " + ioe.getMessage()); } - } - } - } - - /** - * Add font resources from the widget to the AcroForm to make sure embedded fonts are being used - * and not added by ensureFontResources potentially using a fallback font. - * - * @param acroFormResources AcroForm default resources, should not be null. - * @param annotation annotation, should not be null. - */ - private void addFontFromWidget(PDResources acroFormResources, PDAnnotation annotation) - { - PDAppearanceStream normalAppearanceStream = annotation.getNormalAppearanceStream(); - if (normalAppearanceStream == null) - { - return; - } - PDResources widgetResources = normalAppearanceStream.getResources(); - if (widgetResources == null) - { - return; - } - for (COSName fontName : widgetResources.getFontNames()) - { - if (!fontName.getName().startsWith("+")) - { - try + } + + acroForm.setFields(fields); + + for (PDField field : acroForm.getFieldTree()) + { + if (field instanceof PDVariableText) { - if (acroFormResources.getFont(fontName) == null) - { - acroFormResources.put(fontName, widgetResources.getFont(fontName)); - Log.d("PdfBox-Android", "added font resource to AcroForm from widget for font name " + fontName.getName()); - } + ensureFontResources(resources, (PDVariableText) field); } - catch (IOException ioe) + } + } + + private void handleAnnotations(PDAcroForm acroForm, PDResources acroFormResources, + List fields, List annotations, + Map nonTerminalFieldsMap) + { + for (PDAnnotation annot : annotations) + { + if (annot instanceof PDAnnotationWidget) + { + addFontFromWidget(acroFormResources, annot); + + COSDictionary parent = annot.getCOSObject().getCOSDictionary(COSName.PARENT); + if (parent != null) + { + PDField resolvedField = resolveNonRootField(acroForm, parent, nonTerminalFieldsMap); + if (resolvedField != null) + { + fields.add(resolvedField); + } + } + else + { + fields.add(PDFieldFactory.createField(acroForm, annot.getCOSObject(), null)); + } + } + } + } + + /** + * Add font resources from the widget to the AcroForm to make sure embedded fonts are being used + * and not added by ensureFontResources potentially using a fallback font. + * + * @param acroFormResources AcroForm default resources, should not be null. + * @param annotation annotation, should not be null. + */ + private void addFontFromWidget(PDResources acroFormResources, PDAnnotation annotation) + { + PDAppearanceStream normalAppearanceStream = annotation.getNormalAppearanceStream(); + if (normalAppearanceStream == null) + { + return; + } + PDResources widgetResources = normalAppearanceStream.getResources(); + if (widgetResources == null) + { + return; + } + for (COSName fontName : widgetResources.getFontNames()) + { + if (!fontName.getName().startsWith("+")) + { + try + { + if (acroFormResources.getFont(fontName) == null) + { + acroFormResources.put(fontName, widgetResources.getFont(fontName)); + if (PDFBoxConfig.isDebugEnabled()) + { + Log.d("PdfBox-Android", "added font resource to AcroForm from widget for font name " + fontName.getName()); + } + } + } + catch (IOException ioe) + { + Log.d("PdfBox-Android", "unable to add font to AcroForm for font name " + fontName.getName()); + } + } + else if (PDFBoxConfig.isDebugEnabled()) + { + Log.d("PdfBox-Android", "font resource for widget was a subsetted font - ignored: " + fontName.getName()); + } + } + } + + /* + * Widgets having a /Parent entry are non root fields. Go up until the root node is found + * and handle from there. + */ + private PDField resolveNonRootField(PDAcroForm acroForm, COSDictionary parent, Map nonTerminalFieldsMap) + { + while (parent.containsKey(COSName.PARENT)) + { + parent = parent.getCOSDictionary(COSName.PARENT); + if (parent == null) + { + return null; + } + } + + if (nonTerminalFieldsMap.get(parent.getString(COSName.T)) == null) + { + PDField field = PDFieldFactory.createField(acroForm, parent, null); + if (field != null) + { + nonTerminalFieldsMap.put(field.getFullyQualifiedName(), field); + } + return field; + } + + // this should not happen, likely broken PDF + return null; + } + + /* + * Lookup the font used in the default appearance and if this is + * not available try to find a suitable font and use that. + * This may not be the original font but a similar font replacement + * + * TODO: implement a font lookup similar as discussed in PDFBOX-2661 so that already existing + * font resources might be accepatble. + * In such case this must be implemented in PDDefaultAppearanceString too! + */ + private void ensureFontResources(PDResources defaultResources, PDVariableText field) + { + String daString = field.getDefaultAppearance(); + if (daString.startsWith("/") && daString.length() > 1) + { + COSName fontName = COSName.getPDFName(daString.substring(1, daString.indexOf(" "))); + try { - Log.d("PdfBox-Android", "unable to add font to AcroForm for font name " + fontName.getName()); + if (defaultResources.getFont(fontName) == null) + { + if (PDFBoxConfig.isDebugEnabled()) + { + Log.d("PdfBox-Android", "trying to add missing font resource for field " + field.getFullyQualifiedName()); + } + FontMapper mapper = FontMappers.instance(); + FontMapping fontMapping = mapper.getTrueTypeFont(fontName.getName() , null); + if (fontMapping != null) + { + PDType0Font pdFont = PDType0Font.load(document, fontMapping.getFont(), false); + if (PDFBoxConfig.isDebugEnabled()) + { + Log.d("PdfBox-Android", "looked up font for " + fontName.getName() + " - found " + fontMapping.getFont().getName()); + } + defaultResources.put(fontName, pdFont); + } + else if (PDFBoxConfig.isDebugEnabled()) + { + Log.d("PdfBox-Android", "no suitable font found for field " + field.getFullyQualifiedName() + " for font name " + fontName.getName()); + } + } } - } - else - { - Log.d("PdfBox-Android", "font resource for widget was a subsetted font - ignored: " + fontName.getName()); - } - } - } - - /* - * Widgets having a /Parent entry are non root fields. Go up until the root node is found - * and handle from there. - */ - private PDField resolveNonRootField(PDAcroForm acroForm, COSDictionary parent, Map nonTerminalFieldsMap) - { - while (parent.containsKey(COSName.PARENT)) - { - parent = parent.getCOSDictionary(COSName.PARENT); - if (parent == null) - { - return null; - } - } - - if (nonTerminalFieldsMap.get(parent.getString(COSName.T)) == null) - { - PDField field = PDFieldFactory.createField(acroForm, parent, null); - if (field != null) - { - nonTerminalFieldsMap.put(field.getFullyQualifiedName(), field); - } - return field; - } - - // this should not happen, likely broken PDF - return null; - } - - /* - * Lookup the font used in the default appearance and if this is - * not available try to find a suitable font and use that. - * This may not be the original font but a similar font replacement - * - * TODO: implement a font lookup similar as discussed in PDFBOX-2661 so that already existing - * font resources might be accepatble. - * In such case this must be implemented in PDDefaultAppearanceString too! - */ - private void ensureFontResources(PDResources defaultResources, PDVariableText field) - { - String daString = field.getDefaultAppearance(); - if (daString.startsWith("/") && daString.length() > 1) - { - COSName fontName = COSName.getPDFName(daString.substring(1, daString.indexOf(" "))); - try - { - if (defaultResources.getFont(fontName) == null) + catch (IOException ioe) { - Log.d("PdfBox-Android", "trying to add missing font resource for field " + field.getFullyQualifiedName()); - FontMapper mapper = FontMappers.instance(); - FontMapping fontMapping = mapper.getTrueTypeFont(fontName.getName() , null); - if (fontMapping != null) - { - PDType0Font pdFont = PDType0Font.load(document, fontMapping.getFont(), false); - Log.d("PdfBox-Android", "looked up font for " + fontName.getName() + " - found " + fontMapping.getFont().getName()); - defaultResources.put(fontName, pdFont); - } - else - { - Log.d("PdfBox-Android", "no suitable font found for field " + field.getFullyQualifiedName() + " for font name " + fontName.getName()); - } + Log.d("PdfBox-Android", "Unable to handle font resources for field " + field.getFullyQualifiedName() + ": " + ioe.getMessage()); } - } - catch (IOException ioe) - { - Log.d("PdfBox-Android", "Unable to handle font resources for field " + field.getFullyQualifiedName() + ": " + ioe.getMessage()); - } - } - } + } + } } \ No newline at end of file diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/FileSystemFontProvider.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/FileSystemFontProvider.java index 7e77b56f2..254517211 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/FileSystemFontProvider.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/FileSystemFontProvider.java @@ -22,16 +22,18 @@ import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; -import java.io.FileReader; -import java.io.FileWriter; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; import java.net.URI; import java.security.AccessControlException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.zip.CRC32; import com.tom_roush.fontbox.FontBoxFont; import com.tom_roush.fontbox.cff.CFFCIDFont; @@ -41,15 +43,14 @@ import com.tom_roush.fontbox.ttf.OTFParser; import com.tom_roush.fontbox.ttf.OpenTypeFont; import com.tom_roush.fontbox.ttf.TTFParser; -import com.tom_roush.fontbox.ttf.TrueTypeCollection; import com.tom_roush.fontbox.ttf.TrueTypeCollection.TrueTypeFontProcessor; +import com.tom_roush.fontbox.ttf.TrueTypeCollection; import com.tom_roush.fontbox.ttf.TrueTypeFont; import com.tom_roush.fontbox.type1.Type1Font; import com.tom_roush.fontbox.util.autodetect.FontFileFinder; import com.tom_roush.pdfbox.android.PDFBoxConfig; import com.tom_roush.pdfbox.io.IOUtils; import com.tom_roush.pdfbox.util.Charsets; - /** * A FontProvider which searches for fonts on the local filesystem. * @@ -57,6 +58,7 @@ */ final class FileSystemFontProvider extends FontProvider { + private final List fontInfoList = new ArrayList(); private final FontCache cache; @@ -73,11 +75,13 @@ private static class FSFontInfo extends FontInfo private final PDPanoseClassification panose; private final File file; private final FileSystemFontProvider parent; + private final String hash; + private final long lastModified; private FSFontInfo(File file, FontFormat format, String postScriptName, - CIDSystemInfo cidSystemInfo, int usWeightClass, int sFamilyClass, - int ulCodePageRange1, int ulCodePageRange2, int macStyle, byte[] panose, - FileSystemFontProvider parent) + CIDSystemInfo cidSystemInfo, int usWeightClass, int sFamilyClass, + int ulCodePageRange1, int ulCodePageRange2, int macStyle, byte[] panose, + FileSystemFontProvider parent, String hash, long lastModified) { this.file = file; this.format = format; @@ -89,8 +93,10 @@ private FSFontInfo(File file, FontFormat format, String postScriptName, this.ulCodePageRange2 = ulCodePageRange2; this.macStyle = macStyle; this.panose = panose != null && panose.length >= PDPanoseClassification.LENGTH ? - new PDPanoseClassification(panose) : null; + new PDPanoseClassification(panose) : null; this.parent = parent; + this.hash = hash; + this.lastModified = lastModified; } @Override @@ -114,7 +120,7 @@ public CIDSystemInfo getCIDSystemInfo() /** * {@inheritDoc} *

      - * The method returns null if there is there was an error opening the font. + * The method returns null if there was an error opening the font. * */ @Override @@ -184,7 +190,7 @@ public PDPanoseClassification getPanose() @Override public String toString() { - return super.toString() + " " + file; + return super.toString() + " " + file + " " + hash + " " + lastModified; } private TrueTypeFont getTrueTypeFont(String postScriptName, File file) @@ -309,15 +315,18 @@ private Type1Font getType1Font(String postScriptName, File file) } } - /** - * Represents ignored fonts (i.e. bitmap fonts). - */ - private static final class FSIgnored extends FSFontInfo + private FSFontInfo createFSIgnored(File file, FontFormat format, String postScriptName) { - private FSIgnored(File file, FontFormat format, String postScriptName) + String hash; + try + { + hash = computeHash(file); + } + catch (IOException ex) { - super(file, format, postScriptName, null, 0, 0, 0, 0, 0, null, null); + hash = ""; } + return new FSFontInfo(file, format, postScriptName, null, 0, 0, 0, 0, 0, null, null, hash, file.lastModified()); } /** @@ -381,11 +390,11 @@ private FSIgnored(File file, FontFormat format, String postScriptName) } else { - Log.w("PdfBox-Android", "Building on-disk font cache, this may take a while"); + Log.i("PdfBox-Android", "Building on-disk font cache, this may take a while"); scanFonts(files); saveDiskCache(); - Log.w("PdfBox-Android", "Finished building on-disk font cache, found " + fontInfoList.size() - + " fonts"); + Log.i("PdfBox-Android", "Finished building on-disk font cache, found " + fontInfoList.size() + + " fonts"); } } } @@ -452,7 +461,8 @@ private void saveDiskCache() try { File file = getDiskCacheFile(); - writer = new BufferedWriter(new FileWriter(file)); + writer = new BufferedWriter( + new OutputStreamWriter(new FileOutputStream(file), Charsets.UTF_8)); } catch (SecurityException e) { @@ -461,52 +471,7 @@ private void saveDiskCache() for (FSFontInfo fontInfo : fontInfoList) { - writer.write(fontInfo.postScriptName.trim()); - writer.write("|"); - writer.write(fontInfo.format.toString()); - writer.write("|"); - if (fontInfo.cidSystemInfo != null) - { - writer.write(fontInfo.cidSystemInfo.getRegistry() + '-' + - fontInfo.cidSystemInfo.getOrdering() + '-' + - fontInfo.cidSystemInfo.getSupplement()); - } - writer.write("|"); - if (fontInfo.usWeightClass > -1) - { - writer.write(Integer.toHexString(fontInfo.usWeightClass)); - } - writer.write("|"); - if (fontInfo.sFamilyClass > -1) - { - writer.write(Integer.toHexString(fontInfo.sFamilyClass)); - } - writer.write("|"); - writer.write(Integer.toHexString(fontInfo.ulCodePageRange1)); - writer.write("|"); - writer.write(Integer.toHexString(fontInfo.ulCodePageRange2)); - writer.write("|"); - if (fontInfo.macStyle > -1) - { - writer.write(Integer.toHexString(fontInfo.macStyle)); - } - writer.write("|"); - if (fontInfo.panose != null) - { - byte[] bytes = fontInfo.panose.getBytes(); - for (int i = 0; i < 10; i ++) - { - String str = Integer.toHexString(bytes[i]); - if (str.length() == 1) - { - writer.write('0'); - } - writer.write(str); - } - } - writer.write("|"); - writer.write(fontInfo.file.getAbsolutePath()); - writer.newLine(); + writeFontInfo(writer, fontInfo); } } catch (IOException e) @@ -521,6 +486,60 @@ private void saveDiskCache() } } + private void writeFontInfo(BufferedWriter writer, FSFontInfo fontInfo) throws IOException + { + writer.write(fontInfo.postScriptName.trim()); + writer.write("|"); + writer.write(fontInfo.format.toString()); + writer.write("|"); + if (fontInfo.cidSystemInfo != null) + { + writer.write(fontInfo.cidSystemInfo.getRegistry() + '-' + + fontInfo.cidSystemInfo.getOrdering() + '-' + + fontInfo.cidSystemInfo.getSupplement()); + } + writer.write("|"); + if (fontInfo.usWeightClass > -1) + { + writer.write(Integer.toHexString(fontInfo.usWeightClass)); + } + writer.write("|"); + if (fontInfo.sFamilyClass > -1) + { + writer.write(Integer.toHexString(fontInfo.sFamilyClass)); + } + writer.write("|"); + writer.write(Integer.toHexString(fontInfo.ulCodePageRange1)); + writer.write("|"); + writer.write(Integer.toHexString(fontInfo.ulCodePageRange2)); + writer.write("|"); + if (fontInfo.macStyle > -1) + { + writer.write(Integer.toHexString(fontInfo.macStyle)); + } + writer.write("|"); + if (fontInfo.panose != null) + { + byte[] bytes = fontInfo.panose.getBytes(); + for (int i = 0; i < 10; i ++) + { + String str = Integer.toHexString(bytes[i]); + if (str.length() == 1) + { + writer.write('0'); + } + writer.write(str); + } + } + writer.write("|"); + writer.write(fontInfo.file.getAbsolutePath()); + writer.write("|"); + writer.write(fontInfo.hash); + writer.write("|"); + writer.write(Long.toString(fontInfo.file.lastModified())); + writer.newLine(); + } + /** * Loads the font metadata cache from disk. */ @@ -535,15 +554,16 @@ private List loadDiskCache(List files) List results = new ArrayList(); // Get the disk cache - File file = null; + File diskCacheFile = null; boolean fileExists = false; try { - file = getDiskCacheFile(); - fileExists = file.exists(); + diskCacheFile = getDiskCacheFile(); + fileExists = diskCacheFile.exists(); } catch (SecurityException e) { + Log.d("PdfBox-Android", "Error checking for file existence", e); } if (fileExists) @@ -551,11 +571,12 @@ private List loadDiskCache(List files) BufferedReader reader = null; try { - reader = new BufferedReader(new FileReader(file)); + reader = new BufferedReader( + new InputStreamReader(new FileInputStream(diskCacheFile), Charsets.UTF_8)); String line; while ((line = reader.readLine()) != null) { - String[] parts = line.split("\\|", 10); + String[] parts = line.split("\\|", 12); if (parts.length < 10) { Log.w("PdfBox-Android", "Incorrect line '" + line + "' in font disk cache is skipped"); @@ -572,6 +593,8 @@ private List loadDiskCache(List files) int macStyle = -1; byte[] panose = null; File fontFile; + String hash = ""; + long lastModified = 0; postScriptName = parts[0]; format = FontFormat.valueOf(parts[1]); @@ -605,12 +628,42 @@ private List loadDiskCache(List files) } } fontFile = new File(parts[9]); + if (parts.length >= 12 && !parts[10].isEmpty() && !parts[11].isEmpty()) + { + hash = parts[10]; + lastModified = Long.parseLong(parts[11]); + } if (fontFile.exists()) { - FSFontInfo info = new FSFontInfo(fontFile, format, postScriptName, - cidSystemInfo, usWeightClass, sFamilyClass, ulCodePageRange1, - ulCodePageRange2, macStyle, panose, this); - results.add(info); + boolean keep = false; + // if the file exists, find out whether it's the same file. + // first check whether time is different and if yes, whether hash is different + if (fontFile.lastModified() != lastModified) + { + String newHash = computeHash(fontFile); + if (newHash.equals(hash)) + { + keep = true; + lastModified = fontFile.lastModified(); + hash = newHash; + } + } + else + { + keep = true; + } + if (keep) + { + FSFontInfo info = new FSFontInfo(fontFile, format, postScriptName, + cidSystemInfo, usWeightClass, sFamilyClass, ulCodePageRange1, + ulCodePageRange2, macStyle, panose, this, hash, lastModified); + results.add(info); + } + else + { + Log.d("PdfBox-Android", "Font file " + fontFile.getAbsolutePath() + " is different"); + continue; // don't remove from "pending" + } } else { @@ -633,7 +686,7 @@ private List loadDiskCache(List files) if (!pending.isEmpty()) { // re-build the entire cache if we encounter un-cached fonts (could be optimised) - Log.w("PdfBox-Android", "New fonts found, font cache will be re-built"); + Log.i("PdfBox-Android", pending.size() + " new font files found, font cache will be re-built"); return null; } @@ -661,6 +714,7 @@ public void process(TrueTypeFont ttf) throws IOException catch (IOException e) { Log.w("PdfBox-Android", "Could not load font file: " + ttcFile, e); + fontInfoList.add(createFSIgnored(ttcFile, FontFormat.TTF, "*skipexception*")); } finally { @@ -676,16 +730,19 @@ public void process(TrueTypeFont ttf) throws IOException */ private void addTrueTypeFont(File ttfFile) throws IOException { + FontFormat fontFormat = null; try { if (ttfFile.getPath().toLowerCase().endsWith(".otf")) { + fontFormat = FontFormat.OTF; OTFParser parser = new OTFParser(false, true); OpenTypeFont otf = parser.parse(ttfFile); addTrueTypeFontImpl(otf, ttfFile); } else { + fontFormat = FontFormat.TTF; TTFParser parser = new TTFParser(false, true); TrueTypeFont ttf = parser.parse(ttfFile); addTrueTypeFontImpl(ttf, ttfFile); @@ -694,6 +751,7 @@ private void addTrueTypeFont(File ttfFile) throws IOException catch (IOException e) { Log.w("PdfBox-Android", "Could not load font file: " + ttfFile, e); + fontInfoList.add(createFSIgnored(ttfFile, fontFormat, "*skipexception*")); } } @@ -707,7 +765,7 @@ private void addTrueTypeFontImpl(TrueTypeFont ttf, File file) throws IOException // read PostScript name, if any if (ttf.getName() != null && ttf.getName().contains("|")) { - fontInfoList.add(new FSIgnored(file, FontFormat.TTF, "*skippipeinname*")); + fontInfoList.add(createFSIgnored(file, FontFormat.TTF, "*skippipeinname*")); Log.w("PdfBox-Android", "Skipping font with '|' in name " + ttf.getName() + " in file " + file); } else if (ttf.getName() != null) @@ -715,7 +773,7 @@ else if (ttf.getName() != null) // ignore bitmap fonts if (ttf.getHeader() == null) { - fontInfoList.add(new FSIgnored(file, FontFormat.TTF, ttf.getName())); + fontInfoList.add(createFSIgnored(file, FontFormat.TTF, ttf.getName())); return; } int macStyle = ttf.getHeader().getMacStyle(); @@ -736,23 +794,31 @@ else if (ttf.getName() != null) panose = os2WindowsMetricsTable.getPanose(); } + InputStream is = ttf.getOriginalData(); + String hash = computeHash(is); + is.close(); + String format; - if (ttf instanceof OpenTypeFont && ((OpenTypeFont)ttf).isPostScript()) + if (ttf instanceof OpenTypeFont && ((OpenTypeFont) ttf).isPostScript()) { format = "OTF"; - CFFFont cff = ((OpenTypeFont)ttf).getCFF().getFont(); CIDSystemInfo ros = null; - if (cff instanceof CFFCIDFont) + OpenTypeFont otf = (OpenTypeFont) ttf; + if (otf.isSupportedOTF() && otf.getCFF() != null) { - CFFCIDFont cidFont = (CFFCIDFont)cff; - String registry = cidFont.getRegistry(); - String ordering = cidFont.getOrdering(); - int supplement = cidFont.getSupplement(); - ros = new CIDSystemInfo(registry, ordering, supplement); + CFFFont cff = otf.getCFF().getFont(); + if (cff instanceof CFFCIDFont) + { + CFFCIDFont cidFont = (CFFCIDFont) cff; + String registry = cidFont.getRegistry(); + String ordering = cidFont.getOrdering(); + int supplement = cidFont.getSupplement(); + ros = new CIDSystemInfo(registry, ordering, supplement); + } } fontInfoList.add(new FSFontInfo(file, FontFormat.OTF, ttf.getName(), ros, - usWeightClass, sFamilyClass, ulCodePageRange1, ulCodePageRange2, - macStyle, panose, this)); + usWeightClass, sFamilyClass, ulCodePageRange1, ulCodePageRange2, + macStyle, panose, this, hash, file.lastModified())); } else { @@ -771,8 +837,8 @@ else if (ttf.getName() != null) format = "TTF"; fontInfoList.add(new FSFontInfo(file, FontFormat.TTF, ttf.getName(), ros, - usWeightClass, sFamilyClass, ulCodePageRange1, ulCodePageRange2, - macStyle, panose, this)); + usWeightClass, sFamilyClass, ulCodePageRange1, ulCodePageRange2, + macStyle, panose, this, hash, file.lastModified())); } if (PDFBoxConfig.isDebugEnabled()) @@ -781,20 +847,20 @@ else if (ttf.getName() != null) if (name != null) { Log.d("PdfBox-Android", format +": '" + name.getPostScriptName() + "' / '" + - name.getFontFamily() + "' / '" + - name.getFontSubFamily() + "'"); + name.getFontFamily() + "' / '" + + name.getFontSubFamily() + "'"); } } } else { - fontInfoList.add(new FSIgnored(file, FontFormat.TTF, "*skipnoname*")); + fontInfoList.add(createFSIgnored(file, FontFormat.TTF, "*skipnoname*")); Log.w("PdfBox-Android", "Missing 'name' entry for PostScript name in font " + file); } } catch (IOException e) { - fontInfoList.add(new FSIgnored(file, FontFormat.TTF, "*skipexception*")); + fontInfoList.add(createFSIgnored(file, FontFormat.TTF, "*skipexception*")); Log.w("PdfBox-Android", "Could not load font file: " + file, e); } finally @@ -814,23 +880,25 @@ private void addType1Font(File pfbFile) throws IOException Type1Font type1 = Type1Font.createWithPFB(input); if (type1.getName() == null) { - fontInfoList.add(new FSIgnored(pfbFile, FontFormat.PFB, "*skipnoname*")); + fontInfoList.add(createFSIgnored(pfbFile, FontFormat.PFB, "*skipnoname*")); Log.w("PdfBox-Android", "Missing 'name' entry for PostScript name in font " + pfbFile); return; } if (type1.getName().contains("|")) { - fontInfoList.add(new FSIgnored(pfbFile, FontFormat.PFB, "*skippipeinname*")); + fontInfoList.add(createFSIgnored(pfbFile, FontFormat.PFB, "*skippipeinname*")); Log.w("PdfBox-Android", "Skipping font with '|' in name " + type1.getName() + " in file " + pfbFile); return; } + String hash = computeHash(pfbFile); fontInfoList.add(new FSFontInfo(pfbFile, FontFormat.PFB, type1.getName(), - null, -1, -1, 0, 0, -1, null, this)); + null, -1, -1, 0, 0, -1, null, this, hash, pfbFile.lastModified())); if (PDFBoxConfig.isDebugEnabled()) { + fontInfoList.add(createFSIgnored(pfbFile, FontFormat.PFB, "*skipexception*")); Log.d("PdfBox-Android", "PFB: '" + type1.getName() + "' / '" + type1.getFamilyName() + "' / '" + - type1.getWeight() + "'"); + type1.getWeight() + "'"); } } catch (IOException e) @@ -864,4 +932,33 @@ public List getFontInfo() { return fontInfoList; } + + private static String computeHash(File file) throws IOException + { + InputStream is = null; + try + { + is = new FileInputStream(file); + return computeHash(is); + } + finally + { + IOUtils.closeQuietly(is); + } + } + + private static String computeHash(InputStream is) throws IOException + { + CRC32 crc = new CRC32(); + + byte[] buffer = new byte[4096]; + int readBytes; + while ((readBytes = is.read(buffer)) != -1) + { + crc.update(buffer, 0, readBytes); + } + + long l = crc.getValue(); + return Long.toHexString(l); + } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/FontMapperImpl.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/FontMapperImpl.java index 13e1a462f..0c5807572 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/FontMapperImpl.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/FontMapperImpl.java @@ -33,14 +33,13 @@ import java.util.PriorityQueue; import java.util.Set; +import com.tom_roush.pdfbox.android.PDFBoxResourceLoader; import com.tom_roush.fontbox.FontBoxFont; import com.tom_roush.fontbox.ttf.OpenTypeFont; import com.tom_roush.fontbox.ttf.TTFParser; import com.tom_roush.fontbox.ttf.TrueTypeFont; import com.tom_roush.fontbox.type1.Type1Font; import com.tom_roush.pdfbox.android.PDFBoxConfig; -import com.tom_roush.pdfbox.android.PDFBoxResourceLoader; - /** * Font mapper, locates non-embedded fonts via a pluggable FontProvider. * @@ -48,6 +47,7 @@ */ final class FontMapperImpl implements FontMapper { + private static final FontCache fontCache = new FontCache(); // todo: static cache isn't ideal private FontProvider fontProvider; private Map fontInfoByName; @@ -60,51 +60,50 @@ final class FontMapperImpl implements FontMapper { // substitutes for standard 14 fonts addSubstitutes("Courier", - new ArrayList(Arrays.asList("CourierNew", "CourierNewPSMT", "LiberationMono", - "NimbusMonL-Regu", "DroidSansMono"))); + new ArrayList(Arrays.asList("CourierNew", "CourierNewPSMT", "LiberationMono", + "NimbusMonL-Regu", "DroidSansMono"))); addSubstitutes("Courier-Bold", - new ArrayList(Arrays.asList("CourierNewPS-BoldMT", "CourierNew-Bold", - "LiberationMono-Bold", "NimbusMonL-Bold", "DroidSansMono"))); + new ArrayList(Arrays.asList("CourierNewPS-BoldMT", "CourierNew-Bold", + "LiberationMono-Bold", "NimbusMonL-Bold", "DroidSansMono"))); addSubstitutes("Courier-Oblique", - new ArrayList(Arrays.asList("CourierNewPS-ItalicMT","CourierNew-Italic", - "LiberationMono-Italic", "NimbusMonL-ReguObli", "DroidSansMono"))); + new ArrayList(Arrays.asList("CourierNewPS-ItalicMT","CourierNew-Italic", + "LiberationMono-Italic", "NimbusMonL-ReguObli", "DroidSansMono"))); addSubstitutes("Courier-BoldOblique", - new ArrayList(Arrays.asList("CourierNewPS-BoldItalicMT", - "CourierNew-BoldItalic", "LiberationMono-BoldItalic", - "NimbusMonL-BoldObli", "DroidSansMono"))); + new ArrayList(Arrays.asList("CourierNewPS-BoldItalicMT", + "CourierNew-BoldItalic", "LiberationMono-BoldItalic", + "NimbusMonL-BoldObli", "DroidSansMono"))); addSubstitutes("Helvetica", - new ArrayList(Arrays.asList("ArialMT", "Arial", "LiberationSans", - "NimbusSanL-Regu", "Roboto-Regular"))); + new ArrayList(Arrays.asList("ArialMT", "Arial", "LiberationSans", + "NimbusSanL-Regu", "Roboto-Regular"))); addSubstitutes("Helvetica-Bold", - new ArrayList(Arrays.asList("Arial-BoldMT", "Arial-Bold", - "LiberationSans-Bold", "NimbusSanL-Bold", "Roboto-Bold"))); + new ArrayList(Arrays.asList("Arial-BoldMT", "Arial-Bold", + "LiberationSans-Bold", "NimbusSanL-Bold", "Roboto-Bold"))); addSubstitutes("Helvetica-Oblique", - new ArrayList(Arrays.asList("Arial-ItalicMT", "Arial-Italic", - "Helvetica-Italic", "LiberationSans-Italic", "NimbusSanL-ReguItal", "Roboto-Italic"))); + new ArrayList(Arrays.asList("Arial-ItalicMT", "Arial-Italic", + "Helvetica-Italic", "LiberationSans-Italic", "NimbusSanL-ReguItal", "Roboto-Italic"))); addSubstitutes("Helvetica-BoldOblique", - new ArrayList(Arrays.asList("Arial-BoldItalicMT", "Helvetica-BoldItalic", - "LiberationSans-BoldItalic", "NimbusSanL-BoldItal", "Roboto-BoldItalic"))); + new ArrayList(Arrays.asList("Arial-BoldItalicMT", "Helvetica-BoldItalic", + "LiberationSans-BoldItalic", "NimbusSanL-BoldItal", "Roboto-BoldItalic"))); addSubstitutes("Times-Roman", - new ArrayList(Arrays.asList("TimesNewRomanPSMT", "TimesNewRoman", - "TimesNewRomanPS", "LiberationSerif", "NimbusRomNo9L-Regu", "Roboto-Regular"))); + new ArrayList(Arrays.asList("TimesNewRomanPSMT", "TimesNewRoman", + "TimesNewRomanPS", "LiberationSerif", "NimbusRomNo9L-Regu", "Roboto-Regular"))); addSubstitutes("Times-Bold", - new ArrayList(Arrays.asList("TimesNewRomanPS-BoldMT", "TimesNewRomanPS-Bold", - "TimesNewRoman-Bold", "LiberationSerif-Bold", - "NimbusRomNo9L-Medi", "DroidSerif-Bold", "Roboto-Bold"))); + new ArrayList(Arrays.asList("TimesNewRomanPS-BoldMT", "TimesNewRomanPS-Bold", + "TimesNewRoman-Bold", "LiberationSerif-Bold", + "NimbusRomNo9L-Medi", "DroidSerif-Bold", "Roboto-Bold"))); addSubstitutes("Times-Italic", - new ArrayList(Arrays.asList("TimesNewRomanPS-ItalicMT", - "TimesNewRomanPS-Italic", "TimesNewRoman-Italic", "LiberationSerif-Italic", - "NimbusRomNo9L-ReguItal", "DroidSerif-Italic", "Roboto-Italic"))); + new ArrayList(Arrays.asList("TimesNewRomanPS-ItalicMT", + "TimesNewRomanPS-Italic", "TimesNewRoman-Italic", "LiberationSerif-Italic", + "NimbusRomNo9L-ReguItal", "DroidSerif-Italic", "Roboto-Italic"))); addSubstitutes("Times-BoldItalic", - new ArrayList(Arrays.asList("TimesNewRomanPS-BoldItalicMT", - "TimesNewRomanPS-BoldItalic", "TimesNewRoman-BoldItalic", - "LiberationSerif-BoldItalic", "NimbusRomNo9L-MediItal", "DroidSerif-BoldItalic", "Roboto-BoldItalic"))); + new ArrayList(Arrays.asList("TimesNewRomanPS-BoldItalicMT", + "TimesNewRomanPS-BoldItalic", "TimesNewRoman-BoldItalic", + "LiberationSerif-BoldItalic", "NimbusRomNo9L-MediItal", "DroidSerif-BoldItalic", "Roboto-BoldItalic"))); addSubstitutes("Symbol", - new ArrayList(Arrays.asList("Symbol", "SymbolMT", "StandardSymL"))); + new ArrayList(Arrays.asList("Symbol", "SymbolMT", "StandardSymL"))); addSubstitutes("ZapfDingbats", new ArrayList( - Arrays.asList("ZapfDingbatsITCbyBT-Regular", "ZapfDingbatsITC", "Dingbats", - "MS-Gothic"))); - // TODO: PdfBox-Android load extra fonts? (DroidSerif for times and a symbol font) + Arrays.asList("ZapfDingbatsITCbyBT-Regular", "ZapfDingbatsITC", "Dingbats", + "MS-Gothic", "DejaVuSans"))); // Acrobat also uses alternative names for Standard 14 fonts, which we map to those above // these include names such as "Arial" and "TimesNewRoman" @@ -119,6 +118,7 @@ final class FontMapperImpl implements FontMapper // ------------------------- + // TODO: PdfBox-Android load extra fonts? (DroidSerif for times and a symbol font) try { String ttfName = "com/tom_roush/pdfbox/resources/ttf/LiberationSans-Regular.ttf"; @@ -270,8 +270,8 @@ private String getFallbackFontName(PDFontDescriptor fontDescriptor) { String lower = fontDescriptor.getFontName().toLowerCase(); isBold = lower.contains("bold") || - lower.contains("black") || - lower.contains("heavy"); + lower.contains("black") || + lower.contains("heavy"); } // font descriptor flags should describe the style @@ -343,7 +343,7 @@ else if (fontDescriptor.isItalic()) */ @Override public FontMapping getTrueTypeFont(String baseFont, - PDFontDescriptor fontDescriptor) + PDFontDescriptor fontDescriptor) { TrueTypeFont ttf = (TrueTypeFont)findFont(FontFormat.TTF, baseFont); if (ttf != null) @@ -372,7 +372,7 @@ public FontMapping getTrueTypeFont(String baseFont, */ @Override public FontMapping getFontBoxFont(String baseFont, - PDFontDescriptor fontDescriptor) + PDFontDescriptor fontDescriptor) { FontBoxFont font = findFontBoxFont(baseFont); if (font != null) @@ -435,10 +435,7 @@ private FontBoxFont findFont(FontFormat format, String postScriptName) } // make sure the font provider is initialized - if (fontProvider == null) - { - getProvider(); - } + getProvider(); // first try to match the PostScript name FontInfo info = getFont(format, postScriptName); @@ -471,6 +468,18 @@ private FontBoxFont findFont(FontFormat format, String postScriptName) return info.getFont(); } + if (postScriptName.contains(",")) + { + postScriptName = postScriptName.substring(0, postScriptName.indexOf(",")); + // PDFBOX-5806: try cutting font style and getting the basefont + // eg. for "Wingdings,Bolt" to "Wingding-Regular" (including the following step) + info = getFont(format, postScriptName); + if (info != null) + { + return info.getFont(); + } + } + // try appending "-Regular", works for Wingdings on windows info = getFont(format, postScriptName + "-Regular"); if (info != null) @@ -486,10 +495,11 @@ private FontBoxFont findFont(FontFormat format, String postScriptName) */ private FontInfo getFont(FontFormat format, String postScriptName) { + int index = postScriptName.indexOf('+'); // strip subset tag (happens when we substitute a corrupt embedded font, see PDFBOX-2642) - if (postScriptName.contains("+")) + if (index > -1) { - postScriptName = postScriptName.substring(postScriptName.indexOf('+') + 1); + postScriptName = postScriptName.substring(index + 1); } // look up the PostScript name @@ -514,7 +524,7 @@ private FontInfo getFont(FontFormat format, String postScriptName) */ @Override public CIDFontMapping getCIDFont(String baseFont, PDFontDescriptor fontDescriptor, - PDCIDSystemInfo cidSystemInfo) + PDCIDSystemInfo cidSystemInfo) { // try name match or substitute with OTF OpenTypeFont otf1 = (OpenTypeFont)findFont(FontFormat.OTF, baseFont); @@ -530,7 +540,7 @@ public CIDFontMapping getCIDFont(String baseFont, PDFontDescriptor fontDescripto return new CIDFontMapping(null, ttf, false); } - if (cidSystemInfo != null) + if (cidSystemInfo != null && fontDescriptor != null) { // "In Acrobat 3.0.1 and later, Type 0 fonts that use a CMap whose CIDSystemInfo // dictionary defines the Adobe-GB1, Adobe-CNS1 Adobe-Japan1, or Adobe-Korea1 character @@ -575,8 +585,11 @@ else if (font != null) * @param cidSystemInfo Font's CIDSystemInfo, may be null. */ private PriorityQueue getFontMatches(PDFontDescriptor fontDescriptor, - PDCIDSystemInfo cidSystemInfo) + PDCIDSystemInfo cidSystemInfo) { + // make sure the font provider is initialized + getProvider(); + PriorityQueue queue = new PriorityQueue(20); for (FontInfo info : fontInfoByName.values()) @@ -597,7 +610,7 @@ private PriorityQueue getFontMatches(PDFontDescriptor fontDescriptor, { if (panose.getFamilyKind() == 0 && (info.getPostScriptName().toLowerCase().contains("barcode") || - info.getPostScriptName().startsWith("Code")) && + info.getPostScriptName().startsWith("Code")) && !probablyBarcodeFont(fontDescriptor)) { // PDFBOX-4268: ignore barcode font if we aren't searching for one. @@ -610,15 +623,15 @@ private PriorityQueue getFontMatches(PDFontDescriptor fontDescriptor, match.score += 2; } else if (panose.getSerifStyle() >= 2 && panose.getSerifStyle() <= 5 && - info.getPanose().getSerifStyle() >= 2 && - info.getPanose().getSerifStyle() <= 5) + info.getPanose().getSerifStyle() >= 2 && + info.getPanose().getSerifStyle() <= 5) { // cove (serif) match.score += 1; } else if (panose.getSerifStyle() >= 11 && panose.getSerifStyle() <= 13 && - info.getPanose().getSerifStyle() >= 11 && - info.getPanose().getSerifStyle() <= 13) + info.getPanose().getSerifStyle() >= 11 && + info.getPanose().getSerifStyle() <= 13) { // sans-serif match.score += 1; @@ -680,7 +693,7 @@ private boolean probablyBarcodeFont(PDFontDescriptor fontDescriptor) fn = ""; } return ff.startsWith("Code") || ff.toLowerCase().contains("barcode") || - fn.startsWith("Code") || fn.toLowerCase().contains("barcode"); + fn.startsWith("Code") || fn.toLowerCase().contains("barcode"); } /** @@ -689,10 +702,15 @@ private boolean probablyBarcodeFont(PDFontDescriptor fontDescriptor) */ private boolean isCharSetMatch(PDCIDSystemInfo cidSystemInfo, FontInfo info) { + String ordering = cidSystemInfo.getOrdering(); + if (ordering == null) + { + return false; + } if (info.getCIDSystemInfo() != null) { return info.getCIDSystemInfo().getRegistry().equals(cidSystemInfo.getRegistry()) && - info.getCIDSystemInfo().getOrdering().equals(cidSystemInfo.getOrdering()); + info.getCIDSystemInfo().getOrdering().equals(ordering); } else { @@ -709,26 +727,26 @@ private boolean isCharSetMatch(PDCIDSystemInfo cidSystemInfo, FontInfo info) // PDFBOX-4793 and PDF.js 10699: This font has only Korean, but has bits 17-21 set. codePageRange &= ~(JIS_JAPAN | CHINESE_SIMPLIFIED | CHINESE_TRADITIONAL); } - if (cidSystemInfo.getOrdering().equals("GB1") && - (codePageRange & CHINESE_SIMPLIFIED) == CHINESE_SIMPLIFIED) + if (ordering.equals("GB1") && + (codePageRange & CHINESE_SIMPLIFIED) == CHINESE_SIMPLIFIED) { return true; } - else if (cidSystemInfo.getOrdering().equals("CNS1") && - (codePageRange & CHINESE_TRADITIONAL) == CHINESE_TRADITIONAL) + else if (ordering.equals("CNS1") && + (codePageRange & CHINESE_TRADITIONAL) == CHINESE_TRADITIONAL) { return true; } - else if (cidSystemInfo.getOrdering().equals("Japan1") && - (codePageRange & JIS_JAPAN) == JIS_JAPAN) + else if (ordering.equals("Japan1") && + (codePageRange & JIS_JAPAN) == JIS_JAPAN) { return true; } else { - return cidSystemInfo.getOrdering().equals("Korea1") && - ((codePageRange & KOREAN_WANSUNG) == KOREAN_WANSUNG || - (codePageRange & KOREAN_JOHAB) == KOREAN_JOHAB); + return ordering.equals("Korea1") && + ((codePageRange & KOREAN_WANSUNG) == KOREAN_WANSUNG || + (codePageRange & KOREAN_JOHAB) == KOREAN_JOHAB); } } } @@ -765,9 +783,9 @@ private FontMatch printMatches(PriorityQueue queue) FontMatch match = queue.poll(); FontInfo info = match.info; System.out.println(match.score + " | " + info.getMacStyle() + " " + - info.getFamilyClass() + " " + info.getPanose() + " " + - info.getCIDSystemInfo() + " " + info.getPostScriptName() + " " + - info.getFormat()); + info.getFamilyClass() + " " + info.getPanose() + " " + + info.getCIDSystemInfo() + " " + info.getPostScriptName() + " " + + info.getFormat()); } System.out.println("-------"); return bestMatch; diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDCIDFont.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDCIDFont.java index 6e4bfcd59..122b31b11 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDCIDFont.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDCIDFont.java @@ -32,7 +32,6 @@ import com.tom_roush.pdfbox.io.IOUtils; import com.tom_roush.pdfbox.pdmodel.common.COSObjectable; import com.tom_roush.pdfbox.util.Vector; - /** * A CIDFont. A CIDFont is a PDF object that contains information about a CIDFont program. Although * its Type value is Font, a CIDFont is not actually a font. @@ -43,6 +42,7 @@ */ public abstract class PDCIDFont implements COSObjectable, PDFontLike, PDVectorFont { + protected final PDType0Font parent; private Map widths; @@ -71,6 +71,7 @@ public abstract class PDCIDFont implements COSObjectable, PDFontLike, PDVectorFo private void readWidths() { + // see 9.7.4.3, "Glyph Metrics in CIDFonts" widths = new HashMap(); COSBase wBase = dict.getDictionaryObject(COSName.W); if (wBase instanceof COSArray) @@ -217,7 +218,7 @@ public PDFontDescriptor getFontDescriptor() { if (fontDescriptor == null) { - COSDictionary fd = (COSDictionary) dict.getDictionaryObject(COSName.FONT_DESC); + COSDictionary fd = dict.getCOSDictionary(COSName.FONT_DESC); if (fd != null) { fontDescriptor = new PDFontDescriptor(fd); diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDCIDFontType2.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDCIDFontType2.java index f450bc431..e61b93cfc 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDCIDFontType2.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDCIDFontType2.java @@ -35,7 +35,6 @@ import com.tom_roush.pdfbox.pdmodel.common.PDRectangle; import com.tom_roush.pdfbox.pdmodel.common.PDStream; import com.tom_roush.pdfbox.util.Matrix; - /** * Type 2 CIDFont (TrueType). * @@ -43,6 +42,7 @@ */ public class PDCIDFontType2 extends PDCIDFont { + private final TrueTypeFont ttf; private final int[] cid2gid; private final boolean isEmbedded; @@ -142,19 +142,25 @@ private TrueTypeFont findFontOrSubstitute() throws IOException TrueTypeFont ttfFont; CIDFontMapping mapping = FontMappers.instance() - .getCIDFont(getBaseFont(), getFontDescriptor(), - getCIDSystemInfo()); + .getCIDFont(getBaseFont(), getFontDescriptor(), + getCIDSystemInfo()); if (mapping.isCIDFont()) { ttfFont = mapping.getFont(); } else { - ttfFont = (TrueTypeFont)mapping.getTrueTypeFont(); + ttfFont = (TrueTypeFont) mapping.getTrueTypeFont(); + if (ttfFont == null) + { + // shouldn't happen?! + throw new IOException("mapping.getTrueTypeFont() returns null, please report"); + } } if (mapping.isFallback()) { - Log.w("PdfBox-Android", "Using fallback font " + ttfFont.getName() + " for CID-keyed TrueType font " + getBaseFont()); + Log.w("PdfBox-Android", "Using fallback font " + ttfFont.getName() + + " for CID-keyed TrueType font " + getBaseFont()); } return ttfFont; } @@ -186,13 +192,13 @@ private BoundingBox generateBoundingBox() throws IOException { PDRectangle bbox = getFontDescriptor().getFontBoundingBox(); if (bbox != null && - (Float.compare(bbox.getLowerLeftX(), 0) != 0 || - Float.compare(bbox.getLowerLeftY(), 0) != 0 || - Float.compare(bbox.getUpperRightX(), 0) != 0 || - Float.compare(bbox.getUpperRightY(), 0) != 0)) + (Float.compare(bbox.getLowerLeftX(), 0) != 0 || + Float.compare(bbox.getLowerLeftY(), 0) != 0 || + Float.compare(bbox.getUpperRightX(), 0) != 0 || + Float.compare(bbox.getUpperRightY(), 0) != 0)) { return new BoundingBox(bbox.getLowerLeftX(), bbox.getLowerLeftY(), - bbox.getUpperRightX(), bbox.getUpperRightY()); + bbox.getUpperRightX(), bbox.getUpperRightY()); } } return ttf.getFontBBox(); @@ -233,9 +239,13 @@ public int codeToGID(int code) throws IOException // font's 'cmap' table. The means by which this is accomplished are implementation- // dependent. // omit the CID2GID mapping if the embedded font is replaced by an external font - if (cid2gid != null && !isDamaged) + String name = getName(); + if (cid2gid != null && !isDamaged && name != null && name.equals(ttf.getName())) { // Acrobat allows non-embedded GIDs - todo: can we find a test PDF for this? + // PDFBOX-5612: should happen only if it's really the same font + // this is not perfect, we may have to improve this because some identical fonts + // have different names Log.w("PdfBox-Android", "Using non-embedded GIDs in font " + getName()); int cid = codeToCID(code); if (cid < cid2gid.length) @@ -293,16 +303,13 @@ else if (unicode.length() > 1) } else { - // "Identity" is the default CIDToGIDMap - if (cid < ttf.getNumberOfGlyphs()) + // "Identity" is the default for CFF-based OpenTypeFonts + if (ttf instanceof OpenTypeFont && ((OpenTypeFont) ttf).isPostScript()) { return cid; } - else - { - // out of range CIDs map to GID 0 - return 0; - } + // "Identity" is the default for TrueTypeFonts if the CID is within the range + return cid < ttf.getNumberOfGlyphs() ? cid : 0; } } } @@ -312,7 +319,7 @@ public float getHeight(int code) throws IOException { // todo: really we want the BBox, (for text extraction:) return (ttf.getHorizontalHeader().getAscender() + -ttf.getHorizontalHeader().getDescender()) - / ttf.getUnitsPerEm(); // todo: shouldn't this be the yMax/yMin? + / (float) ttf.getUnitsPerEm(); // todo: shouldn't this be the yMax/yMin? } @Override @@ -375,7 +382,7 @@ public byte[] encode(int unicode) if (cid == 0) { throw new IllegalArgumentException( - String.format("No glyph for U+%04X (%c) in font %s", unicode, (char) unicode, getName())); + String.format("No glyph for U+%04X (%c) in font %s", unicode, (char) unicode, getName())); } // CID is always 2-bytes (16-bit) for TrueType diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDCIDFontType2Embedder.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDCIDFontType2Embedder.java index 76dbf5946..2dc3c9ac5 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDCIDFontType2Embedder.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDCIDFontType2Embedder.java @@ -23,14 +23,20 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; +import com.tom_roush.fontbox.cff.CFFCIDFont; +import com.tom_roush.fontbox.cff.CFFCharset; +import com.tom_roush.fontbox.cff.CFFFont; +import com.tom_roush.fontbox.ttf.CFFTable; import com.tom_roush.fontbox.ttf.GlyphData; import com.tom_roush.fontbox.ttf.GlyphTable; import com.tom_roush.fontbox.ttf.HorizontalMetricsTable; +import com.tom_roush.fontbox.ttf.OpenTypeFont; import com.tom_roush.fontbox.ttf.TrueTypeFont; import com.tom_roush.fontbox.ttf.VerticalHeaderTable; import com.tom_roush.fontbox.ttf.VerticalMetricsTable; @@ -40,7 +46,6 @@ import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.PDDocument; import com.tom_roush.pdfbox.pdmodel.common.PDStream; - /** * Embedded PDCIDFontType2 builder. Helper class to populate a PDCIDFontType2 and its parent * PDType0Font from a TTF. @@ -51,6 +56,7 @@ final class PDCIDFontType2Embedder extends TrueTypeEmbedder { + private final PDDocument document; private final PDType0Font parent; private final COSDictionary dict; @@ -67,7 +73,7 @@ final class PDCIDFontType2Embedder extends TrueTypeEmbedder * @throws IOException if the TTF could not be read */ PDCIDFontType2Embedder(PDDocument document, COSDictionary dict, TrueTypeFont ttf, - boolean embedSubset, PDType0Font parent, boolean vertical) throws IOException + boolean embedSubset, PDType0Font parent, boolean vertical) throws IOException { super(document, dict, ttf, embedSubset); this.document = document; @@ -98,7 +104,7 @@ final class PDCIDFontType2Embedder extends TrueTypeEmbedder */ @Override protected void buildSubset(InputStream ttfSubset, String tag, Map gidToCid) - throws IOException + throws IOException { // build CID2GIDMap, because the content stream has been written with the old GIDs TreeMap cidToGid = new TreeMap(); @@ -125,6 +131,20 @@ protected void buildSubset(InputStream ttfSubset, String tag, Map newGIDToOldCID) throws IOException { + // PDFBOX-6210: + // When several code points map to one glyph, prefer the one actually used in the + // document (first occurrence wins) instead of cmapLookup.getCharCodes(gid).get(0), + // which is the lowest code point and often an unexpected compatibility character. + Map inputCodePointByGID = new HashMap(); + for (int codePoint : getSubsetCodePoints()) + { + int inputGid = cmapLookup.getGlyphId(codePoint); + if (inputGid > 0 && inputCodePointByGID.get(inputGid) == null) + { + inputCodePointByGID.put(inputGid, codePoint); + } + } + ToUnicodeWriter toUniWriter = new ToUnicodeWriter(); boolean hasSurrogates = false; for (int gid = 1, max = ttf.getMaximumProfile().getNumGlyphs(); gid <= max; gid++) @@ -149,10 +169,14 @@ private void buildToUnicodeCMap(Map newGIDToOldCID) throws IOE // skip composite glyph components that have no code point List codes = cmapLookup.getCharCodes(cid); // old GID -> Unicode - if (codes != null) + // PDFBOX-6210: try to get the codepoint that is actually use in the document + // instead of cmapLookup.getCharCodes(gid).get(0) + // set inputCodePoint to null to test pre-PDFBOX-6210 behavior + Integer inputCodePoint = inputCodePointByGID.get(cid); + if (inputCodePoint != null || codes != null) { - // use the first entry even for ambiguous mappings - int codePoint = codes.get(0); + // fall back to the cmap's first entry for glyphs with no recorded input + int codePoint = inputCodePoint != null ? inputCodePoint : codes.get(0); if (codePoint > 0xFFFF) { hasSurrogates = true; @@ -216,12 +240,48 @@ private COSDictionary createCIDFont() throws IOException buildVerticalMetrics(cidFont); } + if (ttf instanceof OpenTypeFont && !needsSubset()) + { + checkForCidGidIdentity(); + } + // CIDToGIDMap cidFont.setItem(COSName.CID_TO_GID_MAP, COSName.IDENTITY); return cidFont; } + private void checkForCidGidIdentity() throws IOException + { + // PDFBOX-6172: if somebody is using a not subsetted otf font, check whether cid == gid + // (subsetted will fail anyway) + OpenTypeFont otf = (OpenTypeFont) ttf; + CFFTable cffTable = otf.getCFF(); + if (cffTable == null) + { + return; + } + CFFFont cff = cffTable.getFont(); + if (!(cff instanceof CFFCIDFont)) + { + return; + } + CFFCharset charset = cff.getCharset(); + if (charset == null) + { + return; + } + int glyphCount = otf.getNumberOfGlyphs(); + for (int gid = 0; gid < glyphCount; gid++) + { + int cid = charset.getCIDForGID(gid); + if (gid != cid) + { + throw new IllegalStateException("CID and GID not identical: CID " + cid + " != GID " + gid + ", use a ttf font instead"); + } + } + } + private void addNameTag(String tag) { String name = fontDescriptor.getFontName(); @@ -285,7 +345,7 @@ private void buildWidths(TreeMap cidToGid) throws IOException COSArray widths = new COSArray(); COSArray ws = new COSArray(); int prev = Integer.MIN_VALUE; - // Use a sorted list to get an optimal width array + // Use a sorted list to get an optimal width array Set keys = cidToGid.keySet(); HorizontalMetricsTable horizontalMetricsTable = ttf.getHorizontalMetrics(); for (int cid : keys) @@ -580,63 +640,63 @@ private COSArray getVerticalMetrics(int[] values) throws IOException switch (state) { - case FIRST: - if (cid == lastCid + 1 && w1Value == lastW1Value && vxValue == lastVxValue && vyValue == lastVyValue) - { - state = State.SERIAL; - } - else if (cid == lastCid + 1) - { - state = State.BRACKET; - inner = new COSArray(); - inner.add(COSInteger.get(lastW1Value)); - inner.add(COSInteger.get(lastVxValue)); - inner.add(COSInteger.get(lastVyValue)); - } - else - { - inner = new COSArray(); - inner.add(COSInteger.get(lastW1Value)); - inner.add(COSInteger.get(lastVxValue)); - inner.add(COSInteger.get(lastVyValue)); - outer.add(inner); - outer.add(COSInteger.get(cid)); - } - break; - case BRACKET: - if (cid == lastCid + 1 && w1Value == lastW1Value && vxValue == lastVxValue && vyValue == lastVyValue) - { - state = State.SERIAL; - outer.add(inner); - outer.add(COSInteger.get(lastCid)); - } - else if (cid == lastCid + 1) - { - inner.add(COSInteger.get(lastW1Value)); - inner.add(COSInteger.get(lastVxValue)); - inner.add(COSInteger.get(lastVyValue)); - } - else - { - state = State.FIRST; - inner.add(COSInteger.get(lastW1Value)); - inner.add(COSInteger.get(lastVxValue)); - inner.add(COSInteger.get(lastVyValue)); - outer.add(inner); - outer.add(COSInteger.get(cid)); - } - break; - case SERIAL: - if (cid != lastCid + 1 || w1Value != lastW1Value || vxValue != lastVxValue || vyValue != lastVyValue) - { - outer.add(COSInteger.get(lastCid)); - outer.add(COSInteger.get(lastW1Value)); - outer.add(COSInteger.get(lastVxValue)); - outer.add(COSInteger.get(lastVyValue)); - outer.add(COSInteger.get(cid)); - state = State.FIRST; - } - break; + case FIRST: + if (cid == lastCid + 1 && w1Value == lastW1Value && vxValue == lastVxValue && vyValue == lastVyValue) + { + state = State.SERIAL; + } + else if (cid == lastCid + 1) + { + state = State.BRACKET; + inner = new COSArray(); + inner.add(COSInteger.get(lastW1Value)); + inner.add(COSInteger.get(lastVxValue)); + inner.add(COSInteger.get(lastVyValue)); + } + else + { + inner = new COSArray(); + inner.add(COSInteger.get(lastW1Value)); + inner.add(COSInteger.get(lastVxValue)); + inner.add(COSInteger.get(lastVyValue)); + outer.add(inner); + outer.add(COSInteger.get(cid)); + } + break; + case BRACKET: + if (cid == lastCid + 1 && w1Value == lastW1Value && vxValue == lastVxValue && vyValue == lastVyValue) + { + state = State.SERIAL; + outer.add(inner); + outer.add(COSInteger.get(lastCid)); + } + else if (cid == lastCid + 1) + { + inner.add(COSInteger.get(lastW1Value)); + inner.add(COSInteger.get(lastVxValue)); + inner.add(COSInteger.get(lastVyValue)); + } + else + { + state = State.FIRST; + inner.add(COSInteger.get(lastW1Value)); + inner.add(COSInteger.get(lastVxValue)); + inner.add(COSInteger.get(lastVyValue)); + outer.add(inner); + outer.add(COSInteger.get(cid)); + } + break; + case SERIAL: + if (cid != lastCid + 1 || w1Value != lastW1Value || vxValue != lastVxValue || vyValue != lastVyValue) + { + outer.add(COSInteger.get(lastCid)); + outer.add(COSInteger.get(lastW1Value)); + outer.add(COSInteger.get(lastVxValue)); + outer.add(COSInteger.get(lastVyValue)); + outer.add(COSInteger.get(cid)); + state = State.FIRST; + } + break; } lastW1Value = w1Value; lastVxValue = vxValue; @@ -646,25 +706,25 @@ else if (cid == lastCid + 1) switch (state) { - case FIRST: - inner = new COSArray(); - inner.add(COSInteger.get(lastW1Value)); - inner.add(COSInteger.get(lastVxValue)); - inner.add(COSInteger.get(lastVyValue)); - outer.add(inner); - break; - case BRACKET: - inner.add(COSInteger.get(lastW1Value)); - inner.add(COSInteger.get(lastVxValue)); - inner.add(COSInteger.get(lastVyValue)); - outer.add(inner); - break; - case SERIAL: - outer.add(COSInteger.get(lastCid)); - outer.add(COSInteger.get(lastW1Value)); - outer.add(COSInteger.get(lastVxValue)); - outer.add(COSInteger.get(lastVyValue)); - break; + case FIRST: + inner = new COSArray(); + inner.add(COSInteger.get(lastW1Value)); + inner.add(COSInteger.get(lastVxValue)); + inner.add(COSInteger.get(lastVyValue)); + outer.add(inner); + break; + case BRACKET: + inner.add(COSInteger.get(lastW1Value)); + inner.add(COSInteger.get(lastVxValue)); + inner.add(COSInteger.get(lastVyValue)); + outer.add(inner); + break; + case SERIAL: + outer.add(COSInteger.get(lastCid)); + outer.add(COSInteger.get(lastW1Value)); + outer.add(COSInteger.get(lastVxValue)); + outer.add(COSInteger.get(lastVyValue)); + break; } return outer; } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDFont.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDFont.java index 2e0c9169f..c8e1b8089 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDFont.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDFont.java @@ -30,6 +30,7 @@ import com.tom_roush.fontbox.afm.FontMetrics; import com.tom_roush.fontbox.cmap.CMap; +import com.tom_roush.pdfbox.android.PDFBoxConfig; import com.tom_roush.pdfbox.cos.COSArray; import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSDictionary; @@ -42,7 +43,6 @@ import com.tom_roush.pdfbox.pdmodel.font.encoding.GlyphList; import com.tom_roush.pdfbox.util.Matrix; import com.tom_roush.pdfbox.util.Vector; - /** * This is the base class for all PDF fonts. * @@ -147,18 +147,23 @@ private CMap loadUnicodeCmap() cmap = readCMap(toUnicode); if (cmap != null && !cmap.hasUnicodeMappings()) { - Log.w("PdfBox-Android", "Invalid ToUnicode CMap in font " + getName()); + String name = getName(); + Log.w("PdfBox-Android", "Invalid ToUnicode CMap in font " + name); String cmapName = cmap.getName() != null ? cmap.getName() : ""; String ordering = cmap.getOrdering() != null ? cmap.getOrdering() : ""; COSBase encoding = dict.getDictionaryObject(COSName.ENCODING); if (cmapName.contains("Identity") // - || ordering.contains("Identity") // - || COSName.IDENTITY_H.equals(encoding) // - || COSName.IDENTITY_V.equals(encoding)) + || ordering.contains("Identity") // + || COSName.IDENTITY_H.equals(encoding) // + || COSName.IDENTITY_V.equals(encoding)) { - // assume that if encoding is identity, then the reverse is also true - cmap = CMapManager.getPredefinedCMap(COSName.IDENTITY_H.getName()); - Log.w("PdfBox-Android", "Using predefined identity CMap instead"); + COSDictionary encodingDict = dict.getCOSDictionary(COSName.ENCODING); + if (encodingDict == null || !encodingDict.containsKey(COSName.DIFFERENCES)) + { + // assume that if encoding is identity, then the reverse is also true + cmap = CMapManager.getPredefinedCMap(COSName.IDENTITY_H.getName()); + Log.w("PdfBox-Android", "Using predefined identity CMap instead"); + } } } } @@ -323,7 +328,7 @@ public float getWidth(int code) throws IOException */ public final byte[] encode(String text) throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); + ByteArrayOutputStream out = new ByteArrayOutputStream(Math.max(32, text.length())); int offset = 0; while (offset < text.length()) { @@ -458,8 +463,8 @@ public String toUnicode(int code) throws IOException { if (toUnicodeCMap.getName() != null && toUnicodeCMap.getName().startsWith("Identity-") && - (dict.getDictionaryObject(COSName.TO_UNICODE) instanceof COSName || - !toUnicodeCMap.hasUnicodeMappings())) + (dict.getDictionaryObject(COSName.TO_UNICODE) instanceof COSName || + !toUnicodeCMap.hasUnicodeMappings())) { // handle the undocumented case of using Identity-H/V as a ToUnicode CMap, this // isn't actually valid as the Identity-x CMaps are code->CID maps, not @@ -527,7 +532,7 @@ public Matrix getFontMatrix() } /** - * Determines the width of the space character. + * Determines the width of the space character. This is very important for text extraction. * * @return the width of the space character */ @@ -547,7 +552,25 @@ public float getSpaceWidth() } else { - fontWidthOfSpace = getWidth(32); + try + { + // PDFBOX-5920: try with encoding, which gets the correct code + fontWidthOfSpace = getStringWidth(" "); + } + catch (UnsupportedOperationException ex) + { + // Happens if encoding isn't implemented + Log.d("PdfBox-Android", ex.getMessage(), ex); + } + catch (IllegalArgumentException ex) + { + // Happens if space is not available in the font + Log.d("PdfBox-Android", ex.getMessage(), ex); + } + if (fontWidthOfSpace <= 0) + { + fontWidthOfSpace = getWidth(32); + } } // try to get it from the font itself @@ -564,9 +587,14 @@ public float getSpaceWidth() } catch (Exception e) { - Log.e("PdfBox-Android", "Can't determine the width of the space character, assuming 250", e); + Log.e("PdfBox-Android", "Can't determine the width of the space character for font " + + getName() + ", assuming 250", e); fontWidthOfSpace = 250f; } + if (PDFBoxConfig.isDebugEnabled()) + { + Log.d("PdfBox-Android", "Space width for font " + getName() + " is " + fontWidthOfSpace); + } } return fontWidthOfSpace; } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDFontDescriptor.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDFontDescriptor.java index c1d9ec6b2..dce202ac2 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDFontDescriptor.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDFontDescriptor.java @@ -25,7 +25,6 @@ import com.tom_roush.pdfbox.pdmodel.common.COSObjectable; import com.tom_roush.pdfbox.pdmodel.common.PDRectangle; import com.tom_roush.pdfbox.pdmodel.common.PDStream; - /** * A font descriptor. * @@ -849,7 +848,7 @@ public void setCIDSet( PDStream stream ) */ public PDPanose getPanose() { - COSDictionary style = (COSDictionary)dic.getDictionaryObject(COSName.STYLE); + COSDictionary style = dic.getCOSDictionary(COSName.STYLE); if (style != null) { COSString panose = (COSString)style.getDictionaryObject(COSName.PANOSE); diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDSimpleFont.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDSimpleFont.java index 5520c6922..06282112e 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDSimpleFont.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDSimpleFont.java @@ -34,7 +34,7 @@ import com.tom_roush.pdfbox.pdmodel.font.encoding.MacRomanEncoding; import com.tom_roush.pdfbox.pdmodel.font.encoding.StandardEncoding; import com.tom_roush.pdfbox.pdmodel.font.encoding.WinAnsiEncoding; - +import com.tom_roush.pdfbox.pdmodel.font.encoding.ZapfDingbatsEncoding; /** * A simple font. Simple fonts use a PostScript encoding vector. * @@ -42,6 +42,7 @@ */ public abstract class PDSimpleFont extends PDFont { + protected Encoding encoding; protected GlyphList glyphList; private Boolean isSymbolic; @@ -85,11 +86,21 @@ protected void readEncoding() throws IOException if (encodingBase instanceof COSName) { COSName encodingName = (COSName) encodingBase; - this.encoding = Encoding.getInstance(encodingName); - if (this.encoding == null) + if ("ZapfDingbats".equals(getName()) && !isEmbedded()) { - Log.w("PdfBox-Android", "Unknown encoding: " + encodingName.getName()); - this.encoding = readEncodingFromFont(); // fallback + // PDFBOX- and PDF.js issue 16464: ignore other encodings + // this segment will work only if readEncoding() is called after the data + // for getName() and isEmbedded() is available + this.encoding = ZapfDingbatsEncoding.INSTANCE; + } + else + { + this.encoding = Encoding.getInstance(encodingName); + if (this.encoding == null) + { + Log.w("PdfBox-Android", "Unknown encoding: " + encodingName.getName()); + this.encoding = readEncodingFromFont(); // fallback + } } } else if (encodingBase instanceof COSDictionary) @@ -101,7 +112,7 @@ else if (encodingBase instanceof COSDictionary) COSName baseEncoding = encodingDict.getCOSName(COSName.BASE_ENCODING); boolean hasValidBaseEncoding = baseEncoding != null && - Encoding.getInstance(baseEncoding) != null; + Encoding.getInstance(baseEncoding) != null; if (!hasValidBaseEncoding && Boolean.TRUE.equals(symbolic)) { @@ -198,8 +209,8 @@ else if (isStandard14()) return true; } else if (encoding instanceof WinAnsiEncoding || - encoding instanceof MacRomanEncoding || - encoding instanceof StandardEncoding) + encoding instanceof MacRomanEncoding || + encoding instanceof StandardEncoding) { return false; } @@ -213,8 +224,8 @@ else if (encoding instanceof DictionaryEncoding) // skip } else if (!(WinAnsiEncoding.INSTANCE.contains(name) && - MacRomanEncoding.INSTANCE.contains(name) && - StandardEncoding.INSTANCE.contains(name))) + MacRomanEncoding.INSTANCE.contains(name) && + StandardEncoding.INSTANCE.contains(name))) { return true; } @@ -297,12 +308,12 @@ public String toUnicode(int code, GlyphList customGlyphList) throws IOException if (name != null) { Log.w("PdfBox-Android", "No Unicode mapping for " + name + " (" + code + ") in font " + - getName()); + getName()); } else { Log.w("PdfBox-Android", "No Unicode mapping for character code " + code + " in font " + - getName()); + getName()); } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDTrueTypeFont.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDTrueTypeFont.java index d152e120f..cba2eaf91 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDTrueTypeFont.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDTrueTypeFont.java @@ -29,6 +29,7 @@ import com.tom_roush.fontbox.ttf.CmapSubtable; import com.tom_roush.fontbox.ttf.CmapTable; import com.tom_roush.fontbox.ttf.GlyphData; +import com.tom_roush.fontbox.ttf.GlyphTable; import com.tom_roush.fontbox.ttf.PostScriptTable; import com.tom_roush.fontbox.ttf.TTFParser; import com.tom_roush.fontbox.ttf.TrueTypeFont; @@ -47,9 +48,7 @@ import com.tom_roush.pdfbox.pdmodel.font.encoding.StandardEncoding; import com.tom_roush.pdfbox.pdmodel.font.encoding.Type1Encoding; import com.tom_roush.pdfbox.pdmodel.font.encoding.WinAnsiEncoding; - import static com.tom_roush.pdfbox.pdmodel.font.UniUtil.getUniNameOfCodePoint; - /** * TrueType font. * @@ -57,6 +56,7 @@ */ public class PDTrueTypeFont extends PDSimpleFont implements PDVectorFont { + private static final int START_RANGE_F000 = 0xF000; private static final int START_RANGE_F100 = 0xF100; private static final int START_RANGE_F200 = 0xF200; @@ -87,7 +87,7 @@ public class PDTrueTypeFont extends PDSimpleFont implements PDVectorFont * @throws IOException If there is an error loading the data. */ public static PDTrueTypeFont load(PDDocument doc, File file, Encoding encoding) - throws IOException + throws IOException { return new PDTrueTypeFont(doc, new TTFParser().parse(file), encoding, true); } @@ -105,7 +105,7 @@ public static PDTrueTypeFont load(PDDocument doc, File file, Encoding encoding) * @throws IOException If there is an error loading the data. */ public static PDTrueTypeFont load(PDDocument doc, InputStream input, Encoding encoding) - throws IOException + throws IOException { return new PDTrueTypeFont(doc, new TTFParser().parse(input), encoding, true); } @@ -125,7 +125,7 @@ public static PDTrueTypeFont load(PDDocument doc, InputStream input, Encoding en * @throws IOException If there is an error loading the data. */ public static PDTrueTypeFont load(PDDocument doc, TrueTypeFont ttf, Encoding encoding) - throws IOException + throws IOException { return new PDTrueTypeFont(doc, ttf, encoding, false); } @@ -160,14 +160,14 @@ public static PDTrueTypeFont loadTTF(PDDocument doc, File file) throws IOExcepti public static PDTrueTypeFont loadTTF(PDDocument doc, InputStream input) throws IOException { return new PDTrueTypeFont(doc, new TTFParser().parse(input), WinAnsiEncoding.INSTANCE, - true); + true); } private CmapSubtable cmapWinUnicode = null; private CmapSubtable cmapWinSymbol = null; private CmapSubtable cmapMacRoman = null; private boolean cmapInitialized = false; - private Map gidToCode; // for embedding + private final Map gidToCode = new HashMap(); // for embedding private final TrueTypeFont ttf; private final boolean isEmbedded; @@ -214,13 +214,13 @@ public PDTrueTypeFont(COSDictionary fontDictionary) throws IOException if (ttfFont == null) { FontMapping mapping = FontMappers.instance() - .getTrueTypeFont(getBaseFont(), - getFontDescriptor()); + .getTrueTypeFont(getBaseFont(), + getFontDescriptor()); ttfFont = mapping.getFont(); if (mapping.isFallback()) { - Log.w("PdfBox-Android", "Using fallback font '" + ttfFont + "' for '" + getBaseFont() + "'"); + Log.w("PdfBox-Android", "Using fallback font " + ttfFont + " for " + getBaseFont()); } } ttf = ttfFont; @@ -248,7 +248,7 @@ protected Encoding readEncodingFromFont() throws IOException // non-symbolic fonts don't have a built-in encoding per se, but there encoding is // assumed to be StandardEncoding by the PDF spec unless an explicit Encoding is present // which will override this anyway - if (getSymbolicFlag() != null &&!getSymbolicFlag()) + if (Boolean.FALSE.equals(getSymbolicFlag())) { return StandardEncoding.INSTANCE; } @@ -293,11 +293,11 @@ protected Encoding readEncodingFromFont() throws IOException * Creates a new TrueType font for embedding. */ private PDTrueTypeFont(PDDocument document, TrueTypeFont ttf, Encoding encoding, - boolean closeTTF) - throws IOException + boolean closeTTF) + throws IOException { PDTrueTypeFontEmbedder embedder = new PDTrueTypeFontEmbedder(document, dict, ttf, - encoding); + encoding); this.encoding = encoding; this.ttf = ttf; setFontDescriptor(embedder.getFontDescriptor()); @@ -340,7 +340,7 @@ private BoundingBox generateBoundingBox() throws IOException if (bbox != null) { return new BoundingBox(bbox.getLowerLeftX(), bbox.getLowerLeftY(), - bbox.getUpperRightX(), bbox.getUpperRightY()); + bbox.getUpperRightX(), bbox.getUpperRightY()); } } return ttf.getFontBBox(); @@ -393,8 +393,8 @@ protected byte[] encode(int unicode) throws IOException if (!encoding.contains(getGlyphList().codePointToName(unicode))) { throw new IllegalArgumentException( - String.format("U+%04X is not available in this font's encoding: %s", - unicode, encoding.getEncodingName())); + String.format("U+%04X is not available in font %s encoding: %s", + unicode, getName(), encoding.getEncodingName())); } String name = getGlyphList().codePointToName(unicode); @@ -407,7 +407,7 @@ protected byte[] encode(int unicode) throws IOException if (!ttf.hasGlyph(uniName)) { throw new IllegalArgumentException( - String.format("No glyph for U+%04X in font %s", unicode, getName())); + String.format("No glyph for U+%04X in font %s", unicode, getName())); } } @@ -430,7 +430,7 @@ protected byte[] encode(int unicode) throws IOException if (code == null) { throw new IllegalArgumentException( - String.format("U+%04X is not available in this font's Encoding", unicode)); + String.format("U+%04X is not available in font %s encoding", unicode, getName())); } return new byte[] { (byte)(int)code }; @@ -442,12 +442,10 @@ protected byte[] encode(int unicode) throws IOException */ protected Map getGIDToCode() throws IOException { - if (gidToCode != null) + if (!gidToCode.isEmpty()) { return gidToCode; } - - gidToCode = new HashMap(); for (int code = 0; code <= 255; code++) { int gid = codeToGID(code); @@ -469,7 +467,14 @@ public boolean isEmbedded() public Path getPath(int code) throws IOException { int gid = codeToGID(code); - GlyphData glyph = ttf.getGlyph().getGlyph(gid); + GlyphTable glyphTable = ttf.getGlyph(); + if (glyphTable == null) + { + // needs to be caught earlier, see PDFBOX-5587 and PDFBOX-3488 + throw new IOException("glyf table is missing in font " + getName() + + ", please report this file"); + } + GlyphData glyph = glyphTable.getGlyph(gid); // some glyphs have no outlines (e.g. space, table, newline) if (glyph == null) @@ -679,18 +684,18 @@ else if (CmapTable.ENCODING_WIN_SYMBOL == cmap.getPlatformEncodingId()) } } else if (CmapTable.PLATFORM_MACINTOSH == cmap.getPlatformId() - && CmapTable.ENCODING_MAC_ROMAN == cmap.getPlatformEncodingId()) + && CmapTable.ENCODING_MAC_ROMAN == cmap.getPlatformEncodingId()) { cmapMacRoman = cmap; } else if (CmapTable.PLATFORM_UNICODE == cmap.getPlatformId() - && CmapTable.ENCODING_UNICODE_1_0 == cmap.getPlatformEncodingId()) + && CmapTable.ENCODING_UNICODE_1_0 == cmap.getPlatformEncodingId()) { // PDFBOX-4755 / PDF.js #5501 cmapWinUnicode = cmap; } else if (CmapTable.PLATFORM_UNICODE == cmap.getPlatformId() - && CmapTable.ENCODING_UNICODE_2_0_BMP == cmap.getPlatformEncodingId()) + && CmapTable.ENCODING_UNICODE_2_0_BMP == cmap.getPlatformEncodingId()) { // PDFBOX-5484 cmapWinUnicode = cmap; diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDTrueTypeFontEmbedder.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDTrueTypeFontEmbedder.java index 6c16c5af8..ac19747a5 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDTrueTypeFontEmbedder.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDTrueTypeFontEmbedder.java @@ -23,6 +23,8 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; + import com.tom_roush.fontbox.ttf.HorizontalMetricsTable; import com.tom_roush.fontbox.ttf.TrueTypeFont; import com.tom_roush.pdfbox.cos.COSDictionary; @@ -31,7 +33,6 @@ import com.tom_roush.pdfbox.pdmodel.common.COSArrayList; import com.tom_roush.pdfbox.pdmodel.font.encoding.Encoding; import com.tom_roush.pdfbox.pdmodel.font.encoding.GlyphList; - /** * Embedded PDTrueTypeFont builder. Helper class to populate a PDTrueTypeFont from a TTF. * @@ -52,7 +53,7 @@ final class PDTrueTypeFontEmbedder extends TrueTypeEmbedder * @throws IOException if the TTF could not be read */ PDTrueTypeFontEmbedder(PDDocument document, COSDictionary dict, TrueTypeFont ttf, - Encoding encoding) throws IOException + Encoding encoding) throws IOException { super(document, dict, ttf, false); dict.setItem(COSName.SUBTYPE, COSName.TRUE_TYPE); @@ -79,9 +80,9 @@ private void setWidths(COSDictionary font, GlyphList glyphList) throws IOExcepti HorizontalMetricsTable hmtx = ttf.getHorizontalMetrics(); Map codeToName = getFontEncoding().getCodeToNameMap(); - - int firstChar = Collections.min(codeToName.keySet()); - int lastChar = Collections.max(codeToName.keySet()); + Set codes = codeToName.keySet(); + int firstChar = Collections.min(codes); + int lastChar = Collections.max(codes); List widths = new ArrayList(lastChar - firstChar + 1); for (int i = 0; i < lastChar - firstChar + 1; i++) @@ -99,10 +100,14 @@ private void setWidths(COSDictionary font, GlyphList glyphList) throws IOExcepti if (code >= firstChar && code <= lastChar) { String uni = glyphList.toUnicode(name); - int charCode = uni.codePointAt(0); - int gid = cmapLookup.getGlyphId(charCode); + int gid = 0; + if (uni != null) + { + int charCode = uni.codePointAt(0); + gid = cmapLookup.getGlyphId(charCode); + } widths.set(entry.getKey() - firstChar, - Math.round(hmtx.getAdvanceWidth(gid) * scaling)); + Math.round(hmtx.getAdvanceWidth(gid) * scaling)); } } @@ -121,7 +126,7 @@ public Encoding getFontEncoding() @Override protected void buildSubset(InputStream ttfSubset, String tag, - Map gidToCid) throws IOException + Map gidToCid) throws IOException { // use PDType0Font instead throw new UnsupportedOperationException(); diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDType0Font.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDType0Font.java index ede6ad58b..7abe5ef9b 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDType0Font.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDType0Font.java @@ -38,7 +38,6 @@ import com.tom_roush.pdfbox.pdmodel.PDDocument; import com.tom_roush.pdfbox.util.Matrix; import com.tom_roush.pdfbox.util.Vector; - /** * A Composite (Type 0) font. * @@ -46,6 +45,7 @@ */ public class PDType0Font extends PDFont implements PDVectorFont { + private final PDCIDFont descendantFont; private CMap cMap, cMapUCS2; private boolean isCMapPredefined; @@ -93,7 +93,7 @@ public static PDType0Font load(PDDocument doc, InputStream input) throws IOExcep * @throws IOException If there is an error reading the font stream. */ public static PDType0Font load(PDDocument doc, InputStream input, boolean embedSubset) - throws IOException + throws IOException { return new PDType0Font(doc, new TTFParser().parse(input), embedSubset, true, false); } @@ -102,14 +102,17 @@ public static PDType0Font load(PDDocument doc, InputStream input, boolean embedS * Loads a TTF to be embedded into a document as a Type 0 font. * * @param doc The PDF document that will hold the embedded font. - * @param ttf A TrueType font. + * @param ttf A TrueType font. Passing an OpenTypeFont font object is possible, but not + * recommended (see exceptions). * @param embedSubset True if the font will be subset before embedding. Set this to false when * creating a font for AcroForm. * @return A Type0 font with a CIDFontType2 descendant. * @throws IOException If there is an error reading the font stream. + * @throws UnsupportedOperationException if embedSubset is true for an OTF font + * @throws IllegalStateException if an OTF font is used but GID != CID */ public static PDType0Font load(PDDocument doc, TrueTypeFont ttf, boolean embedSubset) - throws IOException + throws IOException { return new PDType0Font(doc, ttf, embedSubset, false, false); } @@ -150,7 +153,7 @@ public static PDType0Font loadVertical(PDDocument doc, InputStream input) throws * @throws IOException If there is an error reading the font stream. */ public static PDType0Font loadVertical(PDDocument doc, InputStream input, boolean embedSubset) - throws IOException + throws IOException { return new PDType0Font(doc, new TTFParser().parse(input), embedSubset, true, true); } @@ -159,13 +162,16 @@ public static PDType0Font loadVertical(PDDocument doc, InputStream input, boolea * Loads a TTF to be embedded into a document as a vertical Type 0 font. * * @param doc The PDF document that will hold the embedded font. - * @param ttf A TrueType font. + * @param ttf A TrueType font. Passing an OpenTypeFont font object is possible, but not + * recommended (see exceptions). * @param embedSubset True if the font will be subset before embedding * @return A Type0 font with a CIDFontType2 descendant. * @throws IOException If there is an error reading the font stream. + * @throws UnsupportedOperationException if embedSubset is true for an OTF font + * @throws IllegalStateException if an OTF font is used but GID != CID */ public static PDType0Font loadVertical(PDDocument doc, TrueTypeFont ttf, boolean embedSubset) - throws IOException + throws IOException { return new PDType0Font(doc, ttf, embedSubset, false, true); } @@ -195,7 +201,7 @@ public PDType0Font(COSDictionary fontDictionary) throws IOException throw new IOException("Missing descendant font dictionary"); } if (!COSName.FONT.equals( - ((COSDictionary) descendantFontDictBase).getCOSName(COSName.TYPE, COSName.FONT))) + ((COSDictionary) descendantFontDictBase).getCOSName(COSName.TYPE, COSName.FONT))) { throw new IOException("Missing or wrong type in descendant font dictionary"); } @@ -217,7 +223,7 @@ public PDType0Font(COSDictionary fontDictionary) throws IOException * @throws IOException */ private PDType0Font(PDDocument document, TrueTypeFont ttf, boolean embedSubset, - boolean closeTTF, boolean vertical) throws IOException + boolean closeTTF, boolean vertical) throws IOException { if (vertical) { @@ -305,10 +311,10 @@ else if (!cMap.hasCIDMappings()) { String ordering = ros.getOrdering(); isDescendantCJK = "Adobe".equals(ros.getRegistry()) && - ("GB1".equals(ordering) || - "CNS1".equals(ordering) || - "Japan1".equals(ordering) || - "Korea1".equals(ordering)); + ("GB1".equals(ordering) || + "CNS1".equals(ordering) || + "Japan1".equals(ordering) || + "Korea1".equals(ordering)); } } @@ -338,8 +344,8 @@ private void fetchCMapUCS2() throws IOException if (cidSystemInfo != null) { strName = cidSystemInfo.getRegistry() + "-" + - cidSystemInfo.getOrdering() + "-" + - cidSystemInfo.getSupplement(); + cidSystemInfo.getOrdering() + "-" + + cidSystemInfo.getSupplement(); } } else if (name != null) @@ -492,6 +498,14 @@ public String toUnicode(int code) throws IOException return unicode; } + // Use identity mapping if the given ToUnicode CMap doesn't provide any valid mapping + // a predefined map shall only be used if there isn't any ToUnicode CMap + // PDFBOX-6022: not when there's a predefined cmap + if (getToUnicodeCMap() != null && !isCMapPredefined) + { + return Character.toString((char) code); + } + if ((isCMapPredefined || isDescendantCJK) && cMapUCS2 != null) { // if the font is composite and uses a predefined cmap (excluding Identity-H/V) then diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDType1CFont.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDType1CFont.java index 74cb9f500..c08144d20 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDType1CFont.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDType1CFont.java @@ -41,9 +41,7 @@ import com.tom_roush.pdfbox.pdmodel.font.encoding.StandardEncoding; import com.tom_roush.pdfbox.pdmodel.font.encoding.Type1Encoding; import com.tom_roush.pdfbox.util.Matrix; - import static com.tom_roush.pdfbox.pdmodel.font.UniUtil.getUniNameOfCodePoint; - /** * Type 1-equivalent CFF font. * @@ -52,6 +50,7 @@ */ public class PDType1CFont extends PDSimpleFont { + private final Map glyphHeights = new HashMap(); private Float avgWidth = null; private Matrix fontMatrix; @@ -124,13 +123,14 @@ public PDType1CFont(COSDictionary fontDictionary) throws IOException } else { + String baseFont = getBaseFont(); FontMapping mapping = FontMappers.instance() - .getFontBoxFont(getBaseFont(), fd); + .getFontBoxFont(baseFont, fd); genericFont = mapping.getFont(); if (mapping.isFallback()) { - Log.w("PdfBox-Android", "Using fallback font " + genericFont.getName() + " for " + getBaseFont()); + Log.w("PdfBox-Android", "Using fallback font " + genericFont.getName() + " for " + baseFont); } isEmbedded = false; } @@ -212,11 +212,11 @@ private BoundingBox generateBoundingBox() throws IOException if (getFontDescriptor() != null) { PDRectangle bbox = getFontDescriptor().getFontBoundingBox(); if (bbox != null - && (bbox.getLowerLeftX() != 0 || bbox.getLowerLeftY() != 0 - || bbox.getUpperRightX() != 0 || bbox.getUpperRightY() != 0)) + && (bbox.getLowerLeftX() != 0 || bbox.getLowerLeftY() != 0 + || bbox.getUpperRightX() != 0 || bbox.getUpperRightY() != 0)) { return new BoundingBox(bbox.getLowerLeftX(), bbox.getLowerLeftY(), - bbox.getUpperRightX(), bbox.getUpperRightY()); + bbox.getUpperRightX(), bbox.getUpperRightY()); } } return genericFont.getFontBBox(); @@ -275,9 +275,9 @@ public final Matrix getFontMatrix() if (numbers != null && numbers.size() == 6) { fontMatrix = new Matrix( - numbers.get(0).floatValue(), numbers.get(1).floatValue(), - numbers.get(2).floatValue(), numbers.get(3).floatValue(), - numbers.get(4).floatValue(), numbers.get(5).floatValue()); + numbers.get(0).floatValue(), numbers.get(1).floatValue(), + numbers.get(2).floatValue(), numbers.get(3).floatValue(), + numbers.get(4).floatValue(), numbers.get(5).floatValue()); } else { @@ -323,7 +323,7 @@ public float getHeight(int code) throws IOException Log.w("PdfBox-Android", "No embedded CFF font, returning 0"); return 0; } - height = (float) cffFont.getType1CharString(name).getBounds().height(); + height = cffFont.getType1CharString(name).getBounds().height(); glyphHeights.put(name, height); } else @@ -340,8 +340,8 @@ protected byte[] encode(int unicode) throws IOException if (!encoding.contains(name)) { throw new IllegalArgumentException( - String.format("U+%04X ('%s') is not available in this font's encoding: %s", - unicode, name, encoding.getEncodingName())); + String.format("U+%04X ('%s') is not available in font %s encoding: %s", + unicode, name, getName(), encoding.getEncodingName())); } String nameInFont = getNameInFont(name); @@ -351,7 +351,7 @@ protected byte[] encode(int unicode) throws IOException if (nameInFont.equals(".notdef") || !genericFont.hasGlyph(nameInFont)) { throw new IllegalArgumentException( - String.format("No glyph for U+%04X in font %s", unicode, getName())); + String.format("No glyph for U+%04X in font %s", unicode, getName())); } int code = inverted.get(name); @@ -371,6 +371,12 @@ public float getStringWidth(String string) throws IOException { int codePoint = string.codePointAt(i); String name = getGlyphList().codePointToName(codePoint); + if (!cffFont.hasGlyph(name)) + { + throw new IllegalArgumentException( + String.format("U+%04X ('%s') is not available in font %s", + codePoint, name, getName())); + } width += cffFont.getType1CharString(name).getWidth(); } return width; diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDType1FontEmbedder.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDType1FontEmbedder.java index b12eacdfa..6cbc0f3ae 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDType1FontEmbedder.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDType1FontEmbedder.java @@ -20,6 +20,7 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.List; + import com.tom_roush.fontbox.afm.FontMetrics; import com.tom_roush.fontbox.pfb.PfbParser; import com.tom_roush.fontbox.type1.Type1Font; @@ -34,7 +35,6 @@ import com.tom_roush.pdfbox.pdmodel.font.encoding.Encoding; import com.tom_roush.pdfbox.pdmodel.font.encoding.GlyphList; import com.tom_roush.pdfbox.pdmodel.font.encoding.Type1Encoding; - /** * Embedded PDType1Font builder. Helper class to populate a PDType1Font from a PFB and AFM. * @@ -54,7 +54,7 @@ class PDType1FontEmbedder * @throws IOException If there is an error loading the data. */ PDType1FontEmbedder(PDDocument doc, COSDictionary dict, InputStream pfbStream, - Encoding encoding) throws IOException + Encoding encoding) throws IOException { dict.setItem(COSName.SUBTYPE, COSName.TYPE1); @@ -76,7 +76,6 @@ class PDType1FontEmbedder PDFontDescriptor fd = buildFontDescriptor(type1); PDStream fontStream = new PDStream(doc, pfbParser.getInputStream(), COSName.FLATE_DECODE); - fontStream.getCOSObject().setInt("Length", pfbParser.size()); for (int i = 0; i < pfbParser.getLengths().length; i++) { fontStream.getCOSObject().setInt("Length" + (i + 1), pfbParser.getLengths()[i]); @@ -108,7 +107,7 @@ class PDType1FontEmbedder static PDFontDescriptor buildFontDescriptor(Type1Font type1) { boolean isSymbolic = type1.getEncoding() - instanceof com.tom_roush.fontbox.encoding.BuiltInEncoding; + instanceof com.tom_roush.fontbox.encoding.BuiltInEncoding; BoundingBox bbox = type1.getFontBBox(); PDFontDescriptor fd = new PDFontDescriptor(); diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDType3CharProc.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDType3CharProc.java index 647223ef2..2ed58433c 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDType3CharProc.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDType3CharProc.java @@ -27,7 +27,6 @@ import com.tom_roush.pdfbox.contentstream.PDContentStream; import com.tom_roush.pdfbox.contentstream.operator.Operator; import com.tom_roush.pdfbox.cos.COSBase; -import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.cos.COSNumber; import com.tom_roush.pdfbox.cos.COSStream; @@ -37,7 +36,6 @@ import com.tom_roush.pdfbox.pdmodel.common.PDRectangle; import com.tom_roush.pdfbox.pdmodel.common.PDStream; import com.tom_roush.pdfbox.util.Matrix; - /** * A Type 3 character procedure. This is a standalone PDF content stream. * @@ -45,6 +43,7 @@ */ public final class PDType3CharProc implements COSObjectable, PDContentStream { + private final PDType3Font font; private final COSStream charStream; @@ -84,7 +83,7 @@ public PDResources getResources() // PDFBOX-5294 Log.w("PdfBox-Android", "Using resources dictionary found in charproc entry"); Log.w("PdfBox-Android", "This should have been in the font or in the page dictionary"); - return new PDResources((COSDictionary) charStream.getDictionaryObject(COSName.RESOURCES)); + return new PDResources(charStream.getCOSDictionary(COSName.RESOURCES)); } return font.getResources(); } @@ -123,10 +122,10 @@ public PDRectangle getGlyphBBox() throws IOException float x = ((COSNumber) arguments.get(2)).floatValue(); float y = ((COSNumber) arguments.get(3)).floatValue(); return new PDRectangle( - x, - y, - ((COSNumber) arguments.get(4)).floatValue() - x, - ((COSNumber) arguments.get(5)).floatValue() - y); + x, + y, + ((COSNumber) arguments.get(4)).floatValue() - x, + ((COSNumber) arguments.get(5)).floatValue() - y); } else { diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDType3Font.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDType3Font.java index 7790c8e4f..30001c23a 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDType3Font.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/PDType3Font.java @@ -39,7 +39,6 @@ import com.tom_roush.pdfbox.pdmodel.font.encoding.GlyphList; import com.tom_roush.pdfbox.util.Matrix; import com.tom_roush.pdfbox.util.Vector; - /** * A PostScript Type 3 Font. * @@ -125,8 +124,8 @@ public Path getPath(String name) throws IOException @Override public boolean hasGlyph(String name) throws IOException { - return getCharProcs() == null ? false - : getCharProcs().getCOSStream(COSName.getPDFName(name)) != null; + COSDictionary cp = getCharProcs(); + return cp != null && cp.getCOSStream(COSName.getPDFName(name)) != null; } @Override @@ -182,6 +181,11 @@ public float getWidthFromFont(int code) throws IOException return charProc.getWidth(); } + /** + * {@inheritDoc} + * + * @return true because type 3 fonts are embedded by design. + */ @Override public boolean isEmbedded() { @@ -243,7 +247,7 @@ public Matrix getFontMatrix() { COSArray matrix = dict.getCOSArray(COSName.FONT_MATRIX); fontMatrix = checkFontMatrixValues(matrix) ? Matrix.createMatrix(matrix) - : super.getFontMatrix(); + : super.getFontMatrix(); } return fontMatrix; } @@ -300,7 +304,7 @@ public PDResources getResources() */ public PDRectangle getFontBBox() { - COSBase base = dict.getDictionaryObject(COSName.FONT_BBOX); + COSBase base = dict.getDictionaryObject(COSName.FONT_BBOX); PDRectangle retval = null; if (base instanceof COSArray) { @@ -328,7 +332,7 @@ private BoundingBox generateBoundingBox() return new BoundingBox(); } if (rect.getLowerLeftX() == 0 && rect.getLowerLeftY() == 0 - && rect.getUpperRightX() == 0 && rect.getUpperRightY() == 0) + && rect.getUpperRightX() == 0 && rect.getUpperRightY() == 0) { // Plan B: get the max bounding box of the glyphs COSDictionary cp = getCharProcs(); @@ -348,13 +352,13 @@ private BoundingBox generateBoundingBox() continue; } rect.setLowerLeftX( - Math.min(rect.getLowerLeftX(), glyphBBox.getLowerLeftX())); + Math.min(rect.getLowerLeftX(), glyphBBox.getLowerLeftX())); rect.setLowerLeftY( - Math.min(rect.getLowerLeftY(), glyphBBox.getLowerLeftY())); + Math.min(rect.getLowerLeftY(), glyphBBox.getLowerLeftY())); rect.setUpperRightX( - Math.max(rect.getUpperRightX(), glyphBBox.getUpperRightX())); + Math.max(rect.getUpperRightX(), glyphBBox.getUpperRightX())); rect.setUpperRightY( - Math.max(rect.getUpperRightY(), glyphBBox.getUpperRightY())); + Math.max(rect.getUpperRightY(), glyphBBox.getUpperRightY())); } catch (IOException ex) { @@ -365,7 +369,7 @@ private BoundingBox generateBoundingBox() } } return new BoundingBox(rect.getLowerLeftX(), rect.getLowerLeftY(), - rect.getUpperRightX(), rect.getUpperRightY()); + rect.getUpperRightX(), rect.getUpperRightY()); } /** diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/TrueTypeEmbedder.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/TrueTypeEmbedder.java index b55e63c52..61940fa64 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/TrueTypeEmbedder.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/TrueTypeEmbedder.java @@ -24,8 +24,8 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; -import java.util.HashSet; +import java.util.Arrays; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -45,7 +45,7 @@ import com.tom_roush.pdfbox.pdmodel.PDDocument; import com.tom_roush.pdfbox.pdmodel.common.PDRectangle; import com.tom_roush.pdfbox.pdmodel.common.PDStream; - +import com.tom_roush.pdfbox.util.Charsets; /** * Common functionality for embedding TrueType fonts. * @@ -58,6 +58,12 @@ abstract class TrueTypeEmbedder implements Subsetter private static final int OBLIQUE = 512; private static final String BASE25 = "BCDEFGHIJKLMNOPQRSTUVWXYZ"; + // PDF spec required tables (if present), all others will be removed + private static final List TABLES = + Arrays.asList("head", "hhea", "loca", "maxp", "cvt ", "prep", "glyf", "hmtx", "fpgm", + // Windows ClearType + "gasp"); + private final PDDocument document; protected TrueTypeFont ttf; protected PDFontDescriptor fontDescriptor; @@ -71,14 +77,14 @@ abstract class TrueTypeEmbedder implements Subsetter protected final CmapSubtable cmap; protected final CmapLookup cmapLookup; - private final Set subsetCodePoints = new HashSet(); + private final Set subsetCodePoints = new LinkedHashSet(); private final boolean embedSubset; /** * Creates a new TrueType font for embedding. */ TrueTypeEmbedder(PDDocument document, COSDictionary dict, TrueTypeFont ttf, - boolean embedSubset) throws IOException + boolean embedSubset) throws IOException { this.document = document; this.embedSubset = embedSubset; @@ -98,7 +104,7 @@ abstract class TrueTypeEmbedder implements Subsetter InputStream is = ttf.getOriginalData(); byte[] b = new byte[4]; is.mark(b.length); - if (is.read(b) == b.length && new String(b).equals("ttcf")) + if (is.read(b) == b.length && new String(b, Charsets.US_ASCII).equals("ttcf")) { is.close(); throw new IOException("Full embedding of TrueType font collections not supported"); @@ -156,9 +162,10 @@ public void buildFontFile2(InputStream ttfStream) throws IOException */ boolean isEmbeddingPermitted(TrueTypeFont ttf) throws IOException { - if (ttf.getOS2Windows() != null) + OS2WindowsMetricsTable os2 = ttf.getOS2Windows(); + if (os2 != null) { - int fsType = ttf.getOS2Windows().getFsType(); + int fsType = os2.getFsType(); int maskedFsType = fsType & 0x000F; // PDFBOX-5191: don't check the bit because permissions are exclusive if (maskedFsType == OS2WindowsMetricsTable.FSTYPE_RESTRICTED) @@ -167,7 +174,7 @@ boolean isEmbeddingPermitted(TrueTypeFont ttf) throws IOException return false; } else if ((fsType & OS2WindowsMetricsTable.FSTYPE_BITMAP_ONLY) == - OS2WindowsMetricsTable.FSTYPE_BITMAP_ONLY) + OS2WindowsMetricsTable.FSTYPE_BITMAP_ONLY) { // bitmap embedding only return false; @@ -181,11 +188,12 @@ else if ((fsType & OS2WindowsMetricsTable.FSTYPE_BITMAP_ONLY) == */ private boolean isSubsettingPermitted(TrueTypeFont ttf) throws IOException { - if (ttf.getOS2Windows() != null) + OS2WindowsMetricsTable os2 = ttf.getOS2Windows(); + if (os2 != null) { - int fsType = ttf.getOS2Windows().getFsType(); + int fsType = os2.getFsType(); if ((fsType & OS2WindowsMetricsTable.FSTYPE_NO_SUBSETTING) == - OS2WindowsMetricsTable.FSTYPE_NO_SUBSETTING) + OS2WindowsMetricsTable.FSTYPE_NO_SUBSETTING) { return false; } @@ -324,6 +332,18 @@ public void addToSubset(int codePoint) subsetCodePoints.add(codePoint); } + /** + * Returns the Unicode code points that were passed to {@link #addToSubset(int)}, i.e. the code + * points actually used in the document, in first-occurrence order. Used when building the + * ToUnicode CMap to map a glyph back to the code point that was really typed. + * + * @return the code points added to the subset, in insertion order + */ + Set getSubsetCodePoints() + { + return subsetCodePoints; + } + @Override public void subset() throws IOException { @@ -337,23 +357,13 @@ public void subset() throws IOException throw new IllegalStateException("Subsetting is disabled"); } - // PDF spec required tables (if present), all others will be removed - List tables = new ArrayList(); - tables.add("head"); - tables.add("hhea"); - tables.add("loca"); - tables.add("maxp"); - tables.add("cvt "); - tables.add("prep"); - tables.add("glyf"); - tables.add("hmtx"); - tables.add("fpgm"); - // Windows ClearType - tables.add("gasp"); - // set the GIDs to subset - TTFSubsetter subsetter = new TTFSubsetter(ttf, tables); + TTFSubsetter subsetter = new TTFSubsetter(ttf, TABLES); subsetter.addAll(subsetCodePoints); + subsetter.forceInvisible('\u200B'); // ZWSP + subsetter.forceInvisible('\u200C'); // ZWNJ + subsetter.forceInvisible('\u2060'); // WJ + subsetter.forceInvisible('\uFEFF'); // ZWNBSP // calculate deterministic tag based on the chosen subset Map gidToCid = subsetter.getGIDMap(); @@ -381,16 +391,15 @@ public boolean needsSubset() * Rebuild a font subset. */ protected abstract void buildSubset(InputStream ttfSubset, String tag, - Map gidToCid) throws IOException; + Map gidToCid) throws IOException; /** * Returns an uppercase 6-character unique tag for the given subset. */ public String getTag(Map gidToCid) { - // deterministic - long num = gidToCid.hashCode(); - + // hash might be negative due to an overflow if the map contains lots of values + long num = Math.abs(gidToCid.hashCode()); // base25 encode StringBuilder sb = new StringBuilder(); do diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/encoding/DictionaryEncoding.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/encoding/DictionaryEncoding.java index 57862ef78..7e9afc10f 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/encoding/DictionaryEncoding.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/encoding/DictionaryEncoding.java @@ -1,4 +1,3 @@ -/* /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with @@ -17,14 +16,16 @@ */ package com.tom_roush.pdfbox.pdmodel.font.encoding; +import android.util.Log; + import java.util.HashMap; import java.util.Map; + import com.tom_roush.pdfbox.cos.COSArray; import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.cos.COSNumber; - /** * This will perform the encoding from a dictionary. * @@ -32,6 +33,7 @@ */ public class DictionaryEncoding extends Encoding { + private final COSDictionary encoding; private final Encoding baseEncoding; private final Map differences = new HashMap(); @@ -75,7 +77,24 @@ public DictionaryEncoding(COSName baseEncoding, COSArray differences) public DictionaryEncoding(COSDictionary fontEncoding) { encoding = fontEncoding; - baseEncoding = null; + COSName name = encoding.getCOSName(COSName.BASE_ENCODING); + if (name != null) + { + baseEncoding = Encoding.getInstance(name); // null when the name is invalid + if (baseEncoding != null) + { + // PDFBOX-5963 + // PDF Specification: "Differences array shall specify the complete character + // encoding for this font" but other viewers read it, thus we do too. + Log.w("PdfBox-Android", "/BaseEncoding in type 3 font"); + codeToName.putAll(baseEncoding.codeToName); + inverted.putAll(baseEncoding.inverted); + } + } + else + { + baseEncoding = null; + } applyDifferences(); } @@ -117,7 +136,7 @@ public DictionaryEncoding(COSDictionary fontEncoding, boolean isNonSymbolic, Enc // triggering this error indicates a bug in PDFBox. Every font should always have // a built-in encoding, if not, we parsed it incorrectly. throw new IllegalArgumentException("Symbolic fonts must have a built-in " + - "encoding"); + "encoding"); } } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/encoding/GlyphList.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/encoding/GlyphList.java index ef5a54702..000d8aff0 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/encoding/GlyphList.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/encoding/GlyphList.java @@ -26,15 +26,15 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import com.tom_roush.pdfbox.android.PDFBoxResourceLoader; import com.tom_roush.pdfbox.io.IOUtils; - +import com.tom_roush.pdfbox.android.PDFBoxResourceLoader; /** * PostScript glyph list, maps glyph names to sequences of Unicode characters. * Instances of GlyphList are immutable. */ public final class GlyphList { + // Adobe Glyph List (AGL) private static final GlyphList DEFAULT = load("glyphlist.txt", 4281); @@ -83,7 +83,7 @@ private static GlyphList load(String filename, int numberOfEntries) if (location != null) { throw new UnsupportedOperationException("glyphlist_ext is no longer supported, " - + "use GlyphList.DEFAULT.addGlyphs(Properties) instead"); + + "use GlyphList.DEFAULT.addGlyphs(Properties) instead"); } } catch (SecurityException e) // can occur on System.getProperty @@ -162,12 +162,6 @@ private void loadList(InputStream input) throws IOException String name = parts[0]; String[] unicodeList = parts[1].split(" "); - if (nameToUnicode.containsKey(name)) - { - Log.w("PdfBox-Android", "duplicate value for " + name + " -> " + parts[1] + " " + - nameToUnicode.get(name)); - } - int[] codePoints = new int[unicodeList.length]; int index = 0; for (String hex : unicodeList) @@ -177,18 +171,22 @@ private void loadList(InputStream input) throws IOException String string = new String(codePoints, 0 , codePoints.length); // forward mapping - nameToUnicode.put(name, string); - + String oldMapping = nameToUnicode.put(name, string); + if (oldMapping != null) + { + Log.w("PdfBox-Android", "duplicate value for " + name + " -> " + parts[1] + " " + + nameToUnicode.get(name)); + } // reverse mapping - // PDFBOX-3884: take the various standard encodings as canonical, + // PDFBOX-3884: take the various standard encodings as canonical, // e.g. tilde over ilde final boolean forceOverride = - WinAnsiEncoding.INSTANCE.contains(name) || - MacRomanEncoding.INSTANCE.contains(name) || - MacExpertEncoding.INSTANCE.contains(name) || - SymbolEncoding.INSTANCE.contains(name) || - ZapfDingbatsEncoding.INSTANCE.contains(name); - if (!unicodeToName.containsKey(string) || forceOverride) + WinAnsiEncoding.INSTANCE.contains(name) || + MacRomanEncoding.INSTANCE.contains(name) || + MacExpertEncoding.INSTANCE.contains(name) || + SymbolEncoding.INSTANCE.contains(name) || + ZapfDingbatsEncoding.INSTANCE.contains(name); + if (forceOverride || !unicodeToName.containsKey(string)) { unicodeToName.put(string, name); } @@ -261,38 +259,14 @@ public String toUnicode(String name) { unicode = toUnicode(name.substring(0, name.indexOf('.'))); } - else if (name.startsWith("uni") && name.length() == 7) + else if ((name.length() == 7 && name.startsWith("uni")) + || (name.length() == 5 && name.startsWith("u"))) { // test for Unicode name in the format uniXXXX where X is hex - int nameLength = name.length(); - StringBuilder uniStr = new StringBuilder(); - try - { - for (int chPos = 3; chPos + 4 <= nameLength; chPos += 4) - { - int codePoint = Integer.parseInt(name.substring(chPos, chPos + 4), 16); - if (codePoint > 0xD7FF && codePoint < 0xE000) - { - Log.w("PdfBox-Android", "Unicode character name with disallowed code area: " + name); - } - else - { - uniStr.append((char) codePoint); - } - } - unicode = uniStr.toString(); - } - catch (NumberFormatException nfe) - { - Log.w("PdfBox-Android", "Not a number in Unicode character name: " + name); - } - } - else if (name.startsWith("u") && name.length() == 5) - { - // test for an alternate Unicode name representation uXXXX + int start = name.length() == 7 ? 3 : 1; try { - int codePoint = Integer.parseInt(name.substring(1), 16); + int codePoint = Integer.parseInt(name.substring(start), 16); if (codePoint > 0xD7FF && codePoint < 0xE000) { Log.w("PdfBox-Android", "Unicode character name with disallowed code area: " + name); diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/encoding/MacOSRomanEncoding.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/encoding/MacOSRomanEncoding.java index d9934007c..ed0862a31 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/encoding/MacOSRomanEncoding.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/font/encoding/MacOSRomanEncoding.java @@ -17,7 +17,6 @@ package com.tom_roush.pdfbox.pdmodel.font.encoding; import com.tom_roush.pdfbox.cos.COSBase; - /** * This is the Mac OS Roman encoding, which is similar to the * MacRomanEncoding with the addition of 15 entries @@ -29,22 +28,22 @@ public class MacOSRomanEncoding extends MacRomanEncoding * on top of {@link MacRomanEncoding}. */ private static final Object[][] MAC_OS_ROMAN_ENCODING_TABLE = { - {255, "notequal"}, - {260, "infinity"}, - {262, "lessequal"}, - {263, "greaterequal"}, - {266, "partialdiff"}, - {267, "summation"}, - {270, "product"}, - {271, "pi"}, - {272, "integral"}, - {275, "Omega"}, - {303, "radical"}, - {305, "approxequal"}, - {306, "Delta"}, - {327, "lozenge"}, - {333, "Euro"}, - {360, "apple"} + { 0255, "notequal" }, // + { 0260, "infinity" }, // + { 0262, "lessequal" }, // + { 0263, "greaterequal" }, // + { 0266, "partialdiff" }, // + { 0267, "summation" }, // + { 0270, "product" }, // + { 0271, "pi" }, // + { 0272, "integral" }, // + { 0275, "Omega" }, // + { 0303, "radical" }, // + { 0305, "approxequal" }, // + { 0306, "Delta" }, // + { 0327, "lozenge" }, // + { 0333, "Euro" }, // + { 0360, "apple" } }; /** diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/PDLineDashPattern.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/PDLineDashPattern.java index 464e6c3d1..8133e15d4 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/PDLineDashPattern.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/PDLineDashPattern.java @@ -22,7 +22,6 @@ import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSInteger; import com.tom_roush.pdfbox.pdmodel.common.COSObjectable; - /** * A line dash pattern for stroking paths. * Instances of PDLineDashPattern are immutable. @@ -51,6 +50,26 @@ public PDLineDashPattern() public PDLineDashPattern(COSArray array, int phase) { this.array = array.toFloatArray(); + // PDF 2.0 specification, 8.4.3.6 Line dash pattern: + // "If the dash phase is negative, it shall be incremented by twice the sum of all + // lengths in the dash array until it is positive" + if (phase < 0) + { + float sum2 = 0; + for (float f : this.array) + { + sum2 += f; + } + sum2 *= 2; + if (sum2 > 0) + { + phase += (-phase < sum2) ? sum2 : (Math.floor(-phase / sum2) + 1) * sum2; + } + else + { + phase = 0; + } + } this.phase = phase; } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDColor.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDColor.java index cd51536ff..c44e554b6 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDColor.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDColor.java @@ -25,7 +25,6 @@ import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.cos.COSNumber; - /** * A color value, consisting of one or more color components, or for pattern color spaces, * a name and optional color components. @@ -37,6 +36,7 @@ */ public final class PDColor { + private final float[] components; private final COSName patternName; private final PDColorSpace colorSpace; @@ -102,6 +102,12 @@ public PDColor(float[] components, PDColorSpace colorSpace) this.components = components.clone(); this.patternName = null; this.colorSpace = colorSpace; + if (colorSpace != null && colorSpace.getNumberOfComponents() != components.length) + { + // PDFBOX-5882 + Log.w("PdfBox-Android", "Colorspace component count " + colorSpace.getNumberOfComponents() + + " doesn't match components length " + components.length); + } } /** @@ -168,7 +174,7 @@ public boolean isPattern() * Returns the packed RGB value for this color, if any. * @return RGB * @throws IOException if the color conversion fails - * @throws IllegalStateException if this color value is a pattern. + * @throws UnsupportedOperationException if this color value is a pattern. */ public int toRGB() throws IOException { @@ -209,6 +215,6 @@ public PDColorSpace getColorSpace() public String toString() { return "PDColor{components=" + Arrays.toString(components) + - ", patternName=" + patternName + "}"; + ", patternName=" + patternName + "}"; } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDColorSpace.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDColorSpace.java index 63bc8a64d..c8e2fb247 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDColorSpace.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDColorSpace.java @@ -127,7 +127,7 @@ else if (name == COSName.DEVICERGB) { return PDDeviceRGB.INSTANCE; } - else if (name == COSName.DEVICEGRAY) + else if (name == COSName.DEVICEGRAY || name == COSName.CALGRAY) { return PDDeviceGray.INSTANCE; } @@ -169,8 +169,8 @@ else if (colorSpace instanceof COSArray) if (name == COSName.CALGRAY) { // return new PDCalGray(array); TODO: PdfBox-Android - Log.e("PdfBox-Android", "Unsupported color space kind: " + name + ". Will try DeviceRGB instead"); - return PDDeviceRGB.INSTANCE; + Log.w("PdfBox-Android", "Unsupported color space kind: " + name + ". Will try DeviceGray instead"); + return PDDeviceGray.INSTANCE; } else if (name == COSName.CALRGB) { @@ -186,15 +186,13 @@ else if (name == COSName.DEVICEN) } else if (name == COSName.INDEXED) { -// return new PDIndexed(array); - Log.e("PdfBox-Android", "Unsupported color space kind: " + name + ". Will try DeviceRGB instead"); - return PDDeviceRGB.INSTANCE; + return new PDIndexed(array, resources); } else if (name == COSName.SEPARATION) { // return new PDSeparation(array); - Log.e("PdfBox-Android", "Unsupported color space kind: " + name + ". Will try DeviceRGB instead"); - return PDDeviceRGB.INSTANCE; + Log.w("PdfBox-Android", "Unsupported color space kind: " + name + ". Will try DeviceGray instead"); + return PDDeviceGray.INSTANCE; } else if (name == COSName.ICCBASED) { @@ -254,20 +252,26 @@ private static PDColorSpace createFromCOSObject(COSObject colorSpace, PDResource throws IOException { PDColorSpace cs; - if (resources != null && resources.getResourceCache() != null) + if (resources != null) { ResourceCache resourceCache = resources.getResourceCache(); - cs = resourceCache.getColorSpace(colorSpace); - if (cs != null) + if (resourceCache != null) { - return cs; + cs = resourceCache.getColorSpace(colorSpace); + if (cs != null) + { + return cs; + } + } + cs = create(colorSpace.getObject(), resources); + if (resourceCache != null && cs != null) + { + resourceCache.put(colorSpace, cs); } } - cs = create(colorSpace.getObject(), resources); - if (resources != null && resources.getResourceCache() != null && cs != null) + else { - ResourceCache resourceCache = resources.getResourceCache(); - resourceCache.put(colorSpace, cs); + cs = create(colorSpace.getObject()); } return cs; } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDDeviceCMYK.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDDeviceCMYK.java index bb732bcb3..b3bb8aedd 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDDeviceCMYK.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDDeviceCMYK.java @@ -76,15 +76,43 @@ public float[] toRGB(float[] value) throws IOException float y = value[2]; float k = value[3]; - float r = 255 * (1 - c) * (1 - k); - float g = 255 * (1 - m) * (1 - k); - float b = 255 * (1 - y) * (1 - k); + float r = (1 - c) * (1 - k); + float g = (1 - m) * (1 - k); + float b = (1 - y) * (1 - k); return new float[] { r, g, b }; } @Override public Bitmap toRGBImage(Bitmap raster) throws IOException { - return raster; + int width = raster.getWidth(); + int height = raster.getHeight(); + final int maxChunkPixels = 262144; + final int chunkRows = Math.min(height, Math.max(1, maxChunkPixels / width)); + final int bufferSize = chunkRows * width; + int[] imgPixels = new int[bufferSize]; + int[] outPixels = new int[bufferSize]; + Bitmap image = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + + for (int y = 0; y < height; y += chunkRows) + { + int currentChunkRows = Math.min(chunkRows, height - y); + int currentPixelCount = currentChunkRows * width; + raster.getPixels(imgPixels, 0, width, 0, y, width, currentChunkRows); + for (int i = 0; i < currentPixelCount; i++) + { + int pixel = imgPixels[i]; + float c = android.graphics.Color.red(pixel) / 255.0f; + float m = android.graphics.Color.green(pixel) / 255.0f; + float yVal = android.graphics.Color.blue(pixel) / 255.0f; + float k = android.graphics.Color.alpha(pixel) / 255.0f; + int r = Math.round(255 * (1 - c) * (1 - k)); + int g = Math.round(255 * (1 - m) * (1 - k)); + int b = Math.round(255 * (1 - yVal) * (1 - k)); + outPixels[i] = android.graphics.Color.argb(255, r, g, b); + } + image.setPixels(outPixels, 0, width, 0, y, width, currentChunkRows); + } + return image; } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDDeviceGray.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDDeviceGray.java index 9e50eec1e..af77927c1 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDDeviceGray.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDDeviceGray.java @@ -81,23 +81,25 @@ public Bitmap toRGBImage(Bitmap raster) throws IOException int width = raster.getWidth(); int height = raster.getHeight(); - int[] imgPixels = new int[width]; + final int maxChunkPixels = 262144; + final int chunkRows = Math.min(height, Math.max(1, maxChunkPixels / width)); + final int bufferSize = chunkRows * width; + int[] imgPixels = new int[bufferSize]; + int[] outPixels = new int[bufferSize]; Bitmap image = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - int[] outPixels = new int[width]; - int gray; - int rgb; - for (int y = 0; y < height; y++) + for (int y = 0; y < height; y += chunkRows) { - raster.getPixels(imgPixels, 0, width, 0, y, width, 1); - for (int x = 0; x < width; x++) + int currentChunkRows = Math.min(chunkRows, height - y); + int currentPixelCount = currentChunkRows * width; + raster.getPixels(imgPixels, 0, width, 0, y, width, currentChunkRows); + for (int i = 0; i < currentPixelCount; i++) { - gray = Color.alpha(imgPixels[x]); - rgb = Color.argb(255, gray, gray, gray); - outPixels[x] = rgb; + int gray = Color.alpha(imgPixels[i]); + outPixels[i] = Color.argb(255, gray, gray, gray); } - image.setPixels(outPixels, 0, width, 0, y, width, 1); + image.setPixels(outPixels, 0, width, 0, y, width, currentChunkRows); } return image; diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDICCBased.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDICCBased.java index 3e18887cb..bf5c4d736 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDICCBased.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDICCBased.java @@ -28,6 +28,7 @@ import com.tom_roush.pdfbox.cos.COSStream; import com.tom_roush.pdfbox.pdmodel.PDDocument; import com.tom_roush.pdfbox.pdmodel.PDResources; +import com.tom_roush.pdfbox.pdmodel.ResourceCache; import com.tom_roush.pdfbox.pdmodel.common.PDStream; /** @@ -95,25 +96,26 @@ public static PDICCBased create(COSArray iccArray, PDResources resources) throws { checkArray(iccArray); COSBase base = iccArray.get(1); - COSObject indirect = null; - if (base instanceof COSObject) + if (base instanceof COSObject && resources != null) { - indirect = (COSObject) base; - } - if (indirect != null && resources != null && resources.getResourceCache() != null) - { - PDColorSpace space = resources.getResourceCache().getColorSpace(indirect); - if (space instanceof PDICCBased) + ResourceCache resourceCache = resources.getResourceCache(); + if (resourceCache != null) { - return (PDICCBased) space; + COSObject indirect = (COSObject) base; + PDColorSpace space = resourceCache.getColorSpace(indirect); + if (space instanceof PDICCBased) + { + return (PDICCBased) space; + } + else + { + PDICCBased newSpace = new PDICCBased(iccArray); + resourceCache.put(indirect, newSpace); + return newSpace; + } } } - PDICCBased space = new PDICCBased(iccArray); - if (indirect != null && resources != null && resources.getResourceCache() != null) - { - resources.getResourceCache().put(indirect, space); - } - return space; + return new PDICCBased(iccArray); } private static void checkArray(COSArray iccArray) throws IOException diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDIndexed.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDIndexed.java new file mode 100644 index 000000000..172fdde75 --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDIndexed.java @@ -0,0 +1,267 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tom_roush.pdfbox.pdmodel.graphics.color; + +import android.graphics.Bitmap; + +import java.io.IOException; + +import com.tom_roush.pdfbox.cos.COSArray; +import com.tom_roush.pdfbox.cos.COSBase; +import com.tom_roush.pdfbox.cos.COSInteger; +import com.tom_roush.pdfbox.cos.COSName; +import com.tom_roush.pdfbox.cos.COSNull; +import com.tom_roush.pdfbox.cos.COSNumber; +import com.tom_roush.pdfbox.cos.COSStream; +import com.tom_roush.pdfbox.cos.COSString; +import com.tom_roush.pdfbox.pdmodel.PDResources; +import com.tom_roush.pdfbox.pdmodel.common.PDStream; + +/** + * An Indexed colour space specifies that an area is to be painted using a colour table + * of arbitrary colours from another color space. + * + * @author John Hewson + * @author Ben Litchfield + */ +public final class PDIndexed extends PDSpecialColorSpace +{ + private final PDColor initialColor = new PDColor(new float[] { 0 }, this); + + private PDColorSpace baseColorSpace = null; + + // cached lookup data + private byte[] lookupData; + private float[][] colorTable; + private int actualMaxIndex; + private int[] rgbColorTable; + + public PDIndexed() + { + array = new COSArray(); + array.add(COSName.INDEXED); + array.add(COSName.DEVICERGB); + array.add(COSInteger.get(255)); + array.add(COSNull.NULL); + } + + public PDIndexed(COSArray indexedArray) throws IOException + { + this(indexedArray, null); + } + + public PDIndexed(COSArray indexedArray, PDResources resources) throws IOException + { + array = indexedArray; + baseColorSpace = PDColorSpace.create(array.get(1), resources); + readColorTable(); + initRgbColorTable(); + } + + @Override + public String getName() + { + return COSName.INDEXED.getName(); + } + + @Override + public int getNumberOfComponents() + { + return 1; + } + + @Override + public float[] getDefaultDecode(int bitsPerComponent) + { + return new float[] { 0, (float) Math.pow(2, bitsPerComponent) - 1 }; + } + + @Override + public PDColor getInitialColor() + { + return initialColor; + } + + private void initRgbColorTable() throws IOException + { + if (baseColorSpace == null) + { + rgbColorTable = new int[actualMaxIndex + 1]; + return; + } + + int numBaseComponents = baseColorSpace.getNumberOfComponents(); + rgbColorTable = new int[actualMaxIndex + 1]; + + float[] base = new float[numBaseComponents]; + for (int i = 0; i <= actualMaxIndex; i++) + { + for (int c = 0; c < numBaseComponents; c++) + { + base[c] = colorTable[i][c]; + } + float[] rgb = baseColorSpace.toRGB(base); + int r = Math.round(rgb[0] * 255f); + int g = Math.round(rgb[1] * 255f); + int b = Math.round(rgb[2] * 255f); + r = Math.max(0, Math.min(255, r)); + g = Math.max(0, Math.min(255, g)); + b = Math.max(0, Math.min(255, b)); + rgbColorTable[i] = (0xFF << 24) | (r << 16) | (g << 8) | b; + } + } + + @Override + public float[] toRGB(float[] value) + { + if (value.length != 1) + { + throw new IllegalArgumentException("Indexed color spaces must have one color value"); + } + + int index = Math.round(value[0]); + index = Math.max(0, Math.min(index, actualMaxIndex)); + + int argb = rgbColorTable[index]; + return new float[] { + ((argb >> 16) & 0xFF) / 255f, + ((argb >> 8) & 0xFF) / 255f, + (argb & 0xFF) / 255f + }; + } + + /** + * Returns the ARGB packed integer color for a given palette index. + * + * @param index the palette index + * @return 0xAARRGGBB packed color + */ + public int getRgbColor(int index) + { + if (rgbColorTable == null || rgbColorTable.length == 0) + { + return 0xFF000000; + } + index = Math.max(0, Math.min(index, actualMaxIndex)); + return rgbColorTable[index]; + } + + public int[] getRgbColorTable() + { + return rgbColorTable; + } + + @Override + public Bitmap toRGBImage(Bitmap raster) throws IOException + { + int width = raster.getWidth(); + int height = raster.getHeight(); + int[] src = new int[width]; + int[] out = new int[width]; + + Bitmap rgbImage = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + for (int y = 0; y < height; y++) + { + raster.getPixels(src, 0, width, 0, y, width, 1); + for (int x = 0; x < width; x++) + { + int index = (src[x] >> 24) & 0xFF; + if (raster.getConfig() != Bitmap.Config.ALPHA_8) + { + index = src[x] & 0xFF; + } + out[x] = getRgbColor(index); + } + rgbImage.setPixels(out, 0, width, 0, y, width, 1); + } + return rgbImage; + } + + public PDColorSpace getBaseColorSpace() + { + return baseColorSpace; + } + + private int getHival() + { + COSBase base = array.getObject(2); + if (base instanceof COSNumber) + { + return ((COSNumber) base).intValue(); + } + return 255; + } + + private void readLookupData() throws IOException + { + if (lookupData == null) + { + COSBase lookupTable = array.getObject(3); + if (lookupTable instanceof COSString) + { + lookupData = ((COSString) lookupTable).getBytes(); + } + else if (lookupTable instanceof COSStream) + { + lookupData = new PDStream((COSStream) lookupTable).toByteArray(); + } + else if (lookupTable == null) + { + lookupData = new byte[0]; + } + else + { + throw new IOException("Error: Unknown type for lookup table " + lookupTable); + } + } + } + + private void readColorTable() throws IOException + { + readLookupData(); + + int maxIndex = Math.min(getHival(), 255); + int numComponents = baseColorSpace != null ? baseColorSpace.getNumberOfComponents() : 3; + + if (numComponents > 0 && lookupData.length / numComponents < maxIndex + 1) + { + maxIndex = lookupData.length / numComponents - 1; + } + actualMaxIndex = Math.max(0, maxIndex); + + colorTable = new float[actualMaxIndex + 1][numComponents]; + for (int i = 0, offset = 0; i <= actualMaxIndex; i++) + { + for (int c = 0; c < numComponents; c++) + { + if (offset < lookupData.length) + { + colorTable[i][c] = (lookupData[offset] & 0xff) / 255f; + offset++; + } + } + } + } + + @Override + public String toString() + { + return "Indexed{base:" + baseColorSpace + " " + + "hival:" + getHival() + " " + + "lookup:(" + (colorTable != null ? colorTable.length : 0) + " entries)}"; + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDJPXColorSpace.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDJPXColorSpace.java index c24bc79a4..0cd12d20d 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDJPXColorSpace.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDJPXColorSpace.java @@ -87,6 +87,12 @@ public PDColor getInitialColor() @Override public float[] toRGB(float[] value) { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) + { + android.graphics.Color c = android.graphics.Color.valueOf(value[0], value[1], value[2], 1f, colorSpace); + android.graphics.Color rgb = c.convert(android.graphics.ColorSpace.get(android.graphics.ColorSpace.Named.SRGB)); + return new float[] { rgb.red(), rgb.green(), rgb.blue() }; + } throw new UnsupportedOperationException("JPX color spaces don't support drawing"); } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDOutputIntent.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDOutputIntent.java index d9ee159a2..999c32e8d 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDOutputIntent.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDOutputIntent.java @@ -26,7 +26,6 @@ import com.tom_roush.pdfbox.pdmodel.PDDocument; import com.tom_roush.pdfbox.pdmodel.common.COSObjectable; import com.tom_roush.pdfbox.pdmodel.common.PDStream; - /** * An Output Intent describes the colour reproduction characteristics of a possible output * device or production condition. @@ -39,6 +38,14 @@ public final class PDOutputIntent implements COSObjectable { private final COSDictionary dictionary; + /** + * Create an output intent of GTS_PDFA1 subtype. + * + * @param doc The document. + * @param colorProfile the ICC color profile input stream. You can close it after construction. + * @throws IOException If an I/O error occurs while reading the stream. + * @throws IllegalArgumentException If the stream does not contain valid ICC Profile data. + */ public PDOutputIntent(PDDocument doc, InputStream colorProfile) throws IOException { dictionary = new COSDictionary(); @@ -61,7 +68,7 @@ public COSBase getCOSObject() public COSStream getDestOutputIntent() { - return (COSStream) dictionary.getDictionaryObject(COSName.DEST_OUTPUT_PROFILE); + return dictionary.getCOSStream(COSName.DEST_OUTPUT_PROFILE); } public String getInfo() @@ -105,7 +112,7 @@ public void setRegistryName(String value) } private PDStream configureOutputProfile(PDDocument doc, InputStream colorProfile) - throws IOException + throws IOException { // ICC_Profile icc = ICC_Profile.getInstance(colorProfile); // PDStream stream = new PDStream(doc, new ByteArrayInputStream(icc.getData()), COSName.FLATE_DECODE); @@ -114,4 +121,4 @@ private PDStream configureOutputProfile(PDDocument doc, InputStream colorProfile stream.getStream().setInt(COSName.N, 3); return stream; } -} +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/form/PDFormXObject.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/form/PDFormXObject.java index 22bd079c4..42d2062c0 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/form/PDFormXObject.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/form/PDFormXObject.java @@ -35,7 +35,6 @@ import com.tom_roush.pdfbox.pdmodel.documentinterchange.markedcontent.PDPropertyList; import com.tom_roush.pdfbox.pdmodel.graphics.PDXObject; import com.tom_roush.pdfbox.util.Matrix; - /* TODO There are further Form XObjects to implement: @@ -119,15 +118,15 @@ public void setFormType(int formType) } /** - * Returns the group attributes dictionary. + * Returns the transparency group attributes dictionary. * - * @return the group attributes dictionary + * @return the transparency group attributes dictionary, never null. */ public PDTransparencyGroupAttributes getGroup() { if( group == null ) { - COSDictionary dic = (COSDictionary) getCOSObject().getDictionaryObject(COSName.GROUP); + COSDictionary dic = getCOSObject().getCOSDictionary(COSName.GROUP); if( dic != null ) { group = new PDTransparencyGroupAttributes(dic); @@ -136,6 +135,17 @@ public PDTransparencyGroupAttributes getGroup() return group; } + /** + * Sets the transparency group attributes dictionary. + * + * @param group a transparency group attributes dictionary. + */ + public void setGroup(PDTransparencyGroupAttributes group) + { + this.group = group; + getCOSObject().setItem(COSName.GROUP, group); + } + public PDStream getContentStream() { return new PDStream(getCOSObject()); @@ -191,7 +201,7 @@ public void setResources(PDResources resources) public PDRectangle getBBox() { PDRectangle retval = null; - COSArray array = (COSArray) getCOSObject().getDictionaryObject(COSName.BBOX); + COSArray array = getCOSObject().getCOSArray(COSName.BBOX); if (array != null) { retval = new PDRectangle(array); diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/CCITTFactory.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/CCITTFactory.java index 23c85eb98..7cf80c57e 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/CCITTFactory.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/CCITTFactory.java @@ -71,7 +71,7 @@ public static PDImageXObject createFromImage(PDDocument document, Bitmap image) int width = image.getWidth(); int[] pixels = new int[width]; - ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ByteArrayOutputStream bos = new ByteArrayOutputStream(Math.max(32, (width + 1) * height)); MemoryCacheImageOutputStream mcios = new MemoryCacheImageOutputStream(bos); for (int y = 0; y < height; ++y) @@ -347,6 +347,7 @@ private static void extractFromTiff(RandomAccess reader, OutputStream os, int dataoffset = 0; int datalength = 0; + int fillorder = 1; for (int i = 0; i < numtags; i++) { @@ -408,10 +409,12 @@ private static void extractFromTiff(RandomAccess reader, OutputStream os, } case 266: { - if (val != 1) + // http://www.awaresystems.be/imaging/tiff/tifftags/fillorder.html + if (val != 1 && val != 2) { throw new IOException("FillOrder " + val + " is not supported"); } + fillorder = val; break; } case 273: @@ -498,6 +501,13 @@ private static void extractFromTiff(RandomAccess reader, OutputStream os, while ((amountRead = reader.read(buf, 0, Math.min(8192, datalength))) > 0) { datalength -= amountRead; + if (fillorder == 2) + { + for (int x = 0; x < amountRead; x++) + { + buf[x] = fliptable[buf[x] & 0xFF]; + } + } os.write(buf, 0, amountRead); } @@ -525,4 +535,40 @@ private static int readlong(char endianess, RandomAccess raf) throws IOException } return (raf.read() << 24) | (raf.read() << 16) | (raf.read() << 8) | raf.read(); } + private static final byte[] fliptable = new byte[] + { + (byte) 0x00, (byte) 0x80, (byte) 0x40, (byte) 0xc0, (byte) 0x20, (byte) 0xa0, (byte) 0x60, (byte) 0xe0, + (byte) 0x10, (byte) 0x90, (byte) 0x50, (byte) 0xd0, (byte) 0x30, (byte) 0xb0, (byte) 0x70, (byte) 0xf0, + (byte) 0x08, (byte) 0x88, (byte) 0x48, (byte) 0xc8, (byte) 0x28, (byte) 0xa8, (byte) 0x68, (byte) 0xe8, + (byte) 0x18, (byte) 0x98, (byte) 0x58, (byte) 0xd8, (byte) 0x38, (byte) 0xb8, (byte) 0x78, (byte) 0xf8, + (byte) 0x04, (byte) 0x84, (byte) 0x44, (byte) 0xc4, (byte) 0x24, (byte) 0xa4, (byte) 0x64, (byte) 0xe4, + (byte) 0x14, (byte) 0x94, (byte) 0x54, (byte) 0xd4, (byte) 0x34, (byte) 0xb4, (byte) 0x74, (byte) 0xf4, + (byte) 0x0c, (byte) 0x8c, (byte) 0x4c, (byte) 0xcc, (byte) 0x2c, (byte) 0xac, (byte) 0x6c, (byte) 0xec, + (byte) 0x1c, (byte) 0x9c, (byte) 0x5c, (byte) 0xdc, (byte) 0x3c, (byte) 0xbc, (byte) 0x7c, (byte) 0xfc, + (byte) 0x02, (byte) 0x82, (byte) 0x42, (byte) 0xc2, (byte) 0x22, (byte) 0xa2, (byte) 0x62, (byte) 0xe2, + (byte) 0x12, (byte) 0x92, (byte) 0x52, (byte) 0xd2, (byte) 0x32, (byte) 0xb2, (byte) 0x72, (byte) 0xf2, + (byte) 0x0a, (byte) 0x8a, (byte) 0x4a, (byte) 0xca, (byte) 0x2a, (byte) 0xaa, (byte) 0x6a, (byte) 0xea, + (byte) 0x1a, (byte) 0x9a, (byte) 0x5a, (byte) 0xda, (byte) 0x3a, (byte) 0xba, (byte) 0x7a, (byte) 0xfa, + (byte) 0x06, (byte) 0x86, (byte) 0x46, (byte) 0xc6, (byte) 0x26, (byte) 0xa6, (byte) 0x66, (byte) 0xe6, + (byte) 0x16, (byte) 0x96, (byte) 0x56, (byte) 0xd6, (byte) 0x36, (byte) 0xb6, (byte) 0x76, (byte) 0xf6, + (byte) 0x0e, (byte) 0x8e, (byte) 0x4e, (byte) 0xce, (byte) 0x2e, (byte) 0xae, (byte) 0x6e, (byte) 0xee, + (byte) 0x1e, (byte) 0x9e, (byte) 0x5e, (byte) 0xde, (byte) 0x3e, (byte) 0xbe, (byte) 0x7e, (byte) 0xfe, + (byte) 0x01, (byte) 0x81, (byte) 0x41, (byte) 0xc1, (byte) 0x21, (byte) 0xa1, (byte) 0x61, (byte) 0xe1, + (byte) 0x11, (byte) 0x91, (byte) 0x51, (byte) 0xd1, (byte) 0x31, (byte) 0xb1, (byte) 0x71, (byte) 0xf1, + (byte) 0x09, (byte) 0x89, (byte) 0x49, (byte) 0xc9, (byte) 0x29, (byte) 0xa9, (byte) 0x69, (byte) 0xe9, + (byte) 0x19, (byte) 0x99, (byte) 0x59, (byte) 0xd9, (byte) 0x39, (byte) 0xb9, (byte) 0x79, (byte) 0xf9, + (byte) 0x05, (byte) 0x85, (byte) 0x45, (byte) 0xc5, (byte) 0x25, (byte) 0xa5, (byte) 0x65, (byte) 0xe5, + (byte) 0x15, (byte) 0x95, (byte) 0x55, (byte) 0xd5, (byte) 0x35, (byte) 0xb5, (byte) 0x75, (byte) 0xf5, + (byte) 0x0d, (byte) 0x8d, (byte) 0x4d, (byte) 0xcd, (byte) 0x2d, (byte) 0xad, (byte) 0x6d, (byte) 0xed, + (byte) 0x1d, (byte) 0x9d, (byte) 0x5d, (byte) 0xdd, (byte) 0x3d, (byte) 0xbd, (byte) 0x7d, (byte) 0xfd, + (byte) 0x03, (byte) 0x83, (byte) 0x43, (byte) 0xc3, (byte) 0x23, (byte) 0xa3, (byte) 0x63, (byte) 0xe3, + (byte) 0x13, (byte) 0x93, (byte) 0x53, (byte) 0xd3, (byte) 0x33, (byte) 0xb3, (byte) 0x73, (byte) 0xf3, + (byte) 0x0b, (byte) 0x8b, (byte) 0x4b, (byte) 0xcb, (byte) 0x2b, (byte) 0xab, (byte) 0x6b, (byte) 0xeb, + (byte) 0x1b, (byte) 0x9b, (byte) 0x5b, (byte) 0xdb, (byte) 0x3b, (byte) 0xbb, (byte) 0x7b, (byte) 0xfb, + (byte) 0x07, (byte) 0x87, (byte) 0x47, (byte) 0xc7, (byte) 0x27, (byte) 0xa7, (byte) 0x67, (byte) 0xe7, + (byte) 0x17, (byte) 0x97, (byte) 0x57, (byte) 0xd7, (byte) 0x37, (byte) 0xb7, (byte) 0x77, (byte) 0xf7, + (byte) 0x0f, (byte) 0x8f, (byte) 0x4f, (byte) 0xcf, (byte) 0x2f, (byte) 0xaf, (byte) 0x6f, (byte) 0xef, + (byte) 0x1f, (byte) 0x9f, (byte) 0x5f, (byte) 0xdf, (byte) 0x3f, (byte) 0xbf, (byte) 0x7f, (byte) 0xff, + }; + } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/JPEGFactory.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/JPEGFactory.java index 086c0411a..4acb99f2e 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/JPEGFactory.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/JPEGFactory.java @@ -21,6 +21,7 @@ import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; +import android.util.Log; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -35,6 +36,7 @@ import com.tom_roush.pdfbox.io.IOUtils; import com.tom_roush.pdfbox.pdmodel.PDDocument; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColorSpace; +import com.tom_roush.pdfbox.pdmodel.graphics.color.PDDeviceCMYK; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDDeviceGray; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDDeviceRGB; @@ -81,7 +83,19 @@ public static PDImageXObject createFromByteArray(PDDocument document, byte[] byt Dimensions meta = retrieveDimensions(byteStream); - PDColorSpace colorSpace = PDDeviceRGB.INSTANCE; // All images are RGB after being loaded by Bitmaps + PDColorSpace colorSpace; + if (meta.numComponents == 1) + { + colorSpace = PDDeviceGray.INSTANCE; + } + else if (meta.numComponents == 4) + { + colorSpace = PDDeviceCMYK.INSTANCE; + } + else + { + colorSpace = PDDeviceRGB.INSTANCE; + } // create PDImageXObject from stream PDImageXObject pdImage = new PDImageXObject(document, byteStream, @@ -107,10 +121,98 @@ private static Dimensions retrieveDimensions(ByteArrayInputStream stream) throws Dimensions meta = new Dimensions(); meta.width = options.outWidth; meta.height = options.outHeight; + parseJpegMetadata(stream, meta); return meta; } + private static void parseJpegMetadata(ByteArrayInputStream stream, Dimensions meta) + { + stream.mark(0); + meta.numComponents = 3; + try + { + if (stream.read() != 0xFF || stream.read() != 0xD8) + { + return; + } + int b; + while ((b = stream.read()) != -1) + { + if (b == 0xFF) + { + while ((b = stream.read()) == 0xFF) + { + // skip padding bytes + } + if (b == -1) + { + break; + } + + // Check SOF markers + if ((b >= 0xC0 && b <= 0xC3) || (b >= 0xC5 && b <= 0xC7) || (b >= 0xC9 && b <= 0xCB)) + { + // SOF marker: skip length (2 bytes) + stream.skip(2); + int precision = stream.read(); + int h1 = stream.read(); + int h2 = stream.read(); + int w1 = stream.read(); + int w2 = stream.read(); + int components = stream.read(); + if (components > 0) + { + meta.numComponents = components; + } + if (meta.height <= 0 && h1 != -1 && h2 != -1) + { + meta.height = (h1 << 8) | h2; + } + if (meta.width <= 0 && w1 != -1 && w2 != -1) + { + meta.width = (w1 << 8) | w2; + } + break; + } + else if (b == 0xDA || b == 0xD9) + { + // SOS (Start of Scan) or EOI + break; + } + else if (b >= 0xD0 && b <= 0xD7) + { + // RST markers have no payload + continue; + } + else + { + // Other markers: read 2-byte length and skip payload + int lenHigh = stream.read(); + int lenLow = stream.read(); + if (lenHigh == -1 || lenLow == -1) + { + break; + } + int length = (lenHigh << 8) | lenLow; + if (length > 2) + { + stream.skip(length - 2); + } + } + } + } + } + catch (Exception e) + { + Log.w("PdfBox-Android", "Could not parse JPEG components: " + e.getMessage()); + } + finally + { + stream.reset(); + } + } + /** * Creates a new JPEG PDImageXObject from a Bitmap. *

      @@ -232,20 +334,28 @@ private static PDImageXObject createAlphaFromARGBImage(PDDocument document, Bitm return null; } - int[] pixels = new int[image.getHeight() * image.getWidth()]; - image.getPixels(pixels, 0, image.getWidth(), 0, 0, image.getWidth(), image.getHeight()); + final int width = image.getWidth(); + final int height = image.getHeight(); + final int maxChunkPixels = 262144; + final int chunkRows = Math.min(height, Math.max(1, maxChunkPixels / width)); + final int bufferSize = chunkRows * width; + int[] pixels = new int[bufferSize]; - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - int bpc; - bpc = 8; - for (int pixel : pixels) + ByteArrayOutputStream bos = new ByteArrayOutputStream(width * height); + int bpc = 8; + for (int y = 0; y < height; y += chunkRows) { - bos.write(Color.alpha(pixel)); + int currentChunkRows = Math.min(chunkRows, height - y); + int currentPixelCount = currentChunkRows * width; + image.getPixels(pixels, 0, width, 0, y, width, currentChunkRows); + for (int i = 0; i < currentPixelCount; i++) + { + bos.write((pixels[i] >> 24) & 0xFF); + } } - // } return prepareImageXObject(document, bos.toByteArray(), - image.getWidth(), image.getHeight(), bpc, PDDeviceGray.INSTANCE); + width, height, bpc, PDDeviceGray.INSTANCE); } /** diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/LosslessFactory.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/LosslessFactory.java index b5f0bbc36..ade81adde 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/LosslessFactory.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/LosslessFactory.java @@ -123,22 +123,29 @@ private static PDImageXObject createFromGrayImage(Bitmap image, PDDocument docum { int height = image.getHeight(); int width = image.getWidth(); - int[] rgbLineBuffer = new int[width]; + final int maxChunkPixels = 262144; + final int chunkRows = Math.min(height, Math.max(1, maxChunkPixels / width)); + final int bufferSize = chunkRows * width; + int[] rgbLineBuffer = new int[bufferSize]; int bpc = 8; ByteArrayOutputStream baos = new ByteArrayOutputStream(((width*bpc/8)+(width*bpc%8 != 0 ? 1:0))*height); MemoryCacheImageOutputStream mcios = new MemoryCacheImageOutputStream(baos); - for (int y = 0; y < height; ++y) + for (int y = 0; y < height; y += chunkRows) { - image.getPixels(rgbLineBuffer, 0, width, 0, y, width, 1); - for (int pixel : rgbLineBuffer) + int currentChunkRows = Math.min(chunkRows, height - y); + image.getPixels(rgbLineBuffer, 0, width, 0, y, width, currentChunkRows); + for (int r = 0; r < currentChunkRows; r++) { - mcios.writeBits(pixel & 0xFF, bpc); - } - - int bitOffset = mcios.getBitOffset(); - if (bitOffset != 0) - { - mcios.writeBits(0, 8 - bitOffset); + int rowOffset = r * width; + for (int x = 0; x < width; x++) + { + mcios.writeBits(rgbLineBuffer[rowOffset + x] & 0xFF, bpc); + } + int bitOffset = mcios.getBitOffset(); + if (bitOffset != 0) + { + mcios.writeBits(0, 8 - bitOffset); + } } } mcios.flush(); @@ -151,7 +158,10 @@ private static PDImageXObject createFromRGBImage(Bitmap image, PDDocument docume { int height = image.getHeight(); int width = image.getWidth(); - int[] rgbLineBuffer = new int[width]; + final int maxChunkPixels = 262144; + final int chunkRows = Math.min(height, Math.max(1, maxChunkPixels / width)); + final int bufferSize = chunkRows * width; + int[] rgbLineBuffer = new int[bufferSize]; int bpc = 8; PDDeviceColorSpace deviceColorSpace = PDDeviceRGB.INSTANCE; byte[] imageData = new byte[width * height * 3]; @@ -168,20 +178,20 @@ private static PDImageXObject createFromRGBImage(Bitmap image, PDDocument docume { alphaImageData = new byte[0]; } - for (int y = 0; y < height; ++y) + for (int y = 0; y < height; y += chunkRows) { - image.getPixels(rgbLineBuffer, 0, width, 0, y, width, 1); - for (int pixel : rgbLineBuffer) + int currentChunkRows = Math.min(chunkRows, height - y); + int currentPixelCount = currentChunkRows * width; + image.getPixels(rgbLineBuffer, 0, width, 0, y, width, currentChunkRows); + for (int i = 0; i < currentPixelCount; i++) { + int pixel = rgbLineBuffer[i]; imageData[byteIdx++] = (byte) ((pixel >> 16) & 0xFF); imageData[byteIdx++] = (byte) ((pixel >> 8) & 0xFF); imageData[byteIdx++] = (byte) (pixel & 0xFF); if (image.hasAlpha()) { - { - // write a byte - alphaImageData[alphaByteIdx++] = (byte) ((pixel >> 24) & 0xFF); - } + alphaImageData[alphaByteIdx++] = (byte) ((pixel >> 24) & 0xFF); } } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImage.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImage.java index 2d7dc0013..defd047e7 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImage.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImage.java @@ -28,7 +28,6 @@ import com.tom_roush.pdfbox.filter.DecodeOptions; import com.tom_roush.pdfbox.pdmodel.common.COSObjectable; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColorSpace; - /** * An image in a PDF document. * @@ -38,7 +37,7 @@ public interface PDImage extends COSObjectable { /** * Returns the content of this image as a Bitmap with ARGB_8888. - * The size of the returned image is the larger of the size of the image itself or its mask. + * The size of the returned image is the larger of the size of the image itself or its mask. * @return content of this image as a buffered image. * @throws IOException */ @@ -112,7 +111,7 @@ public interface PDImage extends COSObjectable boolean isStencil(); /** - * Sets whether or not the image is a stencil. + * Sets whether the image is a stencil. * This corresponds to the {@code ImageMask} entry in the image stream's dictionary. * @param isStencil True to make the image a stencil. */ @@ -147,7 +146,9 @@ public interface PDImage extends COSObjectable int getHeight(); /** - * Sets the height of the image. + * Sets the height of the image. This is for internal PDFBox usage and not to set the size of + * the image on the page. + * * @param height The height of the image. */ void setHeight(int height); @@ -158,7 +159,9 @@ public interface PDImage extends COSObjectable int getWidth(); /** - * Sets the width of the image. + * Sets the width of the image. This is for internal PDFBox usage and not to set the size of + * the image on the page. + * * @param width The width of the image. */ void setWidth(int width); diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImageXObject.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImageXObject.java index adf7be29a..a15c668da 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImageXObject.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImageXObject.java @@ -65,6 +65,11 @@ public final class PDImageXObject extends PDXObject implements PDImage { private SoftReference cachedImage; private PDColorSpace colorSpace; + // indicates whether this image has an JPX-based filter applied + private boolean hasJPXFilter = false; + // is set to true after reading some values from a JPX-based image + private boolean jpxValuesInitialized = false; + private Bitmap jpxSMask = null; // initialize to MAX_VALUE as we prefer lower subsampling when keeping/replacing cache. private int cachedImageSubsampling = Integer.MAX_VALUE; @@ -129,38 +134,49 @@ public PDImageXObject(PDStream stream, PDResources resources) throws IOException super(stream, COSName.IMAGE); this.resources = resources; List filters = stream.getFilters(); - // JPX would be decode twice in rending. here is no need. just to ensure no parameters is missing. if (filters != null && !filters.isEmpty() && COSName.JPX_DECODE.equals(filters.get(filters.size() - 1))) { - // skip decode jpx is no parameters is missing. see PDFBOX-5375 PDFBOX-3340 - List requireKeys = Arrays.asList(COSName.WIDTH, COSName.HEIGHT, COSName.COLORSPACE); - boolean needDecode = false; - COSStream cos = stream.getCOSObject(); - for (COSName k : requireKeys) - { - if (!cos.containsKey(k)) - { - needDecode = true; - break; - } - } - if (!needDecode) - { - return; - } - COSInputStream is = null; - try + hasJPXFilter = true; + } + } + + /** + * Initializes the values of the image dictionary, colorspace and SMask (if any) if this is a + * JPX-based image, so that they can be used without decoding the image again later. + */ + private void initJPXValues() + { + if (!hasJPXFilter || jpxValuesInitialized) + { + return; + } + // some of the dictionary values of the COSStream may be overwritten by values which are extracted from the + // image itself, such as + // width and height of the image + // bits per component + // the colorspace of the image is used if the dictionary doesn't provide any value + PDStream stream = getStream(); + COSInputStream is = null; + try + { + is = stream.createInputStream(); + DecodeResult decodeResult = is.getDecodeResult(); + stream.getCOSObject().addAll(decodeResult.getParameters()); + if (colorSpace == null) { - is = stream.createInputStream(); - DecodeResult decodeResult = is.getDecodeResult(); - stream.getCOSObject().addAll(decodeResult.getParameters()); // getJPXColorSpace would be null in most cases - this.colorSpace = decodeResult.getJPXColorSpace(); - } - finally - { - IOUtils.closeQuietly(is); + colorSpace = decodeResult.getJPXColorSpace(); } + jpxSMask = decodeResult.getJPXSMask(); + jpxValuesInitialized = true; + } + catch (IOException exception) + { + Log.d("PdfBox-Android", "Can't initialize JPX based values", exception); + } + finally + { + IOUtils.closeQuietly(is); } } @@ -495,17 +511,25 @@ public Bitmap getImage(Rect region, int subsampling) throws IOException final Bitmap image; final PDImageXObject softMask = getSoftMask(); final PDImageXObject mask = getMask(); + initJPXValues(); + if (jpxSMask != null) + { + // PDFBOX-5657: handle JPEG2000 SMaskInData + image = applyMask(SampledImageReader.getRGBImage(this, region, subsampling, getColorKeyMask()), + jpxSMask, false, true, null); + } // soft mask (overrides explicit mask) - if (softMask != null) + else if (softMask != null) { image = applyMask(SampledImageReader.getRGBImage(this, region, subsampling, getColorKeyMask()), - softMask.getOpaqueImage(), softMask.getInterpolate(), true, extractMatte(softMask)); + softMask.getOpaqueImage(region, subsampling), softMask.getInterpolate(), true, + extractMatte(softMask)); } // explicit mask - to be applied only if /ImageMask true else if (mask != null && mask.isStencil()) { image = applyMask(SampledImageReader.getRGBImage(this, region, subsampling, getColorKeyMask()), - mask.getOpaqueImage(), mask.getInterpolate(), false, null); + mask.getOpaqueImage(region, subsampling), mask.getInterpolate(), false, null); } else { @@ -572,7 +596,25 @@ public Bitmap getStencilImage(Paint paint) throws IOException */ public Bitmap getOpaqueImage() throws IOException { - return SampledImageReader.getRGBImage(this, null); + return getOpaqueImage(null, 1); + } + + /** + * Returns an RGB image containing the opaque image stream without any masks applied. If this Image XObject + * is a mask then the image will contain the raw mask. + * + * @param region The region of the source image to get, or null if the entire image is needed. The actual region + * will be clipped to the dimensions of the source image. + * + * @param subsampling The amount of rows and columns to advance for every output pixel, a value of 1 meaning every + * pixel will be read. It must not be larger than the image width or height. + * + * @return the image without any masks applied + * @throws IOException if the image cannot be read + */ + public Bitmap getOpaqueImage(Rect region, int subsampling) throws IOException + { + return SampledImageReader.getRGBImage(this, region, subsampling, null); } /** @@ -586,10 +628,10 @@ public Bitmap getOpaqueImage() throws IOException * @param matte an optional RGB matte if a soft mask. * @return an ARGB image (can be the altered original image) */ - private Bitmap applyMask(Bitmap image, Bitmap mask, boolean interpolateMask, + Bitmap applyMask(Bitmap image, Bitmap mask, boolean interpolateMask, boolean isSoft, float[] matte) { - if (mask == null) + if (image == null || mask == null) { return image; } @@ -598,16 +640,10 @@ private Bitmap applyMask(Bitmap image, Bitmap mask, boolean interpolateMask, final int height = Math.max(image.getHeight(), mask.getHeight()); // scale mask to fit image, or image to fit mask, whichever is larger. - // also make sure that mask is 8 bit gray and image is ARGB as this - // is what needs to be returned. if (mask.getWidth() < width || mask.getHeight() < height) { mask = scaleImage(mask, width, height, interpolateMask); } - if (mask.getConfig() != Bitmap.Config.ALPHA_8 || !image.isMutable()) - { - mask = mask.copy(Bitmap.Config.ALPHA_8, true); - } if (image.getWidth() < width || image.getHeight() < height) { @@ -617,41 +653,32 @@ private Bitmap applyMask(Bitmap image, Bitmap mask, boolean interpolateMask, { image = image.copy(Bitmap.Config.ARGB_8888, true); } - int[] pixels = new int[width]; - int[] maskPixels = new int[width]; + final boolean isMaskAlpha8 = mask.getConfig() == Bitmap.Config.ALPHA_8; + final int maxChunkPixels = 262144; + final int chunkRows = Math.min(height, Math.max(1, maxChunkPixels / width)); + final int bufferSize = chunkRows * width; + int[] pixels = new int[bufferSize]; + int[] maskPixels = new int[bufferSize]; - // compose alpha into ARGB image, either: - // - very fast by direct bit combination if not a soft mask and a 8 bit alpha source. - // - fast by letting the sample model do a bulk band operation if no matte is set. - // - slow and complex by matte calculations on individual pixel components. - if (!isSoft && image.getByteCount() == mask.getByteCount()) + if (matte == null) { - for (int y = 0; y < height; y++) + for (int y = 0; y < height; y += chunkRows) { - image.getPixels(pixels, 0, width, 0, y, width, 1); - mask.getPixels(maskPixels, 0, width, 0, y, width, 1); - for (int i = 0, c = width; c > 0; i++, c--) - { - pixels[i] = pixels[i] & 0xffffff | ~maskPixels[i] & 0xff000000; - } - image.setPixels(pixels, 0, width, 0, y, width, 1); - } - } - else if (matte == null) - { - for (int y = 0; y < height; y++) - { - image.getPixels(pixels, 0, width, 0, y, width, 1); - mask.getPixels(maskPixels, 0, width, 0, y, width, 1); - for (int x = 0; x < width; x++) + int currentChunkRows = Math.min(chunkRows, height - y); + int currentPixelCount = currentChunkRows * width; + image.getPixels(pixels, 0, width, 0, y, width, currentChunkRows); + mask.getPixels(maskPixels, 0, width, 0, y, width, currentChunkRows); + for (int i = 0; i < currentPixelCount; i++) { + int maskVal = maskPixels[i]; + int a = isMaskAlpha8 ? ((maskVal >> 24) & 0xFF) : ((maskVal >> 16) & 0xFF); if (!isSoft) { - maskPixels[x] ^= -1; + a = 255 - a; } - pixels[x] = pixels[x] & 0xffffff | maskPixels[x] & 0xff000000; + pixels[i] = (pixels[i] & 0x00FFFFFF) | (a << 24); } - image.setPixels(pixels, 0, width, 0, y, width, 1); + image.setPixels(pixels, 0, width, 0, y, width, currentChunkRows); } } else @@ -674,28 +701,31 @@ else if (matte == null) final int m0h = m0 / 255 + (1 << fraction - 1); final int m1h = m1 / 255 + (1 << fraction - 1); final int m2h = m2 / 255 + (1 << fraction - 1); - for (int y = 0; y < height; y++) + for (int y = 0; y < height; y += chunkRows) { - image.getPixels(pixels, 0, width, 0, y, width, 1); - mask.getPixels(maskPixels, 0, width, 0, y, width, 1); - for (int x = 0; x < width; x++) + int currentChunkRows = Math.min(chunkRows, height - y); + int currentPixelCount = currentChunkRows * width; + image.getPixels(pixels, 0, width, 0, y, width, currentChunkRows); + mask.getPixels(maskPixels, 0, width, 0, y, width, currentChunkRows); + for (int i = 0; i < currentPixelCount; i++) { - int a = Color.alpha(maskPixels[x]); + int maskVal = maskPixels[i]; + int a = isMaskAlpha8 ? ((maskVal >> 24) & 0xFF) : ((maskVal >> 16) & 0xFF); if (a == 0) { - pixels[x] = pixels[x] & 0xffffff; + pixels[i] = pixels[i] & 0x00FFFFFF; continue; } - int rgb = pixels[x]; + int rgb = pixels[i]; int r = Color.red(rgb); int g = Color.green(rgb); int b = Color.blue(rgb); r = clampColor(((r * factor - m0) / a + m0h) >> fraction); g = clampColor(((g * factor - m1) / a + m1h) >> fraction); b = clampColor(((b * factor - m2) / a + m2h) >> fraction); - pixels[x] = Color.argb(a, r, g, b); + pixels[i] = Color.argb(a, r, g, b); } - image.setPixels(pixels, 0, width, 0, y, width, 1); + image.setPixels(pixels, 0, width, 0, y, width, currentChunkRows); } } return image; @@ -709,9 +739,9 @@ private static int clampColor(int color) /** * High-quality image scaling. */ - private Bitmap scaleImage(Bitmap image, int width, int height, boolean interpolate) + Bitmap scaleImage(Bitmap image, int width, int height, boolean interpolate) { - return Bitmap.createScaledBitmap(image, width, height, !interpolate); + return Bitmap.createScaledBitmap(image, width, height, interpolate); } /** @@ -733,7 +763,7 @@ public PDImageXObject getMask() throws IOException if (cosStream != null) { // always DeviceGray - return new PDImageXObject(new PDStream(cosStream), null); + return new PDImageXObject(new PDStream(cosStream), resources); } return null; } @@ -764,7 +794,7 @@ public PDImageXObject getSoftMask() throws IOException if (cosStream != null) { // always DeviceGray - return new PDImageXObject(new PDStream(cosStream), null); + return new PDImageXObject(new PDStream(cosStream), resources); } return null; } @@ -778,6 +808,7 @@ public int getBitsPerComponent() } else { + initJPXValues(); return getCOSObject().getInt(COSName.BITS_PER_COMPONENT, COSName.BPC); } } @@ -818,9 +849,13 @@ public PDColorSpace getColorSpace() throws IOException else if (isStencil()) { // stencil mask color space must be gray, it is often missing - return PDDeviceGray.INSTANCE; + colorSpace = PDDeviceGray.INSTANCE; } else + { + initJPXValues(); + } + if (colorSpace == null) { // an image without a color space is always broken throw new IOException("could not determine color space"); @@ -829,6 +864,15 @@ else if (isStencil()) return colorSpace; } + /** + * Returns the resources associated with this image, or null if there are none. + * @return the resources + */ + public PDResources getResources() + { + return resources; + } + @Override public InputStream createInputStream() throws IOException { @@ -864,6 +908,7 @@ public void setColorSpace(PDColorSpace cs) @Override public int getHeight() { + initJPXValues(); return getCOSObject().getInt(COSName.HEIGHT); } @@ -876,6 +921,7 @@ public void setHeight(int h) @Override public int getWidth() { + initJPXValues(); return getCOSObject().getInt(COSName.WIDTH); } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDInlineImage.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDInlineImage.java index b246ff0e8..b35ff27a4 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDInlineImage.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDInlineImage.java @@ -38,7 +38,6 @@ import com.tom_roush.pdfbox.pdmodel.common.COSArrayList; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColorSpace; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDDeviceGray; - /** * An inline image object which uses a special syntax to express the data for a * small image directly within the content stream. @@ -67,7 +66,7 @@ public final class PDInlineImage implements PDImage * @throws IOException if the stream cannot be decoded */ public PDInlineImage(COSDictionary parameters, byte[] data, PDResources resources) - throws IOException + throws IOException { this.parameters = parameters; this.resources = resources; @@ -83,15 +82,17 @@ public PDInlineImage(COSDictionary parameters, byte[] data, PDResources resource { ByteArrayInputStream in = new ByteArrayInputStream(data); ByteArrayOutputStream out = new ByteArrayOutputStream(data.length); + byte[] ba = null; for (int i = 0; i < filters.size(); i++) { // TODO handling of abbreviated names belongs here, rather than in other classes out.reset(); Filter filter = FilterFactory.INSTANCE.getFilter(filters.get(i)); decodeResult = filter.decode(in, out, parameters, i); - in = new ByteArrayInputStream(out.toByteArray()); + ba = out.toByteArray(); + in = new ByteArrayInputStream(ba); } - this.decodedData = out.toByteArray(); + this.decodedData = ba; } // repair parameters @@ -279,7 +280,12 @@ public void setDecode(COSArray decode) @Override public COSArray getDecode() { - return (COSArray) parameters.getDictionaryObject(COSName.D, COSName.DECODE); + COSBase decode = parameters.getDictionaryObject(COSName.D, COSName.DECODE); + if (decode instanceof COSArray) + { + return (COSArray) decode; + } + return null; } @Override @@ -357,6 +363,8 @@ public Bitmap getImage(Rect region, int subsampling) throws IOException return SampledImageReader.getRGBImage(this, region, subsampling, null); } + + @Override public Bitmap getStencilImage(Paint paint) throws IOException { diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/SampledImageReader.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/SampledImageReader.java index fcb45983a..287fc9b34 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/SampledImageReader.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/image/SampledImageReader.java @@ -36,6 +36,7 @@ import com.tom_roush.pdfbox.filter.DecodeOptions; import com.tom_roush.pdfbox.io.IOUtils; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColorSpace; +import com.tom_roush.pdfbox.pdmodel.graphics.color.PDIndexed; /** * Reads a sampled image from a PDF file. @@ -75,10 +76,11 @@ public static Bitmap getStencilImage(PDImage pdImage, Paint paint) throws IOExce // avoid getting a Bitmap for the mask to lessen memory footprint. // Such masks are always bpc=1 and have no colorspace, but have a decode. // (see 8.9.6.2 Stencil Masking) + InputStream imageStream = pdImage.createInputStream(); ImageInputStream iis = null; try { - iis = new MemoryCacheImageInputStream(pdImage.createInputStream()); + iis = new MemoryCacheImageInputStream(imageStream); final float[] decode = getDecodeArray(pdImage); int value = decode[0] < decode[1] ? 1 : 0; int rowLen = width / 8; @@ -125,6 +127,8 @@ public static Bitmap getStencilImage(PDImage pdImage, Paint paint) throws IOExce { iis.close(); } + // MemoryCacheImageInputStream doesn't close the wrapped stream + imageStream.close(); } return masked; @@ -144,7 +148,7 @@ public static Bitmap getRGBImage(PDImage pdImage, COSArray colorKey) throws IOEx return getRGBImage(pdImage, null, 1, colorKey); } - private static Rect clipRegion(PDImage pdImage, Rect region) + static Rect clipRegion(PDImage pdImage, Rect region) { if (region == null) { @@ -156,7 +160,7 @@ private static Rect clipRegion(PDImage pdImage, Rect region) int y = Math.max(0, region.top); int width = Math.min(region.width(), pdImage.getWidth() - x); int height = Math.min(region.height(), pdImage.getHeight() - y); - return new Rect(x, y, width, height); + return new Rect(x, y, x + width, y + height); } } @@ -208,7 +212,8 @@ public static Bitmap getRGBImage(PDImage pdImage, Rect region, int subsampling, // in PDColorSpace#toRGBImage expects an 8-bit range, i.e. 0-255. final float[] defaultDecode = pdImage.getColorSpace().getDefaultDecode(8); final float[] decode = getDecodeArray(pdImage); - if (pdImage.getSuffix() != null && pdImage.getSuffix().equals("jpg") && subsampling == 1) + if (pdImage.getSuffix() != null && pdImage.getSuffix().equals("jpg") && subsampling == 1 + && region == null && colorKey == null && Arrays.equals(decode, defaultDecode)) { return BitmapFactory.decodeStream(pdImage.createInputStream()); } @@ -281,7 +286,7 @@ private static Bitmap from1Bit(PDImage pdImage, Rect clipped, final int subsampl // and then simply shift bits out to the left, detecting set bits via sign final boolean nosubsampling = currentSubsampling == 1; final int stride = (inputWidth + 7) / 8; - final int invert = /*colorSpace instanceof PDIndexed TODO: PdfBox-Android ||*/ decode[0] < decode[1] ? 0 : -1; + final int invert = (colorSpace instanceof PDIndexed || decode[0] < decode[1]) ? 0 : -1; final int endX = startx + scanWidth; final byte[] buff = new byte[stride]; for (int y = 0; y < starty + scanHeight; y++) @@ -364,15 +369,16 @@ private static Bitmap from8bit(PDImage pdImage, Rect clipped, final int subsampl scanHeight = clipped.height(); } final int numComponents = pdImage.getColorSpace().getNumberOfComponents(); + final PDColorSpace colorSpace = pdImage.getColorSpace(); if (startx == 0 && starty == 0 && scanWidth == width && scanHeight == height) { // we just need to copy all sample data, then convert to RGB image. - return createBitmapFromRawStream(input, inputWidth, numComponents, currentSubsampling); + return createBitmapFromRawStream(input, inputWidth, numComponents, currentSubsampling, colorSpace); } else { Bitmap origin = createBitmapFromRawStream(input, inputWidth, numComponents, - currentSubsampling); + currentSubsampling, colorSpace); if (currentSubsampling > 1) { startx /= currentSubsampling; @@ -387,18 +393,48 @@ private static Bitmap from8bit(PDImage pdImage, Rect clipped, final int subsampl } } - private static Bitmap createBitmapFromRawStream(InputStream input, int originalWidth, int numComponents, - int sampleSize) throws IOException + static Bitmap createBitmapFromRawStream(InputStream input, int originalWidth, int numComponents, + int sampleSize, PDColorSpace colorSpace) throws IOException { + if (originalWidth <= 0 || numComponents <= 0) + { + throw new IOException("Invalid image dimensions or components: width=" + originalWidth + ", components=" + numComponents); + } + byte[] bytes = IOUtils.toByteArray(input); int originalHeight = bytes.length / numComponents / originalWidth; + if (originalHeight <= 0) + { + throw new IOException("Image stream contains insufficient data: " + bytes.length + " bytes for width=" + originalWidth + ", components=" + numComponents); + } + + if (colorSpace instanceof PDIndexed) + { + PDIndexed indexed = (PDIndexed) colorSpace; + int[] pixels = new int[originalWidth * originalHeight]; + int len = Math.min(bytes.length, pixels.length); + for (int i = 0; i < len; i++) + { + pixels[i] = indexed.getRgbColor(bytes[i] & 0xFF); + } + Bitmap bitmap = Bitmap.createBitmap(originalWidth, originalHeight, Bitmap.Config.ARGB_8888); + bitmap.setPixels(pixels, 0, originalWidth, 0, 0, originalWidth, originalHeight); + if (sampleSize > 1) + { + int width = originalWidth / sampleSize; + int height = originalHeight / sampleSize; + bitmap = Bitmap.createScaledBitmap(bitmap, width, height, true); + } + return bitmap; + } + if (numComponents == 1) { byte[] result = new byte[originalWidth * originalHeight * 4]; for (int i = originalWidth * originalHeight - 1; i >= 0; i--) { int to = i * 4; - result[to + 3] = bytes[i]; + result[to + 3] = (byte) 255; result[to] = bytes[i]; result[to + 1] = bytes[i]; result[to + 2] = bytes[i]; @@ -419,6 +455,31 @@ else if (numComponents == 3) } bytes = result; } + else if (numComponents == 4) + { + byte[] result = new byte[originalWidth * originalHeight * 4]; + for (int i = originalWidth * originalHeight - 1; i >= 0; i--) + { + int to = i * 4; + int from = i * 4; + float c = (bytes[from] & 0xFF) / 255.0f; + float m = (bytes[from + 1] & 0xFF) / 255.0f; + float y = (bytes[from + 2] & 0xFF) / 255.0f; + float k = (bytes[from + 3] & 0xFF) / 255.0f; + int r = Math.round(255 * (1 - c) * (1 - k)); + int g = Math.round(255 * (1 - m) * (1 - k)); + int b = Math.round(255 * (1 - y) * (1 - k)); + result[to] = (byte) r; + result[to + 1] = (byte) g; + result[to + 2] = (byte) b; + result[to + 3] = (byte) 255; + } + bytes = result; + } + else + { + throw new IOException("Unsupported number of components for raw stream conversion: " + numComponents); + } Bitmap bitmap = Bitmap.createBitmap(originalWidth, originalHeight, Bitmap.Config.ARGB_8888); bitmap.copyPixelsFromBuffer(ByteBuffer.wrap(bytes)); if (sampleSize > 1) diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/optionalcontent/PDOptionalContentGroup.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/optionalcontent/PDOptionalContentGroup.java index c15c23e14..f23db849b 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/optionalcontent/PDOptionalContentGroup.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/optionalcontent/PDOptionalContentGroup.java @@ -20,7 +20,6 @@ import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.documentinterchange.markedcontent.PDPropertyList; import com.tom_roush.pdfbox.rendering.RenderDestination; - /** * An optional content group (OCG). */ @@ -43,10 +42,10 @@ public PDOptionalContentGroup(String name) public PDOptionalContentGroup(COSDictionary dict) { super(dict); - if (!dict.getItem(COSName.TYPE).equals(COSName.OCG)) + if (!dict.getDictionaryObject(COSName.TYPE).equals(COSName.OCG)) { throw new IllegalArgumentException( - "Provided dictionary is not of type '" + COSName.OCG + "'"); + "Provided dictionary is not of type '" + COSName.OCG + "'"); } } @@ -57,16 +56,16 @@ public PDOptionalContentGroup(COSDictionary dict) public enum RenderState { /** The "ON" value. */ - ON(COSName.ON), - /** The "OFF" value. */ - OFF(COSName.OFF); + ON(COSName.ON), + /** The "OFF" value. */ + OFF(COSName.OFF); - private final COSName name; + private final COSName name; - private RenderState(COSName value) - { - this.name = value; - } + private RenderState(COSName value) + { + this.name = value; + } /** * Returns the base state represented by the given {@link COSName}. diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/optionalcontent/PDOptionalContentMembershipDictionary.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/optionalcontent/PDOptionalContentMembershipDictionary.java index 4b6bfeb51..f977c781d 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/optionalcontent/PDOptionalContentMembershipDictionary.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/optionalcontent/PDOptionalContentMembershipDictionary.java @@ -19,12 +19,12 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; + import com.tom_roush.pdfbox.cos.COSArray; import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.documentinterchange.markedcontent.PDPropertyList; - /** * An optional content membership dictionary (OCMD). * @@ -32,92 +32,92 @@ */ public class PDOptionalContentMembershipDictionary extends PDPropertyList { - /** - * Creates a new optional content membership dictionary (OCMD). - */ - public PDOptionalContentMembershipDictionary() - { - this.dict.setItem(COSName.TYPE, COSName.OCMD); - } + /** + * Creates a new optional content membership dictionary (OCMD). + */ + public PDOptionalContentMembershipDictionary() + { + this.dict.setItem(COSName.TYPE, COSName.OCMD); + } - /** - * Creates a new instance based on a given {@link COSDictionary}. - * @param dict the dictionary - */ - public PDOptionalContentMembershipDictionary(COSDictionary dict) - { - super(dict); - if (!dict.getItem(COSName.TYPE).equals(COSName.OCMD)) - { - throw new IllegalArgumentException( - "Provided dictionary is not of type '" + COSName.OCMD + "'"); - } - } + /** + * Creates a new instance based on a given {@link COSDictionary}. + * @param dict the dictionary + */ + public PDOptionalContentMembershipDictionary(COSDictionary dict) + { + super(dict); + if (!dict.getDictionaryObject(COSName.TYPE).equals(COSName.OCMD)) + { + throw new IllegalArgumentException( + "Provided dictionary is not of type '" + COSName.OCMD + "'"); + } + } - /** - * Get a list of optional content groups. - * - * @return List of optional content groups, never null. - */ - public List getOCGs() - { - COSBase base = dict.getDictionaryObject(COSName.OCGS); - if (base instanceof COSDictionary) - { - return Collections.singletonList(PDPropertyList.create((COSDictionary) base)); - } + /** + * Get a list of optional content groups. + * + * @return List of optional content groups, never null. + */ + public List getOCGs() + { + COSBase base = dict.getDictionaryObject(COSName.OCGS); + if (base instanceof COSDictionary) + { + return Collections.singletonList(PDPropertyList.create((COSDictionary) base)); + } - if (base instanceof COSArray) - { - COSArray ar = (COSArray) base; - List list = new ArrayList(); - for (int i = 0; i < ar.size(); ++i) - { - COSBase elem = ar.getObject(i); - if (elem instanceof COSDictionary) + if (base instanceof COSArray) + { + COSArray ar = (COSArray) base; + List list = new ArrayList(); + for (int i = 0; i < ar.size(); ++i) { - list.add(PDPropertyList.create((COSDictionary) elem)); + COSBase elem = ar.getObject(i); + if (elem instanceof COSDictionary) + { + list.add(PDPropertyList.create((COSDictionary) elem)); + } } - } - return list; - } + return list; + } - return Collections.emptyList(); - } + return Collections.emptyList(); + } - /** - * Set optional content groups as a list. - * - * @param ocgs list of optional content groups to set. - */ - public void setOCGs(List ocgs) - { - COSArray ar = new COSArray(); - for (PDPropertyList prop : ocgs) - { - ar.add(prop); - } - dict.setItem(COSName.OCGS, ar); - } + /** + * Set optional content groups as a list. + * + * @param ocgs list of optional content groups to set. + */ + public void setOCGs(List ocgs) + { + COSArray ar = new COSArray(); + for (PDPropertyList prop : ocgs) + { + ar.add(prop); + } + dict.setItem(COSName.OCGS, ar); + } - /** - * Get the visibility policy name. Valid names are AllOff, AllOn, AnyOff, AnyOn (default). - * - * @return the visibility policy, never null. - */ - public COSName getVisibilityPolicy() - { - return dict.getCOSName(COSName.P, COSName.ANY_ON); - } + /** + * Get the visibility policy name. Valid names are AllOff, AllOn, AnyOff, AnyOn (default). + * + * @return the visibility policy, never null. + */ + public COSName getVisibilityPolicy() + { + return dict.getCOSName(COSName.P, COSName.ANY_ON); + } - /** - * Sets the visibility policy name. Valid names are AllOff, AllOn, AnyOff, AnyOn (default). - * @param visibilityPolicy - */ - public void setVisibilityPolicy(COSName visibilityPolicy) - { - dict.setItem(COSName.P, visibilityPolicy); - } + /** + * Sets the visibility policy name. Valid names are AllOff, AllOn, AnyOff, AnyOn (default). + * @param visibilityPolicy + */ + public void setVisibilityPolicy(COSName visibilityPolicy) + { + dict.setItem(COSName.P, visibilityPolicy); + } - //TODO support /VE some day + //TODO support /VE some day } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/optionalcontent/PDOptionalContentProperties.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/optionalcontent/PDOptionalContentProperties.java index 5fc0b6e2e..7bfae9ac0 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/optionalcontent/PDOptionalContentProperties.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/optionalcontent/PDOptionalContentProperties.java @@ -25,7 +25,6 @@ import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.cos.COSObject; import com.tom_roush.pdfbox.pdmodel.common.COSObjectable; - /** * This class represents the optional content properties dictionary. * @@ -112,6 +111,11 @@ public COSDictionary getCOSObject() return this.dict; } + /** + * Return the /OCGs array. + * + * @return /OCGs array, never null. + */ private COSArray getOCGs() { COSArray ocgs = this.dict.getCOSArray(COSName.OCGS); @@ -123,6 +127,11 @@ private COSArray getOCGs() return ocgs; } + /** + * Return the /D dictionary. + * + * @return the /D dictionary, never null. + */ private COSDictionary getD() { COSBase base = this.dict.getDictionaryObject(COSName.D); @@ -154,6 +163,10 @@ public PDOptionalContentGroup getGroup(String name) for (COSBase o : ocgs) { COSDictionary ocg = toDictionary(o); + if (ocg == null) + { + continue; + } String groupName = ocg.getString(COSName.NAME); if (groupName.equals(name)) { @@ -173,7 +186,7 @@ public void addGroup(PDOptionalContentGroup ocg) ocgs.add(ocg.getCOSObject()); //By default, add new group to the "Order" entry so it appears in the user interface - COSArray order = (COSArray)getD().getDictionaryObject(COSName.ORDER); + COSArray order = getD().getCOSArray(COSName.ORDER); if (order == null) { order = new COSArray(); @@ -192,19 +205,23 @@ public Collection getOptionalContentGroups() COSArray ocgs = getOCGs(); for (COSBase base : ocgs) { - coll.add(new PDOptionalContentGroup(toDictionary(base))); + COSDictionary dictionary = toDictionary(base); + if (dictionary != null) + { + coll.add(new PDOptionalContentGroup(dictionary)); + } } return coll; } /** * Returns the base state for optional content groups. - * @return the base state + * @return the base state, never null. */ public BaseState getBaseState() { COSDictionary d = getD(); - COSName name = (COSName)d.getItem(COSName.BASE_STATE); + COSName name = d.getCOSName(COSName.BASE_STATE, COSName.ON); return BaseState.valueOf(name); } @@ -235,7 +252,14 @@ public String[] getGroupNames() { COSBase obj = ocgs.get(i); COSDictionary ocg = toDictionary(obj); - groups[i] = ocg.getString(COSName.NAME); + if (ocg == null) + { + groups[i] = ""; + } + else + { + groups[i] = ocg.getString(COSName.NAME); + } } return groups; } @@ -273,6 +297,10 @@ public boolean isGroupEnabled(String groupName) for (COSBase o : ocgs) { COSDictionary ocg = toDictionary(o); + if (ocg == null) + { + continue; + } String name = ocg.getString(COSName.NAME); if (groupName.equals(name) && isGroupEnabled(new PDOptionalContentGroup(ocg))) { @@ -333,14 +361,20 @@ public boolean isGroupEnabled(PDOptionalContentGroup group) private COSDictionary toDictionary(COSBase o) { + COSBase base; if (o instanceof COSObject) { - return (COSDictionary)((COSObject)o).getObject(); + base = ((COSObject) o).getObject(); } else { - return (COSDictionary)o; + base = o; + } + if (base instanceof COSDictionary) + { + return (COSDictionary) base; } + return null; } /** @@ -358,6 +392,10 @@ public boolean setGroupEnabled(String groupName, boolean enable) for (COSBase o : ocgs) { COSDictionary ocg = toDictionary(o); + if (ocg == null) + { + continue; + } String name = ocg.getString(COSName.NAME); if (groupName.equals(name) && setGroupEnabled(new PDOptionalContentGroup(ocg), enable)) { diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDMeshBasedShadingType.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDMeshBasedShadingType.java index 7fc01565a..5330835b4 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDMeshBasedShadingType.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDMeshBasedShadingType.java @@ -22,6 +22,7 @@ import java.io.EOFException; import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -84,9 +85,9 @@ final List collectPatches(AffineTransform xform, Matrix matrix, int contr List list = new ArrayList(); long maxSrcCoord = (long) Math.pow(2, getBitsPerCoordinate()) - 1; long maxSrcColor = (long) Math.pow(2, getBitsPerComponent()) - 1; - COSStream cosStream = (COSStream) dict; + InputStream imageStream = ((COSStream) dict).createInputStream(); - ImageInputStream mciis = new MemoryCacheImageInputStream(cosStream.createInputStream()); + ImageInputStream mciis = new MemoryCacheImageInputStream(imageStream); try { PointF[] implicitEdge = new PointF[4]; @@ -148,6 +149,8 @@ final List collectPatches(AffineTransform xform, Matrix matrix, int contr finally { mciis.close(); + // MemoryCacheImageInputStream doesn't close the wrapped stream + imageStream.close(); } return list; } @@ -201,21 +204,23 @@ protected Patch readPatch(ImageInputStream input, boolean isFree, PointF[] impli try { + int bitsPerCoordinate = getBitsPerCoordinate(); for (int i = pStart; i < controlPoints; i++) { - long x = input.readBits(getBitsPerCoordinate()); - long y = input.readBits(getBitsPerCoordinate()); + long x = input.readBits(bitsPerCoordinate); + long y = input.readBits(bitsPerCoordinate); float px = interpolate(x, maxSrcCoord, rangeX.getMin(), rangeX.getMax()); float py = interpolate(y, maxSrcCoord, rangeY.getMin(), rangeY.getMax()); PointF p = matrix.transformPoint(px, py); xform.transform(p, p); points[i] = p; } + int bitsPerComponent = getBitsPerComponent(); for (int i = cStart; i < 4; i++) { for (int j = 0; j < numberOfColorComponents; j++) { - long c = input.readBits(getBitsPerComponent()); + long c = input.readBits(bitsPerComponent); color[i][j] = interpolate(c, maxSrcColor, colRange[j].getMin(), colRange[j].getMax()); } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShading.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShading.java index 0903f949a..30defe906 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShading.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShading.java @@ -155,7 +155,7 @@ public COSArray getBackground() { if (background == null) { - background = (COSArray) dictionary.getDictionaryObject(COSName.BACKGROUND); + background = dictionary.getCOSArray(COSName.BACKGROUND); } return background; } @@ -171,7 +171,7 @@ public PDRectangle getBBox() { if (bBox == null) { - COSArray array = (COSArray) dictionary.getDictionaryObject(COSName.BBOX); + COSArray array = dictionary.getCOSArray(COSName.BBOX); if (array != null) { bBox = new PDRectangle(array); diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType1.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType1.java index 7b9b3a94b..ab46646f3 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType1.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType1.java @@ -82,7 +82,7 @@ public COSArray getDomain() { if (domain == null) { - domain = (COSArray) getCOSObject().getDictionaryObject(COSName.DOMAIN); + domain = getCOSObject().getCOSArray(COSName.DOMAIN); } return domain; } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType2.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType2.java index d041a0fe0..addc80229 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType2.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType2.java @@ -54,7 +54,7 @@ public COSArray getExtend() { if (extend == null) { - extend = (COSArray) getCOSObject().getDictionaryObject(COSName.EXTEND); + extend = getCOSObject().getCOSArray(COSName.EXTEND); } return extend; } @@ -79,7 +79,7 @@ public COSArray getDomain() { if (domain == null) { - domain = (COSArray) getCOSObject().getDictionaryObject(COSName.DOMAIN); + domain = getCOSObject().getCOSArray(COSName.DOMAIN); } return domain; } @@ -104,7 +104,7 @@ public COSArray getCoords() { if (coords == null) { - coords = (COSArray) getCOSObject().getDictionaryObject(COSName.COORDS); + coords = getCOSObject().getCOSArray(COSName.COORDS); } return coords; } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType4.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType4.java index 5397ba11b..5ad9e47e8 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType4.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType4.java @@ -20,6 +20,7 @@ import android.util.Log; import java.io.EOFException; +import java.io.InputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -109,9 +110,9 @@ List collectTriangles(AffineTransform xform, Matrix matrix) List list = new ArrayList(); long maxSrcCoord = (long) Math.pow(2, getBitsPerCoordinate()) - 1; long maxSrcColor = (long) Math.pow(2, getBitsPerComponent()) - 1; - COSStream stream = (COSStream) dict; + InputStream imageStream = ((COSStream) dict).createInputStream(); - ImageInputStream mciis = new MemoryCacheImageInputStream(stream.createInputStream()); + ImageInputStream mciis = new MemoryCacheImageInputStream(imageStream); try { byte flag = (byte) 0; @@ -164,7 +165,8 @@ List collectTriangles(AffineTransform xform, Matrix matrix) lastIndex = list.size() - 1; if (lastIndex < 0) { - Log.e("PdfBox-Android", "broken data stream: " + list.size()); + Log.e("PdfBox-Android", "broken data stream: " + list.size() + ", aborting"); + eof = true; } else { @@ -182,7 +184,8 @@ List collectTriangles(AffineTransform xform, Matrix matrix) } break; default: - Log.w("PdfBox-Android", "bad flag: " + flag); + Log.w("PdfBox-Android", "bad flag " + flag + ", aborting"); + eof = true; break; } } @@ -195,6 +198,8 @@ List collectTriangles(AffineTransform xform, Matrix matrix) finally { mciis.close(); + // MemoryCacheImageInputStream doesn't close the wrapped stream + imageStream.close(); } return list; } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType5.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType5.java index 8a70fd22c..14adb752c 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType5.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDShadingType5.java @@ -20,6 +20,7 @@ import java.io.EOFException; import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -107,9 +108,9 @@ List collectTriangles(AffineTransform xform, Matrix matrix) thro List vlist = new ArrayList(); long maxSrcCoord = (long) Math.pow(2, getBitsPerCoordinate()) - 1; long maxSrcColor = (long) Math.pow(2, getBitsPerComponent()) - 1; - COSStream cosStream = (COSStream) dict; - ImageInputStream mciis = new MemoryCacheImageInputStream(cosStream.createInputStream()); + InputStream imageStream = ((COSStream) dict).createInputStream(); + ImageInputStream mciis = new MemoryCacheImageInputStream(imageStream); try { boolean eof = false; @@ -130,6 +131,8 @@ List collectTriangles(AffineTransform xform, Matrix matrix) thro finally { mciis.close(); + // MemoryCacheImageInputStream doesn't close the wrapped stream + imageStream.close(); } int rowNum = vlist.size() / numPerRow; if (rowNum < 2) @@ -153,28 +156,33 @@ private List createShadedTriangleList(int rowNum, int numPerRow, { PointF[] ps = new PointF[3]; // array will be shallow-cloned in ShadedTriangle constructor float[][] cs = new float[3][]; - List list = new ArrayList(); + List list = new ArrayList((rowNum - 1) * (numPerRow - 1)); for (int i = 0; i < rowNum - 1; i++) { for (int j = 0; j < numPerRow - 1; j++) { - ps[0] = latticeArray[i][j].point; - ps[1] = latticeArray[i][j + 1].point; - ps[2] = latticeArray[i + 1][j].point; + Vertex vertex1 = latticeArray[i][j]; + Vertex vertex2 = latticeArray[i][j + 1]; + Vertex vertex3 = latticeArray[i + 1][j]; + Vertex vertex4 = latticeArray[i + 1][j + 1]; - cs[0] = latticeArray[i][j].color; - cs[1] = latticeArray[i][j + 1].color; - cs[2] = latticeArray[i + 1][j].color; + ps[0] = vertex1.point; + ps[1] = vertex2.point; + ps[2] = vertex3.point; + + cs[0] = vertex1.color; + cs[1] = vertex2.color; + cs[2] = vertex3.color; list.add(new ShadedTriangle(ps, cs)); - ps[0] = latticeArray[i][j + 1].point; - ps[1] = latticeArray[i + 1][j].point; - ps[2] = latticeArray[i + 1][j + 1].point; + ps[0] = vertex2.point; + ps[1] = vertex3.point; + ps[2] = vertex4.point; - cs[0] = latticeArray[i][j + 1].color; - cs[1] = latticeArray[i + 1][j].color; - cs[2] = latticeArray[i + 1][j + 1].color; + cs[0] = vertex2.color; + cs[1] = vertex3.color; + cs[2] = vertex4.color; list.add(new ShadedTriangle(ps, cs)); } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDTriangleBasedShadingType.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDTriangleBasedShadingType.java index 7878e86d5..71566866b 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDTriangleBasedShadingType.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/PDTriangleBasedShadingType.java @@ -24,6 +24,7 @@ import com.tom_roush.harmony.awt.geom.AffineTransform; import com.tom_roush.harmony.javax.imageio.stream.ImageInputStream; +import com.tom_roush.pdfbox.android.PDFBoxConfig; import com.tom_roush.pdfbox.cos.COSArray; import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; @@ -86,7 +87,10 @@ public int getBitsPerCoordinate() if (bitsPerCoordinate == -1) { bitsPerCoordinate = getCOSObject().getInt(COSName.BITS_PER_COORDINATE, -1); - Log.d("PdfBox-Android", "bitsPerCoordinate: " + (Math.pow(2, bitsPerCoordinate) - 1)); + if (PDFBoxConfig.isDebugEnabled()) + { + Log.d("PdfBox-Android", "bitsPerCoordinate: " + (Math.pow(2, bitsPerCoordinate) - 1)); + } } return bitsPerCoordinate; } @@ -127,7 +131,7 @@ private COSArray getDecodeValues() { if (decode == null) { - decode = (COSArray) getCOSObject().getDictionaryObject(COSName.DECODE); + decode = getCOSObject().getCOSArray(COSName.DECODE); } return decode; } @@ -191,6 +195,10 @@ protected Vertex readVertex(ImageInputStream input, long maxSrcCoord, long maxSr PDRange rangeX, PDRange rangeY, PDRange[] colRangeTab, Matrix matrix, AffineTransform xform) throws IOException { + if (bitsPerCoordinate <= 0 || numberOfColorComponents <= 0 || bitsPerColorComponent <= 0) + { + throw new IOException("nothing to read, check bitsPerCoordinate, numberOfColorComponents and bitsPerColorComponent"); + } float[] colorComponentTab = new float[numberOfColorComponents]; long x = input.readBits(bitsPerCoordinate); long y = input.readBits(bitsPerCoordinate); diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/ShadedTriangle.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/ShadedTriangle.java index b9b549038..7cb1c42a4 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/ShadedTriangle.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/shading/ShadedTriangle.java @@ -66,20 +66,23 @@ class ShadedTriangle if (degree == 2) { - if (overlaps(corner[1], corner[2]) && !overlaps(corner[0], corner[2])) + PointF corner0 = corner[0]; + PointF corner1 = corner[1]; + PointF corner2 = corner[2]; + if (overlaps(corner1, corner2) && !overlaps(corner0, corner2)) { - Point p0 = new Point((int) Math.round(corner[0].x), - (int) Math.round(corner[0].y)); - Point p1 = new Point((int) Math.round(corner[2].x), - (int) Math.round(corner[2].y)); + Point p0 = new Point((int) Math.round(corner0.x), + (int) Math.round(corner0.y)); + Point p1 = new Point((int) Math.round(corner2.x), + (int) Math.round(corner2.y)); line = new Line(p0, p1, color[0], color[2]); } else { - Point p0 = new Point((int) Math.round(corner[1].x), - (int) Math.round(corner[1].y)); - Point p1 = new Point((int) Math.round(corner[2].x), - (int) Math.round(corner[2].y)); + Point p0 = new Point((int) Math.round(corner1.x), + (int) Math.round(corner1.y)); + Point p1 = new Point((int) Math.round(corner2.x), + (int) Math.round(corner2.y)); line = new Line(p0, p1, color[1], color[2]); } } @@ -102,7 +105,7 @@ class ShadedTriangle */ private int calcDeg(PointF[] p) { - Set set = new HashSet(); + Set set = new HashSet(3); for (PointF itp : p) { Point np = new Point((int) Math.round(itp.x * 1000), (int) Math.round(itp.y * 1000)); @@ -234,11 +237,12 @@ private double getArea(PointF a, PointF b, PointF c) public float[] calcColor(PointF p) { int numberOfColorComponents = color[0].length; - float[] pCol = new float[numberOfColorComponents]; + float[] pCol = null; switch (degree) { case 1: + pCol = new float[numberOfColorComponents]; for (int i = 0; i < numberOfColorComponents; i++) { // average @@ -248,8 +252,10 @@ public float[] calcColor(PointF p) case 2: // linear interpolation Point tp = new Point((int) Math.round(p.x), (int) Math.round(p.y)); - return line.calcColor(tp); + pCol = line.calcColor(tp); + break; default: + pCol = new float[numberOfColorComponents]; float aw = (float) (getArea(p, corner[1], corner[2]) / area); float bw = (float) (getArea(p, corner[2], corner[0]) / area); float cw = (float) (getArea(p, corner[0], corner[1]) / area); diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/state/PDExtendedGraphicsState.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/state/PDExtendedGraphicsState.java index c9542645c..44b714436 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/state/PDExtendedGraphicsState.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/state/PDExtendedGraphicsState.java @@ -26,11 +26,11 @@ import com.tom_roush.pdfbox.cos.COSFloat; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.cos.COSNumber; +import com.tom_roush.pdfbox.pdmodel.ResourceCache; import com.tom_roush.pdfbox.pdmodel.common.COSObjectable; import com.tom_roush.pdfbox.pdmodel.graphics.PDFontSetting; import com.tom_roush.pdfbox.pdmodel.graphics.PDLineDashPattern; import com.tom_roush.pdfbox.pdmodel.graphics.blend.BlendMode; - /** * An extended graphics state dictionary. * @@ -39,6 +39,7 @@ public class PDExtendedGraphicsState implements COSObjectable { private final COSDictionary dict; + private final ResourceCache cache; /** * Default constructor, creates blank graphics state. @@ -47,6 +48,7 @@ public PDExtendedGraphicsState() { dict = new COSDictionary(); dict.setItem(COSName.TYPE, COSName.EXT_G_STATE); + cache = null; } /** @@ -55,8 +57,20 @@ public PDExtendedGraphicsState() * @param dictionary The existing graphics state. */ public PDExtendedGraphicsState(COSDictionary dictionary) + { + this(dictionary, null); + } + + /** + * Create a graphics state from an existing dictionary. + * + * @param dictionary The existing graphics state. + * @param resourceCache Resource cache, may be null. + */ + public PDExtendedGraphicsState(COSDictionary dictionary, ResourceCache resourceCache) { dict = dictionary; + cache = resourceCache; } /** @@ -162,7 +176,7 @@ else if (key.equals(COSName.TR)) { if (dict.containsKey(COSName.TR2)) { - // "If both TR and TR2 are present in the same graphics state parameter dictionary, + // "If both TR and TR2 are present in the same graphics state parameter dictionary, // TR2 shall take precedence." continue; } @@ -411,7 +425,7 @@ public Float getOverprintMode() return getFloatItem(COSName.OPM); } - /** + /** * This will set the overprint mode(OPM). * * @param overprintMode The overprint mode. It will be truncated to an integer. This parameter @@ -607,16 +621,13 @@ public void setBlendMode(BlendMode bm) */ public PDSoftMask getSoftMask() { - if (!dict.containsKey(COSName.SMASK)) - { - return null; - } - return PDSoftMask.create(dict.getDictionaryObject(COSName.SMASK)); + COSBase smask = dict.getDictionaryObject(COSName.SMASK); + return smask == null ? null : PDSoftMask.create(smask, cache); } /** - /** + /** * This will get the text knockout flag. * * @return The text knockout flag. diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/state/PDGraphicsState.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/state/PDGraphicsState.java index 969d949d8..be905a0a6 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/state/PDGraphicsState.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/state/PDGraphicsState.java @@ -21,9 +21,7 @@ import android.graphics.Region; import java.util.ArrayList; -import java.util.IdentityHashMap; import java.util.List; -import java.util.Map; import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.pdmodel.common.PDRectangle; @@ -44,7 +42,9 @@ public class PDGraphicsState implements Cloneable { private boolean isClippingPathDirty; private List clippingPaths = new ArrayList(1); - private Map clippingCache = new IdentityHashMap(); + private Region clippingPathCache = null; + private Matrix textMatrix = null; + private Matrix textLineMatrix = null; private Matrix currentTransformationMatrix = new Matrix(); private PDColor strokingColor = PDDeviceGray.INSTANCE.getInitialColor(); private PDColor nonStrokingColor = PDDeviceGray.INSTANCE.getInitialColor(); @@ -503,7 +503,9 @@ public PDGraphicsState clone() clone.nonStrokingColor = nonStrokingColor; // immutable clone.lineDashPattern = lineDashPattern; // immutable clone.clippingPaths = clippingPaths; // not cloned, see intersectClippingPath - clone.clippingCache = clippingCache; + clone.clippingPathCache = clippingPathCache; + clone.textLineMatrix = textLineMatrix == null ? null : textLineMatrix.clone(); + clone.textMatrix = textMatrix == null ? null : textMatrix.clone(); clone.isClippingPathDirty = false; return clone; } @@ -616,6 +618,8 @@ private void intersectClippingPath(Path path, boolean clonePath) // add path to current clipping paths, combined later (see getCurrentClippingPath) clippingPaths.add(clonePath ? new Path(path) : path); + // clear cache + clippingPathCache = null; } /** @@ -635,17 +639,14 @@ public void intersectClippingPath(Region area) */ public Region getCurrentClippingPath() { + // If there is just a single clipping path, no intersections are needed. if (clippingPaths.size() == 1) { - // If there is just a single clipping path, no intersections are needed. - Path path = clippingPaths.get(0); - Region area = clippingCache.get(path); - if (area == null) + if (clippingPathCache == null) { - area = GraphicsUtil.getPathRegion(path); - clippingCache.put(path, area); + clippingPathCache = GraphicsUtil.getPathRegion(clippingPaths.get(0)); } - return area; + return clippingPathCache; } // If there are multiple clipping paths, combine them to a single area. Path clippingPath = new Path(clippingPaths.get(0)); @@ -654,10 +655,10 @@ public Region getCurrentClippingPath() clippingPath.op(clippingPaths.get(i), Path.Op.INTERSECT); } Region clippingRegion = GraphicsUtil.getPathRegion(clippingPath); - // Replace the list of individual clipping paths with the intersection, and add it to the cache. + clippingPathCache = clippingRegion; + // Replace the list of individual clipping paths with the intersection clippingPaths = new ArrayList(1); clippingPaths.add(clippingPath); - clippingCache.put(clippingPath, clippingRegion); return clippingRegion; } @@ -703,4 +704,44 @@ public void setTransfer(COSBase transfer) { this.transfer = transfer; } + /** + * Returns the text line matrix. + * + * @return The text line matrix. + */ + public Matrix getTextLineMatrix() + { + return textLineMatrix; + } + + /** + * Sets the text line matrix. + * + * @param value The text line matrix. + */ + public void setTextLineMatrix(Matrix value) + { + textLineMatrix = value; + } + + /** + * Returns the text matrix. + * + * @return The text matrix. + */ + public Matrix getTextMatrix() + { + return textMatrix; + } + + /** + * Sets the text matrix. + * + * @param value The text matrix. + */ + public void setTextMatrix(Matrix value) + { + textMatrix = value; + } + } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/state/PDSoftMask.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/state/PDSoftMask.java index 85c6f9196..15676fadd 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/state/PDSoftMask.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/graphics/state/PDSoftMask.java @@ -24,12 +24,13 @@ import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; +import com.tom_roush.pdfbox.pdmodel.PDResources; +import com.tom_roush.pdfbox.pdmodel.ResourceCache; import com.tom_roush.pdfbox.pdmodel.common.COSObjectable; import com.tom_roush.pdfbox.pdmodel.common.function.PDFunction; import com.tom_roush.pdfbox.pdmodel.graphics.PDXObject; import com.tom_roush.pdfbox.pdmodel.graphics.form.PDTransparencyGroup; import com.tom_roush.pdfbox.util.Matrix; - /** * Soft mask. * @@ -43,6 +44,19 @@ public final class PDSoftMask implements COSObjectable * @param dictionary SMask */ public static PDSoftMask create(COSBase dictionary) + { + return create(dictionary, null); + } + + /** + * Creates a new soft mask. + * + * @param dictionary SMask + * @param resourceCache Resource cache, may be null. + * + * @return the newly created instance of PDSoftMask + */ + public static PDSoftMask create(COSBase dictionary, ResourceCache resourceCache) { if (dictionary instanceof COSName) { @@ -58,7 +72,7 @@ public static PDSoftMask create(COSBase dictionary) } else if (dictionary instanceof COSDictionary) { - return new PDSoftMask((COSDictionary) dictionary); + return new PDSoftMask((COSDictionary) dictionary, resourceCache); } else { @@ -67,7 +81,9 @@ else if (dictionary instanceof COSDictionary) } } + private final COSDictionary dictionary; + private final ResourceCache resourceCache; private COSName subType = null; private PDTransparencyGroup group = null; private COSArray backdropColor = null; @@ -84,8 +100,20 @@ else if (dictionary instanceof COSDictionary) * @param dictionary The soft mask dictionary. */ public PDSoftMask(COSDictionary dictionary) + { + this(dictionary, null); + } + + /** + * Creates a new soft mask. + * + * @param dictionary The soft mask dictionary. + * @param resourceCache Resource cache, may be null. + */ + public PDSoftMask(COSDictionary dictionary, ResourceCache resourceCache) { this.dictionary = dictionary; + this.resourceCache = resourceCache; } @Override @@ -101,7 +129,7 @@ public COSName getSubType() { if (subType == null) { - subType = (COSName) getCOSObject().getDictionaryObject(COSName.S); + subType = getCOSObject().getCOSName(COSName.S); } return subType; } @@ -119,7 +147,8 @@ public PDTransparencyGroup getGroup() throws IOException COSBase cosGroup = getCOSObject().getDictionaryObject(COSName.G); if (cosGroup != null) { - PDXObject x = PDXObject.createXObject(cosGroup, null); + PDResources resources = new PDResources(new COSDictionary(), resourceCache); + PDXObject x = PDXObject.createXObject(cosGroup, resources); if (x instanceof PDTransparencyGroup) { group = (PDTransparencyGroup) x; @@ -136,7 +165,7 @@ public COSArray getBackdropColor() { if (backdropColor == null) { - backdropColor = (COSArray) getCOSObject().getDictionaryObject(COSName.BC); + backdropColor = getCOSObject().getCOSArray(COSName.BC); } return backdropColor; } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/action/PDActionEmbeddedGoTo.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/action/PDActionEmbeddedGoTo.java index 36e333995..2ec8f4ff9 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/action/PDActionEmbeddedGoTo.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/action/PDActionEmbeddedGoTo.java @@ -27,7 +27,6 @@ import com.tom_roush.pdfbox.pdmodel.common.filespecification.PDFileSpecification; import com.tom_roush.pdfbox.pdmodel.interactive.documentnavigation.destination.PDDestination; import com.tom_roush.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageDestination; - /** * This represents a embedded go-to action that can be executed in a PDF document. * @@ -91,7 +90,7 @@ public void setDestination(PDDestination d) if (!(page instanceof COSInteger)) { throw new IllegalArgumentException( - "Destination of a GoToE action must be an integer"); + "Destination of a GoToE action must be an integer"); } } } @@ -124,13 +123,14 @@ public void setFile(PDFileSpecification fs) * This will specify whether to open the destination document in a new window, in the same * window, or behave in accordance with the current user preference. * - * @return A flag specifying how to open the destination document. + * @return A flag specifying how to open the destination document, never null. */ public OpenMode getOpenInNewWindow() { - if (getCOSObject().getDictionaryObject(COSName.NEW_WINDOW) instanceof COSBoolean) + COSBase dictionaryObject = getCOSObject().getDictionaryObject(COSName.NEW_WINDOW); + if (dictionaryObject instanceof COSBoolean) { - COSBoolean b = (COSBoolean) getCOSObject().getDictionaryObject(COSName.NEW_WINDOW); + COSBoolean b = (COSBoolean) dictionaryObject; return b.getValue() ? OpenMode.NEW_WINDOW : OpenMode.SAME_WINDOW; } return OpenMode.USER_PREFERENCE; diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/action/PDActionLaunch.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/action/PDActionLaunch.java index 745133cc2..dcdacc11e 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/action/PDActionLaunch.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/action/PDActionLaunch.java @@ -18,12 +18,11 @@ import java.io.IOException; +import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSBoolean; import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; - import com.tom_roush.pdfbox.pdmodel.common.filespecification.PDFileSpecification; - /** * This represents a launch action that can be executed in a PDF document. * @@ -246,9 +245,10 @@ public void setOpenInNewWindow( boolean value ) */ public OpenMode getOpenInNewWindow() { - if (getCOSObject().getDictionaryObject(COSName.NEW_WINDOW) instanceof COSBoolean) + COSBase dictionaryObject = getCOSObject().getDictionaryObject(COSName.NEW_WINDOW); + if (dictionaryObject instanceof COSBoolean) { - COSBoolean b = (COSBoolean) getCOSObject().getDictionaryObject(COSName.NEW_WINDOW); + COSBoolean b = (COSBoolean) dictionaryObject; return b.getValue() ? OpenMode.NEW_WINDOW : OpenMode.SAME_WINDOW; } return OpenMode.USER_PREFERENCE; diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/action/PDActionRemoteGoTo.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/action/PDActionRemoteGoTo.java index 8bfc86ec5..9e2e757ed 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/action/PDActionRemoteGoTo.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/action/PDActionRemoteGoTo.java @@ -23,7 +23,6 @@ import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.common.filespecification.PDFileSpecification; - /** * This represents a remote go-to action that can be executed in a PDF document. * @@ -65,7 +64,7 @@ public PDActionRemoteGoTo( COSDictionary a ) @Deprecated public String getS() { - return action.getNameAsString( COSName.S ); + return action.getNameAsString( COSName.S ); } /** @@ -78,7 +77,7 @@ public String getS() @Deprecated public void setS( String s ) { - action.setName( COSName.S, s ); + action.setName( COSName.S, s ); } /** @@ -172,9 +171,10 @@ public void setOpenInNewWindow( boolean value ) */ public OpenMode getOpenInNewWindow() { - if (getCOSObject().getDictionaryObject(COSName.NEW_WINDOW) instanceof COSBoolean) + COSBase dictionaryObject = getCOSObject().getDictionaryObject(COSName.NEW_WINDOW); + if (dictionaryObject instanceof COSBoolean) { - COSBoolean b = (COSBoolean) getCOSObject().getDictionaryObject(COSName.NEW_WINDOW); + COSBoolean b = (COSBoolean) dictionaryObject; return b.getValue() ? OpenMode.NEW_WINDOW : OpenMode.SAME_WINDOW; } return OpenMode.USER_PREFERENCE; diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/action/PDActionSound.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/action/PDActionSound.java index f8ff905d5..17133b0d3 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/action/PDActionSound.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/action/PDActionSound.java @@ -23,7 +23,6 @@ import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.cos.COSNumber; import com.tom_roush.pdfbox.cos.COSStream; - /** * This represents a Sound action that can be executed in a PDF document * @@ -155,7 +154,7 @@ public void setSynchronous(boolean synchronous) } /** - * Gets the synchronous flag. It specifyes whether to play the sound synchronously or + * Gets the synchronous flag. It specifies whether to play the sound synchronously or * asynchronously. When true, the reader allows no further user interaction other than canceling * the sound until the sound has been completely played. * diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/action/PDAdditionalActions.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/action/PDAdditionalActions.java index da73fb822..a49a68f64 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/action/PDAdditionalActions.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/action/PDAdditionalActions.java @@ -19,7 +19,6 @@ import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.common.COSObjectable; - /** * This represents a dictionary of actions that occur due to events. * @@ -65,7 +64,7 @@ public COSDictionary getCOSObject() */ public PDAction getF() { - return PDActionFactory.createAction( (COSDictionary)actions.getDictionaryObject(COSName.F ) ); + return PDActionFactory.createAction(actions.getCOSDictionary(COSName.F)); } /** diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/action/PDAnnotationAdditionalActions.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/action/PDAnnotationAdditionalActions.java index 300bd19d6..3b5a6f9c4 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/action/PDAnnotationAdditionalActions.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/action/PDAnnotationAdditionalActions.java @@ -19,7 +19,6 @@ import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.common.COSObjectable; - /** * This class represents an annotation's dictionary of actions * that occur due to events. @@ -125,7 +124,7 @@ public void setX( PDAction x ) */ public PDAction getD() { - COSDictionary d = (COSDictionary)actions.getDictionaryObject(COSName.D); + COSDictionary d = actions.getCOSDictionary(COSName.D ); PDAction retval = null; if( d != null ) { @@ -143,7 +142,7 @@ public PDAction getD() */ public void setD( PDAction d ) { - actions.setItem(COSName.D, d); + actions.setItem( COSName.D, d ); } /** diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/action/PDFormFieldAdditionalActions.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/action/PDFormFieldAdditionalActions.java index d3258bfcc..9144565e1 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/action/PDFormFieldAdditionalActions.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/action/PDFormFieldAdditionalActions.java @@ -19,7 +19,6 @@ import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.common.COSObjectable; - /** * This class represents a form field's dictionary of actions * that occur due to events. @@ -70,7 +69,7 @@ public COSDictionary getCOSObject() */ public PDAction getK() { - COSDictionary k = (COSDictionary) actions.getDictionaryObject(COSName.K); + COSDictionary k = actions.getCOSDictionary(COSName.K); PDAction retval = null; if( k != null ) { @@ -101,7 +100,7 @@ public void setK( PDAction k ) */ public PDAction getF() { - COSDictionary f = (COSDictionary) actions.getDictionaryObject(COSName.F); + COSDictionary f = actions.getCOSDictionary(COSName.F); PDAction retval = null; if( f != null ) { @@ -132,7 +131,7 @@ public void setF( PDAction f ) */ public PDAction getV() { - COSDictionary v = (COSDictionary) actions.getDictionaryObject(COSName.V); + COSDictionary v = actions.getCOSDictionary(COSName.V); PDAction retval = null; if( v != null ) { @@ -165,7 +164,7 @@ public void setV( PDAction v ) */ public PDAction getC() { - COSDictionary c = (COSDictionary) actions.getDictionaryObject(COSName.C); + COSDictionary c = actions.getCOSDictionary(COSName.C); PDAction retval = null; if( c != null ) { diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/action/PDPageAdditionalActions.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/action/PDPageAdditionalActions.java index c99209d36..6043d5788 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/action/PDPageAdditionalActions.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/action/PDPageAdditionalActions.java @@ -19,7 +19,6 @@ import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.common.COSObjectable; - /** * This class represents a page object's dictionary of actions * that occur due to events. @@ -70,7 +69,7 @@ public COSDictionary getCOSObject() */ public PDAction getO() { - COSDictionary o = (COSDictionary) actions.getDictionaryObject(COSName.O); + COSDictionary o = actions.getCOSDictionary(COSName.O); PDAction retval = null; if( o != null ) { diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/PDAnnotation.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/PDAnnotation.java index 486cf4082..5f103be28 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/PDAnnotation.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/PDAnnotation.java @@ -16,6 +16,7 @@ */ package com.tom_roush.pdfbox.pdmodel.interactive.annotation; +import android.graphics.Rect; import android.util.Log; import java.io.IOException; @@ -37,7 +38,6 @@ import com.tom_roush.pdfbox.pdmodel.graphics.color.PDDeviceCMYK; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDDeviceGray; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDDeviceRGB; - /** * A PDF annotation. * @@ -45,6 +45,10 @@ */ public abstract class PDAnnotation implements COSObjectable { + /** + * Log instance. + */ + /** * An annotation flag. */ @@ -93,7 +97,7 @@ public abstract class PDAnnotation implements COSObjectable * Create the correct annotation from the base COS object. * * @param base The COS object that is the annotation. - * @return The correctly typed annotation object. + * @return The correctly typed annotation object, never null. * * @throws IOException If the annotation type is unknown. */ @@ -125,7 +129,7 @@ else if (PDAnnotationRubberStamp.SUB_TYPE.equals(subtype)) annot = new PDAnnotationRubberStamp(annotDic); } else if (PDAnnotationSquareCircle.SUB_TYPE_SQUARE.equals(subtype) - || PDAnnotationSquareCircle.SUB_TYPE_CIRCLE.equals(subtype)) + || PDAnnotationSquareCircle.SUB_TYPE_CIRCLE.equals(subtype)) { annot = new PDAnnotationSquareCircle(annotDic); } @@ -134,9 +138,9 @@ else if (PDAnnotationText.SUB_TYPE.equals(subtype)) annot = new PDAnnotationText(annotDic); } else if (PDAnnotationTextMarkup.SUB_TYPE_HIGHLIGHT.equals(subtype) - || PDAnnotationTextMarkup.SUB_TYPE_UNDERLINE.equals(subtype) - || PDAnnotationTextMarkup.SUB_TYPE_SQUIGGLY.equals(subtype) - || PDAnnotationTextMarkup.SUB_TYPE_STRIKEOUT.equals(subtype)) + || PDAnnotationTextMarkup.SUB_TYPE_UNDERLINE.equals(subtype) + || PDAnnotationTextMarkup.SUB_TYPE_SQUIGGLY.equals(subtype) + || PDAnnotationTextMarkup.SUB_TYPE_STRIKEOUT.equals(subtype)) { // see 12.5.6.10 Text Markup Annotations annot = new PDAnnotationTextMarkup(annotDic); @@ -146,11 +150,11 @@ else if (PDAnnotationWidget.SUB_TYPE.equals(subtype)) annot = new PDAnnotationWidget(annotDic); } else if (PDAnnotationMarkup.SUB_TYPE_FREETEXT.equals(subtype) - || PDAnnotationMarkup.SUB_TYPE_POLYGON.equals(subtype) - || PDAnnotationMarkup.SUB_TYPE_POLYLINE.equals(subtype) - || PDAnnotationMarkup.SUB_TYPE_CARET.equals(subtype) - || PDAnnotationMarkup.SUB_TYPE_INK.equals(subtype) - || PDAnnotationMarkup.SUB_TYPE_SOUND.equals(subtype)) + || PDAnnotationMarkup.SUB_TYPE_POLYGON.equals(subtype) + || PDAnnotationMarkup.SUB_TYPE_POLYLINE.equals(subtype) + || PDAnnotationMarkup.SUB_TYPE_CARET.equals(subtype) + || PDAnnotationMarkup.SUB_TYPE_INK.equals(subtype) + || PDAnnotationMarkup.SUB_TYPE_SOUND.equals(subtype)) { annot = new PDAnnotationMarkup(annotDic); } @@ -236,14 +240,14 @@ public int hashCode() */ public PDRectangle getRectangle() { - COSArray rectArray = (COSArray) dictionary.getDictionaryObject(COSName.RECT); + COSArray rectArray = dictionary.getCOSArray(COSName.RECT); PDRectangle rectangle = null; if (rectArray != null) { if (rectArray.size() == 4 && rectArray.getObject(0) instanceof COSNumber - && rectArray.getObject(1) instanceof COSNumber - && rectArray.getObject(2) instanceof COSNumber - && rectArray.getObject(3) instanceof COSNumber) + && rectArray.getObject(1) instanceof COSNumber + && rectArray.getObject(2) instanceof COSNumber + && rectArray.getObject(3) instanceof COSNumber) { rectangle = new PDRectangle(rectArray); } @@ -315,6 +319,20 @@ public void setAppearanceState(String as) getCOSObject().setName(COSName.AS, as); } + /** + * This will set the annotations appearance state name. + * + *

      Note that the PDF specification defines the AS entry as a name, but some PDFs use a string. + * This method will write a name, which is correct and should be preferred but may cause issues + * with some viewers if the PDF being edited already uses a string.

      + * + * @param as The COSName of the appearance stream. + */ + public void setAppearanceState(COSName as) + { + getCOSObject().setItem(COSName.AS, as); + } + /** * This will get the appearance dictionary associated with this annotation. This may return null. * @@ -790,17 +808,17 @@ protected PDColor getColor(COSName itemName) PDColorSpace colorSpace = null; switch (((COSArray) c).size()) { - case 1: - colorSpace = PDDeviceGray.INSTANCE; - break; - case 3: - colorSpace = PDDeviceRGB.INSTANCE; - break; - case 4: - colorSpace = PDDeviceCMYK.INSTANCE; - break; - default: - break; + case 1: + colorSpace = PDDeviceGray.INSTANCE; + break; + case 3: + colorSpace = PDDeviceRGB.INSTANCE; + break; + case 4: + colorSpace = PDDeviceCMYK.INSTANCE; + break; + default: + break; } return new PDColor((COSArray) c, colorSpace); } @@ -818,7 +836,9 @@ public String getSubtype() } /** - * This will set the corresponding page for this annotation. + * This will set the corresponding page for this annotation. This is optional but recommended. + * Not doing it can cause trouble when + * PDFs get signed. * * @param page is the corresponding page */ diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/PDAnnotationLine.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/PDAnnotationLine.java index ad680c65f..28125e061 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/PDAnnotationLine.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/PDAnnotationLine.java @@ -25,7 +25,6 @@ import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColor; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.handlers.PDAppearanceHandler; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.handlers.PDLineAppearanceHandler; - /** * This is the class that represents a line annotation. Introduced in PDF 1.3 specification * @@ -147,7 +146,7 @@ public void setLine(float[] l) */ public float[] getLine() { - COSArray l = (COSArray) getCOSObject().getDictionaryObject(COSName.L); + COSArray l = getCOSObject().getCOSArray(COSName.L); return l.toFloatArray(); } @@ -391,7 +390,7 @@ public void setCaptionPositioning(String captionPositioning) */ public void setCaptionHorizontalOffset(float offset) { - COSArray array = (COSArray) this.getCOSObject().getDictionaryObject(COSName.CO); + COSArray array = this.getCOSObject().getCOSArray(COSName.CO); if (array == null) { array = new COSArray(); @@ -412,7 +411,7 @@ public void setCaptionHorizontalOffset(float offset) public float getCaptionHorizontalOffset() { float retval = 0.f; - COSArray array = (COSArray) this.getCOSObject().getDictionaryObject(COSName.CO); + COSArray array = this.getCOSObject().getCOSArray(COSName.CO); if (array != null) { retval = array.toFloatArray()[0]; @@ -428,7 +427,7 @@ public float getCaptionHorizontalOffset() */ public void setCaptionVerticalOffset(float offset) { - COSArray array = (COSArray) this.getCOSObject().getDictionaryObject(COSName.CO); + COSArray array = this.getCOSObject().getCOSArray(COSName.CO); if (array == null) { array = new COSArray(); @@ -449,7 +448,7 @@ public void setCaptionVerticalOffset(float offset) public float getCaptionVerticalOffset() { float retval = 0.f; - COSArray array = (COSArray) this.getCOSObject().getDictionaryObject(COSName.CO); + COSArray array = this.getCOSObject().getCOSArray(COSName.CO); if (array != null) { retval = array.toFloatArray()[1]; diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/PDAnnotationMarkup.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/PDAnnotationMarkup.java index b2a92ee30..5845f46d8 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/PDAnnotationMarkup.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/PDAnnotationMarkup.java @@ -16,9 +16,12 @@ */ package com.tom_roush.pdfbox.pdmodel.interactive.annotation; +import android.graphics.Rect; + import java.io.IOException; import java.util.Calendar; +// needed for the javadoc generation import com.tom_roush.pdfbox.cos.COSArray; import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSDictionary; @@ -37,10 +40,7 @@ import com.tom_roush.pdfbox.pdmodel.interactive.annotation.handlers.PDPolygonAppearanceHandler; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.handlers.PDPolylineAppearanceHandler; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.handlers.PDSoundAppearanceHandler; - -// needed for the javadoc generation import com.tom_roush.pdfbox.pdmodel.interactive.form.PDVariableText; - /** * This class represents the additional fields of a Markup type Annotation. See section 12.5.6 of ISO32000-1:2008 * (starting with page 390) for details on annotation types. @@ -156,7 +156,7 @@ public void setTitlePopup(String t) */ public PDAnnotationPopup getPopup() { - COSDictionary popup = (COSDictionary) getCOSObject().getDictionaryObject("Popup"); + COSDictionary popup = getCOSObject().getCOSDictionary(COSName.POPUP); if (popup != null) { return new PDAnnotationPopup(popup); @@ -174,11 +174,11 @@ public PDAnnotationPopup getPopup() */ public void setPopup(PDAnnotationPopup popup) { - getCOSObject().setItem("Popup", popup); + getCOSObject().setItem(COSName.POPUP, popup); } /** - * This will retrieve the constant opacity value used when rendering the annotation (excluing any popup). + * This will retrieve the constant opacity value used when rendering the annotation (excluding any popup). * * @return the constant opacity value. */ @@ -188,7 +188,7 @@ public float getConstantOpacity() } /** - * This will set the constant opacity value used when rendering the annotation (excluing any popup). + * This will set the constant opacity value used when rendering the annotation (excluding any popup). * * @param ca the constant opacity value. */ @@ -457,7 +457,7 @@ public void setBorderEffect(PDBorderEffectDictionary be) */ public PDBorderEffectDictionary getBorderEffect() { - COSDictionary be = (COSDictionary) getCOSObject().getDictionaryObject(COSName.BE); + COSDictionary be = getCOSObject().getCOSDictionary(COSName.BE); if (be != null) { return new PDBorderEffectDictionary(be); @@ -829,6 +829,7 @@ public float[][] getPath() { COSArray array = (COSArray) base; float[][] pathArray = new float[array.size()][]; + float[] emptyArray = new float[0]; for (int i = 0; i < array.size(); ++i) { COSBase base2 = array.getObject(i); @@ -838,7 +839,7 @@ public float[][] getPath() } else { - pathArray[i] = new float[0]; + pathArray[i] = emptyArray; } } return pathArray; diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/PDAnnotationPopup.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/PDAnnotationPopup.java index c3d42bcdf..2717d14e5 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/PDAnnotationPopup.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/PDAnnotationPopup.java @@ -20,7 +20,6 @@ import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; - /** * This is the class that represents a popup annotation. Introduced in PDF 1.3 specification * @@ -89,17 +88,21 @@ public void setParent(PDAnnotationMarkup annot) */ public PDAnnotationMarkup getParent() { - PDAnnotationMarkup am = null; try { - am = (PDAnnotationMarkup) PDAnnotation.createAnnotation(getCOSObject() - .getDictionaryObject(COSName.PARENT, COSName.P)); + PDAnnotation ann = PDAnnotation.createAnnotation(getCOSObject() + .getDictionaryObject(COSName.PARENT, COSName.P)); + if (!(ann instanceof PDAnnotationMarkup)) + { + return null; + } + return (PDAnnotationMarkup) ann; } catch (IOException ioe) { // Couldn't construct the annotation, so return null i.e. do nothing + return null; } - return am; } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/PDAnnotationSquareCircle.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/PDAnnotationSquareCircle.java index accfab4cf..f0c79b09a 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/PDAnnotationSquareCircle.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/PDAnnotationSquareCircle.java @@ -16,6 +16,8 @@ */ package com.tom_roush.pdfbox.pdmodel.interactive.annotation; +import android.graphics.Rect; + import com.tom_roush.pdfbox.cos.COSArray; import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSDictionary; @@ -27,7 +29,6 @@ import com.tom_roush.pdfbox.pdmodel.interactive.annotation.handlers.PDAppearanceHandler; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.handlers.PDCircleAppearanceHandler; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.handlers.PDSquareAppearanceHandler; - /** * This is the class that represents a rectangular or eliptical annotation Introduced in PDF 1.3 specification . * @@ -106,7 +107,7 @@ public void setBorderEffect(PDBorderEffectDictionary be) */ public PDBorderEffectDictionary getBorderEffect() { - COSDictionary be = (COSDictionary) getCOSObject().getDictionaryObject(COSName.BE); + COSDictionary be = getCOSObject().getCOSDictionary(COSName.BE); if (be != null) { return new PDBorderEffectDictionary(be); @@ -137,7 +138,7 @@ public void setRectDifference(PDRectangle rd) */ public PDRectangle getRectDifference() { - COSArray rd = (COSArray) getCOSObject().getDictionaryObject(COSName.RD); + COSArray rd = getCOSObject().getCOSArray(COSName.RD); if (rd != null) { return new PDRectangle(rd); @@ -248,7 +249,7 @@ public float[] getRectDifferences() return new float[]{}; } - /** + /** * Set a custom appearance handler for generating the annotations appearance streams. * * @param appearanceHandler diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/PDBorderStyleDictionary.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/PDBorderStyleDictionary.java index 6e7734d38..14e7057e9 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/PDBorderStyleDictionary.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/PDBorderStyleDictionary.java @@ -16,14 +16,12 @@ */ package com.tom_roush.pdfbox.pdmodel.interactive.annotation; -import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSArray; +import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSInteger; import com.tom_roush.pdfbox.cos.COSName; - import com.tom_roush.pdfbox.pdmodel.common.COSObjectable; import com.tom_roush.pdfbox.pdmodel.graphics.PDLineDashPattern; - /** * This class represents a PDF /BS entry the border style dictionary. * @@ -145,7 +143,7 @@ public void setStyle(String s) /** * This will retrieve the border style, see the STYLE_* constants for valid values. * - * @return the style of the border + * @return the style of the border, never null. */ public String getStyle() { @@ -169,7 +167,7 @@ public void setDashStyle(COSArray dashArray) */ public PDLineDashPattern getDashStyle() { - COSArray d = (COSArray) getCOSObject().getDictionaryObject(COSName.D); + COSArray d = getCOSObject().getCOSArray(COSName.D); if (d == null) { d = new COSArray(); diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/AnnotationBorder.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/AnnotationBorder.java index 6451befd6..11fa86a03 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/AnnotationBorder.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/AnnotationBorder.java @@ -21,7 +21,6 @@ import com.tom_roush.pdfbox.cos.COSNumber; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotation; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDBorderStyleDictionary; - /** * Class to collect all sort of border info about annotations. * @@ -29,60 +28,65 @@ */ class AnnotationBorder { - float[] dashArray = null; - boolean underline = false; - float width = 0; + float[] dashArray = null; + boolean underline = false; + float width = 0; - // return border info. BorderStyle must be provided as parameter because - // method is not available in the base class - static AnnotationBorder getAnnotationBorder(PDAnnotation annotation, - PDBorderStyleDictionary borderStyle) - { - AnnotationBorder ab = new AnnotationBorder(); - if (borderStyle == null) - { - COSArray border = annotation.getBorder(); - if (border.size() >= 3 && border.getObject(2) instanceof COSNumber) - { - ab.width = ((COSNumber) border.getObject(2)).floatValue(); - } - if (border.size() > 3) - { - COSBase base3 = border.getObject(3); - if (base3 instanceof COSArray) + // return border info. BorderStyle must be provided as parameter because + // method is not available in the base class + static AnnotationBorder getAnnotationBorder(PDAnnotation annotation, + PDBorderStyleDictionary borderStyle) + { + AnnotationBorder ab = new AnnotationBorder(); + if (borderStyle == null) + { + COSArray border = annotation.getBorder(); + if (border.size() >= 3) + { + COSBase base = border.getObject(2); + if (base instanceof COSNumber) + { + ab.width = ((COSNumber) base).floatValue(); + } + } + if (border.size() > 3) + { + COSBase base3 = border.getObject(3); + if (base3 instanceof COSArray) + { + ab.dashArray = ((COSArray) base3).toFloatArray(); + } + } + } + else + { + ab.width = borderStyle.getWidth(); + String style = borderStyle.getStyle(); + if (style.equals(PDBorderStyleDictionary.STYLE_DASHED)) + { + ab.dashArray = borderStyle.getDashStyle().getDashArray(); + } + if (style.equals(PDBorderStyleDictionary.STYLE_UNDERLINE)) + { + ab.underline = true; + } + } + if (ab.dashArray != null) + { + boolean allZero = true; + for (float f : ab.dashArray) { - ab.dashArray = ((COSArray) base3).toFloatArray(); + if (Float.compare(f, 0) != 0) + { + allZero = false; + break; + } } - } - } - else - { - ab.width = borderStyle.getWidth(); - if (borderStyle.getStyle().equals(PDBorderStyleDictionary.STYLE_DASHED)) - { - ab.dashArray = borderStyle.getDashStyle().getDashArray(); - } - if (borderStyle.getStyle().equals(PDBorderStyleDictionary.STYLE_UNDERLINE)) - { - ab.underline = true; - } - } - if (ab.dashArray != null) - { - boolean allZero = true; - for (float f : ab.dashArray) - { - if (Float.compare(f, 0) != 0) + if (allZero) { - allZero = false; - break; + ab.dashArray = null; } - } - if (allZero) - { - ab.dashArray = null; - } - } - return ab; - } + } + return ab; + } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDAbstractAppearanceHandler.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDAbstractAppearanceHandler.java index 8af9d6597..708288a26 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDAbstractAppearanceHandler.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDAbstractAppearanceHandler.java @@ -17,6 +17,8 @@ package com.tom_roush.pdfbox.pdmodel.interactive.annotation.handlers; +import android.graphics.Rect; + import java.io.IOException; import java.util.Collections; import java.util.HashSet; @@ -36,7 +38,6 @@ import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAppearanceEntry; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream; - /** * Generic handler to generate the fields appearance. * @@ -215,7 +216,7 @@ PDAppearanceEntry getRolloverAppearance() PDRectangle getPaddedRectangle(PDRectangle rectangle, float padding) { return new PDRectangle(rectangle.getLowerLeftX() + padding, rectangle.getLowerLeftY() + padding, - rectangle.getWidth() - 2 * padding, rectangle.getHeight() - 2 * padding); + rectangle.getWidth() - 2 * padding, rectangle.getHeight() - 2 * padding); } /** @@ -237,9 +238,9 @@ PDRectangle addRectDifferences(PDRectangle rectangle, float[] differences) } return new PDRectangle(rectangle.getLowerLeftX() - differences[0], - rectangle.getLowerLeftY() - differences[1], - rectangle.getWidth() + differences[0] + differences[2], - rectangle.getHeight() + differences[1] + differences[3]); + rectangle.getLowerLeftY() - differences[1], + rectangle.getWidth() + differences[0] + differences[2], + rectangle.getHeight() + differences[1] + differences[3]); } /** @@ -260,9 +261,9 @@ PDRectangle applyRectDifferences(PDRectangle rectangle, float[] differences) return rectangle; } return new PDRectangle(rectangle.getLowerLeftX() + differences[0], - rectangle.getLowerLeftY() + differences[1], - rectangle.getWidth() - differences[0] - differences[2], - rectangle.getHeight() - differences[1] - differences[3]); + rectangle.getLowerLeftY() + differences[1], + rectangle.getWidth() - differences[0] - differences[2], + rectangle.getHeight() - differences[1] - differences[3]); } void setOpacity(PDAppearanceContentStream contentStream, float opacity) throws IOException @@ -293,7 +294,7 @@ void setOpacity(PDAppearanceContentStream contentStream, float opacity) throws I * @throws IOException */ void drawStyle(String style, final PDAppearanceContentStream cs, float x, float y, - float width, boolean hasStroke, boolean hasBackground, boolean ending) throws IOException + float width, boolean hasStroke, boolean hasBackground, boolean ending) throws IOException { int sign = ending ? -1 : 1; @@ -327,9 +328,9 @@ else if (PDAnnotationLine.LE_SLASH.equals(style)) // the line is 18 x linewidth at an angle of 60° float width9 = width * 9; cs.moveTo(x + (float) (Math.cos(Math.toRadians(60)) * width9), - y + (float) (Math.sin(Math.toRadians(60)) * width9)); + y + (float) (Math.sin(Math.toRadians(60)) * width9)); cs.lineTo(x + (float) (Math.cos(Math.toRadians(240)) * width9), - y + (float) (Math.sin(Math.toRadians(240)) * width9)); + y + (float) (Math.sin(Math.toRadians(240)) * width9)); } if (PDAnnotationLine.LE_R_CLOSED_ARROW.equals(style) || @@ -338,9 +339,9 @@ else if (PDAnnotationLine.LE_SLASH.equals(style)) cs.closePath(); } cs.drawShape(width, hasStroke, - // make sure to only paint a background color (/IC value) - // for interior color styles, even if an /IC value is set. - INTERIOR_COLOR_STYLES.contains(style) && hasBackground); + // make sure to only paint a background color (/IC value) + // for interior color styles, even if an /IC value is set. + INTERIOR_COLOR_STYLES.contains(style) && hasBackground); } /** @@ -489,7 +490,7 @@ private PDAppearanceEntry getNormalAppearance() private PDAppearanceContentStream getAppearanceEntryAsContentStream( - PDAppearanceEntry appearanceEntry, boolean compress) throws IOException + PDAppearanceEntry appearanceEntry, boolean compress) throws IOException { PDAppearanceStream appearanceStream = appearanceEntry.getAppearanceStream(); setTransformationMatrix(appearanceStream); @@ -510,7 +511,7 @@ private void setTransformationMatrix(PDAppearanceStream appearanceStream) PDRectangle bbox = getRectangle(); appearanceStream.setBBox(bbox); AffineTransform transform = AffineTransform.getTranslateInstance(-bbox.getLowerLeftX(), - -bbox.getLowerLeftY()); + -bbox.getLowerLeftY()); appearanceStream.setMatrix(transform); } @@ -535,7 +536,7 @@ PDRectangle handleBorderBox(PDAnnotationSquareCircle annotation, float lineWidth PDRectangle rect = getRectangle(); PDAppearanceStream appearanceStream = annotation.getNormalAppearanceStream(); AffineTransform transform = - AffineTransform.getTranslateInstance(-rect.getLowerLeftX(), -rect.getLowerLeftY()); + AffineTransform.getTranslateInstance(-rect.getLowerLeftX(), -rect.getLowerLeftY()); appearanceStream.setBBox(rect); appearanceStream.setMatrix(transform); } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDCaretAppearanceHandler.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDCaretAppearanceHandler.java index b2bd6567a..bf02f2841 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDCaretAppearanceHandler.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDCaretAppearanceHandler.java @@ -24,11 +24,11 @@ import com.tom_roush.pdfbox.pdmodel.PDAppearanceContentStream; import com.tom_roush.pdfbox.pdmodel.PDDocument; import com.tom_roush.pdfbox.pdmodel.common.PDRectangle; +import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColor; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotation; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationMarkup; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream; import com.tom_roush.pdfbox.util.Matrix; - /** * Handler to generate the caret annotations appearance. * @@ -36,6 +36,7 @@ */ public class PDCaretAppearanceHandler extends PDAbstractAppearanceHandler { + public PDCaretAppearanceHandler(PDAnnotation annotation) { super(annotation); @@ -56,13 +57,16 @@ public void generateNormalAppearance() { contentStream = getNormalAppearanceAsContentStream(); - contentStream.setStrokingColor(getColor()); - contentStream.setNonStrokingColor(getColor()); + PDColor color = getColor(); + contentStream.setStrokingColor(color); + contentStream.setNonStrokingColor(color); setOpacity(contentStream, annotation.getConstantOpacity()); PDRectangle rect = getRectangle(); - PDRectangle bbox = new PDRectangle(rect.getWidth(), rect.getHeight()); + float rectWidth = rect.getWidth(); + float rectHeight = rect.getHeight(); + PDRectangle bbox = new PDRectangle(rectWidth, rectHeight); PDAppearanceStream pdAppearanceStream = annotation.getNormalAppearanceStream(); if (!annotation.getCOSObject().containsKey(COSName.RD)) { @@ -72,26 +76,26 @@ public void generateNormalAppearance() // translation values in the matrix and also used for the line width // (not here because it has no effect, see comment near fill() ). // The curves are based on the original rectangle. - float rd = Math.min(rect.getHeight() / 10, 5); + float rd = Math.min(rectHeight / 10, 5); annotation.setRectDifferences(rd); - bbox = new PDRectangle(-rd, -rd, rect.getWidth() + 2 * rd, rect.getHeight() + 2 * rd); + bbox = new PDRectangle(-rd, -rd, rectWidth + 2 * rd, rectHeight + 2 * rd); Matrix matrix = pdAppearanceStream.getMatrix(); pdAppearanceStream.setMatrix(matrix.createAffineTransform()); PDRectangle rect2 = new PDRectangle(rect.getLowerLeftX() - rd, rect.getLowerLeftY() - rd, - rect.getWidth() + 2 * rd, rect.getHeight() + 2 * rd); + rectWidth + 2 * rd, rectHeight + 2 * rd); annotation.setRectangle(rect2); } pdAppearanceStream.setBBox(bbox); - float halfX = rect.getWidth() / 2; - float halfY = rect.getHeight() / 2; + float halfX = rectWidth / 2; + float halfY = rectHeight / 2; contentStream.moveTo(0, 0); contentStream.curveTo(halfX, 0, - halfX, halfY, - halfX, rect.getHeight()); + halfX, halfY, + halfX, rectHeight); contentStream.curveTo(halfX, halfY, - halfX, 0, - rect.getWidth(), 0); + halfX, 0, + rectWidth, 0); contentStream.closePath(); contentStream.fill(); // Adobe has an additional stroke, but it has no effect @@ -118,4 +122,4 @@ public void generateDownAppearance() { // TODO to be implemented } -} \ No newline at end of file +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDFileAttachmentAppearanceHandler.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDFileAttachmentAppearanceHandler.java index 7e74167f5..a9058f4d6 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDFileAttachmentAppearanceHandler.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDFileAttachmentAppearanceHandler.java @@ -26,13 +26,14 @@ import com.tom_roush.pdfbox.pdmodel.common.PDRectangle; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotation; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationFileAttachment; - +import com.tom_roush.pdfbox.util.Matrix; /** * * @author Tilman Hausherr */ public class PDFileAttachmentAppearanceHandler extends PDAbstractAppearanceHandler { + public PDFileAttachmentAppearanceHandler(PDAnnotation annotation) { super(annotation); @@ -48,6 +49,11 @@ public void generateNormalAppearance() { PDAnnotationFileAttachment annotation = (PDAnnotationFileAttachment) getAnnotation(); + PDRectangle rect = getRectangle(); + if (rect == null) + { + return; + } PDAppearanceContentStream contentStream = null; try { @@ -56,14 +62,29 @@ public void generateNormalAppearance() // minimum code of PDTextAppearanceHandler.adjustRectAndBBox() int size = 18; - PDRectangle rect = getRectangle(); rect.setUpperRightX(rect.getLowerLeftX() + size); rect.setLowerLeftY(rect.getUpperRightY() - size); annotation.setRectangle(rect); annotation.getNormalAppearanceStream().setBBox(new PDRectangle(size, size)); - //TODO support Graph, PushPin, Paperclip, Tag - drawPaperclip(contentStream); + // test case: pdf_commenting_new.pdf page 7 + String attachmentName = annotation.getAttachmentName(); + if ("Paperclip".equals(attachmentName)) + { + drawPaperclip(contentStream); + } + else if ("Graph".equals(attachmentName)) + { + drawGraph(contentStream); + } + else if ("Tag".equals(attachmentName)) + { + drawTag(contentStream); + } + else + { + drawPushPin(contentStream); + } } catch (IOException e) { @@ -77,7 +98,7 @@ public void generateNormalAppearance() /** * Draw a paperclip. Shape is from - * Iconscout * (Apache licensed). * * @param contentStream @@ -115,6 +136,199 @@ private void drawPaperclip(final PDAppearanceContentStream contentStream) throws contentStream.fill(); } + private void drawPushPin(final PDAppearanceContentStream contentStream) throws IOException + { + // ty 18 is from the caller, scale 0.022 is by trial and error + contentStream.transform(new Matrix(0.022f, 0, 0, -0.022f, 0f, 18f)); + + // Source: https://www.svgrepo.com/svg/269187/push-pin + // License: CC0 + contentStream.transform(Matrix.getTranslateInstance(586.47f, 178.97f)); + contentStream.moveTo(0, 0); + contentStream.curveTo(13f, 0f, 23.43f, -10.58f, 23.43f, -23.57f); + contentStream.lineTo(23.43f, -70.53f); + contentStream.curveTo(23.43f, -109.32f, -8.19f, -141.06f, -47.03f, -141.06f); + contentStream.lineTo(-329.17f, -141.06f); + contentStream.curveTo(-368.17f, -141.06f, -399.79f, -109.32f, -399.79f, -70.53f); + contentStream.lineTo(-399.79f, -23.57f); + contentStream.curveTo(-399.79f, -10.58f, -389.19f, 0.0f, -376.19f, 0f); + contentStream.lineTo(-305.74f, 0f); + contentStream.lineTo(-305.74f, 129.52f); + contentStream.curveTo(-364.0f, 168.47f, -399.79f, 234.67f, -399.79f, 305.36f); + contentStream.curveTo(-399.79f, 318.34f, -389.19f, 328.76f, -376.19f, 328.76f); + contentStream.lineTo(-211.69f, 328.76f); + contentStream.lineTo(-211.69f, 555.9f); + contentStream.curveTo(-211.69f, 568.88f, -201.1f, 579.3f, -188.1f, 579.3f); + contentStream.curveTo(-175.1f, 579.3f, -164.67f, 568.88f, -164.67f, 555.9f); + contentStream.lineTo(-164.67f, 328.76f); + contentStream.lineTo(0f, 328.76f); + contentStream.curveTo(13.0f, 328.76f, 23.43f, 318.34f, 23.43f, 305.36f); + contentStream.curveTo(23.43f, 234.67f, -12.2f, 168.47f, -70.62f, 129.52f); + contentStream.lineTo(-70.62f, 0f); + contentStream.lineTo(0f, 0f); + contentStream.closePath(); + contentStream.moveTo(-25.2f, 281.79f); + contentStream.lineTo(-351.0f, 281.79f); + contentStream.curveTo(-343.77f, 232.42f, -314.24f, 188.18f, -270.43f, 162.86f); + contentStream.curveTo(-263.21f, 158.69f, -258.71f, 150.99f, -258.71f, 142.5f); + contentStream.lineTo(-258.71f, 0f); + contentStream.lineTo(-117.64f, 0f); + contentStream.lineTo(-117.64f, 142.5f); + contentStream.curveTo(-117.64f, 150.99f, -113.15f, 158.69f, -105.77f, 162.86f); + contentStream.curveTo(-61.95f, 188.18f, -32.42f, 232.42f, -25.2f, 281.79f); + contentStream.closePath(); + contentStream.moveTo(-352.76f, -46.97f); + contentStream.lineTo(-352.76f, -70.53f); + contentStream.curveTo(-352.76f, -83.52f, -342.17f, -93.93f, -329.17f, -93.93f); + contentStream.lineTo(-47.03f, -93.93f); + contentStream.curveTo(-34.03f, -93.93f, -23.59f, -83.52f, -23.59f, -70.53f); + contentStream.lineTo(-23.59f, -46.97f); + contentStream.lineTo(-352.76f, -46.97f); + contentStream.lineTo(-352.76f, -46.97f); + contentStream.closePath(); + contentStream.fill(); + } + + private void drawGraph(final PDAppearanceContentStream contentStream) throws IOException + { + // ty 18 is from the caller, scale 0.022 is by trial and error + contentStream.transform(new Matrix(0.022f, 0, 0, -0.022f, 0f, 18f)); + + // Source: https://www.svgrepo.com/svg/339018/chart-histogram + // Author: Carbon Design https://github.com/carbon-design-system/carbon + // License: Apache + contentStream.transform(Matrix.getTranslateInstance(736.04f, 907.89f)); + contentStream.moveTo(0f, 0f); + contentStream.lineTo(-675.23f, 0f); + contentStream.curveTo(-679.72f, 0f, -683.41f, -3.53f, -683.41f, -8.01f); + contentStream.lineTo(-683.41f, -683.37f); + contentStream.lineTo(-667.22f, -683.37f); + contentStream.lineTo(-667.22f, -353.95f); + contentStream.curveTo(-583.85f, -357.8f, -541.53f, -419.99f, -500.49f, -480.27f); + contentStream.curveTo(-459.93f, -539.74f, -418.09f, -601.46f, -337.61f, -601.46f); + contentStream.curveTo(-257.14f, -601.46f, -215.3f, -539.74f, -174.74f, -480.27f); + contentStream.curveTo(-132.58f, -418.07f, -88.81f, -353.79f, 0f, -353.79f); + contentStream.lineTo(0f, -337.6f); + contentStream.curveTo(-97.31f, -337.6f, -143.48f, -405.41f, -188.2f, -471.13f); + contentStream.curveTo(-228.12f, -529.8f, -265.8f, -585.27f, -337.61f, -585.27f); + contentStream.curveTo(-409.43f, -585.27f, -447.11f, -529.8f, -487.03f, -471.13f); + contentStream.curveTo(-530.47f, -407.33f, -575.36f, -341.45f, -667.22f, -337.76f); + contentStream.lineTo(-667.22f, -16.19f); + contentStream.lineTo(-615.76f, -16.19f); + contentStream.lineTo(-615.76f, -255.68f); + contentStream.curveTo(-615.76f, -260.17f, -612.23f, -263.7f, -607.74f, -263.7f); + contentStream.lineTo(-525.82f, -263.7f); + contentStream.lineTo(-525.82f, -345.77f); + contentStream.curveTo(-525.82f, -350.26f, -522.13f, -353.79f, -517.64f, -353.79f); + contentStream.lineTo(-435.73f, -353.79f); + contentStream.lineTo(-435.73f, -458.31f); + contentStream.curveTo(-435.73f, -462.8f, -432.2f, -466.32f, -427.71f, -466.32f); + contentStream.lineTo(-337.61f, -466.32f); + contentStream.curveTo(-333.13f, -466.32f, -329.6f, -462.8f, -329.6f, -458.31f); + contentStream.lineTo(-329.6f, -421.28f); + contentStream.lineTo(-247.68f, -421.28f); + contentStream.curveTo(-243.19f, -421.28f, -239.5f, -417.75f, -239.5f, -413.26f); + contentStream.lineTo(-239.5f, -331.35f); + contentStream.lineTo(-157.58f, -331.35f); + contentStream.curveTo(-153.1f, -331.35f, -149.41f, -327.66f, -149.41f, -323.17f); + contentStream.lineTo(-149.41f, -218.81f); + contentStream.lineTo(-67.49f, -218.81f); + contentStream.curveTo(-63.0f, -218.81f, -59.47f, -215.13f, -59.47f, -210.64f); + contentStream.lineTo(-59.47f, -16.19f); + contentStream.lineTo(0f, -16.19f); + contentStream.lineTo(0f, 0f); + contentStream.closePath(); + contentStream.moveTo(-149.41f, -16.19f); + contentStream.lineTo(-75.67f, -16.19f); + contentStream.lineTo(-75.67f, -202.62f); + contentStream.lineTo(-149.41f, -202.62f); + contentStream.lineTo(-149.41f, -16.19f); + contentStream.closePath(); + contentStream.moveTo(-239.5f, -16.19f); + contentStream.lineTo(-165.76f, -16.19f); + contentStream.lineTo(-165.76f, -315.16f); + contentStream.lineTo(-239.5f, -315.16f); + contentStream.lineTo(-239.5f, -16.19f); + contentStream.closePath(); + contentStream.moveTo(-329.6f, -16.19f); + contentStream.lineTo(-255.7f, -16.19f); + contentStream.lineTo(-255.7f, -405.09f); + contentStream.lineTo(-329.6f, -405.09f); + contentStream.lineTo(-329.6f, -16.19f); + contentStream.closePath(); + contentStream.moveTo(-419.53f, -16.19f); + contentStream.lineTo(-345.79f, -16.19f); + contentStream.lineTo(-345.79f, -450.13f); + contentStream.lineTo(-419.53f, -450.13f); + contentStream.lineTo(-419.53f, -16.19f); + contentStream.closePath(); + contentStream.moveTo(-509.63f, -16.19f); + contentStream.lineTo(-435.73f, -16.19f); + contentStream.lineTo(-435.73f, -337.6f); + contentStream.lineTo(-509.63f, -337.6f); + contentStream.lineTo(-509.63f, -16.19f); + contentStream.closePath(); + contentStream.moveTo(-599.56f, -16.19f); + contentStream.lineTo(-525.82f, -16.19f); + contentStream.lineTo(-525.82f, -247.51f); + contentStream.lineTo(-599.56f, -247.51f); + contentStream.lineTo(-599.56f, -16.19f); + contentStream.closePath(); + contentStream.fill(); + } + + private void drawTag(final PDAppearanceContentStream contentStream) throws IOException + { + // ty 18 is from the caller, scale 0.022 is by trial and error + contentStream.transform(new Matrix(0.022f, 0, 0, -0.022f, 0f, 18f)); + + // Source: https://www.svgrepo.com/svg/29652/tag + // License: CC0 + contentStream.saveGraphicsState(); + contentStream.transform(Matrix.getTranslateInstance(209.26f, 128.32f)); + contentStream.moveTo(0f, 0f); + contentStream.curveTo(-44.73f, 0f, -80.64f, 36.23f, -80.64f, 80.64f); + contentStream.curveTo(-80.64f, 125.2f, -44.57f, 161.27f, 0f, 161.27f); + contentStream.curveTo(44.56f, 161.27f, 80.47f, 125.04f, 80.47f, 80.64f); + contentStream.curveTo(80.63f, 36.07f, 44.56f, 0f, 0f, 0f); + contentStream.closePath(); + contentStream.moveTo(0f, 132.74f); + contentStream.curveTo(-28.7f, 132.74f, -52.1f, 109.33f, -52.1f, 80.64f); + contentStream.curveTo(-52.1f, 51.94f, -28.7f, 28.54f, 0f, 28.54f); + contentStream.curveTo(28.69f, 28.54f, 51.93f, 51.94f, 51.93f, 80.64f); + contentStream.curveTo(51.93f, 109.33f, 28.85f, 132.74f, 0f, 132.74f); + contentStream.closePath(); + contentStream.fill(); + contentStream.restoreGraphicsState(); + contentStream.saveGraphicsState(); + contentStream.transform(Matrix.getTranslateInstance(382.22f, 79.91f)); + contentStream.moveTo(0f, 0f); + contentStream.curveTo(-14.58f, -16.19f, -35.1f, -24.85f, -57.22f, -24.85f); + contentStream.lineTo(-208.23f, -26.45f); + contentStream.curveTo(-240.45f, -26.45f, -271.23f, -14.75f, -293.35f, 8.66f); + contentStream.curveTo(-316.76f, 30.78f, -328.46f, 61.56f, -328.46f, 93.78f); + contentStream.lineTo(-327.02f, 244.95f); + contentStream.curveTo(-325.57f, 265.47f, -318.2f, 285.98f, -302.17f, 302.18f); + contentStream.lineTo(58.68f, 663.02f); + contentStream.lineTo(360.85f, 360.69f); + contentStream.lineTo(0f, 0f); + contentStream.lineTo(0f, 0f); + contentStream.closePath(); + contentStream.moveTo(57.23f, 621.82f); + contentStream.lineTo(-283.09f, 281.5f); + contentStream.curveTo(-293.35f, 271.24f, -299.12f, 258.09f, -299.12f, 243.34f); + contentStream.lineTo(-300.57f, 93.78f); + contentStream.curveTo(-300.57f, 70.38f, -290.31f, 46.81f, -274.12f, 29.34f); + contentStream.curveTo(-256.64f, 11.7f, -233.08f, 1.44f, -208.23f, 1.44f); + contentStream.lineTo(-58.67f, 2.89f); + contentStream.curveTo(-44.08f, 2.89f, -30.77f, 8.66f, -20.51f, 19.08f); + contentStream.lineTo(319.81f, 359.4f); + contentStream.lineTo(57.23f, 621.82f); + contentStream.closePath(); + contentStream.fill(); + contentStream.restoreGraphicsState(); + } + @Override public void generateRolloverAppearance() { diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDFreeTextAppearanceHandler.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDFreeTextAppearanceHandler.java index f74687574..040c06be9 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDFreeTextAppearanceHandler.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDFreeTextAppearanceHandler.java @@ -15,6 +15,7 @@ */ package com.tom_roush.pdfbox.pdmodel.interactive.annotation.handlers; +import android.graphics.Rect; import android.util.Log; import java.io.IOException; @@ -49,13 +50,12 @@ import com.tom_roush.pdfbox.pdmodel.interactive.annotation.layout.PlainTextFormatter; import com.tom_roush.pdfbox.pdmodel.interactive.form.PDAcroForm; import com.tom_roush.pdfbox.util.Matrix; - import static com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationLine.LE_NONE; - public class PDFreeTextAppearanceHandler extends PDAbstractAppearanceHandler { + private static final Pattern COLOR_PATTERN = - Pattern.compile(".*color\\:\\s*\\#([0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]).*"); + Pattern.compile("color:\\s*+#([0-9a-fA-F]{6})"); private float fontSize = 10; private COSName fontName = COSName.HELV; @@ -124,6 +124,8 @@ public void generateNormalAppearance() } cs.setLineWidth(ab.width); + String lineEndingStyle = annotation.getLineEndingStyle(); + // draw callout line(s) // must be done before retangle paint to avoid a line cutting through cloud // see CTAN-example-Annotations.pdf @@ -133,7 +135,7 @@ public void generateNormalAppearance() float y = pathsArray[i * 2 + 1]; if (i == 0) { - if (SHORT_STYLES.contains(annotation.getLineEndingStyle())) + if (SHORT_STYLES.contains(lineEndingStyle)) { // modify coordinate to shorten the segment // https://stackoverflow.com/questions/7740507/extend-a-line-segment-a-specific-distance @@ -160,16 +162,16 @@ public void generateNormalAppearance() // paint the styles here and after line(s) draw, to avoid line crossing a filled shape if (PDAnnotationMarkup.IT_FREE_TEXT_CALLOUT.equals(annotation.getIntent()) - // check only needed to avoid q cm Q if LE_NONE - && !LE_NONE.equals(annotation.getLineEndingStyle()) - && pathsArray.length >= 4) + // check only needed to avoid q cm Q if LE_NONE + && !LE_NONE.equals(lineEndingStyle) + && pathsArray.length >= 4) { float x2 = pathsArray[2]; float y2 = pathsArray[3]; float x1 = pathsArray[0]; float y1 = pathsArray[1]; cs.saveGraphicsState(); - if (ANGLED_STYLES.contains(annotation.getLineEndingStyle())) + if (ANGLED_STYLES.contains(lineEndingStyle)) { // do a transform so that first "arm" is imagined flat, // like in line handler. @@ -183,7 +185,7 @@ public void generateNormalAppearance() { cs.transform(Matrix.getTranslateInstance(x1, y1)); } - drawStyle(annotation.getLineEndingStyle(), cs, 0, 0, ab.width, hasStroke, hasBackground, false); + drawStyle(lineEndingStyle, cs, 0, 0, ab.width, hasStroke, hasBackground, false); cs.restoreGraphicsState(); } @@ -221,7 +223,7 @@ public void generateNormalAppearance() // note that borderBox is not modified PDRectangle paddedRectangle = getPaddedRectangle(borderBox, ab.width / 2); cs.addRect(paddedRectangle.getLowerLeftX(), paddedRectangle.getLowerLeftY(), - paddedRectangle.getWidth(), paddedRectangle.getHeight()); + paddedRectangle.getWidth(), paddedRectangle.getHeight()); } cs.drawShape(ab.width, hasStroke, hasBackground); @@ -238,7 +240,7 @@ public void generateNormalAppearance() float clipY; float clipWidth = width - ab.width * 4; float clipHeight = rotation == 90 || rotation == 270 ? - borderBox.getWidth() - ab.width * 4 : borderBox.getHeight() - ab.width * 4; + borderBox.getWidth() - ab.width * 4 : borderBox.getHeight() - ab.width * 4; extractFontDetails(annotation); if (document != null) { @@ -291,7 +293,8 @@ public void generateNormalAppearance() cs.addRect(xOffset, clipY, clipWidth, clipHeight); cs.clip(); - if (annotation.getContents() != null) + String annotationContents = annotation.getContents(); + if (annotationContents != null) { cs.beginText(); cs.setFont(font, fontSize); @@ -300,14 +303,14 @@ public void generateNormalAppearance() appearanceStyle.setFont(font); appearanceStyle.setFontSize(fontSize); PlainTextFormatter formatter = new PlainTextFormatter.Builder(cs) - .style(appearanceStyle) - .text(new PlainText(annotation.getContents())) - .width(width - ab.width * 4) - .wrapLines(true) - .initialOffset(xOffset, yOffset) - // Adobe ignores the /Q - //.textAlign(annotation.getQ()) - .build(); + .style(appearanceStyle) + .text(new PlainText(annotationContents)) + .width(width - ab.width * 4) + .wrapLines(true) + .initialOffset(xOffset, yOffset) + // Adobe ignores the /Q + //.textAlign(annotation.getQ()) + .build(); try { formatter.format(); diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDHighlightAppearanceHandler.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDHighlightAppearanceHandler.java index db9855e78..b4b4201ff 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDHighlightAppearanceHandler.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDHighlightAppearanceHandler.java @@ -20,8 +20,6 @@ import java.io.IOException; -import com.tom_roush.pdfbox.cos.COSDictionary; -import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.io.IOUtils; import com.tom_roush.pdfbox.pdmodel.PDAppearanceContentStream; import com.tom_roush.pdfbox.pdmodel.PDDocument; @@ -31,16 +29,17 @@ import com.tom_roush.pdfbox.pdmodel.graphics.blend.BlendMode; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColor; import com.tom_roush.pdfbox.pdmodel.graphics.form.PDFormXObject; +import com.tom_roush.pdfbox.pdmodel.graphics.form.PDTransparencyGroupAttributes; import com.tom_roush.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotation; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationTextMarkup; - /** * * @author Tilman Hausherr */ public class PDHighlightAppearanceHandler extends PDAbstractAppearanceHandler { + public PDHighlightAppearanceHandler(PDAnnotation annotation) { super(annotation); @@ -98,7 +97,7 @@ public void generateNormalAppearance() // horizontal or vertical // if it is diagonal then... uh... float delta = Math.max((pathsArray[i + 0] - pathsArray[i + 4]) / 4, - (pathsArray[i + 1] - pathsArray[i + 5]) / 4); + (pathsArray[i + 1] - pathsArray[i + 5]) / 4); maxDelta = Math.max(delta, maxDelta); } @@ -139,10 +138,7 @@ public void generateNormalAppearance() IOUtils.closeQuietly(mwfofrmCS); } frm1.setBBox(annotation.getRectangle()); - COSDictionary groupDict = new COSDictionary(); - groupDict.setItem(COSName.S, COSName.TRANSPARENCY); - //TODO PDFormXObject.setGroup() is missing - frm1.getCOSObject().setItem(COSName.GROUP, groupDict); + frm1.setGroup(new PDTransparencyGroupAttributes()); cs.drawForm(frm1); frm2.setBBox(annotation.getRectangle()); @@ -171,9 +167,9 @@ public void generateNormalAppearance() delta = (pathsArray[of + 1] - pathsArray[of + 5]) / 4; } else if (Float.compare(pathsArray[of + 1], pathsArray[of + 5]) == 0 && - Float.compare(pathsArray[of + 0], pathsArray[of + 2]) == 0 && - Float.compare(pathsArray[of + 3], pathsArray[of + 7]) == 0 && - Float.compare(pathsArray[of + 4], pathsArray[of + 6]) == 0) + Float.compare(pathsArray[of + 0], pathsArray[of + 2]) == 0 && + Float.compare(pathsArray[of + 3], pathsArray[of + 7]) == 0 && + Float.compare(pathsArray[of + 4], pathsArray[of + 6]) == 0) { // vertical highlight delta = (pathsArray[of + 0] - pathsArray[of + 4]) / 4; @@ -185,15 +181,15 @@ else if (Float.compare(pathsArray[of + 1], pathsArray[of + 5]) == 0 && { // horizontal highlight frm2CS.curveTo(pathsArray[of + 4] - delta, pathsArray[of + 5] + delta, - pathsArray[of + 0] - delta, pathsArray[of + 1] - delta, - pathsArray[of + 0], pathsArray[of + 1]); + pathsArray[of + 0] - delta, pathsArray[of + 1] - delta, + pathsArray[of + 0], pathsArray[of + 1]); } else if (Float.compare(pathsArray[of + 5], pathsArray[of + 1]) == 0) { // vertical highlight frm2CS.curveTo(pathsArray[of + 4] + delta, pathsArray[of + 5] + delta, - pathsArray[of + 0] - delta, pathsArray[of + 1] + delta, - pathsArray[of + 0], pathsArray[of + 1]); + pathsArray[of + 0] - delta, pathsArray[of + 1] + delta, + pathsArray[of + 0], pathsArray[of + 1]); } else { @@ -204,15 +200,15 @@ else if (Float.compare(pathsArray[of + 5], pathsArray[of + 1]) == 0) { // horizontal highlight frm2CS.curveTo(pathsArray[of + 2] + delta, pathsArray[of + 3] - delta, - pathsArray[of + 6] + delta, pathsArray[of + 7] + delta, - pathsArray[of + 6], pathsArray[of + 7]); + pathsArray[of + 6] + delta, pathsArray[of + 7] + delta, + pathsArray[of + 6], pathsArray[of + 7]); } else if (Float.compare(pathsArray[of + 3], pathsArray[of + 7]) == 0) { // vertical highlight frm2CS.curveTo(pathsArray[of + 2] - delta, pathsArray[of + 3] - delta, - pathsArray[of + 6] + delta, pathsArray[of + 7] - delta, - pathsArray[of + 6], pathsArray[of + 7]); + pathsArray[of + 6] + delta, pathsArray[of + 7] - delta, + pathsArray[of + 6], pathsArray[of + 7]); } else { diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDLineAppearanceHandler.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDLineAppearanceHandler.java index 2fd045b74..4a606ce9e 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDLineAppearanceHandler.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDLineAppearanceHandler.java @@ -29,14 +29,13 @@ import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotation; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationLine; import com.tom_roush.pdfbox.util.Matrix; - import static com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationLine.LE_NONE; - /** * */ public class PDLineAppearanceHandler extends PDAbstractAppearanceHandler { + static final int FONT_SIZE = 9; public PDLineAppearanceHandler(PDAnnotation annotation) @@ -162,6 +161,8 @@ public void generateNormalAppearance() cs.moveTo(lineLength, llo); cs.lineTo(lineLength, llo + ll + lle); + String startPointEndingStyle = annotation.getStartPointEndingStyle(); + String endPointEndingStyle = annotation.getEndPointEndingStyle(); if (annotation.getCaption() && !contents.isEmpty()) { // Note that Adobe places the text as a caption even if /CP is not set @@ -174,7 +175,7 @@ public void generateNormalAppearance() float contentLength = 0; try { - contentLength = font.getStringWidth(annotation.getContents()) / 1000 * FONT_SIZE; + contentLength = font.getStringWidth(contents) / 1000 * FONT_SIZE; //TODO How to decide the size of the font? // 9 seems to be standard, but if the text doesn't fit, a scaling is done @@ -183,7 +184,7 @@ public void generateNormalAppearance() catch (IllegalArgumentException ex) { // Adobe Reader displays placeholders instead - Log.e("PdfBox-Android", "line text '" + annotation.getContents() + "' can't be shown", ex); + Log.e("PdfBox-Android", "line text '" + contents + "' can't be shown", ex); } float xOffset = (lineLength - contentLength) / 2; float yOffset; @@ -192,7 +193,7 @@ public void generateNormalAppearance() // draw the line horizontally, using the rotation CTM to get to correct final position // that's the easiest way to calculate the positions for the line before and after inline caption - if (SHORT_STYLES.contains(annotation.getStartPointEndingStyle())) + if (SHORT_STYLES.contains(startPointEndingStyle)) { cs.moveTo(lineEndingSize, y); } @@ -214,7 +215,7 @@ public void generateNormalAppearance() cs.lineTo(xOffset - lineEndingSize, y); cs.moveTo(lineLength - xOffset + lineEndingSize, y); } - if (SHORT_STYLES.contains(annotation.getEndPointEndingStyle())) + if (SHORT_STYLES.contains(endPointEndingStyle)) { cs.lineTo(lineLength - lineEndingSize, y); } @@ -234,8 +235,8 @@ public void generateNormalAppearance() cs.beginText(); cs.setFont(font, FONT_SIZE); cs.newLineAtOffset(xOffset + captionHorizontalOffset, - y + yOffset + captionVerticalOffset); - cs.showText(annotation.getContents()); + y + yOffset + captionVerticalOffset); + cs.showText(contents); cs.endText(); } @@ -249,7 +250,7 @@ public void generateNormalAppearance() } else { - if (SHORT_STYLES.contains(annotation.getStartPointEndingStyle())) + if (SHORT_STYLES.contains(startPointEndingStyle)) { cs.moveTo(lineEndingSize, y); } @@ -257,7 +258,7 @@ public void generateNormalAppearance() { cs.moveTo(0, y); } - if (SHORT_STYLES.contains(annotation.getEndPointEndingStyle())) + if (SHORT_STYLES.contains(endPointEndingStyle)) { cs.lineTo(lineLength - lineEndingSize, y); } @@ -282,13 +283,13 @@ public void generateNormalAppearance() } // check for LE_NONE only needed to avoid q cm Q for that case - if (!LE_NONE.equals(annotation.getStartPointEndingStyle())) + if (!LE_NONE.equals(startPointEndingStyle)) { cs.saveGraphicsState(); - if (ANGLED_STYLES.contains(annotation.getStartPointEndingStyle())) + if (ANGLED_STYLES.contains(startPointEndingStyle)) { cs.transform(Matrix.getRotateInstance(angle, x1, y1)); - drawStyle(annotation.getStartPointEndingStyle(), cs, 0, y, lineEndingSize, hasStroke, hasBackground, false); + drawStyle(startPointEndingStyle, cs, 0, y, lineEndingSize, hasStroke, hasBackground, false); } else { @@ -298,19 +299,19 @@ public void generateNormalAppearance() // We use the angle we already know and the distance y to translate to the new coordinate. float xx1 = x1 - (float) (y * Math.sin(angle)); float yy1 = y1 + (float) (y * Math.cos(angle)); - drawStyle(annotation.getStartPointEndingStyle(), cs, xx1, yy1, lineEndingSize, hasStroke, hasBackground, false); + drawStyle(startPointEndingStyle, cs, xx1, yy1, lineEndingSize, hasStroke, hasBackground, false); } cs.restoreGraphicsState(); } // check for LE_NONE only needed to avoid q cm Q for that case - if (!LE_NONE.equals(annotation.getEndPointEndingStyle())) + if (!LE_NONE.equals(endPointEndingStyle)) { // save / restore not needed because it's the last one - if (ANGLED_STYLES.contains(annotation.getEndPointEndingStyle())) + if (ANGLED_STYLES.contains(endPointEndingStyle)) { cs.transform(Matrix.getRotateInstance(angle, x2, y2)); - drawStyle(annotation.getEndPointEndingStyle(), cs, 0, y, lineEndingSize, hasStroke, hasBackground, true); + drawStyle(endPointEndingStyle, cs, 0, y, lineEndingSize, hasStroke, hasBackground, true); } else { @@ -320,7 +321,7 @@ public void generateNormalAppearance() // We use the angle we already know and the distance y to translate to the new coordinate. float xx2 = x2 - (float) (y * Math.sin(angle)); float yy2 = y2 + (float) (y * Math.cos(angle)); - drawStyle(annotation.getEndPointEndingStyle(), cs, xx2, yy2, lineEndingSize, hasStroke, hasBackground, true); + drawStyle(endPointEndingStyle, cs, xx2, yy2, lineEndingSize, hasStroke, hasBackground, true); } } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDLinkAppearanceHandler.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDLinkAppearanceHandler.java index cc4a19baa..8ec2e93d0 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDLinkAppearanceHandler.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDLinkAppearanceHandler.java @@ -17,6 +17,7 @@ package com.tom_roush.pdfbox.pdmodel.interactive.annotation.handlers; +import android.graphics.Rect; import android.util.Log; import java.io.IOException; @@ -33,13 +34,13 @@ import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotation; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDBorderStyleDictionary; - /** * Handler to generate the link annotations appearance. * */ public class PDLinkAppearanceHandler extends PDAbstractAppearanceHandler { + public PDLinkAppearanceHandler(PDAnnotation annotation) { super(annotation); @@ -95,9 +96,9 @@ public void generateNormalAppearance() if (!rect.contains(pathsArray[i * 2], pathsArray[i * 2 + 1])) { Log.w("PdfBox-Android", "At least one /QuadPoints entry (" + - pathsArray[i * 2] + ";" + pathsArray[i * 2 + 1] + - ") is outside of rectangle, " + rect + - ", /QuadPoints are ignored and /Rect is used instead"); + pathsArray[i * 2] + ";" + pathsArray[i * 2 + 1] + + ") is outside of rectangle, " + rect + + ", /QuadPoints are ignored and /Rect is used instead"); pathsArray = null; break; } @@ -111,11 +112,11 @@ public void generateNormalAppearance() pathsArray[0] = borderEdge.getLowerLeftX(); pathsArray[1] = borderEdge.getLowerLeftY(); pathsArray[2] = borderEdge.getUpperRightX(); - pathsArray[3] = borderEdge.getLowerLeftY(); - pathsArray[4] = borderEdge.getUpperRightX(); + pathsArray[3] = pathsArray[1]; + pathsArray[4] = pathsArray[2]; pathsArray[5] = borderEdge.getUpperRightY(); - pathsArray[6] = borderEdge.getLowerLeftX(); - pathsArray[7] = borderEdge.getUpperRightY(); + pathsArray[6] = pathsArray[0]; + pathsArray[7] = pathsArray[5]; } boolean underline = false; @@ -161,11 +162,11 @@ public void generateRolloverAppearance() @Override public void generateDownAppearance() { - // No down appearance generated for a link annotation + // No down appearance generated for a link annotation } /** - * Get the line with of the border. + * Get the line width of the border. * * Get the width of the line used to draw a border around the annotation. * This may either be specified by the annotation dictionaries Border diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDPolylineAppearanceHandler.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDPolylineAppearanceHandler.java index 761cabf41..005eaf662 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDPolylineAppearanceHandler.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDPolylineAppearanceHandler.java @@ -21,9 +21,6 @@ import java.io.IOException; -import com.tom_roush.pdfbox.cos.COSArray; -import com.tom_roush.pdfbox.cos.COSBase; -import com.tom_roush.pdfbox.cos.COSNumber; import com.tom_roush.pdfbox.io.IOUtils; import com.tom_roush.pdfbox.pdmodel.PDAppearanceContentStream; import com.tom_roush.pdfbox.pdmodel.PDDocument; @@ -31,17 +28,15 @@ import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColor; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotation; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationMarkup; -import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDBorderStyleDictionary; import com.tom_roush.pdfbox.util.Matrix; - import static com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationLine.LE_NONE; - /** * Handler to generate the polyline annotations appearance. * */ public class PDPolylineAppearanceHandler extends PDAbstractAppearanceHandler { + public PDPolylineAppearanceHandler(PDAnnotation annotation) { super(annotation); @@ -112,13 +107,15 @@ public void generateNormalAppearance() } cs.setLineWidth(ab.width); + String startPointEndingStyle = annotation.getStartPointEndingStyle(); + String endPointEndingStyle = annotation.getEndPointEndingStyle(); for (int i = 0; i < pathsArray.length / 2; ++i) { float x = pathsArray[i * 2]; float y = pathsArray[i * 2 + 1]; if (i == 0) { - if (SHORT_STYLES.contains(annotation.getStartPointEndingStyle())) + if (SHORT_STYLES.contains(startPointEndingStyle)) { // modify coordinate to shorten the segment // https://stackoverflow.com/questions/7740507/extend-a-line-segment-a-specific-distance @@ -135,8 +132,7 @@ public void generateNormalAppearance() } else { - if (i == pathsArray.length / 2 - 1 && - SHORT_STYLES.contains(annotation.getEndPointEndingStyle())) + if (i == pathsArray.length / 2 - 1 && SHORT_STYLES.contains(endPointEndingStyle)) { // modify coordinate to shorten the segment // https://stackoverflow.com/questions/7740507/extend-a-line-segment-a-specific-distance @@ -159,7 +155,7 @@ public void generateNormalAppearance() // which would be more work and produce code difficult to understand // paint the styles here and after polyline draw, to avoid line crossing a filled shape - if (!LE_NONE.equals(annotation.getStartPointEndingStyle())) + if (!LE_NONE.equals(startPointEndingStyle)) { // check only needed to avoid q cm Q if LE_NONE float x2 = pathsArray[2]; @@ -167,7 +163,7 @@ public void generateNormalAppearance() float x1 = pathsArray[0]; float y1 = pathsArray[1]; cs.saveGraphicsState(); - if (ANGLED_STYLES.contains(annotation.getStartPointEndingStyle())) + if (ANGLED_STYLES.contains(startPointEndingStyle)) { double angle = Math.atan2(y2 - y1, x2 - x1); cs.transform(Matrix.getRotateInstance(angle, x1, y1)); @@ -176,11 +172,11 @@ public void generateNormalAppearance() { cs.transform(Matrix.getTranslateInstance(x1, y1)); } - drawStyle(annotation.getStartPointEndingStyle(), cs, 0, 0, ab.width, hasStroke, hasBackground, false); + drawStyle(startPointEndingStyle, cs, 0, 0, ab.width, hasStroke, hasBackground, false); cs.restoreGraphicsState(); } - if (!LE_NONE.equals(annotation.getEndPointEndingStyle())) + if (!LE_NONE.equals(endPointEndingStyle)) { // check only needed to avoid q cm Q if LE_NONE float x1 = pathsArray[pathsArray.length - 4]; @@ -188,7 +184,7 @@ public void generateNormalAppearance() float x2 = pathsArray[pathsArray.length - 2]; float y2 = pathsArray[pathsArray.length - 1]; // save / restore not needed because it's the last one - if (ANGLED_STYLES.contains(annotation.getEndPointEndingStyle())) + if (ANGLED_STYLES.contains(endPointEndingStyle)) { double angle = Math.atan2(y2 - y1, x2 - x1); cs.transform(Matrix.getRotateInstance(angle, x2, y2)); @@ -197,7 +193,7 @@ public void generateNormalAppearance() { cs.transform(Matrix.getTranslateInstance(x2, y2)); } - drawStyle(annotation.getEndPointEndingStyle(), cs, 0, 0, ab.width, hasStroke, hasBackground, true); + drawStyle(endPointEndingStyle, cs, 0, 0, ab.width, hasStroke, hasBackground, true); } } catch (IOException ex) @@ -221,45 +217,4 @@ public void generateDownAppearance() { // No down appearance generated for a polyline annotation } - - //TODO DRY, this code is from polygonAppearanceHandler so it's double - - /** - * Get the line with of the border. - * - * Get the width of the line used to draw a border around the annotation. - * This may either be specified by the annotation dictionaries Border - * setting or by the W entry in the BS border style dictionary. If both are - * missing the default width is 1. - * - * @return the line width - */ - // TODO: according to the PDF spec the use of the BS entry is annotation - // specific - // so we will leave that to be implemented by individual handlers. - // If at the end all annotations support the BS entry this can be handled - // here and removed from the individual handlers. - float getLineWidth() - { - PDAnnotationMarkup annotation = (PDAnnotationMarkup) getAnnotation(); - - PDBorderStyleDictionary bs = annotation.getBorderStyle(); - - if (bs != null) - { - return bs.getWidth(); - } - - COSArray borderCharacteristics = annotation.getBorder(); - if (borderCharacteristics.size() >= 3) - { - COSBase base = borderCharacteristics.getObject(2); - if (base instanceof COSNumber) - { - return ((COSNumber) base).floatValue(); - } - } - - return 1; - } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDTextAppearanceHandler.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDTextAppearanceHandler.java index 6a2ae6693..0a0a107a5 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDTextAppearanceHandler.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/handlers/PDTextAppearanceHandler.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.util.HashSet; +import java.util.List; import java.util.Set; import com.tom_roush.pdfbox.cos.COSName; @@ -34,13 +35,13 @@ import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotation; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationText; import com.tom_roush.pdfbox.util.Matrix; - /** * * @author Tilman Hausherr */ public class PDTextAppearanceHandler extends PDAbstractAppearanceHandler { + private static final Set SUPPORTED_NAMES = new HashSet(); static @@ -110,7 +111,7 @@ public void generateNormalAppearance() } else if (PDAnnotationText.NAME_CROSS.equals(annotationTypeName)) { - drawCross(annotation, contentStream); + drawZapf(annotation, contentStream, 19, 0, "a22"); // 0x2716 } else if (PDAnnotationText.NAME_CIRCLE.equals(annotationTypeName)) { @@ -134,11 +135,11 @@ else if (PDAnnotationText.NAME_NEW_PARAGRAPH.equals(annotationTypeName)) } else if (PDAnnotationText.NAME_STAR.equals(annotationTypeName)) { - drawStar(annotation, contentStream); + drawZapf(annotation, contentStream, 19, 0, "a35"); // 0x2605 } else if (PDAnnotationText.NAME_CHECK.equals(annotationTypeName)) { - drawCheck(annotation, contentStream); + drawZapf(annotation, contentStream, 19, 50, "a20"); // 0x2714 } else if (PDAnnotationText.NAME_RIGHT_ARROW.equals(annotationTypeName)) { @@ -146,7 +147,7 @@ else if (PDAnnotationText.NAME_RIGHT_ARROW.equals(annotationTypeName)) } else if (PDAnnotationText.NAME_RIGHT_POINTER.equals(annotationTypeName)) { - drawRightPointer(annotation, contentStream); + drawZapf(annotation, contentStream, 17, 50, "a174"); // 0x27A4 } else if (PDAnnotationText.NAME_CROSS_HAIRS.equals(annotationTypeName)) { @@ -208,7 +209,7 @@ private PDRectangle adjustRectAndBBox(PDAnnotationText annotation, float width, } private void drawNote(PDAnnotationText annotation, final PDAppearanceContentStream contentStream) - throws IOException + throws IOException { PDRectangle bbox = adjustRectAndBBox(annotation, 18, 20); contentStream.setMiterLimit(4); @@ -233,7 +234,7 @@ private void drawNote(PDAnnotationText annotation, final PDAppearanceContentStre } private void drawCircles(PDAnnotationText annotation, final PDAppearanceContentStream contentStream) - throws IOException + throws IOException { PDRectangle bbox = adjustRectAndBBox(annotation, 20, 20); @@ -248,6 +249,10 @@ private void drawCircles(PDAnnotationText annotation, final PDAppearanceContentS float smallR = 6.36f; float largeR = 9.756f; + // adjustments because the bottom of the circle is flat + contentStream.transform(Matrix.getScaleInstance(0.95f, 0.95f)); + contentStream.transform(Matrix.getTranslateInstance(0, 0.5f)); + contentStream.setMiterLimit(4); contentStream.setLineJoinStyle(1); contentStream.setLineCapStyle(0); @@ -273,7 +278,7 @@ private void drawCircles(PDAnnotationText annotation, final PDAppearanceContentS } private void drawInsert(PDAnnotationText annotation, final PDAppearanceContentStream contentStream) - throws IOException + throws IOException { PDRectangle bbox = adjustRectAndBBox(annotation, 17, 20); @@ -287,44 +292,8 @@ private void drawInsert(PDAnnotationText annotation, final PDAppearanceContentSt contentStream.closeAndFillAndStroke(); } - private void drawCross(PDAnnotationText annotation, final PDAppearanceContentStream contentStream) - throws IOException - { - PDRectangle bbox = adjustRectAndBBox(annotation, 19, 19); - - // should be a square, but who knows... - float min = Math.min(bbox.getWidth(), bbox.getHeight()); - - // small = offset nearest bbox edge - // large = offset second nearest bbox edge - float small = min / 10; - float large = min / 5; - - contentStream.setMiterLimit(4); - contentStream.setLineJoinStyle(1); - contentStream.setLineCapStyle(0); - contentStream.setLineWidth(0.59f); // value from Adobe - - contentStream.moveTo(small, large); - contentStream.lineTo(large, small); - contentStream.lineTo(min / 2, min / 2 - small); - contentStream.lineTo(min - large, small); - contentStream.lineTo(min - small, large); - contentStream.lineTo(min / 2 + small, min / 2); - contentStream.lineTo(min - small, min - large); - contentStream.lineTo(min - large, min - small); - contentStream.lineTo(min / 2, min / 2 + small); - contentStream.lineTo(large, min - small); - contentStream.lineTo(small, min - large); - contentStream.lineTo(min / 2 - small, min / 2); - contentStream.closeAndFillAndStroke(); - - // alternatively, this could also be drawn with Zapf Dingbats "a21" - // see DrawStar() - } - private void drawHelp(PDAnnotationText annotation, final PDAppearanceContentStream contentStream) - throws IOException + throws IOException { PDRectangle bbox = adjustRectAndBBox(annotation, 20, 20); @@ -366,7 +335,7 @@ private void drawHelp(PDAnnotationText annotation, final PDAppearanceContentStre } private void drawParagraph(PDAnnotationText annotation, final PDAppearanceContentStream contentStream) - throws IOException + throws IOException { PDRectangle bbox = adjustRectAndBBox(annotation, 20, 20); @@ -408,7 +377,7 @@ private void drawParagraph(PDAnnotationText annotation, final PDAppearanceConten } private void drawNewParagraph(PDAnnotationText annotation, final PDAppearanceContentStream contentStream) - throws IOException + throws IOException { adjustRectAndBBox(annotation, 13, 20); @@ -433,78 +402,13 @@ private void drawNewParagraph(PDAnnotationText annotation, final PDAppearanceCon contentStream.fill(); } - private void drawStar(PDAnnotationText annotation, final PDAppearanceContentStream contentStream) - throws IOException - { - PDRectangle bbox = adjustRectAndBBox(annotation, 20, 19); - - float min = Math.min(bbox.getWidth(), bbox.getHeight()); - - contentStream.setMiterLimit(4); - contentStream.setLineJoinStyle(1); - contentStream.setLineCapStyle(0); - contentStream.setLineWidth(0.59f); // value from Adobe - - contentStream.transform(Matrix.getScaleInstance(0.001f * min / 0.8f, 0.001f * min / 0.8f)); - - // we get the shape of a Zapf Dingbats star (0x2605) and use that one. - // Adobe uses a different font (which one?), or created the shape from scratch. - Path path = PDType1Font.ZAPF_DINGBATS.getPath("a35"); - addPath(contentStream, path); - contentStream.fillAndStroke(); - } - - //TODO this is mostly identical to drawStar, except for scale, translation and symbol - // maybe use a table with all values and draw from there - // this could also optionally use outer circle - private void drawCheck(PDAnnotationText annotation, final PDAppearanceContentStream contentStream) - throws IOException - { - PDRectangle bbox = adjustRectAndBBox(annotation, 20, 19); - - float min = Math.min(bbox.getWidth(), bbox.getHeight()); - - contentStream.setMiterLimit(4); - contentStream.setLineJoinStyle(1); - contentStream.setLineCapStyle(0); - contentStream.setLineWidth(0.59f); // value from Adobe - - contentStream.transform(Matrix.getScaleInstance(0.001f * min / 0.8f, 0.001f * min / 0.8f)); - contentStream.transform(Matrix.getTranslateInstance(0, 50)); - - // we get the shape of a Zapf Dingbats check (0x2714) and use that one. - // Adobe uses a different font (which one?), or created the shape from scratch. - Path path = PDType1Font.ZAPF_DINGBATS.getPath("a20"); - addPath(contentStream, path); - contentStream.fillAndStroke(); - } - - //TODO this is mostly identical to drawStar, except for scale, translation and symbol - private void drawRightPointer(PDAnnotationText annotation, final PDAppearanceContentStream contentStream) - throws IOException - { - PDRectangle bbox = adjustRectAndBBox(annotation, 20, 17); - - float min = Math.min(bbox.getWidth(), bbox.getHeight()); - - contentStream.setMiterLimit(4); - contentStream.setLineJoinStyle(1); - contentStream.setLineCapStyle(0); - contentStream.setLineWidth(0.59f); // value from Adobe - - contentStream.transform(Matrix.getScaleInstance(0.001f * min / 0.8f, 0.001f * min / 0.8f)); - contentStream.transform(Matrix.getTranslateInstance(0, 50)); - - // we get the shape of a Zapf Dingbats right pointer (0x27A4) and use that one. - // Adobe uses a different font (which one?), or created the shape from scratch. - Path path = PDType1Font.ZAPF_DINGBATS.getPath("a174"); - addPath(contentStream, path); - contentStream.fillAndStroke(); - } - private void drawCrossHairs(PDAnnotationText annotation, final PDAppearanceContentStream contentStream) - throws IOException + throws IOException { + List fontMatrix = PDType1Font.SYMBOL.getFontBoxFont().getFontMatrix(); + float xScale = (Float) fontMatrix.get(0); + float yScale = (Float) fontMatrix.get(3); + PDRectangle bbox = adjustRectAndBBox(annotation, 20, 20); float min = Math.min(bbox.getWidth(), bbox.getHeight()); @@ -514,7 +418,7 @@ private void drawCrossHairs(PDAnnotationText annotation, final PDAppearanceConte contentStream.setLineCapStyle(0); contentStream.setLineWidth(0.61f); // value from Adobe - contentStream.transform(Matrix.getScaleInstance(0.001f * min / 1.5f, 0.001f * min / 1.5f)); + contentStream.transform(Matrix.getScaleInstance(xScale * min * 1.3333f, yScale * min * 1.3333f)); contentStream.transform(Matrix.getTranslateInstance(0, 50)); // we get the shape of a Symbol crosshair (0x2295) and use that one. @@ -525,7 +429,7 @@ private void drawCrossHairs(PDAnnotationText annotation, final PDAppearanceConte } private void drawUpArrow(PDAnnotationText annotation, final PDAppearanceContentStream contentStream) - throws IOException + throws IOException { adjustRectAndBBox(annotation, 17, 20); @@ -545,7 +449,7 @@ private void drawUpArrow(PDAnnotationText annotation, final PDAppearanceContentS } private void drawUpLeftArrow(PDAnnotationText annotation, final PDAppearanceContentStream contentStream) - throws IOException + throws IOException { adjustRectAndBBox(annotation, 17, 17); @@ -567,7 +471,7 @@ private void drawUpLeftArrow(PDAnnotationText annotation, final PDAppearanceCont } private void drawRightArrow(PDAnnotationText annotation, final PDAppearanceContentStream contentStream) - throws IOException + throws IOException { PDRectangle bbox = adjustRectAndBBox(annotation, 20, 20); @@ -593,15 +497,14 @@ private void drawRightArrow(PDAnnotationText annotation, final PDAppearanceConte contentStream.restoreGraphicsState(); contentStream.saveGraphicsState(); - // rescale so that the glyph fits into circle and move it to circle center - // values gathered by trial and error - contentStream.transform(Matrix.getScaleInstance(0.001f * min / 1.3f, 0.001f * min / 1.3f)); - contentStream.transform(Matrix.getTranslateInstance(200, 300)); - - // we get the shape of a Zapf Dingbats right arrow (0x2794) and use that one. - // Adobe uses a different font (which one?), or created the shape from scratch. - Path path = PDType1Font.ZAPF_DINGBATS.getPath("a160"); - addPath(contentStream, path); + contentStream.moveTo(8, 17.5f); + contentStream.lineTo(8, 13.5f); + contentStream.lineTo(3, 13.5f); + contentStream.lineTo(3, 6.5f); + contentStream.lineTo(8, 6.5f); + contentStream.lineTo(8, 2.5f); + contentStream.lineTo(18, 10); + contentStream.closePath(); contentStream.restoreGraphicsState(); // surprisingly, this one not counterclockwise. drawCircle(contentStream, min / 2, min / 2, min / 2 - 1); @@ -609,7 +512,7 @@ private void drawRightArrow(PDAnnotationText annotation, final PDAppearanceConte } private void drawComment(PDAnnotationText annotation, final PDAppearanceContentStream contentStream) - throws IOException + throws IOException { adjustRectAndBBox(annotation, 18, 18); @@ -659,7 +562,7 @@ private void drawComment(PDAnnotationText annotation, final PDAppearanceContentS } private void drawKey(PDAnnotationText annotation, final PDAppearanceContentStream contentStream) - throws IOException + throws IOException { adjustRectAndBBox(annotation, 13, 18); @@ -701,6 +604,31 @@ private void drawKey(PDAnnotationText annotation, final PDAppearanceContentStrea contentStream.fillAndStroke(); } + private void drawZapf(PDAnnotationText annotation, final PDAppearanceContentStream contentStream, + int by, int ty, String glyphName) throws IOException + { + PDRectangle bbox = adjustRectAndBBox(annotation, 20, by); + + float min = Math.min(bbox.getWidth(), bbox.getHeight()); + + contentStream.setMiterLimit(4); + contentStream.setLineJoinStyle(1); + contentStream.setLineCapStyle(0); + contentStream.setLineWidth(0.59f); // value from Adobe + + List fontMatrix = PDType1Font.ZAPF_DINGBATS.getFontBoxFont().getFontMatrix(); + float xScale = (Float) fontMatrix.get(0); + float yScale = (Float) fontMatrix.get(3); + contentStream.transform(Matrix.getScaleInstance(xScale * min / 0.8f, yScale * min / 0.8f)); + contentStream.transform(Matrix.getTranslateInstance(0, ty)); + + // we get the shape of a Zapf Dingbats glyph and use that one. + // Adobe uses a different font (which one?), or created the shape from scratch. + Path path = PDType1Font.ZAPF_DINGBATS.getPath(glyphName); + addPath(contentStream, path); + contentStream.fillAndStroke(); + } + private void addPath(final PDAppearanceContentStream contentStream, Path path) throws IOException { // TODO: PdfBox-Android Can't retrieve segments from an Android Path diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/layout/PlainText.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/layout/PlainText.java index 9192deba4..4012bb3d1 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/layout/PlainText.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/annotation/layout/PlainText.java @@ -17,14 +17,14 @@ package com.tom_roush.pdfbox.pdmodel.interactive.annotation.layout; import java.io.IOException; +import java.text.AttributedCharacterIterator.Attribute; import java.text.AttributedString; import java.text.BreakIterator; -import java.text.AttributedCharacterIterator.Attribute; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import com.tom_roush.pdfbox.pdmodel.font.PDFont; - /** * A block of text. *

      @@ -35,257 +35,261 @@ */ public class PlainText { - private static final float FONTSCALE = 1000f; - - private final List paragraphs; - - /** - * Construct the text block from a single value. - * - * Constructs the text block from a single value splitting - * into individual {@link Paragraph} when a new line character is - * encountered. - * - * @param textValue the text block string. - */ - public PlainText(String textValue) - { - String[] parts = textValue.replace('\t', ' ').split("\\r\\n|\\n|\\r|\\u2028|\\u2029"); - paragraphs = new ArrayList(parts.length); - for (String part : parts) - { - // Acrobat prints a space for an empty paragraph - if (part.length() == 0) - { - part = " "; - } - paragraphs.add(new Paragraph(part)); - } - } - - /** - * Construct the text block from a list of values. - * - * Constructs the text block from a list of values treating each - * entry as an individual {@link Paragraph}. - * - * @param listValue the text block string. - */ - public PlainText(List listValue) - { - paragraphs = new ArrayList(listValue.size()); - for (String part : listValue) - { - paragraphs.add(new Paragraph(part)); - } - } - - /** - * Get the list of paragraphs. - * - * @return the paragraphs. - */ - List getParagraphs() - { - return paragraphs; - } - - /** - * Attribute keys and attribute values used for text handling. - * - * This is similar to {@link java.awt.font.TextAttribute} but - * handled individually as to avoid a dependency on awt. - * - */ - static class TextAttribute extends Attribute - { - /** - * UID for serializing. - */ - private static final long serialVersionUID = -3138885145941283005L; - - /** - * Attribute width of the text. - */ - public static final Attribute WIDTH = new TextAttribute("width"); - - protected TextAttribute(String name) - { - super(name); - } - - - } - - /** - * A block of text to be formatted as a whole. - *

      - * A block of text can contain multiple paragraphs which will - * be treated individually within the block placement. - *

      - * - */ - static class Paragraph - { - private final String textContent; - - Paragraph(String text) - { - textContent = text; - } - - /** - * Get the paragraph text. - * - * @return the text. - */ - String getText() - { - return textContent; - } - - /** - * Break the paragraph into individual lines. - * - * @param font the font used for rendering the text. - * @param fontSize the fontSize used for rendering the text. - * @param width the width of the box holding the content. - * @return the individual lines. - * @throws IOException - */ - List getLines(PDFont font, float fontSize, float width) throws IOException - { - BreakIterator iterator = BreakIterator.getLineInstance(); - iterator.setText(textContent); - - final float scale = fontSize/FONTSCALE; - - int start = iterator.first(); - int end = iterator.next(); - float lineWidth = 0; - - List textLines = new ArrayList(); - Line textLine = new Line(); - - while (end != BreakIterator.DONE) - { - String word = textContent.substring(start,end); - float wordWidth = font.getStringWidth(word) * scale; - - lineWidth = lineWidth + wordWidth; - - // check if the last word would fit without the whitespace ending it - if (lineWidth >= width && Character.isWhitespace(word.charAt(word.length()-1))) + private static final float FONTSCALE = 1000f; + + private final List paragraphs; + + /** + * Construct the text block from a single value. + * + * Constructs the text block from a single value splitting + * into individual {@link Paragraph} when a new line character is + * encountered. + * + * @param textValue the text block string. + */ + public PlainText(String textValue) + { + String[] parts = textValue.replace('\t', ' ').split("\\r\\n|\\n|\\r|\\u2028|\\u2029"); + paragraphs = new ArrayList(parts.length); + for (String part : parts) + { + // Acrobat prints a space for an empty paragraph + if (part.length() == 0) { - float whitespaceWidth = font.getStringWidth(word.substring(word.length()-1)) * scale; - lineWidth = lineWidth - whitespaceWidth; + part = " "; } - - if (lineWidth >= width) + paragraphs.add(new Paragraph(part)); + } + } + + /** + * Construct the text block from a list of values. + * + * Constructs the text block from a list of values treating each + * entry as an individual {@link Paragraph}. + * + * @param listValue the text block string. + */ + public PlainText(List listValue) + { + paragraphs = new ArrayList(listValue.size()); + for (String part : listValue) + { + paragraphs.add(new Paragraph(part)); + } + } + + /** + * Get the list of paragraphs. + * + * @return the paragraphs. + */ + List getParagraphs() + { + return paragraphs; + } + + /** + * Attribute keys and attribute values used for text handling. + * + * This is similar to {@link java.awt.font.TextAttribute} but + * handled individually as to avoid a dependency on awt. + * + */ + static class TextAttribute extends Attribute + { + /** + * UID for serializing. + */ + private static final long serialVersionUID = -3138885145941283005L; + + /** + * Attribute width of the text. + */ + public static final Attribute WIDTH = new TextAttribute("width"); + + protected TextAttribute(String name) + { + super(name); + } + + + } + + /** + * A block of text to be formatted as a whole. + *

      + * A block of text can contain multiple paragraphs which will + * be treated individually within the block placement. + *

      + * + */ + static class Paragraph + { + private final String textContent; + + Paragraph(String text) + { + textContent = text; + } + + /** + * Get the paragraph text. + * + * @return the text. + */ + String getText() + { + return textContent; + } + + /** + * Break the paragraph into individual lines. + * + * @param font the font used for rendering the text. + * @param fontSize the fontSize used for rendering the text. + * @param width the width of the box holding the content. + * @return the individual lines. + * @throws IOException + */ + List getLines(PDFont font, float fontSize, float width) throws IOException + { + if (width <= 0) { - textLine.setWidth(textLine.calculateWidth(font, fontSize)); - textLines.add(textLine); - textLine = new Line(); - lineWidth = font.getStringWidth(word) * scale; + return Collections.emptyList(); } + BreakIterator iterator = BreakIterator.getLineInstance(); + iterator.setText(textContent); + + final float scale = fontSize/FONTSCALE; - AttributedString as = new AttributedString(word); - as.addAttribute(TextAttribute.WIDTH, wordWidth); - Word wordInstance = new Word(word); - wordInstance.setAttributes(as); - textLine.addWord(wordInstance); - start = end; - end = iterator.next(); - } - textLine.setWidth(textLine.calculateWidth(font, fontSize)); - textLines.add(textLine); - return textLines; - } - } - - /** - * An individual line of text. - */ - static class Line - { - private final List words = new ArrayList(); - private float lineWidth; - - float getWidth() - { - return lineWidth; - } - - void setWidth(float width) - { - lineWidth = width; - } - - float calculateWidth(PDFont font, float fontSize) throws IOException - { - final float scale = fontSize/FONTSCALE; - float calculatedWidth = 0f; - int indexOfWord = 0; - for (Word word : words) - { - calculatedWidth = calculatedWidth + - (Float) word.getAttributes().getIterator().getAttribute(TextAttribute.WIDTH); - String text = word.getText(); - if (indexOfWord == words.size() -1 && Character.isWhitespace(text.charAt(text.length()-1))) + int start = iterator.first(); + int end = iterator.next(); + float lineWidth = 0; + + List textLines = new ArrayList(); + Line textLine = new Line(); + + while (end != BreakIterator.DONE) + { + String word = textContent.substring(start,end); + float wordWidth = font.getStringWidth(word) * scale; + + lineWidth = lineWidth + wordWidth; + + // check if the last word would fit without the whitespace ending it + if (lineWidth >= width && Character.isWhitespace(word.charAt(word.length()-1))) + { + float whitespaceWidth = font.getStringWidth(word.substring(word.length()-1)) * scale; + lineWidth = lineWidth - whitespaceWidth; + } + + if (lineWidth >= width) + { + textLine.setWidth(textLine.calculateWidth(font, fontSize)); + textLines.add(textLine); + textLine = new Line(); + lineWidth = font.getStringWidth(word) * scale; + } + + AttributedString as = new AttributedString(word); + as.addAttribute(TextAttribute.WIDTH, wordWidth); + Word wordInstance = new Word(word); + wordInstance.setAttributes(as); + textLine.addWord(wordInstance); + start = end; + end = iterator.next(); + } + textLine.setWidth(textLine.calculateWidth(font, fontSize)); + textLines.add(textLine); + return textLines; + } + } + + /** + * An individual line of text. + */ + static class Line + { + private final List words = new ArrayList(); + private float lineWidth; + + float getWidth() + { + return lineWidth; + } + + void setWidth(float width) + { + lineWidth = width; + } + + float calculateWidth(PDFont font, float fontSize) throws IOException + { + final float scale = fontSize/FONTSCALE; + float calculatedWidth = 0f; + int indexOfWord = 0; + for (Word word : words) { - float whitespaceWidth = font.getStringWidth(text.substring(text.length()-1)) * scale; - calculatedWidth = calculatedWidth - whitespaceWidth; + calculatedWidth = calculatedWidth + + (Float) word.getAttributes().getIterator().getAttribute(TextAttribute.WIDTH); + String text = word.getText(); + if (indexOfWord == words.size() -1 && Character.isWhitespace(text.charAt(text.length()-1))) + { + float whitespaceWidth = font.getStringWidth(text.substring(text.length()-1)) * scale; + calculatedWidth = calculatedWidth - whitespaceWidth; + } + ++indexOfWord; } - ++indexOfWord; - } - return calculatedWidth; - } - - List getWords() - { - return words; - } - - float getInterWordSpacing(float width) - { - return (width - lineWidth)/(words.size()-1); - } - - void addWord(Word word) - { - words.add(word); - } - } - - /** - * An individual word. - * - * A word is defined as a string which must be kept - * on the same line. - */ - static class Word - { - private AttributedString attributedString; - private final String textContent; - - Word(String text) - { - textContent = text; - } - - String getText() - { - return textContent; - } - - AttributedString getAttributes() - { - return attributedString; - } - - void setAttributes(AttributedString as) - { - this.attributedString = as; - } - } + return calculatedWidth; + } + + List getWords() + { + return words; + } + + float getInterWordSpacing(float width) + { + return (width - lineWidth)/(words.size()-1); + } + + void addWord(Word word) + { + words.add(word); + } + } + + /** + * An individual word. + * + * A word is defined as a string which must be kept + * on the same line. + */ + static class Word + { + private AttributedString attributedString; + private final String textContent; + + Word(String text) + { + textContent = text; + } + + String getText() + { + return textContent; + } + + AttributedString getAttributes() + { + return attributedString; + } + + void setAttributes(AttributedString as) + { + this.attributedString = as; + } + } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/digitalsignature/PDSeedValue.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/digitalsignature/PDSeedValue.java index 3f5164103..a5b3756ae 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/digitalsignature/PDSeedValue.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/digitalsignature/PDSeedValue.java @@ -25,7 +25,6 @@ import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.common.COSArrayList; import com.tom_roush.pdfbox.pdmodel.common.COSObjectable; - /** * This represents a pdf signature seed value dictionary. * @@ -141,9 +140,9 @@ public void setSubFilterRequired(boolean flag) } /** - * - * @return true if the DigestMethod is required - */ + * + * @return true if the DigestMethod is required + */ public boolean isDigestMethodRequired() { return getCOSObject().getFlag( COSName.FF, FLAG_DIGEST_METHOD); @@ -160,9 +159,9 @@ public void setDigestMethodRequired(boolean flag) } /** - * - * @return true if the V entry is required - */ + * + * @return true if the V entry is required + */ public boolean isVRequired() { return getCOSObject().getFlag( COSName.FF, FLAG_V); @@ -179,9 +178,9 @@ public void setVRequired(boolean flag) } /** - * - * @return true if the Reason is required - */ + * + * @return true if the Reason is required + */ public boolean isReasonRequired() { return getCOSObject().getFlag( COSName.FF, FLAG_REASON); @@ -198,9 +197,9 @@ public void setReasonRequired(boolean flag) } /** - * - * @return true if the LegalAttestation is required - */ + * + * @return true if the LegalAttestation is required + */ public boolean isLegalAttestationRequired() { return getCOSObject().getFlag( COSName.FF, FLAG_LEGAL_ATTESTATION); @@ -217,9 +216,9 @@ public void setLegalAttestationRequired(boolean flag) } /** - * - * @return true if the AddRevInfo is required - */ + * + * @return true if the AddRevInfo is required + */ public boolean isAddRevInfoRequired() { return getCOSObject().getFlag( COSName.FF, FLAG_ADD_REV_INFO); @@ -272,7 +271,7 @@ public void setFilter(COSName filter) public List getSubFilter() { List retval = null; - COSArray fields = (COSArray)dictionary.getDictionaryObject(COSName.SUB_FILTER); + COSArray fields = dictionary.getCOSArray(COSName.SUB_FILTER); if (fields != null) { @@ -312,7 +311,7 @@ public void setSubFilter(List subfilter) public List getDigestMethod() { List retval = null; - COSArray fields = (COSArray)dictionary.getDictionaryObject(COSName.DIGEST_METHOD); + COSArray fields = dictionary.getCOSArray(COSName.DIGEST_METHOD); if (fields != null) { @@ -348,10 +347,10 @@ public void setDigestMethod(List digestMethod) for ( COSName cosName : digestMethod ) { if (!(cosName.equals(COSName.DIGEST_SHA1) - || cosName.equals(COSName.DIGEST_SHA256) - || cosName.equals(COSName.DIGEST_SHA384) - || cosName.equals(COSName.DIGEST_SHA512) - || cosName.equals(COSName.DIGEST_RIPEMD160))) + || cosName.equals(COSName.DIGEST_SHA256) + || cosName.equals(COSName.DIGEST_SHA384) + || cosName.equals(COSName.DIGEST_SHA512) + || cosName.equals(COSName.DIGEST_RIPEMD160))) { throw new IllegalArgumentException("Specified digest " + cosName.getName() + " isn't allowed."); } @@ -401,7 +400,7 @@ public void setV(float minimumRequiredCapability) public List getReasons() { List retval = null; - COSArray fields = (COSArray)dictionary.getDictionaryObject(COSName.REASONS); + COSArray fields = dictionary.getCOSArray(COSName.REASONS); if (fields != null) { @@ -562,7 +561,7 @@ public void setTimeStamp(PDSeedValueTimeStamp timestamp) public List getLegalAttestation() { List retval = null; - COSArray fields = (COSArray)dictionary.getDictionaryObject(COSName.LEGAL_ATTESTATION); + COSArray fields = dictionary.getCOSArray(COSName.LEGAL_ATTESTATION); if (fields != null) { diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/digitalsignature/PDSeedValueTimeStamp.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/digitalsignature/PDSeedValueTimeStamp.java index 3e576f55a..4fbc0471b 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/digitalsignature/PDSeedValueTimeStamp.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/digitalsignature/PDSeedValueTimeStamp.java @@ -18,10 +18,9 @@ import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; - /** - * If exist, it describe where the signature handler can request a RFC3161 - * timestamp and if it is a must have for the signature. + * This optional dictionary provides the URL for a RFC3161 time-stamping server, and whether + * the timestamp is required for the signature. * * @author Thomas Chojecki */ @@ -85,16 +84,16 @@ public void setURL(String url) */ public boolean isTimestampRequired() { - return dictionary.getInt(COSName.FT, 0) != 0; + return dictionary.getInt(COSName.FF, 0) != 0; } /** - * Sets if a timestamp is reuqired or not. + * Sets if a timestamp is required or not. * * @param flag true if a timestamp is required */ public void setTimestampRequired(boolean flag) { - dictionary.setInt(COSName.FT, flag ? 1 : 0); + dictionary.setInt(COSName.FF, flag ? 1 : 0); } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/digitalsignature/visible/PDFTemplateBuilder.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/digitalsignature/visible/PDFTemplateBuilder.java index 46e053d85..08faa9bd2 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/digitalsignature/visible/PDFTemplateBuilder.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/digitalsignature/visible/PDFTemplateBuilder.java @@ -32,7 +32,6 @@ import com.tom_roush.pdfbox.pdmodel.graphics.image.PDImageXObject; import com.tom_roush.pdfbox.pdmodel.interactive.form.PDAcroForm; import com.tom_roush.pdfbox.pdmodel.interactive.form.PDSignatureField; - /** * That class builds visible signature template which will be added in our PDF document. * @author Vakhtang Koroghlishvili @@ -96,7 +95,7 @@ public interface PDFTemplateBuilder * @throws IOException if something went wrong */ void createSignature(PDSignatureField pdSignatureField, PDPage page, String signerName) - throws IOException; + throws IOException; /** * Create AcroForm Dictionary. @@ -106,7 +105,7 @@ void createSignature(PDSignatureField pdSignatureField, PDPage page, String sign * @throws IOException if something went wrong */ void createAcroFormDictionary(PDAcroForm acroForm, PDSignatureField signatureField) - throws IOException; + throws IOException; /** * Creates SignatureRectangle. @@ -116,7 +115,7 @@ void createAcroFormDictionary(PDAcroForm acroForm, PDSignatureField signatureFie * @throws IOException if something went wrong */ void createSignatureRectangle(PDSignatureField signatureField, - PDVisibleSignDesigner properties) throws IOException; + PDVisibleSignDesigner properties) throws IOException; /** * Creates procSetArray of PDF,Text,ImageB,ImageC,ImageI. @@ -172,17 +171,17 @@ void createSignatureRectangle(PDSignatureField signatureField, * @param bbox bounding box */ void createHolderForm(PDResources holderFormResources, PDStream holderFormStream, - PDRectangle bbox); + PDRectangle bbox); /** * Creates appearance dictionary * - * @param holderForml holder XObject + * @param holderForm holder XObject * @param signatureField the signature field * @throws IOException if something went wrong */ - void createAppearanceDictionary(PDFormXObject holderForml, - PDSignatureField signatureField) throws IOException; + void createAppearanceDictionary(PDFormXObject holderForm, PDSignatureField signatureField) + throws IOException; /** * @@ -209,7 +208,7 @@ void createAppearanceDictionary(PDFormXObject holderForml, * @param holderFormResources holder form resources */ void insertInnerFormToHolderResources(PDFormXObject innerForm, - PDResources holderFormResources); + PDResources holderFormResources); /** * @@ -234,8 +233,8 @@ void insertInnerFormToHolderResources(PDFormXObject innerForm, * @throws IOException if something went wrong */ void createImageForm(PDResources imageFormResources, PDResources innerFormResource, - PDStream imageFormStream, PDRectangle bbox, AffineTransform affineTransform, - PDImageXObject img) throws IOException; + PDStream imageFormStream, PDRectangle bbox, AffineTransform affineTransform, + PDImageXObject img) throws IOException; /** * Creates the background layer form (n0). @@ -245,7 +244,7 @@ void createImageForm(PDResources imageFormResources, PDResources innerFormResour * @throws IOException if something went wrong */ void createBackgroundLayerForm(PDResources innerFormResource, PDRectangle formatter) - throws IOException; + throws IOException; /** * Inject procSetArray @@ -258,8 +257,8 @@ void createBackgroundLayerForm(PDResources innerFormResource, PDRectangle format * @param procSet procset values */ void injectProcSetArray(PDFormXObject innerForm, PDPage page, - PDResources innerFormResources, PDResources imageFormResources, - PDResources holderFormResources, COSArray procSet); + PDResources innerFormResources, PDResources imageFormResources, + PDResources holderFormResources, COSArray procSet); /** * injects appearance streams @@ -274,8 +273,8 @@ void injectProcSetArray(PDFormXObject innerForm, PDPage page, * @throws IOException if something went wrong */ void injectAppearanceStreams(PDStream holderFormStream, PDStream innerFormStream, - PDStream imageFormStream, COSName imageFormName, COSName imageName, - COSName innerFormName, PDVisibleSignDesigner properties) throws IOException; + PDStream imageFormStream, COSName imageFormName, COSName imageName, + COSName innerFormName, PDVisibleSignDesigner properties) throws IOException; /** * just to create visible signature @@ -292,7 +291,7 @@ void injectAppearanceStreams(PDStream holderFormStream, PDStream innerFormStream * @throws IOException if something went wrong */ void createWidgetDictionary(PDSignatureField signatureField, - PDResources holderFormResources) throws IOException; + PDResources holderFormResources) throws IOException; /** * diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/digitalsignature/visible/PDVisibleSigBuilder.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/digitalsignature/visible/PDVisibleSigBuilder.java index 6374e99ff..1e77764d0 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/digitalsignature/visible/PDVisibleSigBuilder.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/digitalsignature/visible/PDVisibleSigBuilder.java @@ -42,7 +42,6 @@ import com.tom_roush.pdfbox.pdmodel.interactive.form.PDAcroForm; import com.tom_roush.pdfbox.pdmodel.interactive.form.PDField; import com.tom_roush.pdfbox.pdmodel.interactive.form.PDSignatureField; - /** * Implementation of {@link PDFTemplateBuilder}. This builds the signature PDF but doesn't keep the * elements, these are kept in its PDF template structure. @@ -66,7 +65,7 @@ public PDVisibleSigBuilder() public void createPage(PDVisibleSignDesigner properties) { PDPage page = new PDPage(new PDRectangle(properties.getPageWidth(), - properties.getPageHeight())); + properties.getPageHeight())); pdfStructure.setPage(page); Log.i("PdfBox-Android", "PDF page has been created"); } @@ -111,7 +110,7 @@ public void createSignatureField(PDAcroForm acroForm) throws IOException @Override public void createSignature(PDSignatureField pdSignatureField, PDPage page, String signerName) - throws IOException + throws IOException { PDSignature pdSignature = new PDSignature(); PDAnnotationWidget widget = pdSignatureField.getWidgets().get(0); @@ -128,7 +127,7 @@ public void createSignature(PDSignatureField pdSignatureField, PDPage page, Stri @Override public void createAcroFormDictionary(PDAcroForm acroForm, PDSignatureField signatureField) - throws IOException + throws IOException { @SuppressWarnings("unchecked") List acroFormFields = acroForm.getFields(); @@ -145,14 +144,14 @@ public void createAcroFormDictionary(PDAcroForm acroForm, PDSignatureField signa @Override public void createSignatureRectangle(PDSignatureField signatureField, - PDVisibleSignDesigner properties) throws IOException + PDVisibleSignDesigner properties) throws IOException { PDRectangle rect = new PDRectangle(); rect.setUpperRightX(properties.getxAxis() + properties.getWidth()); rect.setUpperRightY(properties.getTemplateHeight() - properties.getyAxis()); rect.setLowerLeftY(properties.getTemplateHeight() - properties.getyAxis() - - properties.getHeight()); + properties.getHeight()); rect.setLowerLeftX(properties.getxAxis()); signatureField.getWidgets().get(0).setRectangle(rect); pdfStructure.setSignatureRectangle(rect); @@ -169,7 +168,7 @@ public void createSignatureRectangle(PDSignatureField signatureField, public void createAffineTransform(byte[] params) { AffineTransform transform = new AffineTransform(params[0], params[1], params[2], - params[3], params[4], params[5]); + params[3], params[4], params[5]); pdfStructure.setAffineTransform(transform); Log.i("PdfBox-Android", "Matrix has been added"); } @@ -252,7 +251,7 @@ public void createHolderFormResources() @Override public void createHolderForm(PDResources holderFormResources, PDStream holderFormStream, - PDRectangle bbox) + PDRectangle bbox) { PDFormXObject holderForm = new PDFormXObject(holderFormStream); holderForm.setResources(holderFormResources); @@ -264,13 +263,13 @@ public void createHolderForm(PDResources holderFormResources, PDStream holderFor } @Override - public void createAppearanceDictionary(PDFormXObject holderForml, - PDSignatureField signatureField) throws IOException + public void createAppearanceDictionary(PDFormXObject holderForm, PDSignatureField signatureField) + throws IOException { PDAppearanceDictionary appearance = new PDAppearanceDictionary(); appearance.getCOSObject().setDirect(true); - PDAppearanceStream appearanceStream = new PDAppearanceStream(holderForml.getCOSObject()); + PDAppearanceStream appearanceStream = new PDAppearanceStream(holderForm.getCOSObject()); appearance.setNormalAppearance(appearanceStream); signatureField.getWidgets().get(0).setAppearance(appearance); @@ -285,7 +284,7 @@ public void createInnerFormStream(PDDocument template) PDStream innerFormStream = new PDStream(template); pdfStructure.setInnterFormStream(innerFormStream); Log.i("PdfBox-Android", "Stream of another form (inner form - it will be inside holder form) " + - "has been created"); + "has been created"); } @Override @@ -294,13 +293,13 @@ public void createInnerFormResource() PDResources innerFormResources = new PDResources(); pdfStructure.setInnerFormResources(innerFormResources); Log.i("PdfBox-Android", "Resources of another form (inner form - it will be inside holder form)" + - "have been created"); + "have been created"); } @Override public void createInnerForm(PDResources innerFormResources, - PDStream innerFormStream, - PDRectangle bbox) + PDStream innerFormStream, + PDRectangle bbox) { PDFormXObject innerForm = new PDFormXObject(innerFormStream); innerForm.setResources(innerFormResources); @@ -312,7 +311,7 @@ public void createInnerForm(PDResources innerFormResources, @Override public void insertInnerFormToHolderResources(PDFormXObject innerForm, - PDResources holderFormResources) + PDResources holderFormResources) { holderFormResources.put(COSName.FRM, innerForm); pdfStructure.setInnerFormName(COSName.FRM); @@ -337,8 +336,8 @@ public void createImageFormResources() @Override public void createImageForm(PDResources imageFormResources, PDResources innerFormResource, - PDStream imageFormStream, PDRectangle bbox, AffineTransform at, - PDImageXObject img) throws IOException + PDStream imageFormStream, PDRectangle bbox, AffineTransform at, + PDImageXObject img) throws IOException { PDFormXObject imageForm = new PDFormXObject(imageFormStream); imageForm.setBBox(bbox); @@ -359,7 +358,7 @@ public void createImageForm(PDResources imageFormResources, PDResources innerFor @Override public void createBackgroundLayerForm(PDResources innerFormResource, PDRectangle bbox) - throws IOException + throws IOException { // create blank n0 background layer form PDFormXObject n0Form = new PDFormXObject(pdfStructure.getTemplate().getDocument().createCOSStream()); @@ -372,8 +371,8 @@ public void createBackgroundLayerForm(PDResources innerFormResource, PDRectangle @Override public void injectProcSetArray(PDFormXObject innerForm, PDPage page, - PDResources innerFormResources, PDResources imageFormResources, - PDResources holderFormResources, COSArray procSet) + PDResources innerFormResources, PDResources imageFormResources, + PDResources holderFormResources, COSArray procSet) { innerForm.getResources().getCOSObject().setItem(COSName.PROC_SET, procSet); page.getCOSObject().setItem(COSName.PROC_SET, procSet); @@ -385,9 +384,9 @@ public void injectProcSetArray(PDFormXObject innerForm, PDPage page, @Override public void injectAppearanceStreams(PDStream holderFormStream, PDStream innerFormStream, - PDStream imageFormStream, COSName imageFormName, - COSName imageName, COSName innerFormName, - PDVisibleSignDesigner properties) throws IOException + PDStream imageFormStream, COSName imageFormName, + COSName imageName, COSName innerFormName, + PDVisibleSignDesigner properties) throws IOException { // Use width and height of BBox as values for transformation matrix. int width = (int) this.getStructure().getFormatterRectangle().getWidth(); @@ -418,7 +417,7 @@ public void createVisualSignature(PDDocument template) @Override public void createWidgetDictionary(PDSignatureField signatureField, - PDResources holderFormResources) throws IOException + PDResources holderFormResources) throws IOException { COSDictionary widgetDict = signatureField.getWidgets().get(0).getCOSObject(); widgetDict.setNeedToBeUpdated(true); diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/documentnavigation/outline/PDOutlineItem.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/documentnavigation/outline/PDOutlineItem.java index 06b245bea..9eb1033e3 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/documentnavigation/outline/PDOutlineItem.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/documentnavigation/outline/PDOutlineItem.java @@ -35,7 +35,6 @@ import com.tom_roush.pdfbox.pdmodel.interactive.documentnavigation.destination.PDNamedDestination; import com.tom_roush.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageDestination; import com.tom_roush.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageXYZDestination; - /** * This represents an outline item in a pdf document. The items at each level of the hierarchy form an iterable linked * list, chained together through their Prev and Next entries. @@ -69,7 +68,7 @@ public PDOutlineItem( COSDictionary dic ) * Insert a single sibling after this node. * * @param newSibling The item to insert. - * @throws IllegalArgumentException if the given sibling node is part of a list + * @throws IllegalArgumentException if the given sibling node is part of a list * (i.e. if it has a previous or a next sibling) */ public void insertSiblingAfter(PDOutlineItem newSibling) @@ -96,7 +95,7 @@ else if (parent != null) * Insert a single sibling before this node. * * @param newSibling The item to insert. - * @throws IllegalArgumentException if the given sibling node is part of a list + * @throws IllegalArgumentException if the given sibling node is part of a list * (i.e. if it has a previous or a next sibling) */ public void insertSiblingBefore(PDOutlineItem newSibling) @@ -301,7 +300,7 @@ public void setAction( PDAction action ) public PDStructureElement getStructureElement() { PDStructureElement se = null; - COSDictionary dic = (COSDictionary) getCOSObject().getDictionaryObject(COSName.SE); + COSDictionary dic = getCOSObject().getCOSDictionary(COSName.SE); if( dic != null ) { se = new PDStructureElement( dic ); @@ -327,7 +326,7 @@ public void setStructureElement( PDStructureElement structureElement ) */ public PDColor getTextColor() { - COSArray csValues = (COSArray) getCOSObject().getDictionaryObject(COSName.C); + COSArray csValues = getCOSObject().getCOSArray(COSName.C); if( csValues == null ) { csValues = new COSArray(); diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/documentnavigation/outline/PDOutlineItemIterator.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/documentnavigation/outline/PDOutlineItemIterator.java index ab423c518..15bf967bc 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/documentnavigation/outline/PDOutlineItemIterator.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/documentnavigation/outline/PDOutlineItemIterator.java @@ -16,9 +16,12 @@ */ package com.tom_roush.pdfbox.pdmodel.interactive.documentnavigation.outline; +import java.util.HashSet; import java.util.Iterator; import java.util.NoSuchElementException; +import java.util.Set; +import com.tom_roush.pdfbox.cos.COSDictionary; /** * Iterator over the linked list of {@link PDOutlineItem} siblings. * @@ -29,6 +32,7 @@ class PDOutlineItemIterator implements Iterator { private PDOutlineItem currentItem; private final PDOutlineItem startingItem; + private final Set visited = new HashSet(); PDOutlineItemIterator(PDOutlineItem startingItem) { @@ -47,7 +51,7 @@ public boolean hasNext() return true; } PDOutlineItem sibling = currentItem.getNextSibling(); - return sibling != null && !startingItem.equals(sibling); + return sibling != null && !visited.contains(sibling.getCOSObject()); } @Override @@ -65,6 +69,7 @@ public PDOutlineItem next() { currentItem = currentItem.getNextSibling(); } + visited.add(currentItem.getCOSObject()); return currentItem; } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/documentnavigation/outline/PDOutlineNode.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/documentnavigation/outline/PDOutlineNode.java index 3553fb24a..43a244b4b 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/documentnavigation/outline/PDOutlineNode.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/documentnavigation/outline/PDOutlineNode.java @@ -16,13 +16,14 @@ */ package com.tom_roush.pdfbox.pdmodel.interactive.documentnavigation.outline; +import android.util.Log; + import java.util.Iterator; import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.common.PDDictionaryWrapper; - /** * Base class for a node in the outline of a PDF document. * @@ -291,6 +292,12 @@ void updateParentOpenCount(int delta) PDOutlineNode parent = getParent(); if (parent != null) { + if (getCOSObject() == parent.getCOSObject()) + { + // PDFBOX-5939 + Log.w("PdfBox-Android", "Outline parent points to itself"); + return; + } if (parent.isNodeOpen()) { parent.setOpenCount(parent.getOpenCount() + delta); diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java index a16e6df34..64adf2c2d 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java @@ -26,6 +26,7 @@ import java.io.OutputStream; import java.util.ArrayList; import java.util.List; +import java.util.regex.Pattern; import com.tom_roush.fontbox.util.BoundingBox; import com.tom_roush.harmony.awt.geom.AffineTransform; @@ -54,7 +55,6 @@ import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDBorderStyleDictionary; import com.tom_roush.pdfbox.util.Matrix; - /** * Create the AcroForms field appearance helper. * @@ -62,9 +62,12 @@ * @author Ben Litchfield */ class AppearanceGeneratorHelper { + private static final Operator BMC = Operator.getOperator("BMC"); private static final Operator EMC = Operator.getOperator("EMC"); + private static final Pattern PATTERN = Pattern.compile("\\u000D\\u000A|[\\u000A\\u000B\\u000C\\u000D\\u0085\\u2028\\u2029]"); + private final PDVariableText field; private PDDefaultAppearanceString defaultAppearance; @@ -95,7 +98,6 @@ class AppearanceGeneratorHelper { * The minimum/maximum font sizes used for multiline text auto sizing */ private static final float MINIMUM_FONT_SIZE = 4; - private static final float MAXIMUM_FONT_SIZE = 300; /** * The default padding applied by Acrobat to the fields bbox. @@ -116,7 +118,7 @@ class AppearanceGeneratorHelper { this.defaultAppearance = field.getDefaultAppearanceString(); } catch (IOException ex) { throw new IOException("Could not process default appearance string '" + field.getDefaultAppearance() - + "' for field '" + field.getFullyQualifiedName() + "'", ex); + + "' for field '" + field.getFullyQualifiedName() + "'", ex); } } @@ -128,12 +130,12 @@ private void validateAndEnsureAcroFormResources() { // add font resources which might be available at the field // level but are not at the AcroForm level to the AcroForm // to match Adobe Reader/Acrobat behavior - if (field.getAcroForm().getDefaultResources() == null) { + PDResources acroFormResources = field.getAcroForm().getDefaultResources(); + if (acroFormResources == null) + { return; } - PDResources acroFormResources = field.getAcroForm().getDefaultResources(); - for (PDAnnotationWidget widget : field.getWidgets()) { PDAppearanceStream stream = widget.getNormalAppearanceStream(); @@ -147,9 +149,9 @@ private void validateAndEnsureAcroFormResources() { continue; } COSDictionary widgetFontDict = widgetResources.getCOSObject() - .getCOSDictionary(COSName.FONT); + .getCOSDictionary(COSName.FONT); COSDictionary acroFormFontDict = acroFormResources.getCOSObject() - .getCOSDictionary(COSName.FONT); + .getCOSDictionary(COSName.FONT); for (COSName fontResourceName : widgetResources.getFontNames()) { try @@ -159,7 +161,7 @@ private void validateAndEnsureAcroFormResources() { Log.d("PdfBox-Android", "Adding font resource " + fontResourceName + " from widget to AcroForm"); // use the COS-object to preserve a possible indirect object reference acroFormFontDict.setItem(fontResourceName, - widgetFontDict.getItem(fontResourceName)); + widgetFontDict.getItem(fontResourceName)); } } catch (IOException e) @@ -175,6 +177,8 @@ private void validateAndEnsureAcroFormResources() { * * @param apValue the String value which the appearance should represent * @throws IOException If there is an error creating the stream. + * @throws IllegalArgumentException if the string contains a character that is not in the field + * font, see {@link #setDefaultAppearance(java.lang.String)}. */ public void setAppearanceValue(String apValue) throws IOException { value = getFormattedValue(apValue); @@ -185,8 +189,9 @@ public void setAppearanceValue(String apValue) throws IOException { // set programmatically and Reader is forced to generate the appearance // using PDAcroForm.setNeedAppearances // see PDFBOX-3911 - if (field instanceof PDTextField && !((PDTextField) field).isMultiline()) { - value = value.replaceAll("\\u000D\\u000A|[\\u000A\\u000B\\u000C\\u000D\\u0085\\u2028\\u2029]", " "); + if (field instanceof PDTextField && !((PDTextField) field).isMultiline()) + { + value = PATTERN.matcher(value).replaceAll(" "); } for (PDAnnotationWidget widget : field.getWidgets()) { @@ -209,7 +214,7 @@ public void setAppearanceValue(String apValue) throws IOException { if (rect == null) { widget.getCOSObject().removeItem(COSName.AP); Log.w("PdfBox-Android", "widget of field " + field.getFullyQualifiedName() - + " has no rectangle, no appearance stream created"); + + " has no rectangle, no appearance stream created"); continue; } @@ -231,7 +236,7 @@ public void setAppearanceValue(String apValue) throws IOException { // TODO support appearances other than "normal" } PDAppearanceCharacteristicsDictionary appearanceCharacteristics = - widget.getAppearanceCharacteristics(); + widget.getAppearanceCharacteristics(); /* * Adobe Acrobat always recreates the complete appearance stream if there is an @@ -265,13 +270,13 @@ private String getFormattedValue(String apValue) PDAction actionF = actions.getF(); if (actionF != null) { - if (field.getAcroForm().getScriptingHandler() != null) + ScriptingHandler scriptingHandler = field.getAcroForm().getScriptingHandler(); + if (scriptingHandler != null) { - ScriptingHandler scriptingHandler = field.getAcroForm().getScriptingHandler(); return scriptingHandler.format((PDActionJavaScript) actionF, apValue); } Log.i("PdfBox-Android", "Field contains a formatting action but no ScriptingHandler " + - "has been supplied - formatted value might be incorrect"); + "has been supplied - formatted value might be incorrect"); } return apValue; } @@ -340,13 +345,13 @@ private int resolveRotation(PDAnnotationWidget widget) { * @throws IOException in case we can't write to the appearance stream */ private void initializeAppearanceContent(PDAnnotationWidget widget, - PDAppearanceCharacteristicsDictionary appearanceCharacteristics, - PDAppearanceStream appearanceStream) - throws IOException + PDAppearanceCharacteristicsDictionary appearanceCharacteristics, + PDAppearanceStream appearanceStream) + throws IOException { ByteArrayOutputStream output = new ByteArrayOutputStream(); PDPageContentStream contents = new PDPageContentStream(field.getAcroForm().getDocument(), appearanceStream, - output); + output); // TODO: support more entries like patterns, etc. if (appearanceCharacteristics != null) { @@ -376,7 +381,24 @@ private void initializeAppearanceContent(PDAnnotationWidget widget, PDRectangle bbox = resolveBoundingBox(widget, appearanceStream); PDRectangle clipRect = applyPadding(bbox, Math.max(DEFAULT_PADDING, lineWidth / 2)); contents.addRect(clipRect.getLowerLeftX(), clipRect.getLowerLeftY(), clipRect.getWidth(), - clipRect.getHeight()); + clipRect.getHeight()); + contents.closeAndStroke(); + } + + // draw the dividers for a comb field + if (borderColour != null && shallComb()) { + int maxLen = ((PDTextField) field).getMaxLen(); + PDRectangle bbox = resolveBoundingBox(widget, appearanceStream); + PDRectangle clipRect = applyPadding(bbox, Math.max(DEFAULT_PADDING, lineWidth/2)); + float lowerLeft = clipRect.getLowerLeftX(); + float height = clipRect.getHeight(); + + float combWidth = bbox.getWidth() / maxLen; + + for (int i= 0; i < maxLen - 1; i++) { + contents.moveTo(combWidth + combWidth * i, height); + contents.lineTo(combWidth + combWidth * i, lowerLeft); + } contents.closeAndStroke(); } } @@ -399,7 +421,7 @@ private List tokenize(PDAppearanceStream appearanceStream) throws IOExce * Constructs and sets new contents for given appearance stream. */ private void setAppearanceContent(PDAnnotationWidget widget, PDAppearanceStream appearanceStream) - throws IOException { + throws IOException { // first copy any needed resources from the document’s DR dictionary into // the stream’s Resources dictionary defaultAppearance.copyNeededResourcesTo(appearanceStream); @@ -440,9 +462,9 @@ private void setAppearanceContent(PDAnnotationWidget widget, PDAppearanceStream * Generate and insert text content and clipping around it. */ private void insertGeneratedAppearance(PDAnnotationWidget widget, PDAppearanceStream appearanceStream, - OutputStream output) throws IOException { + OutputStream output) throws IOException { PDPageContentStream contents = new PDPageContentStream(field.getAcroForm().getDocument(), appearanceStream, - output); + output); PDRectangle bbox = resolveBoundingBox(widget, appearanceStream); @@ -455,29 +477,35 @@ private void insertGeneratedAppearance(PDAnnotationWidget widget, PDAppearanceSt if (widget.getBorderStyle() != null) { borderWidth = widget.getBorderStyle().getWidth(); } - PDRectangle clipRect = applyPadding(bbox, Math.max(1f, borderWidth)); - PDRectangle contentRect = applyPadding(clipRect, Math.max(1f, borderWidth)); + float padding = Math.max(1f, borderWidth); + PDRectangle clipRect = applyPadding(bbox, padding); + float clipRectLowerLeftY = clipRect.getLowerLeftY(); + float clipRectHeight = clipRect.getHeight(); + + PDRectangle contentRect = applyPadding(clipRect, padding); contents.saveGraphicsState(); // Acrobat always adds a clipping path - contents.addRect(clipRect.getLowerLeftX(), clipRect.getLowerLeftY(), clipRect.getWidth(), clipRect.getHeight()); + contents.addRect(clipRect.getLowerLeftX(), clipRectLowerLeftY, clipRect.getWidth(), clipRectHeight); contents.clip(); // get the font PDFont font = defaultAppearance.getFont(); - if (font == null) { + if (font == null) + { throw new IllegalArgumentException("font is null, check whether /DA entry is incomplete or incorrect"); } - if (font.getName().contains("+")) { + if (font.getName() != null && font.getName().contains("+")) + { Log.w("PdfBox-Android", "Font '" + defaultAppearance.getFontName().getName() + "' of field '" - + field.getFullyQualifiedName() + "' contains subsetted font '" + font.getName() + "'"); + + field.getFullyQualifiedName() + "' contains subsetted font '" + font.getName() + "'"); Log.w("PdfBox-Android", "This may bring trouble with PDField.setValue(), PDAcroForm.flatten() or " - + "PDAcroForm.refreshAppearances()"); + + "PDAcroForm.refreshAppearances()"); Log.w("PdfBox-Android", "You should replace this font with a non-subsetted font:"); Log.w("PdfBox-Android", "PDFont font = PDType0Font.load(doc, new FileInputStream(fontfile), false);"); Log.w("PdfBox-Android", "acroForm.getDefaultResources().put(COSName.getPDFName(\"" - + defaultAppearance.getFontName().getName() + "\", font);"); + + defaultAppearance.getFontName().getName() + "\", font);"); } // calculate the fontSize (because 0 = autosize) @@ -520,22 +548,28 @@ private void insertGeneratedAppearance(PDAnnotationWidget widget, PDAppearanceSt fontDescentAtSize = fontDescent * fontScaleY; } - if (field instanceof PDTextField && ((PDTextField) field).isMultiline()) { + if (field instanceof PDTextField && ((PDTextField) field).isMultiline()) + { y = contentRect.getUpperRightY() - fontBoundingBoxAtSize; - } else { - // Adobe shows the text 'shifted up' in case the caps don't fit into the - // clipping area - if (fontCapAtSize > clipRect.getHeight()) { - y = clipRect.getLowerLeftY() + -fontDescentAtSize; - } else { + } + else + { + // Adobe shows the text 'shifted up' in case the caps don't fit into the clipping area + if (fontCapAtSize > clipRectHeight) + { + y = clipRectLowerLeftY + -fontDescentAtSize; + } + else + { // calculate the position based on the content rectangle - y = clipRect.getLowerLeftY() + (clipRect.getHeight() - fontCapAtSize) / 2; + y = clipRectLowerLeftY + (clipRectHeight - fontCapAtSize) / 2; // check to ensure that ascents and descents fit - if (y - clipRect.getLowerLeftY() < -fontDescentAtSize) { - - float fontDescentBased = -fontDescentAtSize + contentRect.getLowerLeftY(); - float fontCapBased = contentRect.getHeight() - contentRect.getLowerLeftY() - fontCapAtSize; + if (y - clipRectLowerLeftY < -fontDescentAtSize) + { + float contentRectLowerLeftY = contentRect.getLowerLeftY(); + float fontDescentBased = -fontDescentAtSize + contentRectLowerLeftY; + float fontCapBased = contentRect.getHeight() - contentRectLowerLeftY - fontCapAtSize; y = Math.min(fontDescentBased, Math.max(y, fontCapBased)); } @@ -561,8 +595,8 @@ private void insertGeneratedAppearance(PDAnnotationWidget widget, PDAppearanceSt appearanceStyle.setLeading(font.getBoundingBox().getHeight() * fontScaleY); PlainTextFormatter formatter = new PlainTextFormatter.Builder(contents).style(appearanceStyle) - .text(textContent).width(contentRect.getWidth()).wrapLines(isMultiLine()).initialOffset(x, y) - .textAlign(getTextAlign(widget)).build(); + .text(textContent).width(contentRect.getWidth()).wrapLines(isMultiLine()).initialOffset(x, y) + .textAlign(getTextAlign(widget)).build(); formatter.format(); } @@ -623,8 +657,12 @@ private boolean isMultiLine() { * @return the comb state */ private boolean shallComb() { - return field instanceof PDTextField && ((PDTextField) field).isComb() && !((PDTextField) field).isMultiline() - && !((PDTextField) field).isPassword() && !((PDTextField) field).isFileSelect(); + return field instanceof PDTextField && + ((PDTextField) field).isComb() && + ((PDTextField) field).getMaxLen() != -1 && + !((PDTextField) field).isMultiline() && + !((PDTextField) field).isPassword() && + !((PDTextField) field).isFileSelect(); } /** @@ -637,32 +675,40 @@ private boolean shallComb() { * @throws IOException */ private void insertGeneratedCombAppearance(PDPageContentStream contents, PDAppearanceStream appearanceStream, - PDFont font, float fontSize) throws IOException { + PDFont font, float fontSize) throws IOException + { + if (value == null || value.isEmpty()) + { + return; + } int maxLen = ((PDTextField) field).getMaxLen(); int quadding = field.getQ(); int numChars = Math.min(value.length(), maxLen); - PDRectangle paddingEdge = applyPadding(appearanceStream.getBBox(), 1); - - float combWidth = appearanceStream.getBBox().getWidth() / maxLen; + PDRectangle bBox = appearanceStream.getBBox(); + float combWidth = bBox.getWidth() / maxLen; float ascentAtFontSize = font.getFontDescriptor().getAscent() / FONTSCALE * fontSize; - float baselineOffset = paddingEdge.getLowerLeftY() - + (appearanceStream.getBBox().getHeight() - ascentAtFontSize) / 2; + + float baselineOffset = bBox.getLowerLeftY() + (bBox.getHeight() - ascentAtFontSize) / 2; float prevCharWidth = 0f; - float xOffset = combWidth / 2; + // set initial offset based on width of first char. + float firstCharWidth = font.getStringWidth(value.substring(0, 1)) / FONTSCALE * fontSize; + float initialOffset = (combWidth - firstCharWidth)/2; // add to initial offset if right aligned or centered if (quadding == 2) { - xOffset = xOffset + (maxLen - numChars) * combWidth; + initialOffset = initialOffset + (maxLen - numChars) * combWidth; } else if (quadding == 1) { - xOffset = xOffset + (maxLen - numChars) / 2 * combWidth; + initialOffset = initialOffset + (maxLen - numChars) / 2 * combWidth; } + float xOffset = initialOffset; + for (int i = 0; i < numChars; i++) { String combString = value.substring(i, i + 1); @@ -670,7 +716,11 @@ else if (quadding == 1) xOffset = xOffset + prevCharWidth / 2 - currCharWidth / 2; - contents.newLineAtOffset(xOffset, baselineOffset); + if (i == 0) { + contents.newLineAtOffset(initialOffset, baselineOffset); + } else { + contents.newLineAtOffset(xOffset, baselineOffset); + } contents.showText(combString); baselineOffset = 0; @@ -680,7 +730,7 @@ else if (quadding == 1) } private void insertGeneratedListboxSelectionHighlight(PDPageContentStream contents, - PDAppearanceStream appearanceStream, PDFont font, float fontSize) throws IOException + PDAppearanceStream appearanceStream, PDFont font, float fontSize) throws IOException { PDListBox listBox = (PDListBox) field; List indexEntries = listBox.getSelectedOptionsIndex(); @@ -711,15 +761,15 @@ private void insertGeneratedListboxSelectionHighlight(PDPageContentStream conten contents.setNonStrokingColor(HIGHLIGHT_COLOR[0], HIGHLIGHT_COLOR[1], HIGHLIGHT_COLOR[2]); contents.addRect(paddingEdge.getLowerLeftX(), - paddingEdge.getUpperRightY() - highlightBoxHeight * (selectedIndex - topIndex + 1) + 2, - paddingEdge.getWidth(), highlightBoxHeight); + paddingEdge.getUpperRightY() - highlightBoxHeight * (selectedIndex - topIndex + 1) + 2, + paddingEdge.getWidth(), highlightBoxHeight); contents.fill(); } contents.setNonStrokingColor(0f); } private void insertGeneratedListboxAppearance(PDPageContentStream contents, PDAppearanceStream appearanceStream, - PDRectangle contentRect, PDFont font, float fontSize) throws IOException { + PDRectangle contentRect, PDFont font, float fontSize) throws IOException { contents.setNonStrokingColor(0f); int q = field.getQ(); @@ -816,21 +866,27 @@ private float calculateFontSize(PDFont font, PDRectangle contentRect) throws IOE // Acrobat defaults to 12 for multiline text with size 0 return DEFAULT_FONT_SIZE; } else { - float yScalingFactor = FONTSCALE * font.getFontMatrix().getScaleY(); - float xScalingFactor = FONTSCALE * font.getFontMatrix().getScaleX(); + Matrix fontMatrix = font.getFontMatrix(); + float yScalingFactor = FONTSCALE * fontMatrix.getScaleY(); + float xScalingFactor = FONTSCALE * fontMatrix.getScaleX(); // fit width - float width = font.getStringWidth(value) * font.getFontMatrix().getScaleX(); + float width = font.getStringWidth(value) * fontMatrix.getScaleX(); float widthBasedFontSize = contentRect.getWidth() / width * xScalingFactor; // fit height float height = (font.getFontDescriptor().getCapHeight() + -font.getFontDescriptor().getDescent()) - * font.getFontMatrix().getScaleY(); + * fontMatrix.getScaleY(); if (height <= 0) { - height = font.getBoundingBox().getHeight() * font.getFontMatrix().getScaleY(); + height = font.getBoundingBox().getHeight() * fontMatrix.getScaleY(); } float heightBasedFontSize = contentRect.getHeight() / height * yScalingFactor; + if (Float.isInfinite(widthBasedFontSize)) + { + // PDFBOX-5763: avoids -Infinity if empty value and tiny rectangle + return heightBasedFontSize; + } return Math.min(heightBasedFontSize, widthBasedFontSize); } @@ -924,6 +980,6 @@ private PDRectangle resolveBoundingBox(PDAnnotationWidget fieldWidget, PDAppeara */ private PDRectangle applyPadding(PDRectangle box, float padding) { return new PDRectangle(box.getLowerLeftX() + padding, box.getLowerLeftY() + padding, - box.getWidth() - 2 * padding, box.getHeight() - 2 * padding); + box.getWidth() - 2 * padding, box.getHeight() - 2 * padding); } -} \ No newline at end of file +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDAcroForm.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDAcroForm.java index ab25a2b52..6650d282a 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDAcroForm.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDAcroForm.java @@ -17,10 +17,12 @@ package com.tom_roush.pdfbox.pdmodel.interactive.form; import android.graphics.Path; +import android.graphics.Rect; import android.graphics.RectF; import android.util.Log; import java.io.IOException; +import java.lang.ref.SoftReference; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -34,11 +36,10 @@ import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; -import com.tom_roush.pdfbox.cos.COSNumber; import com.tom_roush.pdfbox.pdmodel.PDDocument; import com.tom_roush.pdfbox.pdmodel.PDPage; -import com.tom_roush.pdfbox.pdmodel.PDPageContentStream; import com.tom_roush.pdfbox.pdmodel.PDPageContentStream.AppendMode; +import com.tom_roush.pdfbox.pdmodel.PDPageContentStream; import com.tom_roush.pdfbox.pdmodel.PDPageTree; import com.tom_roush.pdfbox.pdmodel.PDResources; import com.tom_roush.pdfbox.pdmodel.common.COSArrayList; @@ -48,12 +49,12 @@ import com.tom_roush.pdfbox.pdmodel.fdf.FDFDictionary; import com.tom_roush.pdfbox.pdmodel.fdf.FDFDocument; import com.tom_roush.pdfbox.pdmodel.fdf.FDFField; +import com.tom_roush.pdfbox.pdmodel.font.PDFont; import com.tom_roush.pdfbox.pdmodel.graphics.form.PDFormXObject; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotation; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream; import com.tom_roush.pdfbox.util.Matrix; - /** * An interactive form, also known as an AcroForm. * @@ -61,6 +62,7 @@ */ public final class PDAcroForm implements COSObjectable { + private static final int FLAG_SIGNATURES_EXIST = 1; private static final int FLAG_APPEND_ONLY = 1 << 1; @@ -71,6 +73,8 @@ public final class PDAcroForm implements COSObjectable private ScriptingHandler scriptingHandler; + private final Map> directFontCache = new HashMap>(); + /** * Constructor. * @@ -221,9 +225,9 @@ public void flatten(List fields, boolean refreshAppearances) throws IOE if (!refreshAppearances && getNeedAppearances()) { Log.w("PdfBox-Android", "acroForm.getNeedAppearances() returns true, " + - "visual field appearances may not have been set"); + "visual field appearances may not have been set"); Log.w("PdfBox-Android", "call acroForm.refreshAppearances() or " + - "use the flatten() method with refreshAppearances parameter"); + "use the flatten() method with refreshAppearances parameter"); } // for dynamic XFA forms there is no flatten as this would mean to do a rendering @@ -264,7 +268,7 @@ public void flatten(List fields, boolean refreshAppearances) throws IOE else if (isVisibleAnnotation(annotation)) { PDPageContentStream contentStream = new PDPageContentStream( - document, page, AppendMode.APPEND, true, !isContentStreamWrapped); + document, page, AppendMode.APPEND, true, !isContentStreamWrapped); try { isContentStreamWrapped = true; @@ -322,7 +326,7 @@ private boolean isVisibleAnnotation(PDAnnotation annotation) } /** - * Refreshes the appearance streams and appearance dictionaries for + * Refreshes the appearance streams and appearance dictionaries for * the widget annotations of all fields. * * @throws IOException @@ -339,7 +343,7 @@ public void refreshAppearances() throws IOException } /** - * Refreshes the appearance streams and appearance dictionaries for + * Refreshes the appearance streams and appearance dictionaries for * the widget annotations of the specified fields. * * @param fields @@ -363,7 +367,7 @@ public void refreshAppearances(List fields) throws IOException * A field might have children that are fields (non-terminal field) or does not * have children which are fields (terminal fields). * - * The fields within an AcroForm are organized in a tree structure. The documents root fields + * The fields within an AcroForm are organized in a tree structure. The documents root fields * might either be terminal fields, non-terminal fields or a mixture of both. Non-terminal fields * mark branches which contents can be retrieved using {@link PDNonTerminalField#getChildren()}. * @@ -458,7 +462,7 @@ public boolean isCachingFields() * This will get a field by name, possibly using the cache if setCache is true. * * @param fullyQualifiedName The name of the field to get. - * @return The field with that name of null if one was not found. + * @return The first field with that name of null if one was not found. */ public PDField getField(String fullyQualifiedName) { @@ -471,7 +475,8 @@ public PDField getField(String fullyQualifiedName) // get the field from the field tree for (PDField field : getFieldTree()) { - if (field.getFullyQualifiedName().equals(fullyQualifiedName)) + String fqn = field.getFullyQualifiedName(); + if (fqn == null ? fullyQualifiedName == null : fqn.equals(fullyQualifiedName)) { return field; } @@ -533,7 +538,8 @@ public PDResources getDefaultResources() COSBase base = dictionary.getDictionaryObject(COSName.DR); if (base instanceof COSDictionary) { - retval = new PDResources((COSDictionary) base, document.getResourceCache()); + retval = new PDResources((COSDictionary) base, document.getResourceCache(), + directFontCache); } return retval; } @@ -596,7 +602,7 @@ public void setXFA(PDXFAResource xfa) /** * This will get the document-wide default value for the quadding/justification of variable text - * fields. + * fields. *

      * 0 - Left(default)
      * 1 - Centered
      @@ -607,13 +613,7 @@ public void setXFA(PDXFAResource xfa) */ public int getQ() { - int retval = 0; - COSNumber number = (COSNumber)dictionary.getDictionaryObject(COSName.Q); - if (number != null) - { - retval = number.intValue(); - } - return retval; + return dictionary.getInt(COSName.Q, 0); } /** @@ -667,6 +667,57 @@ public void setAppendOnly(boolean appendOnly) dictionary.setFlag(COSName.SIG_FLAGS, FLAG_APPEND_ONLY, appendOnly); } + /** + * Return the calculation order in which field values should be recalculated when the value of + * any field changes. (Read about "Trigger Events" in the PDF specification) + * + * @return field list. Note these objects may not be identical to PDField objects retrieved from + * other methods (depending on cache setting). The best strategy is to call + * {@link #getCOSObject()} to check for identity. The list is not backed by the /CO COSArray in + * the document. + */ + public List getCalcOrder() + { + COSArray co = dictionary.getCOSArray(COSName.CO); + if (co == null) + { + return Collections.emptyList(); + } + + Iterable fields = isCachingFields() ? fieldCache.values() : getFieldTree(); + + List actuals = new ArrayList(); + for (int i = 0; i < co.size(); i++) + { + COSBase item = co.getObject(i); + for (PDField field : fields) + { + if (field.getCOSObject() == item) + { + actuals.add(field); + break; + } + } + } + return actuals; + } + + /** + * Set the calculation order in which field values should be recalculated when the value of any + * field changes. (Read about "Trigger Events" in the PDF specification) + * + * @param fields The field list. + */ + public void setCalcOrder(List fields) + { + COSArray array = new COSArray(); + for (PDField field : fields) + { + array.add(field); + } + dictionary.setItem(COSName.CO, array); + } + /** * Set a handler to support JavaScript actions in the form. * @@ -719,11 +770,18 @@ private RectF getTransformedAppearanceBBox(PDAppearanceStream appearanceStream) return bounds; } + /** + * Build a map of pages => widgets + * @param fields a list of fields to be flattened + * @param pages the page tree + * @return + * @throws IOException + */ private Map> buildPagesWidgetsMap( - List fields, PDPageTree pages) throws IOException + List fields, PDPageTree pages) throws IOException { Map> pagesAnnotationsMap = - new HashMap>(); + new HashMap>(); boolean hasMissingPageRef = false; for (PDField field : fields) @@ -738,6 +796,7 @@ private Map> buildPagesWidgetsMap( } else { + Log.w("PdfBox-Android", "missing /P entry (page reference) in a widget for field: " + field); hasMissingPageRef = true; } } @@ -748,14 +807,15 @@ private Map> buildPagesWidgetsMap( return pagesAnnotationsMap; } - // If there is a widget with a missing page reference we need to build the map reverse i.e. + // If there is a widget with a missing page reference we need to build the map reverse i.e. // from the annotations to the widget. Log.w("PdfBox-Android", "There has been a widget with a missing page reference, will check all page annotations"); + Set widgetDictionarySet = createWidgetDictionarySet(fields); for (PDPage page : pages) { for (PDAnnotation annotation : page.getAnnotations()) { - if (annotation instanceof PDAnnotationWidget) + if (widgetDictionarySet.contains(annotation.getCOSObject())) { fillPagesAnnotationMap(pagesAnnotationsMap, page, (PDAnnotationWidget) annotation); } @@ -765,8 +825,28 @@ private Map> buildPagesWidgetsMap( return pagesAnnotationsMap; } + /** + * Return a set of all annotation widget dictionaries related to the fields to be flattened. + * + * @param fields + * @return + */ + private Set createWidgetDictionarySet(List fields) + { + Set widgetDictionarySet = new HashSet(); + for (PDField field : fields) + { + List widgets = field.getWidgets(); + for (PDAnnotationWidget widget : widgets) + { + widgetDictionarySet.add(widget.getCOSObject()); + } + } + return widgetDictionarySet; + } + private void fillPagesAnnotationMap(Map> pagesAnnotationsMap, - PDPage page, PDAnnotationWidget widget) + PDPage page, PDAnnotationWidget widget) { Set widgetsForPage = pagesAnnotationsMap.get(page.getCOSObject()); if (widgetsForPage == null) @@ -789,12 +869,12 @@ private void removeFields(List fields) if (field.getParent() == null) { // if the field has no parent, assume it is at root level list, remove it from there - array = (COSArray) dictionary.getDictionaryObject(COSName.FIELDS); + array = dictionary.getCOSArray(COSName.FIELDS); } else { // if the field has a parent, then remove from the list there - array = (COSArray) field.getParent().getCOSObject().getDictionaryObject(COSName.KIDS); + array = field.getParent().getCOSObject().getCOSArray(COSName.KIDS); } array.removeObject(field.getCOSObject()); } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDButton.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDButton.java index 0cf0f1dbd..c29d1e729 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDButton.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDButton.java @@ -32,7 +32,6 @@ import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAppearanceEntry; - /** * A button field represents an interactive control on the screen * that the user can manipulate with the mouse. @@ -185,7 +184,7 @@ public void setValue(String value) throws IOException { checkValue(value); - // if there are export values/an Opt entry there is a different + // if there are export values/an Opt entry there is a different // approach to setting the value boolean hasExportValues = getExportValues().size() > 0; @@ -203,7 +202,7 @@ public void setValue(String value) throws IOException /** * Set the selected option given its index, and try to update the visual appearance. * - * NOTE: this method is only usable if there are export values and used for + * NOTE: this method is only usable if there are export values and used for * radio buttons with FLAG_RADIOS_IN_UNISON not set. * * @param index index of option to be selected @@ -212,11 +211,12 @@ public void setValue(String value) throws IOException */ public void setValue(int index) throws IOException { - if (getExportValues().isEmpty() || index < 0 || index >= getExportValues().size()) + List exportValues = getExportValues(); + if (exportValues.isEmpty() || index < 0 || index >= exportValues.size()) { throw new IllegalArgumentException("index '" + index - + "' is not a valid index for the field " + getFullyQualifiedName() - + ", valid indices are from 0 to " + (getExportValues().size() - 1)); + + "' is not a valid index for the field " + getFullyQualifiedName() + + ", valid indices are from 0 to " + (exportValues.size() - 1)); } updateByValue(String.valueOf(index)); @@ -225,7 +225,6 @@ public void setValue(int index) throws IOException } - /** * Returns the default value, if any. * @@ -269,7 +268,7 @@ public String getValueAsString() *

      The export values are defined in the field dictionaries /Opt key.

      * *

      The option values are used to define the export values - * for the field to + * for the field to *

        *
      • hold values in non-Latin writing systems as name objects, which represent the field value, are limited * to PDFDocEncoding @@ -288,8 +287,13 @@ public List getExportValues() if (value instanceof COSString) { + String stringValue = ((COSString)value).getString(); + if (stringValue.isEmpty()) + { + return Collections.emptyList(); + } List array = new ArrayList(); - array.add(((COSString) value).getString()); + array.add(stringValue); return array; } else if (value instanceof COSArray) @@ -322,27 +326,23 @@ public void setExportValues(List values) @Override void constructAppearances() throws IOException { - List exportValues = getExportValues(); - if (exportValues.size() > 0) + for (PDAnnotationWidget widget : getWidgets()) { - // the value is the index value of the option. So we need to get that - // and use it to set the value - try + PDAppearanceDictionary appearance = widget.getAppearance(); + if (appearance == null) { - int optionsIndex = Integer.parseInt(getValue()); - if (optionsIndex < exportValues.size()) - { - updateByOption(exportValues.get(optionsIndex)); - } - } catch (NumberFormatException e) + continue; + } + PDAppearanceEntry appearanceEntry = appearance.getNormalAppearance(); + COSName value = getCOSObject().getCOSName(COSName.V); + if (((COSDictionary) appearanceEntry.getCOSObject()).containsKey(value)) { - // silently ignore that - // and don't update the appearance + widget.setAppearanceState(value); + } + else + { + widget.setAppearanceState(COSName.Off); } - } - else - { - updateByValue(getValue()); } } @@ -353,7 +353,7 @@ void constructAppearances() throws IOException * a PDF name object. The Off value shall always be 'Off'. If not set or not part of the normal * appearance keys 'Off' is the default

        * - * @return the potential values setting the check box to the On state. + * @return the potential values setting the check box to the On state. * If an empty Set is returned there is no appearance definition. */ public Set getOnValues() @@ -370,7 +370,7 @@ public Set getOnValues() List widgets = this.getWidgets(); for (PDAnnotationWidget widget : widgets) { - onValues.add(getOnValueForWidget(widget)); + onValues.add(getOnValueForWidget(widget)); } return onValues; } @@ -425,31 +425,85 @@ void checkValue(String value) if (COSName.Off.getName().compareTo(value) != 0 && !onValues.contains(value)) { throw new IllegalArgumentException("value '" + value - + "' is not a valid option for the field " + getFullyQualifiedName() - + ", valid values are: " + onValues + " and " + COSName.Off.getName()); + + "' is not a valid option for the field " + getFullyQualifiedName() + + ", valid values are: " + onValues + " and " + COSName.Off.getName()); } } private void updateByValue(String value) throws IOException { - getCOSObject().setName(COSName.V, value); + // Find the matching appearance key from the first widget that has it + COSName matchingKey = null; + // update the appearance state (AS) for (PDAnnotationWidget widget : getWidgets()) { - if (widget.getAppearance() == null) + PDAppearanceDictionary appearance = widget.getAppearance(); + if (appearance == null) { continue; } PDAppearanceEntry appearanceEntry = widget.getAppearance().getNormalAppearance(); - if (((COSDictionary) appearanceEntry.getCOSObject()).containsKey(value)) + COSDictionary appearanceDict = (COSDictionary) appearanceEntry.getCOSObject(); + + // Find the matching appearance key by searching through the actual keys + // and comparing their decoded names. This handles encoding differences: + // the appearance key might be ISO-8859-1 encoded (e.g. /m#e4nnlich for "männlich") + // while the value String is UTF-8. + COSName widgetMatchingKey = findMatchingAppearanceKey(appearanceDict, value); + + // Save the first matching key to use for the V entry + if (widgetMatchingKey != null && matchingKey == null) { - widget.setAppearanceState(value); + matchingKey = widgetMatchingKey; + } + + if (widgetMatchingKey != null) + { + // Use the exact COSName from the appearance dictionary to preserve encoding + widget.setAppearanceState(widgetMatchingKey); } else { - widget.setAppearanceState(COSName.Off.getName()); + // Fall back to Off if no match found for this widget + widget.setAppearanceState(COSName.Off); + } + } + + // Set the V entry once using the first matching key found + if (matchingKey != null) + { + getCOSObject().setItem(COSName.V, matchingKey); + } + else + { + // Fall back to UTF-8 encoding if no match found in any widget + getCOSObject().setName(COSName.V, value); + } + + } + + /** + * Find the appearance dictionary key that matches the given value String. + * This method handles encoding differences - the value might be UTF-8 while + * appearance keys in the PDF could be ISO-8859-1 or other encodings. + * + * @param appearanceDict the appearance dictionary with keys to search + * @param value the value String to match against (typically UTF-8) + * @return the matching COSName key, or null if no match found + */ + private COSName findMatchingAppearanceKey(COSDictionary appearanceDict, String value) + { + // Search all keys in the appearance dictionary and compare their decoded names + // COSName.getName() uses UTF-8 decoding with ISO-8859-1 fallback for non-UTF-8 bytes + for (COSName key : appearanceDict.keySet()) + { + if (value.equals(key.getName())) + { + return key; } } + return null; } private void updateByOption(String value) throws IOException diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDChoice.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDChoice.java index 574e98df9..3e59843ed 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDChoice.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDChoice.java @@ -21,6 +21,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; + import com.tom_roush.pdfbox.cos.COSArray; import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSDictionary; @@ -28,7 +29,6 @@ import com.tom_roush.pdfbox.cos.COSString; import com.tom_roush.pdfbox.pdmodel.common.COSArrayList; import com.tom_roush.pdfbox.pdmodel.interactive.form.FieldUtils.KeyValue; - /** * A choice field contains several text items, one or more of which shall be selected as the field * value. @@ -75,9 +75,9 @@ public PDChoice(PDAcroForm acroForm) * For a choice field the options array can either be an array * of text strings or an array of a two-element arrays.
        * The method always only returns either the text strings or, - * in case of two-element arrays, an array of the first element of + * in case of two-element arrays, an array of the first element of * the two-element arrays - *

        + *

        *

        * Use {@link #getOptionsExportValues()} and {@link #getOptionsDisplayValues()} * to get the entries of two-element arrays. @@ -96,13 +96,13 @@ public List getOptions() * *

        * The Opt array specifies the list of options in the choice field either - * as an array of text strings representing the display value + * as an array of text strings representing the display value * or as an array of a two-element array where the * first element is the export value and the second the display value. *

        *

        * To set both the export and the display value use {@link #setOptions(List, List)} - *

        + *

        * * @param displayValues List containing all possible options. */ @@ -127,7 +127,7 @@ public void setOptions(List displayValues) * *

        * This will set both, the export value and the display value - * of the choice field. If either one of the parameters is null or an + * of the choice field. If either one of the parameters is null or an * empty list is supplied the options will * be removed. *

        @@ -147,7 +147,7 @@ public void setOptions(List exportValues, List displayValues) if (exportValues.size() != displayValues.size()) { throw new IllegalArgumentException( - "The number of entries for exportValue and displayValue shall be the same."); + "The number of entries for exportValue and displayValue shall be the same."); } else { @@ -162,8 +162,9 @@ public void setOptions(List exportValues, List displayValues) for (int i = 0; i exportValues, List displayValues) *

        * For options with an array of text strings the display value and export value * are the same.
        - * For options with an array of two-element arrays the display value is the + * For options with an array of two-element arrays the display value is the * second entry in the two-element array. *

        * @@ -199,7 +200,7 @@ public List getOptionsDisplayValues() *

        * For options with an array of text strings the display value and export value * are the same.
        - * For options with an array of two-element arrays the export value is the + * For options with an array of two-element arrays the export value is the * first entry in the two-element array. *

        * @@ -210,6 +211,18 @@ public List getOptionsExportValues() return getOptions(); } + /** + * This will check if the field has dedicated display and export values. + * + * @return true if export and display values are different + */ + public boolean hasSeparateExportAndDisplayValues() + { + List exportValues = getOptionsExportValues(); + List displayValues = getOptionsDisplayValues(); + return !exportValues.equals(displayValues); + } + /** * This will get the indices of the selected options - the 'I' key. *

        @@ -251,7 +264,7 @@ public void setSelectedOptionsIndex(List values) if (!isMultiSelect()) { throw new IllegalArgumentException( - "Setting the indices is not allowed for choice fields not allowing multiple selections."); + "Setting the indices is not allowed for choice fields not allowing multiple selections."); } getCOSObject().setItem(COSName.I, COSArrayList.converterToCOSArray(values)); } @@ -267,7 +280,7 @@ public void setSelectedOptionsIndex(List values) *

        * If set, the field’s option items shall be sorted alphabetically. * The sorting has to be done when writing the PDF. PDF Readers are supposed to - * display the options in the order in which they occur in the Opt array. + * display the options in the order in which they occur in the Opt array. *

        * * @return true if the options are sorted. @@ -411,12 +424,13 @@ public void setValue(List values) throws IOException { throw new IllegalArgumentException("The list box does not allow multiple selections."); } - if (!getOptions().containsAll(values)) + List options = getOptions(); + if (!options.containsAll(values)) { throw new IllegalArgumentException("The values are not contained in the selectable options."); } getCOSObject().setItem(COSName.V, COSArrayList.convertStringListToCOSStringCOSArray(values)); - updateSelectedOptionsIndex(values); + updateSelectedOptionsIndex(values, options); } else { @@ -476,10 +490,9 @@ public String getValueAsString() /** * Update the 'I' key based on values set. */ - private void updateSelectedOptionsIndex(List values) + private void updateSelectedOptionsIndex(List values, List options) { - List options = getOptions(); - List indices = new ArrayList(); + List indices = new ArrayList(values.size()); for (String value : values) { diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDComboBox.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDComboBox.java index 90f4ee3c6..93b4525ba 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDComboBox.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDComboBox.java @@ -21,7 +21,6 @@ import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; - /** * A combo box consisting of a drop-down list. * May be accompanied by an editable text box in which non-predefined values may be entered. @@ -84,6 +83,16 @@ void constructAppearances() throws IOException if (!values.isEmpty()) { + if (hasSeparateExportAndDisplayValues()) + { + List displayValues = getOptionsDisplayValues(); + int index = getOptions().indexOf(values.get(0)); + if (index != -1 && index < displayValues.size()) + { + apHelper.setAppearanceValue(displayValues.get(index)); + return; + } + } apHelper.setAppearanceValue(values.get(0)); } else diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDField.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDField.java index fe2236581..3622f8245 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDField.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDField.java @@ -30,7 +30,6 @@ import com.tom_roush.pdfbox.pdmodel.fdf.FDFField; import com.tom_roush.pdfbox.pdmodel.interactive.action.PDFormFieldAdditionalActions; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; - /** * A field in an interactive form. */ @@ -224,7 +223,7 @@ public void setFieldFlags(int flags) */ public PDFormFieldAdditionalActions getActions() { - COSDictionary aa = (COSDictionary) dictionary.getDictionaryObject(COSName.AA); + COSDictionary aa = dictionary.getCOSDictionary(COSName.AA); if (aa != null) { return new PDFormFieldAdditionalActions(aa); @@ -232,7 +231,7 @@ public PDFormFieldAdditionalActions getActions() return null; } - /** + /** * This will import a fdf field from a fdf document. * * @param fdfField The fdf field to import. @@ -336,7 +335,7 @@ public PDNonTerminalField getParent() PDField findKid(String[] name, int nameIndex) { PDField retval = null; - COSArray kids = (COSArray) dictionary.getDictionaryObject(COSName.KIDS); + COSArray kids = dictionary.getCOSArray(COSName.KIDS); if (kids != null) { for (int i = 0; retval == null && i < kids.size(); i++) @@ -345,7 +344,7 @@ PDField findKid(String[] name, int nameIndex) if (name[nameIndex].equals(kidDictionary.getString(COSName.T))) { retval = PDField.fromDictionary(acroForm, kidDictionary, - (PDNonTerminalField)this); + (PDNonTerminalField)this); if (retval != null && name.length > nameIndex + 1) { retval = retval.findKid(name, nameIndex + 1); @@ -398,7 +397,7 @@ public void setPartialName(String name) if (name.contains(".")) { throw new IllegalArgumentException( - "A field partial name shall not contain a period character: " + name); + "A field partial name shall not contain a period character: " + name); } dictionary.setString(COSName.T, name); } @@ -478,6 +477,35 @@ public void setMappingName(String mappingName) public String toString() { return getFullyQualifiedName() + "{type: " + getClass().getSimpleName() + " value: " + - getInheritableAttribute(COSName.V) + "}"; + getInheritableAttribute(COSName.V) + "}"; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals (Object o) + { + if (o == this) + { + return true; + } + + if (!(o instanceof PDField)) + { + return false; + } + + COSDictionary toBeCompared = ((PDField) o).getCOSObject(); + return toBeCompared.equals(getCOSObject()); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() + { + return dictionary.hashCode(); } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDFieldFactory.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDFieldFactory.java index dcfefa764..e5f64de4b 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDFieldFactory.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDFieldFactory.java @@ -17,11 +17,13 @@ package com.tom_roush.pdfbox.pdmodel.interactive.form; +import java.util.HashSet; +import java.util.Set; + import com.tom_roush.pdfbox.cos.COSArray; import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; - /** * A PDField factory. */ @@ -42,7 +44,7 @@ private PDFieldFactory() * * @param form the form that the field is part of * @param field the dictionary representing a field element - * @param parent the parent node of the node to be created + * @param parent the parent node of the node to be created * @return the corresponding PDField instance */ public static PDField createField(PDAcroForm form, COSDictionary field, PDNonTerminalField parent) @@ -53,7 +55,7 @@ public static PDField createField(PDAcroForm form, COSDictionary field, PDNonTer // a field name (other than annotations) if (field.containsKey(COSName.KIDS)) { - COSArray kids = (COSArray) field.getDictionaryObject(COSName.KIDS); + COSArray kids = field.getCOSArray(COSName.KIDS); if (kids != null && kids.size() > 0) { for (int i = 0; i < kids.size(); i++) @@ -67,7 +69,7 @@ public static PDField createField(PDAcroForm form, COSDictionary field, PDNonTer } } - String fieldType = findFieldType(field); + String fieldType = findFieldType(field, new HashSet()); if (FIELD_TYPE_CHOICE.equals(fieldType)) { @@ -93,7 +95,7 @@ else if (FIELD_TYPE_BUTTON.equals(fieldType)) } private static PDField createChoiceSubType(PDAcroForm form, COSDictionary field, - PDNonTerminalField parent) + PDNonTerminalField parent) { int flags = field.getInt(COSName.FF, 0); if ((flags & PDChoice.FLAG_COMBO) != 0) @@ -107,7 +109,7 @@ private static PDField createChoiceSubType(PDAcroForm form, COSDictionary field, } private static PDField createButtonSubType(PDAcroForm form, COSDictionary field, - PDNonTerminalField parent) + PDNonTerminalField parent) { int flags = field.getInt(COSName.FF, 0); // BJL: I have found that the radio flag bit is not always set @@ -127,17 +129,23 @@ else if ((flags & PDButton.FLAG_PUSHBUTTON) != 0) } } - private static String findFieldType(COSDictionary dic) + private static String findFieldType(COSDictionary dic, Set seen) { + if (!seen.add(dic)) + { + // PDFBOX-5896: avoid endless recursion + return null; + } String retval = dic.getNameAsString(COSName.FT); if (retval == null) { COSBase base = dic.getDictionaryObject(COSName.PARENT, COSName.P); if (base instanceof COSDictionary) { - retval = findFieldType((COSDictionary) base); + retval = findFieldType((COSDictionary) base, seen); } } + seen.remove(dic); return retval; } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDFieldTree.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDFieldTree.java index d20024762..9d9ed0cb8 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDFieldTree.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDFieldTree.java @@ -28,12 +28,12 @@ import java.util.Set; import com.tom_roush.pdfbox.cos.COSDictionary; - /** * The field tree. */ public class PDFieldTree implements Iterable { + private final PDAcroForm acroForm; /** @@ -69,7 +69,7 @@ private static final class FieldIterator implements Iterator // PDFBOX-5044: to prevent recursion // must be COSDictionary and not PDField, because PDField is newly created each time private final Set set = - Collections.newSetFromMap(new IdentityHashMap()); + Collections.newSetFromMap(new IdentityHashMap()); private FieldIterator(PDAcroForm form) { @@ -115,7 +115,7 @@ private void enqueueKids(PDField node) if (set.contains(kid.getCOSObject())) { Log.e("PdfBox-Android", "Child of field '" + node.getFullyQualifiedName() + - "' already exists elsewhere, ignored to avoid recursion"); + "' already exists elsewhere, ignored to avoid recursion"); } else { diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDNonTerminalField.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDNonTerminalField.java index ddf593e91..27f41a86f 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDNonTerminalField.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDNonTerminalField.java @@ -26,17 +26,15 @@ import com.tom_roush.pdfbox.cos.COSArray; import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSDictionary; -import com.tom_roush.pdfbox.cos.COSInteger; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.pdmodel.common.COSArrayList; import com.tom_roush.pdfbox.pdmodel.fdf.FDFField; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; - /** * A non terminal field in an interactive form. * * A non terminal field is a node in the fields tree node whose descendants - * are fields. + * are fields. * * The attributes such as FT (field type) or V (field value) do not logically * belong to the non terminal field but are inheritable attributes @@ -44,6 +42,7 @@ */ public class PDNonTerminalField extends PDField { + /** * Constructor. * @@ -69,14 +68,8 @@ public PDNonTerminalField(PDAcroForm acroForm) @Override public int getFieldFlags() { - int retval = 0; - COSInteger ff = (COSInteger) getCOSObject().getDictionaryObject(COSName.FF); - if (ff != null) - { - retval = ff.intValue(); - } // There is no need to look up the parent hierarchy within a non terminal field - return retval; + return getCOSObject().getInt(COSName.FF, 0); } @Override @@ -220,7 +213,7 @@ public void setValue(COSBase object) throws IOException // todo: construct appearances of children? } - /** + /** * Sets the plain text value of this field. * * @param value Plain text @@ -256,7 +249,7 @@ public COSBase getDefaultValue() */ public void setDefaultValue(COSBase value) { - getCOSObject().setItem(COSName.V, value); + getCOSObject().setItem(COSName.DV, value); } @Override diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDSignatureField.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDSignatureField.java index 3b7abc6fd..1b6093a58 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDSignatureField.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDSignatureField.java @@ -25,10 +25,10 @@ import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; +import com.tom_roush.pdfbox.pdmodel.common.PDRectangle; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; import com.tom_roush.pdfbox.pdmodel.interactive.digitalsignature.PDSeedValue; import com.tom_roush.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; - /** * A signature field is a form field that contains a digital signature. * @@ -37,6 +37,7 @@ */ public class PDSignatureField extends PDTerminalField { + /** * @see PDTerminalField#PDTerminalField(PDAcroForm) * @@ -133,7 +134,7 @@ public void setValue(PDSignature value) throws IOException public void setValue(String value) { throw new UnsupportedOperationException("Signature fields don't support setting the value as String " - + "- use setValue(PDSignature value) instead"); + + "- use setValue(PDSignature value) instead"); } @@ -193,7 +194,7 @@ public String getValueAsString() */ public PDSeedValue getSeedValue() { - COSDictionary dict = (COSDictionary) getCOSObject().getDictionaryObject(COSName.SV); + COSDictionary dict = getCOSObject().getCOSDictionary(COSName.SV); PDSeedValue sv = null; if (dict != null) { @@ -223,19 +224,19 @@ void constructAppearances() throws IOException PDAnnotationWidget widget = this.getWidgets().get(0); if (widget != null) { + PDRectangle rectangle = widget.getRectangle(); // check if the signature is visible - if (widget.getRectangle() == null || - widget.getRectangle().getHeight() == 0 && widget.getRectangle().getWidth() == 0 || - widget.isNoView() || widget.isHidden()) + if (rectangle == null || rectangle.getHeight() == 0 && rectangle.getWidth() == 0 || + widget.isNoView() || widget.isHidden()) { return; } // TODO: implement appearance generation for signatures (PDFBOX-3524) Log.w("PdfBox-Android", "Appearance generation for signature fields not implemented here. " - + "You need to generate/update that manually, see the " - + "CreateVisibleSignature*.java files in the examples subproject " - + "of the source code download"); + + "You need to generate/update that manually, see the " + + "CreateVisibleSignature*.java files in the examples subproject " + + "of the source code download"); } } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDTerminalField.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDTerminalField.java index 2a3e091e4..513ca7b4e 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDTerminalField.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDTerminalField.java @@ -29,7 +29,6 @@ import com.tom_roush.pdfbox.pdmodel.fdf.FDFField; import com.tom_roush.pdfbox.pdmodel.interactive.action.PDFormFieldAdditionalActions; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; - /** * A field in an interactive form. * Fields may be one of four types: button, text, choice, or signature. @@ -165,7 +164,7 @@ FDFField exportFDF() throws IOException public List getWidgets() { List widgets = new ArrayList(); - COSArray kids = (COSArray)getCOSObject().getDictionaryObject(COSName.KIDS); + COSArray kids = getCOSObject().getCOSArray(COSName.KIDS); if (kids == null) { // the field itself is a widget diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDTextField.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDTextField.java index 27e9864e5..87231755f 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDTextField.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDTextField.java @@ -17,9 +17,9 @@ package com.tom_roush.pdfbox.pdmodel.interactive.form; import java.io.IOException; + import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; - /** * A text field is a box or space for text fill-in data typically entered from a keyboard. * The text may be restricted to a single line or may be permitted to span multiple lines @@ -211,6 +211,8 @@ public void setMaxLen(int maxLen) * * @param value Plain text * @throws IOException if the value could not be set + * @throws IllegalArgumentException if the string contains a character that is not in the field + * font, see {@link #setDefaultAppearance(java.lang.String)}. */ @Override public void setValue(String value) throws IOException diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDVariableText.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDVariableText.java index 787351301..f92344799 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDVariableText.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PDVariableText.java @@ -25,7 +25,7 @@ import com.tom_roush.pdfbox.cos.COSStream; import com.tom_roush.pdfbox.cos.COSString; import com.tom_roush.pdfbox.pdmodel.PDResources; - +import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; /** * Base class for fields which use "Variable Text". * These fields construct an appearance stream dynamically at viewing time. @@ -107,7 +107,7 @@ PDDefaultAppearanceString getDefaultAppearanceString() throws IOException /** * Set the default appearance. * - * This will set the local default appearance for the variable text field only, not + * This will set the local default appearance for the variable text field only, not * affecting a default appearance in the parent hierarchy. * * Providing null as the value will remove the local default appearance. @@ -127,6 +127,19 @@ PDDefaultAppearanceString getDefaultAppearanceString() throws IOException public void setDefaultAppearance(String daValue) { getCOSObject().setString(COSName.DA, daValue); + + // PDFBOX-5797: Sejda files have a /DA entry in kid widgets + if (getCOSObject().containsKey(COSName.KIDS)) + { + for (PDAnnotationWidget widget : getWidgets()) + { + COSDictionary widgetDict = widget.getCOSObject(); + if (widgetDict.containsKey(COSName.DA)) + { + widgetDict.setString(COSName.DA, daValue); + } + } + } } /** @@ -139,8 +152,7 @@ public void setDefaultAppearance(String daValue) */ public String getDefaultStyleString() { - COSString defaultStyleString = (COSString) getCOSObject().getDictionaryObject(COSName.DS); - return defaultStyleString.getString(); + return getCOSObject().getString(COSName.DS); } /** diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PlainText.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PlainText.java index 4856529a1..1e21da18c 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PlainText.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/PlainText.java @@ -17,14 +17,14 @@ package com.tom_roush.pdfbox.pdmodel.interactive.form; import java.io.IOException; +import java.text.AttributedCharacterIterator.Attribute; import java.text.AttributedString; import java.text.BreakIterator; -import java.text.AttributedCharacterIterator.Attribute; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import com.tom_roush.pdfbox.pdmodel.font.PDFont; - /** * A block of text. *

        @@ -43,7 +43,7 @@ class PlainText * Construct the text block from a single value. * * Constructs the text block from a single value splitting - * into individual {@link Paragraph} when a new line character is + * into individual {@link Paragraph} when a new line character is * encountered. * * @param textValue the text block string. @@ -163,6 +163,10 @@ String getText() */ List getLines(PDFont font, float fontSize, float width) throws IOException { + if (width <= 0) + { + return Collections.emptyList(); + } BreakIterator iterator = BreakIterator.getLineInstance(); iterator.setText(textContent); @@ -200,23 +204,31 @@ List getLines(PDFont font, float fontSize, float width) throws IOException lineWidth = font.getStringWidth(word) * scale; } - if (wordWidth > width && textLine.getWords().isEmpty()) + if (word.length() > 1 && wordWidth > width && textLine.getWords().isEmpty()) { // single word does not fit into width + // PDFBOX-6082: at least 1 character wordNeedsSplit = true; - while (true) - { - splitOffset--; - String substring = word.substring(0, splitOffset); - float substringWidth = font.getStringWidth(substring) * scale; - if (substringWidth < width) - { - word = substring; - wordWidth = font.getStringWidth(word) * scale; - lineWidth = wordWidth; - break; - } - } + + // PDFBOX-5049: The original approach was to decrement splitOffset + // until the substring fits, but this can be very expensive for long words and + // narrow widths (e.g. a long URL in a narrow column). + // + // Optimization: instead of decrementing splitOffset one step at a time and + // calling getStringWidth on progressively shorter substrings: + // - compute the scaled width of every individual character once + // - build a prefix-sum array + // - binary-search for the largest prefix that fits + // + // TODO: The special case in PDFBOX-5049 should be handled by not generating an appearance + // stream at all as the the height of the text box is only 1pt and the text is not visible. + + float[] prefixWidth = buildPrefixWidths(word, font, scale); + splitOffset = findMaxFittingChars(prefixWidth, width); + + word = word.substring(0, splitOffset); + wordWidth = prefixWidth[splitOffset]; + lineWidth = wordWidth; } AttributedString as = new AttributedString(word); @@ -239,6 +251,72 @@ List getLines(PDFont font, float fontSize, float width) throws IOException textLines.add(textLine); return textLines; } + + /** + * Build the prefix-sum array of scaled character widths for the given word. + * + * @param word the word to measure. + * @param font the font used to obtain glyph advance widths. + * @param scale {@code fontSize / FONTSCALE}, pre-computed by the caller. + * @return the {@code float[word.length() + 1]} prefix-sum array. + * @throws IOException if the font cannot provide a glyph width. + */ + private static float[] buildPrefixWidths(String word, PDFont font, float scale) + throws IOException + { + int wordLen = word.length(); + float[] prefixWidth = new float[wordLen + 1]; + int i = 0; + while (i < wordLen) + { + int codePoint = word.codePointAt(i); + int charCount = Character.charCount(codePoint); + // Measure this code point as a single string (handles surrogate pairs). + float cpWidth = font.getStringWidth(word.substring(i, i + charCount)) * scale; + // Propagate the cumulative width across all Java chars of this code point. + for (int j = 0; j < charCount; j++) + { + prefixWidth[i + j + 1] = prefixWidth[i + j] + (j == 0 ? cpWidth : 0f); + } + i += charCount; + } + return prefixWidth; + } + + /** + * Find the maximum number of Java chars from a prefix-width array that fit + * within the given available width. + *

        + * Binary search over the pre-computed {@code prefixWidth} array. + * The result is always at least {@code 1} so that the caller is guaranteed to + * make forward progress (PDFBOX-6082). + *

        + * + * @param prefixWidth array as returned by {@link #buildPrefixWidths}; length is + * {@code wordLength + 1}. + * @param width the available line width in the same unit as the widths stored + * in {@code prefixWidth}. + * @return the largest index {@code k >= 1} such that {@code prefixWidth[k] < width}, + * or {@code 1} if even a single character exceeds the available width. + */ + private static int findMaxFittingChars(float[] prefixWidth, float width) + { + int lo = 1; + int hi = prefixWidth.length - 1; + while (lo < hi) + { + int mid = (lo + hi + 1) >>> 1; // upper-mid to avoid infinite loop + if (prefixWidth[mid] < width) + { + lo = mid; + } + else + { + hi = mid - 1; + } + } + return lo; + } } /** @@ -267,7 +345,7 @@ float calculateWidth(PDFont font, float fontSize) throws IOException for (Word word : words) { calculatedWidth = calculatedWidth + - (Float) word.getAttributes().getIterator().getAttribute(TextAttribute.WIDTH); + (Float) word.getAttributes().getIterator().getAttribute(TextAttribute.WIDTH); String text = word.getText(); if (indexOfWord == words.size() -1 && Character.isWhitespace(text.charAt(text.length()-1))) { diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/ScriptingHandler.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/ScriptingHandler.java index a28fadf26..20a585b5f 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/ScriptingHandler.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/form/ScriptingHandler.java @@ -1,4 +1,3 @@ -package com.tom_roush.pdfbox.pdmodel.interactive.form; /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with @@ -15,43 +14,44 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import com.tom_roush.pdfbox.pdmodel.interactive.action.PDActionJavaScript; +package com.tom_roush.pdfbox.pdmodel.interactive.form; +import com.tom_roush.pdfbox.pdmodel.interactive.action.PDActionJavaScript; public interface ScriptingHandler { - /** - * Handle the fields keyboard event action. - * - * @param javaScriptAction the keyboard event action script - * @param value the current field value - * @return the resulting field value - */ - String keyboard(PDActionJavaScript javaScriptAction, String value); + /** + * Handle the fields keyboard event action. + * + * @param javaScriptAction the keyboard event action script + * @param value the current field value + * @return the resulting field value + */ + String keyboard(PDActionJavaScript javaScriptAction, String value); - /** - * Handle the fields format event action. - * - * @param javaScriptAction the format event action script - * @param value the current field value - * @return the formatted field value - */ - String format(PDActionJavaScript javaScriptAction, String value); + /** + * Handle the fields format event action. + * + * @param javaScriptAction the format event action script + * @param value the current field value + * @return the formatted field value + */ + String format(PDActionJavaScript javaScriptAction, String value); - /** - * Handle the fields validate event action. - * - * @param javaScriptAction the validate event action script - * @param value the current field value - * @return the result of the validity check - */ - boolean validate(PDActionJavaScript javaScriptAction, String value); + /** + * Handle the fields validate event action. + * + * @param javaScriptAction the validate event action script + * @param value the current field value + * @return the result of the validity check + */ + boolean validate(PDActionJavaScript javaScriptAction, String value); - /** - * Handle the fields calculate event action. - * - * @param javaScriptAction the calculate event action script - * @param value the current field value - * @return the result of the field calculation - */ - String calculate(PDActionJavaScript javaScriptAction, String value); + /** + * Handle the fields calculate event action. + * + * @param javaScriptAction the calculate event action script + * @param value the current field value + * @return the result of the field calculation + */ + String calculate(PDActionJavaScript javaScriptAction, String value); } diff --git a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/viewerpreferences/PDViewerPreferences.java b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/viewerpreferences/PDViewerPreferences.java index 7bedff9b2..14ed1491f 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/viewerpreferences/PDViewerPreferences.java +++ b/library/src/main/java/com/tom_roush/pdfbox/pdmodel/interactive/viewerpreferences/PDViewerPreferences.java @@ -18,9 +18,7 @@ import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; - import com.tom_roush.pdfbox.pdmodel.common.COSObjectable; - /** * This is the document viewing preferences. * @@ -197,6 +195,14 @@ public enum PRINT_SCALING private final COSDictionary prefs; + /** + * Constructor. + */ + public PDViewerPreferences() + { + this.prefs = new COSDictionary(); + } + /** * Constructor that is used for a preexisting dictionary. * @@ -346,7 +352,7 @@ public void setDisplayDocTitle( boolean value ) public String getNonFullScreenPageMode() { return prefs.getNameAsString( COSName.NON_FULL_SCREEN_PAGE_MODE, - NON_FULL_SCREEN_PAGE_MODE.UseNone.toString()); + NON_FULL_SCREEN_PAGE_MODE.UseNone.toString()); } /** diff --git a/library/src/main/java/com/tom_roush/pdfbox/rendering/PDFRenderer.java b/library/src/main/java/com/tom_roush/pdfbox/rendering/PDFRenderer.java index a3ced3c62..e7fd12c5b 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/rendering/PDFRenderer.java +++ b/library/src/main/java/com/tom_roush/pdfbox/rendering/PDFRenderer.java @@ -173,6 +173,9 @@ public void setImageDownscalingOptimizationThreshold(float imageDownscalingOptim * @param pageIndex the zero-based index of the page to be converted. * @return the rendered page image * @throws IOException if the PDF cannot be read + * @throws IllegalStateException if the requested index isn't found or doesn't point to a valid + * page dictionary + * @throws IndexOutOfBoundsException if the requested index is higher than the page count */ public Bitmap renderImage(int pageIndex) throws IOException { @@ -186,6 +189,9 @@ public Bitmap renderImage(int pageIndex) throws IOException * @param scale the scaling factor, where 1 = 72 DPI * @return the rendered page image * @throws IOException if the PDF cannot be read + * @throws IllegalStateException if the requested index isn't found or doesn't point to a valid + * page dictionary + * @throws IndexOutOfBoundsException if the requested index is higher than the page count */ public Bitmap renderImage(int pageIndex, float scale) throws IOException { @@ -198,6 +204,9 @@ public Bitmap renderImage(int pageIndex, float scale) throws IOException * @param dpi the DPI (dots per inch) to render at * @return the rendered page image * @throws IOException if the PDF cannot be read + * @throws IllegalStateException if the requested index isn't found or doesn't point to a valid + * page dictionary + * @throws IndexOutOfBoundsException if the requested index is higher than the page count */ public Bitmap renderImageWithDPI(int pageIndex, float dpi) throws IOException { @@ -211,6 +220,9 @@ public Bitmap renderImageWithDPI(int pageIndex, float dpi) throws IOException * @param imageType the type of image to return * @return the rendered page image * @throws IOException if the PDF cannot be read + * @throws IllegalStateException if the requested index isn't found or doesn't point to a valid + * page dictionary + * @throws IndexOutOfBoundsException if the requested index is higher than the page count */ public Bitmap renderImageWithDPI(int pageIndex, float dpi, ImageType imageType) throws IOException @@ -225,6 +237,9 @@ public Bitmap renderImageWithDPI(int pageIndex, float dpi, ImageType imageType) * @param imageType the type of image to return * @return the rendered page image * @throws IOException if the PDF cannot be read + * @throws IllegalStateException if the requested index isn't found or doesn't point to a valid + * page dictionary + * @throws IndexOutOfBoundsException if the requested index is higher than the page count */ public Bitmap renderImage(int pageIndex, float scale, ImageType imageType) throws IOException @@ -241,6 +256,9 @@ public Bitmap renderImage(int pageIndex, float scale, ImageType imageType) * @param destination controlling visibility of optional content groups * @return the rendered page image * @throws IOException if the PDF cannot be read + * @throws IllegalStateException if the requested index isn't found or doesn't point to a valid + * page dictionary + * @throws IndexOutOfBoundsException if the requested index is higher than the page count */ public Bitmap renderImage(int pageIndex, float scale, ImageType imageType, RenderDestination destination) throws IOException @@ -341,6 +359,9 @@ public Bitmap renderImage(int pageIndex, float scale, ImageType imageType, Rende * @param paint the Paint that will be used to draw the page * @param canvas the Canvas on which to draw the page * @throws IOException if the PDF cannot be read + * @throws IllegalStateException if the requested index isn't found or doesn't point to a valid + * page dictionary + * @throws IndexOutOfBoundsException if the requested index is higher than the page count */ public void renderPageToGraphics(int pageIndex, Paint paint, Canvas canvas) throws IOException { @@ -358,6 +379,9 @@ public void renderPageToGraphics(int pageIndex, Paint paint, Canvas canvas) thro * @param canvas the Canvas on which to draw the page * @param scale the scaling factor, where 1 = 72 DPI * @throws IOException if the PDF cannot be read + * @throws IllegalStateException if the requested index isn't found or doesn't point to a valid + * page dictionary + * @throws IndexOutOfBoundsException if the requested index is higher than the page count */ public void renderPageToGraphics(int pageIndex, Paint paint, Canvas canvas, float scale) throws IOException @@ -377,6 +401,9 @@ public void renderPageToGraphics(int pageIndex, Paint paint, Canvas canvas, floa * @param scaleX the scale to draw the page at for the x-axis, where 1 = 72 DPI * @param scaleY the scale to draw the page at for the y-axis, where 1 = 72 DPI * @throws IOException if the PDF cannot be read + * @throws IllegalStateException if the requested index isn't found or doesn't point to a valid + * page dictionary + * @throws IndexOutOfBoundsException if the requested index is higher than the page count */ public void renderPageToGraphics(int pageIndex, Paint paint, Canvas canvas, float scaleX, float scaleY) throws IOException @@ -395,6 +422,9 @@ public void renderPageToGraphics(int pageIndex, Paint paint, Canvas canvas, floa * @param scaleY the scale to draw the page at for the y-axis, where 1 = 72 DPI * @param destination controlling visibility of optional content groups * @throws IOException if the PDF cannot be read + * @throws IllegalStateException if the requested index isn't found or doesn't point to a valid + * page dictionary + * @throws IndexOutOfBoundsException if the requested index is higher than the page count */ public void renderPageToGraphics(int pageIndex, Paint paint, Canvas canvas, float scaleX, float scaleY, RenderDestination destination) throws IOException diff --git a/library/src/main/java/com/tom_roush/pdfbox/rendering/PageDrawer.java b/library/src/main/java/com/tom_roush/pdfbox/rendering/PageDrawer.java index ebe11545f..794e96154 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/rendering/PageDrawer.java +++ b/library/src/main/java/com/tom_roush/pdfbox/rendering/PageDrawer.java @@ -43,6 +43,7 @@ import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSDictionary; import com.tom_roush.pdfbox.cos.COSName; +import com.tom_roush.pdfbox.cos.COSStream; import com.tom_roush.pdfbox.pdmodel.PDResources; import com.tom_roush.pdfbox.pdmodel.common.PDRectangle; import com.tom_roush.pdfbox.pdmodel.common.function.PDFunction; @@ -78,6 +79,7 @@ import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotation; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAnnotationUnknown; import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary; +import com.tom_roush.pdfbox.pdmodel.interactive.annotation.PDAppearanceEntry; import com.tom_roush.pdfbox.util.Matrix; import com.tom_roush.pdfbox.util.Vector; @@ -142,6 +144,7 @@ public class PageDrawer extends PDFGraphicsStreamEngine private final RenderDestination destination; private final float imageDownscalingOptimizationThreshold; + private final Map blendModeMap = new HashMap(); /** * Default annotations filter, returns all annotations @@ -281,6 +284,12 @@ private float clampColor(float color) private int getColor(PDColor color) throws IOException { double alphaConstant = this.getGraphicsState().getAlphaConstant(); PDColorSpace colorSpace = color.getColorSpace(); + if (colorSpace == null) // PDFBOX-5782 + { + // color string is empty, will be rendered as transparency + Log.e("PdfBox-Android", "colorSpace is null, will be rendered as transparency"); + return Color.argb(0, 0, 0, 0); + } float[] floats = colorSpace.toRGB(color.getComponents()); int alpha = Long.valueOf(Math.round(alphaConstant * 255.0)).intValue(); int r = Math.round(floats[0] * 255); @@ -416,7 +425,8 @@ private void drawGlyph2D(Glyph2D glyph2D, PDFont font, int code, Vector displace if (!font.isEmbedded() && !font.isVertical() && !font.isStandard14() && font.hasExplicitWidth(code)) { float fontWidth = font.getWidthFromFont(code); - if (fontWidth > 0 && // ignore spaces + if (displacement.getX() > 0 && // PDFBOX-5611: ignore zero widths + fontWidth > 0 && // ignore spaces Math.abs(fontWidth - displacement.getX() * 1000) > 0.0001) { float pdfWidth = displacement.getX() * 1000; @@ -450,7 +460,7 @@ private void drawGlyph2D(Glyph2D glyph2D, PDFont font, int code, Vector displace if (renderingMode.isClip()) { -// textClippings.add(glyph); TODO: PdfBox-Android + textClippings.add(new Path(path)); } } } @@ -592,6 +602,7 @@ private void setStroke() float phaseStart = dashPattern.getPhase(); dashArray = getDashArray(dashPattern); phaseStart = transformWidth(phaseStart); + phaseStart = Math.min(phaseStart, Short.MAX_VALUE); // PDFBOX-5653: avoid huge phase paint.setStrokeWidth(lineWidth); paint.setStrokeCap(state.getLineCap()); @@ -628,9 +639,8 @@ private boolean isAllZeroDash(float[] dashArray) private float[] getDashArray(PDLineDashPattern dashPattern) { float[] dashArray = dashPattern.getDashArray(); - int phase = dashPattern.getPhase(); // avoid empty, infinite and NaN values (PDFBOX-3360) - if (dashArray.length == 0 || Float.isInfinite(phase) || Float.isNaN(phase)) + if (dashArray.length == 0) { return null; } @@ -672,6 +682,13 @@ public void strokePath() throws IOException setClip(); canvas.drawPath(linePath, paint); } + if (clipWindingRule != null) + { + linePath.setFillType(clipWindingRule); + getGraphicsState().intersectClippingPath(linePath); + clipWindingRule = null; + lastClip = null; + } linePath.reset(); } @@ -702,6 +719,14 @@ public void fillPath(Path.FillType windingRule) throws IOException canvas.drawPath(linePath, paint); } + if (clipWindingRule != null) + { + linePath.setFillType(clipWindingRule); + getGraphicsState().intersectClippingPath(linePath); + clipWindingRule = null; + lastClip = null; + } + linePath.reset(); if (noAntiAlias) @@ -732,10 +757,13 @@ private boolean isRectangular(Path path) @Override public void fillAndStrokePath(Path.FillType windingRule) throws IOException { + Path.FillType clipWindingRuleOriginal = clipWindingRule; + clipWindingRule = null; // Cloning needed because fillPath() resets linePath Path path = new Path(linePath); fillPath(windingRule); linePath = path; + clipWindingRule = clipWindingRuleOriginal; strokePath(); } @@ -782,7 +810,13 @@ public void closePath() @Override public void endPath() { -// TODO: PdfBox-Android adding clipping causes rendering issues + if (clipWindingRule != null) + { + linePath.setFillType(clipWindingRule); + getGraphicsState().intersectClippingPath(linePath); + clipWindingRule = null; + lastClip = null; + } linePath.reset(); } @@ -801,40 +835,14 @@ public void drawImage(PDImage pdImage) throws IOException Matrix ctm = getGraphicsState().getCurrentTransformationMatrix(); AffineTransform at = ctm.createAffineTransform(); - if (!pdImage.getInterpolate()) - { - // if the image is scaled down, we use smooth interpolation, eg PDFBOX-2364 - // only when scaled up do we use nearest neighbour, eg PDFBOX-2302 / mori-cvpr01.pdf - // PDFBOX-4930: we use the sizes of the ARGB image. These can be different - // than the original sizes of the base image, when the mask is bigger. - // PDFBOX-5091: also consider subsampling, the sizes are different too. - Bitmap bim; - if (subsamplingAllowed) - { - bim = pdImage.getImage(null, getSubsampling(pdImage, at)); - } - else - { - bim = pdImage.getImage(); - } - Matrix m = new Matrix(at); - boolean isScaledUp = bim.getWidth() < Math.abs(Math.round(m.getScalingFactorX())) || - bim.getHeight() < Math.abs(Math.round(m.getScalingFactorY())); - - if (isScaledUp) - { -// graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, -// RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); - } - } - setClip(); if (pdImage.isStencil()) { -// if (getGraphicsState().getNonStrokingColor().getColorSpace() instanceof PDPattern) TODO: PdfBox-Android -// else -// TODO: PdfBox-Android draw stenciled Bitmap + Paint stencilPaint = new Paint(); + stencilPaint.setColor(getNonStrokingColor()); + Bitmap stencil = pdImage.getStencilImage(stencilPaint); + drawBitmap(pdImage, stencil, at); } else { @@ -842,12 +850,12 @@ public void drawImage(PDImage pdImage) throws IOException { int subsampling = getSubsampling(pdImage, at); // draw the subsampled image - drawBitmap(pdImage.getImage(null, subsampling), at); + drawBitmap(pdImage, pdImage.getImage(null, subsampling), at); } else { // subsampling not allowed, draw the image - drawBitmap(pdImage.getImage(), at); + drawBitmap(pdImage, pdImage.getImage(), at); } } @@ -860,15 +868,15 @@ public void drawImage(PDImage pdImage) throws IOException } /** - * Calculated the subsampling frequency for a given PDImage based on the current transformation - * and its calculated transform + * Calculates the subsampling frequency for a given PDImage based on the current transformation + * and its calculated transform. Extend this method if you want to use your own strategy. * * @param pdImage PDImage to be drawn * @param at Transform that will be applied to the image when drawing * @return The rounded-down ratio of image pixels to drawn pixels. Returned value will always be - * >=1. + * >=1. */ - private int getSubsampling(PDImage pdImage, AffineTransform at) + protected int getSubsampling(PDImage pdImage, AffineTransform at) { // calculate subsampling according to the resulting image size double scale = Math.abs(at.getDeterminant() * xform.getDeterminant()); @@ -891,7 +899,7 @@ private int getSubsampling(PDImage pdImage, AffineTransform at) return subsampling; } - private void drawBitmap(Bitmap image, AffineTransform at) throws IOException + private void drawBitmap(PDImage pdImage, Bitmap image, AffineTransform at) throws IOException { setClip(); AffineTransform imageTransform = new AffineTransform(at); @@ -901,7 +909,15 @@ private void drawBitmap(Bitmap image, AffineTransform at) throws IOException imageTransform.translate(0, -height); PDSoftMask softMask = getGraphicsState().getSoftMask(); - if( softMask != null ) + + // PDFBOX-5307 / PDF.js PR#19269 + // From section 11.6.4.3 Mask Shape and Opacity in the PDF specification: + // "Either form of mask in the image dictionary shall override the current soft mask + // in the graphics state" + boolean hasImageMask = ((COSDictionary) pdImage.getCOSObject()).containsKey(COSName.MASK) || + ((COSDictionary) pdImage.getCOSObject()).containsKey(COSName.SMASK); + + if (softMask != null && !hasImageMask) { RectF rectangle = new RectF(0, 0, width, height); // Paint awtPaint; TODO: PdfBox-Android @@ -952,12 +968,16 @@ private Bitmap applyTransferFunction(Bitmap image, COSBase transfer) throws IOEx } // apply the transfer function to each color, but keep alpha + int width = image.getWidth(); + int height = image.getHeight(); + int[] pixels = new int[width]; float[] input = new float[1]; - for (int x = 0; x < image.getWidth(); ++x) + for (int y = 0; y < height; ++y) { - for (int y = 0; y < image.getHeight(); ++y) + image.getPixels(pixels, 0, width, 0, y, width, 1); + for (int x = 0; x < width; ++x) { - int rgb = image.getPixel(x, y); + int rgb = pixels[x]; int ri = (rgb >> 16) & 0xFF; int gi = (rgb >> 8) & 0xFF; int bi = rgb & 0xFF; @@ -994,8 +1014,9 @@ private Bitmap applyTransferFunction(Bitmap image, COSBase transfer) throws IOEx bo = (int) (bf.eval(input)[0] * 255); bMap[bi] = bo; } - bim.setPixel(x, y, (rgb & 0xFF000000) | (ro << 16) | (go << 8) | bo); + pixels[x] = (rgb & 0xFF000000) | (ro << 16) | (go << 8) | bo; } + bim.setPixels(pixels, 0, width, 0, y, width, 1); } return bim; } @@ -1054,28 +1075,13 @@ public void shadingFill(COSName shadingName) throws IOException public void showAnnotation(PDAnnotation annotation) throws IOException { lastClip = null; - // Device checks shouldn't be needed - if (annotation.isNoView()) - { - return; - } - if (annotation.isHidden()) - { - return; - } - if (annotation.isInvisible() && annotation instanceof PDAnnotationUnknown) - { - // "If set, do not display the annotation if it does not belong to one - // of the standard annotation types and no annotation handler is available." - return; - } - //TODO support NoZoom, example can be found in p5 of PDFBOX-2348 - if (isHiddenOCG(annotation.getOptionalContent())) + if (shouldSkipAnnotation(annotation)) { return; } + //TODO support NoZoom, example can be found in p5 of PDFBOX-2348 PDAppearanceDictionary appearance = annotation.getAppearance(); if (appearance == null || appearance.getNormalAppearance() == null) { @@ -1084,14 +1090,28 @@ public void showAnnotation(PDAnnotation annotation) throws IOException if (annotation.isNoRotate() && getCurrentPage().getRotation() != 0) { + appearance = annotation.getAppearance(); + if (appearance != null) + { + PDAppearanceEntry appearanceEntry = appearance.getNormalAppearance(); + if (appearanceEntry != null && appearanceEntry.isStream() && + hasTransparency(appearanceEntry.getAppearanceStream())) + { + // PDFBOX-4744: avoid appearances with transparency groups until we have fixed + // the rendering. A real solution should probably be + // in PDFStreamEngine.processAnnotation(). + annotation.constructAppearances(); + } + } PDRectangle rect = annotation.getRectangle(); - android.graphics.Matrix savedTransform = canvas.getMatrix(); + canvas.save(); // "The upper-left corner of the annotation remains at the same point in // default user space; the annotation pivots around that point." canvas.rotate(getCurrentPage().getRotation(), rect.getLowerLeftX(), rect.getUpperRightY()); super.showAnnotation(annotation); - canvas.setMatrix(savedTransform); + canvas.restore(); + annotation.setAppearance(appearance); // restore } else { @@ -1099,6 +1119,56 @@ public void showAnnotation(PDAnnotation annotation) throws IOException } } + private boolean shouldSkipAnnotation(PDAnnotation annotation) + { + if (destination == RenderDestination.PRINT && !annotation.isPrinted()) + { + return true; + } + if ((destination == RenderDestination.VIEW || destination == RenderDestination.EXPORT) && + annotation.isNoView()) + { + return true; + } + if (annotation.isHidden()) + { + return true; + } + if (annotation.isInvisible() && annotation instanceof PDAnnotationUnknown) + { + // "If set, do not display the annotation if it does not belong to one + // of the standard annotation types and no annotation handler is available." + return true; + } + return isHiddenOCG(annotation.getOptionalContent()); + } + + private boolean hasTransparency(PDFormXObject form) throws IOException + { + if (form == null) + { + return false; + } + PDResources resources = form.getResources(); + if (resources == null) + { + return false; + } + for (COSName name : resources.getXObjectNames()) + { + PDXObject xObject = resources.getXObject(name); + if (xObject instanceof PDTransparencyGroup) + { + return true; + } + if (xObject instanceof PDFormXObject && hasTransparency((PDFormXObject) xObject)) + { + return true; + } + } + return false; + } + /** * {@inheritDoc} */ @@ -1447,16 +1517,24 @@ private boolean isGray(PDColorSpace colorSpace) private boolean hasBlendMode(PDTransparencyGroup group, Set groupsDone) { - if (groupsDone.contains(group.getCOSObject())) + COSStream groupCOSStream = group.getCOSObject(); + if (groupsDone.contains(groupCOSStream)) { - // The group was already processed. Avoid endless recursion. + // The group is being processed. Avoid endless recursion. return false; } - groupsDone.add(group.getCOSObject()); + groupsDone.add(groupCOSStream); + + Boolean val = blendModeMap.get(groupCOSStream); + if (val != null) + { + return val; + } PDResources resources = group.getResources(); if (resources == null) { + blendModeMap.put(groupCOSStream, false); return false; } for (COSName name : resources.getExtGStateNames()) @@ -1469,6 +1547,7 @@ private boolean hasBlendMode(PDTransparencyGroup group, Set groupsDone) BlendMode blendMode = extGState.getBlendMode(); if (blendMode != BlendMode.NORMAL) { + blendModeMap.put(groupCOSStream, true); return true; } } @@ -1488,10 +1567,12 @@ private boolean hasBlendMode(PDTransparencyGroup group, Set groupsDone) if (xObject instanceof PDTransparencyGroup && hasBlendMode((PDTransparencyGroup)xObject, groupsDone)) { + blendModeMap.put(groupCOSStream, true); return true; } } + blendModeMap.put(groupCOSStream, false); return false; } @@ -1506,11 +1587,23 @@ public void beginMarkedContentSequence(COSName tag, COSDictionary properties) nestedHiddenOCGCount++; return; } - if (tag == null || getPage().getResources() == null) + PDPropertyList propertyList = null; + if (properties != null) { - return; + propertyList = PDPropertyList.create(properties); + } + if (propertyList == null && tag != null) + { + if (getResources() != null) + { + propertyList = getResources().getProperties(tag); + } + if (propertyList == null && getPage().getResources() != null) + { + propertyList = getPage().getResources().getProperties(tag); + } } - if (isHiddenOCG(getPage().getResources().getProperties(tag))) + if (isHiddenOCG(propertyList)) { nestedHiddenOCGCount = 1; } diff --git a/library/src/main/java/com/tom_roush/pdfbox/rendering/TTFGlyph2D.java b/library/src/main/java/com/tom_roush/pdfbox/rendering/TTFGlyph2D.java index a1c52adfa..e5c7abcba 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/rendering/TTFGlyph2D.java +++ b/library/src/main/java/com/tom_roush/pdfbox/rendering/TTFGlyph2D.java @@ -33,12 +33,12 @@ Licensed to the Apache Software Foundation (ASF) under one or more import com.tom_roush.pdfbox.pdmodel.font.PDTrueTypeFont; import com.tom_roush.pdfbox.pdmodel.font.PDType0Font; import com.tom_roush.pdfbox.pdmodel.font.PDVectorFont; - /** * This class provides a glyph to Path conversion for TrueType and OpenType fonts. */ final class TTFGlyph2D implements Glyph2D { + private final PDFont font; private final TrueTypeFont ttf; private PDVectorFont vectorFont; @@ -132,7 +132,7 @@ public Path getPathForGID(int gid, int code) throws IOException int cid = ((PDType0Font) font).codeToCID(code); String cidHex = String.format("%04x", cid); Log.w("PdfBox-Android", "No glyph for code " + code + " (CID " + cidHex + ") in font " + - font.getName()); + font.getName()); } else { @@ -160,6 +160,10 @@ public Path getPathForGID(int gid, int code) throws IOException if (hasScaling) { AffineTransform atScale = AffineTransform.getScaleInstance(scale, scale); + + // PDFBOX-5567: copy to avoid repeated modification on cached path + glyphPath = new Path(glyphPath); + glyphPath.transform(atScale.toMatrix()); } // glyphs.put(gid, glyphPath); TODO: PdfBox-Android diff --git a/library/src/main/java/com/tom_roush/pdfbox/text/PDFMarkedContentExtractor.java b/library/src/main/java/com/tom_roush/pdfbox/text/PDFMarkedContentExtractor.java index c9e1f3142..35b3a04f6 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/text/PDFMarkedContentExtractor.java +++ b/library/src/main/java/com/tom_roush/pdfbox/text/PDFMarkedContentExtractor.java @@ -16,23 +16,24 @@ */ package com.tom_roush.pdfbox.text; +import android.graphics.Point; + import java.io.IOException; import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Deque; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Deque; -import com.tom_roush.pdfbox.cos.COSDictionary; -import com.tom_roush.pdfbox.cos.COSName; -import com.tom_roush.pdfbox.pdmodel.documentinterchange.markedcontent.PDMarkedContent; -import com.tom_roush.pdfbox.pdmodel.graphics.PDXObject; import com.tom_roush.pdfbox.contentstream.operator.markedcontent.BeginMarkedContentSequence; import com.tom_roush.pdfbox.contentstream.operator.markedcontent.BeginMarkedContentSequenceWithProperties; import com.tom_roush.pdfbox.contentstream.operator.markedcontent.DrawObject; import com.tom_roush.pdfbox.contentstream.operator.markedcontent.EndMarkedContentSequence; - +import com.tom_roush.pdfbox.cos.COSDictionary; +import com.tom_roush.pdfbox.cos.COSName; +import com.tom_roush.pdfbox.pdmodel.documentinterchange.markedcontent.PDMarkedContent; +import com.tom_roush.pdfbox.pdmodel.graphics.PDXObject; /** * This is an stream engine to extract the marked content of a pdf. * @@ -183,11 +184,11 @@ protected void processTextPosition( TextPosition text ) float charY = sameTextCharacter.getY(); //only want to suppress if( charCharacter != null && - //charCharacter.equals( textCharacter ) && - within( charX, textX, tolerance ) && - within( charY, - textY, - tolerance ) ) + //charCharacter.equals( textCharacter ) && + within( charX, textX, tolerance ) && + within( charY, + textY, + tolerance ) ) { suppressCharacter = true; break; @@ -200,48 +201,9 @@ protected void processTextPosition( TextPosition text ) } } - if( showCharacter ) + if (showCharacter && !this.currentMarkedContents.isEmpty()) { - List textList = new ArrayList(); - - /* In the wild, some PDF encoded documents put diacritics (accents on - * top of characters) into a separate Tj element. When displaying them - * graphically, the two chunks get overlaid. With text output though, - * we need to do the overlay. This code recombines the diacritic with - * its associated character if the two are consecutive. - */ - if(textList.isEmpty()) - { - textList.add(text); - } - else - { - /* test if we overlap the previous entry. - * Note that we are making an assumption that we need to only look back - * one TextPosition to find what we are overlapping. - * This may not always be true. */ - TextPosition previousTextPosition = textList.get(textList.size()-1); - if(text.isDiacritic() && previousTextPosition.contains(text)) - { - previousTextPosition.mergeDiacritic(text); - } - /* If the previous TextPosition was the diacritic, merge it into this - * one and remove it from the list. */ - else if(previousTextPosition.isDiacritic() && text.contains(previousTextPosition)) - { - text.mergeDiacritic(previousTextPosition); - textList.remove(textList.size()-1); - textList.add(text); - } - else - { - textList.add(text); - } - } - if (!this.currentMarkedContents.isEmpty()) - { - this.currentMarkedContents.peek().addText(text); - } + this.currentMarkedContents.peek().addText(text); } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/text/PDFTextStripper.java b/library/src/main/java/com/tom_roush/pdfbox/text/PDFTextStripper.java index ded8cc2e1..ce8d87d4c 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/text/PDFTextStripper.java +++ b/library/src/main/java/com/tom_roush/pdfbox/text/PDFTextStripper.java @@ -27,11 +27,12 @@ import java.io.Writer; import java.text.Bidi; import java.text.Normalizer; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; +import java.util.Deque; import java.util.HashMap; import java.util.Iterator; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.SortedMap; @@ -41,15 +42,20 @@ import java.util.TreeSet; import java.util.regex.Pattern; +import com.tom_roush.pdfbox.contentstream.operator.markedcontent.BeginMarkedContentSequence; +import com.tom_roush.pdfbox.contentstream.operator.markedcontent.BeginMarkedContentSequenceWithProperties; +import com.tom_roush.pdfbox.contentstream.operator.markedcontent.EndMarkedContentSequence; +import com.tom_roush.pdfbox.cos.COSDictionary; +import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.android.PDFBoxResourceLoader; import com.tom_roush.pdfbox.pdmodel.PDDocument; import com.tom_roush.pdfbox.pdmodel.PDPage; import com.tom_roush.pdfbox.pdmodel.PDPageTree; import com.tom_roush.pdfbox.pdmodel.common.PDRectangle; +import com.tom_roush.pdfbox.pdmodel.documentinterchange.markedcontent.PDMarkedContent; import com.tom_roush.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem; import com.tom_roush.pdfbox.pdmodel.interactive.pagenavigation.PDThreadBead; import com.tom_roush.pdfbox.util.IterativeMergeSort; - /** * This class will take a pdf document and strip out all of the text and ignore the formatting and such. Please note; it * is up to clients of this class to verify that a specific user has the correct permissions to extract text from the @@ -65,6 +71,7 @@ public class PDFTextStripper extends LegacyPDFStreamEngine private static float defaultIndentThreshold = 2.0f; private static float defaultDropThreshold = 2.5f; + // enable the ability to set the default indent/drop thresholds // with -D system properties: // pdftextstripper.indent @@ -123,7 +130,7 @@ public class PDFTextStripper extends LegacyPDFStreamEngine private String articleStart = ""; private String articleEnd = ""; - private int currentPageNo = 0; + private int currentPageNo = 1; private int startPage = 1; private int endPage = Integer.MAX_VALUE; private PDOutlineItem startBookmark = null; @@ -137,6 +144,7 @@ public class PDFTextStripper extends LegacyPDFStreamEngine private boolean shouldSeparateByBeads = true; private boolean sortByPosition = false; private boolean addMoreFormatting = false; + private boolean ignoreContentStreamSpaceGlyphs = false; private float indentThreshold = defaultIndentThreshold; private float dropThreshold = defaultDropThreshold; @@ -147,18 +155,24 @@ public class PDFTextStripper extends LegacyPDFStreamEngine private List beadRectangles = null; + // use a stack so we don't get confused if another BDC within "/ActualText... BDC" block + private final Deque currentMarkedContents = new ArrayDeque(); + // to replace the unicode of the first TextPosition and empty the others + private boolean firstActualTextPosition = false; + private String actualText = null; + /** * The charactersByArticle is used to extract text by article divisions. For example a PDF that has two columns like * a newspaper, we want to extract the first column and then the second column. In this example the PDF would have 2 * beads(or articles), one for each column. The size of the charactersByArticle would be 5, because not all text on - * the screen will fall into one of the articles. The five divisions are shown below - * - * Text before first article - * first article text - * text between first article and second article - * second article text - * text after second article - * + * the screen will fall into one of the articles. The five divisions are shown below: + *
          + *
        1. Text before first article
        2. + *
        3. first article text
        4. + *
        5. text between first article and second article
        6. + *
        7. second article text
        8. + *
        9. text after second article
        10. + *
        * Most PDFs won't have any beads, so charactersByArticle will contain a single entry. */ protected ArrayList> charactersByArticle = new ArrayList>(); @@ -180,6 +194,9 @@ public class PDFTextStripper extends LegacyPDFStreamEngine */ public PDFTextStripper() throws IOException { + addOperator(new BeginMarkedContentSequenceWithProperties()); + addOperator(new BeginMarkedContentSequence()); + addOperator(new EndMarkedContentSequence()); } /** @@ -189,7 +206,7 @@ public PDFTextStripper() throws IOException *

        IMPORTANT: By default, text extraction is done in the same sequence as the text in the PDF page content stream. * PDF is a graphic format, not a text format, and unlike HTML, it has no requirements that text one on page * be rendered in a certain order. The order is the one that was determined by the software that created the - * PDF. To get text sorted from left to right and top to botton, use {@link #setSortByPosition(boolean)}. + * PDF. To get text sorted from left to right and top to bottom, use {@link #setSortByPosition(boolean)}. * * @param doc The document to get the text from. * @return The text of the PDF document. @@ -204,7 +221,7 @@ public String getText(PDDocument doc) throws IOException private void resetEngine() { - currentPageNo = 0; + currentPageNo = 1; document = null; if (charactersByArticle != null) { @@ -248,7 +265,7 @@ public void writeText(PDDocument doc, Writer outputStream) throws IOException protected void processPages(PDPageTree pages) throws IOException { PDPage startBookmarkPage = startBookmark == null ? null - : startBookmark.findDestinationPage(document); + : startBookmark.findDestinationPage(document); if (startBookmarkPage != null) { startBookmarkPageNumber = pages.indexOf(startBookmarkPage) + 1; @@ -260,7 +277,7 @@ protected void processPages(PDPageTree pages) throws IOException } PDPage endBookmarkPage = endBookmark == null ? null - : endBookmark.findDestinationPage(document); + : endBookmark.findDestinationPage(document); if (endBookmarkPage != null) { endBookmarkPageNumber = pages.indexOf(endBookmarkPage) + 1; @@ -272,8 +289,8 @@ protected void processPages(PDPageTree pages) throws IOException } if (startBookmarkPageNumber == -1 && startBookmark != null && endBookmarkPageNumber == -1 - && endBookmark != null - && startBookmark.getCOSObject() == endBookmark.getCOSObject()) + && endBookmark != null + && startBookmark.getCOSObject() == endBookmark.getCOSObject()) { // this is a special case where both the start and end bookmark // are the same but point to nothing. In this case @@ -284,11 +301,11 @@ protected void processPages(PDPageTree pages) throws IOException for (PDPage page : pages) { - currentPageNo++; if (page.hasContents()) { processPage(page); } + currentPageNo++; } } @@ -326,8 +343,8 @@ protected void endDocument(PDDocument document) throws IOException public void processPage(PDPage page) throws IOException { if (currentPageNo >= startPage && currentPageNo <= endPage - && (startBookmarkPageNumber == -1 || currentPageNo >= startBookmarkPageNumber) - && (endBookmarkPageNumber == -1 || currentPageNo <= endBookmarkPageNumber)) + && (startBookmarkPageNumber == -1 || currentPageNo >= startBookmarkPageNumber) + && (endBookmarkPageNumber == -1 || currentPageNo <= endBookmarkPageNumber)) { startPage(page); @@ -367,8 +384,9 @@ public void processPage(PDPage page) throws IOException private void fillBeadRectangles(PDPage page) { - beadRectangles = new ArrayList(); - for (PDThreadBead bead : page.getThreadBeads()) + List threadBeads = page.getThreadBeads(); + beadRectangles = new ArrayList(threadBeads.size()); + for (PDThreadBead bead : threadBeads) { if (bead == null || bead.getRectangle() == null) { @@ -508,6 +526,8 @@ protected void writePage() throws IOException { IterativeMergeSort.sort(textList, comparator); } + // PDFBOX-5487: Remove all space characters if contained within the adjacent letters + removeContainedSpaces(textList); } startArticle(); @@ -535,15 +555,19 @@ protected void writePage() throws IOException PositionWrapper current = new PositionWrapper(position); String characterValue = position.getUnicode(); + // PDFBOX-3774: conditionally ignore spaces from the content stream + if (" ".equals(characterValue) && getIgnoreContentStreamSpaceGlyphs()) + { + continue; + } + // Resets the average character width when we see a change in font // or a change in the font size - if (lastPosition != null && (position.getFont() != lastPosition.getTextPosition() - .getFont() - || position.getFontSize() != lastPosition.getTextPosition().getFontSize())) + if (lastPosition != null + && hasFontOrSizeChanged(position, lastPosition.getTextPosition())) { previousAveCharWidth = -1; } - float positionX; float positionY; float positionWidth; @@ -634,7 +658,7 @@ protected void writePage() throws IOException writeLine(normalize(line)); line.clear(); lastLineStartPosition = handleLineSeparation(current, lastPosition, - lastLineStartPosition, maxHeightForLine); + lastLineStartPosition, maxHeightForLine); expectedStartOfNextWordX = EXPECTED_START_OF_NEXT_WORD_X_RESET_VALUE; maxYForLine = MAX_Y_FOR_LINE_RESET_VALUE; maxHeightForLine = MAX_HEIGHT_FOR_LINE_RESET_VALUE; @@ -642,12 +666,12 @@ protected void writePage() throws IOException } // test if our TextPosition starts after a new word would be expected to start if (expectedStartOfNextWordX != EXPECTED_START_OF_NEXT_WORD_X_RESET_VALUE - && expectedStartOfNextWordX < positionX - // only bother adding a word separator if the last character was not a word separator - && (wordSeparator.isEmpty() || // - (lastPosition.getTextPosition().getUnicode() != null - && !lastPosition.getTextPosition().getUnicode() - .endsWith(wordSeparator)))) + && expectedStartOfNextWordX < positionX + // only bother adding a word separator if the last character was not a word separator + && (wordSeparator.isEmpty() || // + (lastPosition.getTextPosition().getUnicode() != null + && !lastPosition.getTextPosition().getUnicode() + .endsWith(wordSeparator)))) { line.add(LineItem.getWordSeparator()); } @@ -655,7 +679,7 @@ protected void writePage() throws IOException // between the last character and the current one, // reset the max line height as the font size may have completely changed if (Math.abs(position.getX() - - lastPosition.getTextPosition().getX()) > (wordSpacing + deltaSpace)) + - lastPosition.getTextPosition().getX()) > (wordSpacing + deltaSpace)) { maxYForLine = MAX_Y_FOR_LINE_RESET_VALUE; maxHeightForLine = MAX_HEIGHT_FOR_LINE_RESET_VALUE; @@ -703,10 +727,69 @@ protected void writePage() throws IOException writePageEnd(); } + private boolean hasFontOrSizeChanged(TextPosition current, TextPosition last) + { + if (last == null) + { + return false; + } + // compare font sizes + if (Float.compare(current.getFontSize(), last.getFontSize()) != 0) + { + return true; + } + // compare font instances, may not work if the resource cache is disabled + if (current.getFont() == last.getFont()) + { + return false; + } + String currentFontName = current.getFont().getName(); + String lastFontName = last.getFont().getName(); + if (currentFontName != null) + { + // compare font names + return !currentFontName.equals(lastFontName); + } + if (lastFontName != null) + { + // currentFontName is null but lastFontName isn't -> font changes + return true; + } + // both fonts don't have a name -> compare hashes + return current.getFont().hashCode() != last.getFont().hashCode(); + } + private boolean overlap(float y1, float height1, float y2, float height2) { return within(y1, y2, .1f) || y2 <= y1 && y2 >= y1 - height1 - || y1 <= y2 && y1 >= y2 - height2; + || y1 <= y2 && y1 >= y2 - height2; + } + + /** + * Remove all space characters if contained within the adjacent letters + */ + private void removeContainedSpaces(List textList) + { + Iterator iterator = textList.iterator(); + + if (!iterator.hasNext()) + { + return; + } + TextPosition previousPosition = iterator.next(); + + while (iterator.hasNext()) + { + TextPosition position = iterator.next(); + if (" ".equals(position.getUnicode()) && previousPosition.completelyContains(position)) + { + iterator.remove(); + } + else + { + previousPosition = position; + } + } } /** @@ -776,6 +859,35 @@ private boolean within(float first, float second, float variance) return second < first + variance && second > first - variance; } + @Override + public void beginMarkedContentSequence(COSName tag, COSDictionary properties) + { + PDMarkedContent markedContent = PDMarkedContent.create(tag, properties); + currentMarkedContents.push(markedContent); + actualText = markedContent.getActualText(); + if (actualText != null) + { + actualText = actualText.replace("\u00ad", ""); // remove soft hyphens + firstActualTextPosition = true; + } + super.beginMarkedContentSequence(tag, properties); + } + + @Override + public void endMarkedContentSequence() + { + PDMarkedContent markedContent = currentMarkedContents.peek(); + if (markedContent != null) + { + if (markedContent.getActualText() != null) + { + actualText = null; + } + currentMarkedContents.pop(); + } + super.endMarkedContentSequence(); + } + /** * This will process a TextPosition object and add the text to the list of characters on a page. It takes care of * overlapping text. @@ -785,15 +897,27 @@ private boolean within(float first, float second, float variance) @Override protected void processTextPosition(TextPosition text) { + if (actualText != null) + { + if (firstActualTextPosition) + { + text.setUnicode(actualText); + firstActualTextPosition = false; + } + else + { + text.setUnicode(""); + } + } boolean showCharacter = true; - if (suppressDuplicateOverlappingText) + if (suppressDuplicateOverlappingText && actualText == null) { showCharacter = false; String textCharacter = text.getUnicode(); float textX = text.getX(); float textY = text.getY(); TreeMap> sameTextCharacters = characterListMapping - .get(textCharacter); + .get(textCharacter); if (sameTextCharacters == null) { sameTextCharacters = new TreeMap>(); @@ -813,7 +937,7 @@ protected void processTextPosition(TextPosition text) float tolerance = text.getWidth() / textCharacter.length() / 3.0f; SortedMap> xMatches = sameTextCharacters.subMap(textX - tolerance, - textX + tolerance); + textX + tolerance); for (TreeSet xMatch : xMatches.values()) { SortedSet yMatches = xMatch.subSet(textY - tolerance, textY + tolerance); @@ -856,17 +980,17 @@ protected void processTextPosition(TextPosition text) foundArticleDivisionIndex = i * 2 + 1; } else if ((x < rect.getLowerLeftX() || y < rect.getUpperRightY()) - && notFoundButFirstLeftAndAboveArticleDivisionIndex == -1) + && notFoundButFirstLeftAndAboveArticleDivisionIndex == -1) { notFoundButFirstLeftAndAboveArticleDivisionIndex = i * 2; } else if (x < rect.getLowerLeftX() - && notFoundButFirstLeftArticleDivisionIndex == -1) + && notFoundButFirstLeftArticleDivisionIndex == -1) { notFoundButFirstLeftArticleDivisionIndex = i * 2; } else if (y < rect.getUpperRightY() - && notFoundButFirstAboveArticleDivisionIndex == -1) + && notFoundButFirstAboveArticleDivisionIndex == -1) { notFoundButFirstAboveArticleDivisionIndex = i * 2; } @@ -960,6 +1084,10 @@ public int getStartPage() */ public void setStartPage(int startPageValue) { + if (startPageValue <= 0) + { + Log.w("PdfBox-Android", "Parameter must be 1-based, but is " + startPageValue); + } startPage = startPageValue; } @@ -982,6 +1110,10 @@ public int getEndPage() */ public void setEndPage(int endPageValue) { + if (endPageValue <= 0) + { + Log.w("PdfBox-Android", "Parameter must be 1-based, but is " + endPageValue); + } endPage = endPageValue; } @@ -1069,7 +1201,7 @@ protected List> getCharactersByArticle() } /** - * By default the text stripper will attempt to remove text that overlapps each other. Word paints the same + * By default the text stripper will attempt to remove text that overlaps each other. Word paints the same * character several times in order to make it look bold. By setting this to false all text will be extracted, which * means that certain sections will be duplicated, but better performance will be noticed. * @@ -1186,6 +1318,32 @@ public void setSortByPosition(boolean newSortByPosition) sortByPosition = newSortByPosition; } + /** + * Determines whether spaces in the content stream text rendering instructions will be ignored + * during text extraction. + * + * @return true is space glyphs in the content stream text rendering instructions will be + * ignored - default is false + */ + public boolean getIgnoreContentStreamSpaceGlyphs() + { + return ignoreContentStreamSpaceGlyphs; + } + + /** + * Instruct the algorithm to ignore any spaces in the text rendering instructions in the content + * stream, and instead rely purely on the algorithm to determine where word breaks are. + * + * This can improve text extraction results where the content stream is sorted by position and + * has text overlapping spaces, but could cause some word breaks to not be added to the output + * + * @param newIgnoreContentStreamSpaceGlyphs whether PDF Box should ignore context stream spaces + */ + public void setIgnoreContentStreamSpaceGlyphs(boolean newIgnoreContentStreamSpaceGlyphs) + { + ignoreContentStreamSpaceGlyphs = newIgnoreContentStreamSpaceGlyphs; + } + /** * Get the current space width-based tolerance value that is being used to estimate where spaces in text should be * added. Note that the default value for this has been determined from trial and error. @@ -1409,8 +1567,8 @@ public void setArticleEnd(String articleEndValue) * @throws IOException if something went wrong */ private PositionWrapper handleLineSeparation(PositionWrapper current, - PositionWrapper lastPosition, PositionWrapper lastLineStartPosition, - float maxHeightForLine) throws IOException + PositionWrapper lastPosition, PositionWrapper lastLineStartPosition, + float maxHeightForLine) throws IOException { current.setLineStart(); isParagraphSeparation(current, lastPosition, lastLineStartPosition, maxHeightForLine); @@ -1461,7 +1619,7 @@ private PositionWrapper handleLineSeparation(PositionWrapper current, * @param maxHeightForLine max height for text positions since lasLineStartPosition. */ private void isParagraphSeparation(PositionWrapper position, PositionWrapper lastPosition, - PositionWrapper lastLineStartPosition, float maxHeightForLine) + PositionWrapper lastLineStartPosition, float maxHeightForLine) { boolean result = false; if (lastLineStartPosition == null) @@ -1471,13 +1629,13 @@ private void isParagraphSeparation(PositionWrapper position, PositionWrapper las else { float yGap = Math.abs(position.getTextPosition().getYDirAdj() - - lastPosition.getTextPosition().getYDirAdj()); + - lastPosition.getTextPosition().getYDirAdj()); float newYVal = multiplyFloat(getDropThreshold(), maxHeightForLine); // do we need to flip this for rtl? float xGap = position.getTextPosition().getXDirAdj() - - lastLineStartPosition.getTextPosition().getXDirAdj(); + - lastLineStartPosition.getTextPosition().getXDirAdj(); float newXVal = multiplyFloat(getIndentThreshold(), - position.getTextPosition().getWidthOfSpace()); + position.getTextPosition().getWidthOfSpace()); float positionWidth = multiplyFloat(0.25f, position.getTextPosition().getWidth()); if (yGap > newYVal) @@ -1624,8 +1782,8 @@ private Pattern matchListItemPattern(PositionWrapper pw) * numerals, etc. Not meant to be comprehensive. */ private static final String[] LIST_ITEM_EXPRESSIONS = { "\\.", "\\d+\\.", "\\[\\d+\\]", - "\\d+\\)", "[A-Z]\\.", "[a-z]\\.", "[A-Z]\\)", "[a-z]\\)", "[IVXL]+\\.", - "[ivxl]+\\.", }; + "\\d+\\)", "[A-Z]\\.", "[a-z]\\.", "[A-Z]\\)", "[a-z]\\)", "[IVXL]+\\.", + "[ivxl]+\\.", }; private List listOfPatterns = null; @@ -1662,7 +1820,7 @@ protected List getListItemPatterns() { if (listOfPatterns == null) { - listOfPatterns = new ArrayList(); + listOfPatterns = new ArrayList(LIST_ITEM_EXPRESSIONS.length); for (String expression : LIST_ITEM_EXPRESSIONS) { Pattern p = Pattern.compile(expression); @@ -1703,7 +1861,7 @@ protected static Pattern matchPattern(String string, List patterns) * @throws IOException if something went wrong */ private void writeLine(List line) - throws IOException + throws IOException { int numberOfStrings = line.size(); for (int i = 0; i < numberOfStrings; i++) @@ -1725,7 +1883,7 @@ private void writeLine(List line) */ private List normalize(List line) { - List normalized = new LinkedList(); + List normalized = new ArrayList(); StringBuilder lineBuilder = new StringBuilder(); List wordPositions = new ArrayList(); @@ -1747,7 +1905,9 @@ private List normalize(List line) * characters, the order of the characters in a word or words in a line may wrong, due to RTL and LTR marks and * characters! * - * Based on http://www.nesterovsky-bros.com/weblog/2013/07/28/VisualToLogicalConversionInJava.aspx + Based on + * an + * article "Visual to logical conversion in Java". * * @param word The word that shall be processed * @return new word with the correct direction of the containing characters @@ -1769,8 +1929,8 @@ private String handleDirection(String word) for (int i = 0; i < runCount; i++) { - levels[i] = (byte)bidi.getRunLevel(i); - runs[i] = i; + levels[i] = (byte)bidi.getRunLevel(i); + runs[i] = i; } // reorder individual parts based on their levels @@ -1781,9 +1941,9 @@ private String handleDirection(String word) for (int i = 0; i < runCount; i++) { - int index = runs[i]; - int start = bidi.getRunStart(index); - int end = bidi.getRunLimit(index); + int index = runs[i]; + int start = bidi.getRunStart(index); + int end = bidi.getRunLimit(index); int level = levels[index]; @@ -1840,7 +2000,7 @@ private String handleDirection(String word) catch (IOException e) { Log.w("PdfBox-Android", "Could not parse BidiMirroring.txt, mirroring char map will be empty: " - + e.getMessage()); + + e.getMessage()); } finally { @@ -1863,7 +2023,7 @@ private String handleDirection(String word) */ private static void parseBidiFile(InputStream inputStream) throws IOException { - LineNumberReader rd = new LineNumberReader(new InputStreamReader(inputStream)); + LineNumberReader rd = new LineNumberReader(new InputStreamReader(inputStream, "US-ASCII")); do { @@ -1941,15 +2101,25 @@ private String normalizeWord(String word) // They add an extra U+0627 character to compensate. // This removes the extra character for those fonts. if (c == 0xFDF2 && q > 0 - && (word.charAt(q - 1) == 0x0627 || word.charAt(q - 1) == 0xFE8D)) + && (word.charAt(q - 1) == 0x0627 || word.charAt(q - 1) == 0xFE8D)) { builder.append("\u0644\u0644\u0647"); } else { // Trim because some decompositions have an extra space, such as U+FC5E - builder.append(Normalizer - .normalize(word.substring(q, q + 1), Normalizer.Form.NFKC).trim()); + String normalized = Normalizer.normalize( + word.substring(q, q + 1), Normalizer.Form.NFKC).trim(); + + // Hebrew in Alphabetic Presentation Forms from FB1D to FB4F and + // Arabic Presentation Forms-A from FB50 to FDFF and + // Arabic Presentation Forms-B from FE70 to FEFF + if (0xFB1D <= c && normalized.length() > 1) + { + // Reverse the order of decomposed Hebrew and Arabic letters + normalized = new StringBuilder(normalized).reverse().toString(); + } + builder.append(normalized); } p = q + 1; } @@ -1971,19 +2141,19 @@ private String normalizeWord(String word) * @return The StringBuilder that must be used when calling this method. */ private StringBuilder normalizeAdd(List normalized, - StringBuilder lineBuilder, List wordPositions, LineItem item) + StringBuilder lineBuilder, List wordPositions, LineItem item) { if (item.isWordSeparator()) { normalized.add( - createWord(lineBuilder.toString(), new ArrayList(wordPositions))); + createWord(lineBuilder.toString(), new ArrayList(wordPositions))); lineBuilder = new StringBuilder(); wordPositions.clear(); } else { TextPosition text = item.getTextPosition(); - lineBuilder.append(text.getUnicode()); + lineBuilder.append(text.getVisuallyOrderedUnicode()); wordPositions.add(text); } return lineBuilder; diff --git a/library/src/main/java/com/tom_roush/pdfbox/text/PDFTextStripperByArea.java b/library/src/main/java/com/tom_roush/pdfbox/text/PDFTextStripperByArea.java index 574114320..a6ab04c69 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/text/PDFTextStripperByArea.java +++ b/library/src/main/java/com/tom_roush/pdfbox/text/PDFTextStripperByArea.java @@ -26,7 +26,6 @@ import java.util.Map; import com.tom_roush.pdfbox.pdmodel.PDPage; - /** * This will extract text from a specified region in the PDF. * @@ -37,7 +36,7 @@ public class PDFTextStripperByArea extends PDFTextStripper private final List regions = new ArrayList(); private final Map regionArea = new HashMap(); private final Map>> regionCharacterList - = new HashMap>>(); + = new HashMap>>(); private final Map regionText = new HashMap(); /** @@ -60,7 +59,7 @@ public final void setShouldSeparateByBeads(boolean aShouldSeparateByBeads) { } - /** + /** * Add a new region to group text by. * * @param regionName The name of the region. @@ -119,7 +118,7 @@ public void extractRegions( PDPage page ) throws IOException setStartPage(getCurrentPageNo()); setEndPage(getCurrentPageNo()); // reset the stored text for the region so this class can be reused. - ArrayList> regionCharactersByArticle = new ArrayList>(); + ArrayList> regionCharactersByArticle = new ArrayList>(1); regionCharactersByArticle.add( new ArrayList() ); regionCharacterList.put( regionName, regionCharactersByArticle ); regionText.put( regionName, new StringWriter() ); diff --git a/library/src/main/java/com/tom_roush/pdfbox/text/TextPosition.java b/library/src/main/java/com/tom_roush/pdfbox/text/TextPosition.java index 3a7ea53ab..83c7fe38c 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/text/TextPosition.java +++ b/library/src/main/java/com/tom_roush/pdfbox/text/TextPosition.java @@ -25,7 +25,6 @@ import com.tom_roush.pdfbox.pdmodel.font.PDFont; import com.tom_roush.pdfbox.util.Matrix; - /** * This represents a string and a position on the screen of those characters. * @@ -33,6 +32,7 @@ */ public final class TextPosition { + private static final Map DIACRITICS = createDiacritics(); // text matrix for the start of the text object, coordinates are in display units @@ -81,9 +81,9 @@ public final class TextPosition * @param fontSizeInPt The font size in pt units (see {@link #getFontSizeInPt()} for details). */ public TextPosition(int pageRotation, float pageWidth, float pageHeight, Matrix textMatrix, - float endX, float endY, float maxHeight, float individualWidth, - float spaceWidth, String unicode, int[] charCodes, PDFont font, - float fontSize, int fontSizeInPt) + float endX, float endY, float maxHeight, float individualWidth, + float spaceWidth, String unicode, int[] charCodes, PDFont font, + float fontSize, int fontSizeInPt) { this.textMatrix = textMatrix; @@ -169,6 +169,42 @@ public String getUnicode() return unicode; } + void setUnicode(String unicode) + { + this.unicode = unicode; + } + + /** + * Same as {@link #getUnicode()} except that returned text is ensured to be + * visually ordered (i.e. same order you would see them displayed on screen when + * looking from left to right). This is important for Arabic/Hebrew where several + * unicode characters can be composed in one glyph with logical order (the order + * in which it would be normally typed from right to left). + * + * @return The string on the screen in visual order. + */ + public String getVisuallyOrderedUnicode() + { + final String text = getUnicode(); + final int length = text.length(); + int nextIndex; + for (int index = 0; index < length; index = nextIndex) + { + int codePoint = text.codePointAt(index); + nextIndex = index + Character.charCount(codePoint); + byte directionality = Character.getDirectionality(codePoint); + if ((directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC || + directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT) + // Even if the directionality is right to left, still there is no need to + // reverse a single code-point + && (index != 0 || nextIndex < length)) + { + return new StringBuilder(text).reverse().toString(); + } + } + return text; + } + /** * Return the internal PDF character codes of the glyphs in this text. * @@ -547,7 +583,7 @@ public boolean contains(TextPosition tp2) double thisYstart = getYDirAdj(); double tp2Ystart = tp2.getYDirAdj(); if (tp2Ystart + tp2.getHeightDir() < thisYstart || - tp2Ystart > thisYstart + getHeightDir()) + tp2Ystart > thisYstart + getHeightDir()) { return false; } @@ -569,6 +605,55 @@ else if (tp2Xstart < thisXstart && tp2Xend < thisXend) return true; } + /** + * Determine if this TextPosition perfectly contains another (i.e. the other TextPosition + * overlaps 100% with this one and fits entirely inside its bounding box when they are rendered + * on top of each other). + * + * @param tp2 The other TestPosition to compare against + * @return True if tp2 is contained completely inside the bounding box of this text. + */ + public boolean completelyContains(TextPosition tp2) + { + // Note: (0, 0) is in the upper left and y-coordinate is top of TextPosition + // +---thisTop------------+ + // | +--tp2Top---+ | + // | | | | + // thisLeft | tp2Right | + // | tp2Left | thisRight + // | | | | + // | +-tp2Bottom-+ | + // +---------thisBottom---+ + + float thisLeft = getXDirAdj(); + float thisWidth = getWidthDirAdj(); + float thisRight = thisLeft + thisWidth; + + float tp2Left = tp2.getXDirAdj(); + float tp2Width = tp2.getWidthDirAdj(); + float tp2Right = tp2Left + tp2Width; + + if (thisLeft > tp2Left || tp2Right > thisRight) + { + return false; + } + + float thisTop = getYDirAdj(); + float thisHeight = getHeightDir(); + float thisBottom = thisTop + thisHeight; + + float tp2Top = tp2.getYDirAdj(); + float tp2Height = tp2.getHeightDir(); + float tp2Bottom = tp2Top + tp2Height; + + if (thisTop > tp2Top || tp2Bottom > thisBottom) + { + return false; + } + + return true; + } + /** * Merge a single character TextPosition into the current object. This is to be used only for * cases where we have a diacritic that overlaps an existing TextPosition. In a graphical @@ -597,14 +682,14 @@ public void mergeDiacritic(TextPosition diacritic) if (i >= widths.length) { Log.i("PdfBox-Android", "diacritic " + diacritic.getUnicode() + " on ligature " + unicode + - " is not supported yet and is ignored (PDFBOX-2831)"); + " is not supported yet and is ignored (PDFBOX-2831)"); break; } float currCharXEnd = currCharXStart + widths[i]; - // this is the case where there is an overlap of the diacritic character with the - // current character and the previous character. If no previous character, just append - // the diacritic after the current one + // this is the case where there is an overlap of the diacritic character with the + // current character and the previous character. If no previous character, just append + // the diacritic after the current one if (diacXStart < currCharXStart && diacXEnd <= currCharXEnd) { if (i == 0) @@ -671,16 +756,26 @@ private void insertDiacritic(int i, TextPosition diacritic) float[] widths2 = new float[widths.length + 1]; System.arraycopy(widths, 0, widths2, 0, i); + // First we add a zero-width entry for the diacritic in the widths array + widths2[i] = widths[i]; + widths2[i + 1] = 0; + System.arraycopy(widths, i + 1, widths2, i + 2, widths.length - i - 1); + // Unicode combining diacritics always go after the base character, regardless of whether // the string is in presentation order or logical order sb.append(unicode.charAt(i)); - widths2[i] = widths[i]; + + // If a surrogate starts at the current position, make sure we preserve it + if (i < unicode.length() - 1 && Character.isSurrogatePair(unicode.charAt(i), unicode.charAt(i + 1))) + { + sb.append(unicode.charAt(i + 1)); + i++; + } + sb.append(combineDiacritic(diacritic.getUnicode())); - widths2[i + 1] = 0; // get the rest of the string sb.append(unicode.substring(i + 1)); - System.arraycopy(widths, i + 1, widths2, i + 2, widths.length - i - 1); unicode = sb.toString(); widths = widths2; @@ -721,7 +816,7 @@ public boolean isDiacritic() } if ("ー".equals(text)) { - // PDFBOX-3833: ー is not a real diacritic like ¨ or ˆ, it just changes the + // PDFBOX-3833: ー is not a real diacritic like ¨ or ˆ, it just changes the // pronunciation of the previous sound, and is printed after the previous glyph // http://www.japanesewithanime.com/2017/04/prolonged-sound-mark.html // Ignoring it as diacritic avoids trouble if it slightly overlaps with the next glyph. @@ -729,10 +824,10 @@ public boolean isDiacritic() } int type = Character.getType(text.charAt(0)); return type == Character.NON_SPACING_MARK || - type == Character.MODIFIER_SYMBOL || - type == Character.MODIFIER_LETTER; + type == Character.MODIFIER_SYMBOL || + type == Character.MODIFIER_LETTER; - } + } /** * Show the string data for this text position. @@ -868,7 +963,7 @@ public boolean equals(Object o) } return font != null ? font.equals(that.font) : that.font == null; - // If changing this method, do not compare mutable fields (PDFBOX-4701) + // If changing this method, do not compare mutable fields (PDFBOX-4701) } @Override diff --git a/library/src/main/java/com/tom_roush/pdfbox/util/Hex.java b/library/src/main/java/com/tom_roush/pdfbox/util/Hex.java index 6728a4c72..b351747b6 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/util/Hex.java +++ b/library/src/main/java/com/tom_roush/pdfbox/util/Hex.java @@ -24,7 +24,6 @@ import java.io.OutputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; - /** * Utility functions for hex encoding. * @@ -32,6 +31,7 @@ */ public final class Hex { + /** * for hex conversion. * @@ -268,7 +268,7 @@ public static byte[] decodeBase64(String base64Value) */ public static byte[] decodeHex(String s) throws IOException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ByteArrayOutputStream baos = new ByteArrayOutputStream((s.length() + 1) / 2); int i = 0; while (i < s.length() - 1) { @@ -278,14 +278,15 @@ public static byte[] decodeHex(String s) throws IOException } else { - String hexByte = s.substring(i, i + 2); - try + int value = 16 * getHexValue(s.charAt(i)) + getHexValue(s.charAt(i + 1)); + if (value >= 0) { - baos.write(Integer.parseInt(hexByte, 16)); // Byte.parseByte won't work with "9C" + baos.write(value); } - catch (NumberFormatException ex) + else { - Log.e("PdfBox-Android", "Can't parse " + hexByte + ", aborting decode", ex); + String hexByte = s.substring(i, i + 2); + Log.e("PdfBox-Android", "Can't parse " + hexByte + ", aborting decode"); break; } i += 2; @@ -293,4 +294,31 @@ public static byte[] decodeHex(String s) throws IOException } return baos.toByteArray(); } + + /** + * Converts a given character to its corresponding hexadecimal value. Valid characters are '0'-'9', 'A'-'F', or + * 'a'-'f'. Returns -256 for invalid characters. + *

        + * The value of -256 is chosen so that to hex digits can be combined before checking for an invalid hex string + * + * @param c the character to be converted to a hexadecimal value + * @return the hexadecimal value of the character, or -256 if the character is invalid + */ + public static int getHexValue(char c) + { + if (c >= '0' && c <= '9') + { + return c - '0'; + } + else if (c >= 'A' && c <= 'F') + { + return c - 'A' + 10; + } + else if (c >= 'a' && c <= 'f') + { + return c - 'a' + 10; + } + return -256; + } + } diff --git a/library/src/main/java/com/tom_roush/pdfbox/util/IterativeMergeSort.java b/library/src/main/java/com/tom_roush/pdfbox/util/IterativeMergeSort.java index 0c374081e..7d9adf8be 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/util/IterativeMergeSort.java +++ b/library/src/main/java/com/tom_roush/pdfbox/util/IterativeMergeSort.java @@ -1,5 +1,3 @@ -package com.tom_roush.pdfbox.util; - /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with @@ -16,18 +14,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.tom_roush.pdfbox.util; + import java.util.Comparator; import java.util.List; import java.util.ListIterator; - /** * This class provides an iterative (bottom-up) implementation of the * MergeSort algorithm for any generic Java * object which implements a {@link Comparator}. * *

        - * This implementation uses an iterative implementation approach over the more - * classical recursive approach in order to save the auxiliary space required + * This implementation uses an iterative implementation approach over the more + * classical recursive approach in order to save the auxiliary space required * by the call stack in recursive implementations. *

        * diff --git a/library/src/main/java/com/tom_roush/pdfbox/util/Matrix.java b/library/src/main/java/com/tom_roush/pdfbox/util/Matrix.java index 96230746a..ebf07b1a5 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/util/Matrix.java +++ b/library/src/main/java/com/tom_roush/pdfbox/util/Matrix.java @@ -25,7 +25,6 @@ import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSFloat; import com.tom_roush.pdfbox.cos.COSNumber; - /** * This class will be used for matrix manipulation. * @@ -357,19 +356,19 @@ public Matrix multiply(Matrix other) public Matrix multiply( Matrix other, Matrix result ) { float[] c = result != null && result != other && result != this ? result.single - : new float[SIZE]; + : new float[SIZE]; multiplyArrays(single, other.single, c); if (!Matrix.isFinite(c[0]) // - || !Matrix.isFinite(c[1]) // - || !Matrix.isFinite(c[2]) // - || !Matrix.isFinite(c[3]) // - || !Matrix.isFinite(c[4]) // - || !Matrix.isFinite(c[5]) // - || !Matrix.isFinite(c[6]) // - || !Matrix.isFinite(c[7]) // - || !Matrix.isFinite(c[8])) + || !Matrix.isFinite(c[1]) // + || !Matrix.isFinite(c[2]) // + || !Matrix.isFinite(c[3]) // + || !Matrix.isFinite(c[4]) // + || !Matrix.isFinite(c[5]) // + || !Matrix.isFinite(c[6]) // + || !Matrix.isFinite(c[7]) // + || !Matrix.isFinite(c[8])) throw new IllegalArgumentException("Multiplying two matrices produces illegal values"); if (result == null) @@ -482,8 +481,8 @@ public Matrix extractScaling() * 0 y 0 * 0 0 1 * - * @param x The xscale operator. - * @param y The yscale operator. + * @param x The x-scale operator. + * @param y The y-scale operator. * @return A new matrix with just the x/y scaling */ public static Matrix getScaleInstance(float x, float y) @@ -603,7 +602,7 @@ public float getScalingFactorX() if (single[1] != 0.0f) { return (float) Math.sqrt(Math.pow(single[0], 2) + - Math.pow(single[1], 2)); + Math.pow(single[1], 2)); } return single[0]; } @@ -618,7 +617,7 @@ public float getScalingFactorY() if (single[3] != 0.0f) { return (float) Math.sqrt(Math.pow(single[3], 2) + - Math.pow(single[4], 2)); + Math.pow(single[4], 2)); } return single[4]; } diff --git a/library/src/main/java/com/tom_roush/pdfbox/util/NumberFormatUtil.java b/library/src/main/java/com/tom_roush/pdfbox/util/NumberFormatUtil.java index b9b21157c..22b5f700e 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/util/NumberFormatUtil.java +++ b/library/src/main/java/com/tom_roush/pdfbox/util/NumberFormatUtil.java @@ -15,6 +15,7 @@ */ package com.tom_roush.pdfbox.util; + /** * This class contains methods to format numbers. * @@ -63,7 +64,7 @@ private NumberFormatUtil() * {@link #MAX_FRACTION_DIGITS}. * * When the number contains more fractional digits than {@code maxFractionDigits} the value will - * be rounded. Rounding is done to the nearest possible value, with the tie breaking rule of + * be rounded. Rounding is done to the nearest possible value, with the tie breaking rule of * rounding away from zero. * * @param value The float value to format @@ -75,10 +76,10 @@ private NumberFormatUtil() public static int formatFloatFast(float value, int maxFractionDigits, byte[] asciiBuffer) { if (Float.isNaN(value) || - Float.isInfinite(value) || - value > Long.MAX_VALUE || - value <= Long.MIN_VALUE || - maxFractionDigits > MAX_FRACTION_DIGITS) + Float.isInfinite(value) || + value > Long.MAX_VALUE || + value <= Long.MIN_VALUE || + maxFractionDigits > MAX_FRACTION_DIGITS) { return -1; } @@ -93,7 +94,7 @@ public static int formatFloatFast(float value, int maxFractionDigits, byte[] asc integerPart = -integerPart; } - //extract fraction part + //extract fraction part long fractionPart = (long) ((Math.abs((double)value) - integerPart) * POWER_OF_TENS[maxFractionDigits] + 0.5d); //Check for rounding to next integer @@ -132,7 +133,7 @@ private static int formatPositiveNumber(long number, int exp, boolean omitTraili int offset = startOffset; long remaining = number; - while (remaining > Integer.MAX_VALUE && (!omitTrailingZeros || remaining > 0)) + while (remaining > Integer.MAX_VALUE) { long digit = remaining / POWER_OF_TENS[exp]; remaining -= (digit * POWER_OF_TENS[exp]); diff --git a/library/src/main/java/com/tom_roush/pdfbox/util/SmallMap.java b/library/src/main/java/com/tom_roush/pdfbox/util/SmallMap.java index 0959f96dd..3a892b59b 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/util/SmallMap.java +++ b/library/src/main/java/com/tom_roush/pdfbox/util/SmallMap.java @@ -23,7 +23,6 @@ import java.util.List; import java.util.Map; import java.util.Set; - /** * Map implementation with a smallest possible memory usage. * It should only be used for maps with small number of items @@ -35,354 +34,354 @@ */ public class SmallMap implements Map { - /** - * stores key-value pair as 2 objects; key first; in case of empty map this might be null - */ - private Object[] mapArr; - - /** Creates empty map. */ - public SmallMap() - { - } - - /** Creates map filled with entries from provided map. */ - public SmallMap(Map initMap) - { - putAll(initMap); - } - - /** - * Returns index of key within map-array or -1 - * if key is not found (or key is null). - */ - private int findKey(Object key) - { - if (isEmpty() || (key==null)) - { - return -1; - } - - for ( int aIdx = 0; aIdx < mapArr.length; aIdx+=2 ) - { - if (key.equals(mapArr[aIdx])) - { - return aIdx; - } - } - - return -1; - } - - /** - * Returns index of value within map-array or -1 - * if value is not found (or value is null). - */ - private int findValue(Object value) - { - if (isEmpty() || (value==null)) - { - return -1; - } - - for ( int aIdx = 1; aIdx < mapArr.length; aIdx+=2 ) - { - if (value.equals(mapArr[aIdx])) - { - return aIdx; - } - } - - return -1; - } - - @Override - public int size() - { - return mapArr == null ? 0 : mapArr.length >> 1; - } - - @Override - public boolean isEmpty() - { - return (mapArr == null) || (mapArr.length == 0); - } - - @Override - public boolean containsKey(Object key) - { - return findKey(key) >= 0; - } - - @Override - public boolean containsValue(Object value) - { - return findValue(value) >= 0; - } - - @SuppressWarnings("unchecked") - @Override - public V get(Object key) - { - int kIdx = findKey(key); - - return kIdx < 0 ? null : (V) mapArr[kIdx+1]; - } - - @Override - public V put(K key, V value) - { - if ((key == null) || (value == null)) - { - throw new NullPointerException( "Key or value must not be null."); - } - - if (mapArr == null) - { - mapArr = new Object[] { key, value }; - return null; - } - else - { - int kIdx = findKey(key); - - if (kIdx < 0) - { - // key unknown - int oldLen = mapArr.length; - Object[] newMapArr = new Object[oldLen+2]; - System.arraycopy(mapArr, 0, newMapArr, 0, oldLen); - newMapArr[oldLen] = key; - newMapArr[oldLen+1] = value; - mapArr = newMapArr; + /** + * stores key-value pair as 2 objects; key first; in case of empty map this might be null + */ + private Object[] mapArr; + + /** Creates empty map. */ + public SmallMap() + { + } + + /** Creates map filled with entries from provided map. */ + public SmallMap(Map initMap) + { + putAll(initMap); + } + + /** + * Returns index of key within map-array or -1 + * if key is not found (or key is null). + */ + private int findKey(Object key) + { + if (isEmpty() || (key==null)) + { + return -1; + } + + for ( int aIdx = 0; aIdx < mapArr.length; aIdx+=2 ) + { + if (key.equals(mapArr[aIdx])) + { + return aIdx; + } + } + + return -1; + } + + /** + * Returns index of value within map-array or -1 + * if value is not found (or value is null). + */ + private int findValue(Object value) + { + if (isEmpty() || (value==null)) + { + return -1; + } + + for ( int aIdx = 1; aIdx < mapArr.length; aIdx+=2 ) + { + if (value.equals(mapArr[aIdx])) + { + return aIdx; + } + } + + return -1; + } + + @Override + public int size() + { + return mapArr == null ? 0 : mapArr.length >> 1; + } + + @Override + public boolean isEmpty() + { + return (mapArr == null) || (mapArr.length == 0); + } + + @Override + public boolean containsKey(Object key) + { + return findKey(key) >= 0; + } + + @Override + public boolean containsValue(Object value) + { + return findValue(value) >= 0; + } + + @SuppressWarnings("unchecked") + @Override + public V get(Object key) + { + int kIdx = findKey(key); + + return kIdx < 0 ? null : (V) mapArr[kIdx+1]; + } + + @Override + public V put(K key, V value) + { + if ((key == null) || (value == null)) + { + throw new NullPointerException( "Key or value must not be null."); + } + + if (mapArr == null) + { + mapArr = new Object[] { key, value }; return null; - } - else - { - // key exists; replace value - @SuppressWarnings("unchecked") - V oldValue = (V) mapArr[kIdx+1]; - mapArr[kIdx+1] = value; - return oldValue; - } - } - } - - @Override - public V remove(Object key) - { - int kIdx = findKey(key); - - if (kIdx < 0) - { - // not found - return null; - } - - @SuppressWarnings("unchecked") - V oldValue = (V) mapArr[kIdx+1]; - int oldLen = mapArr.length; - - if (oldLen == 2) - { - // was last entry - mapArr = null; - } - else - { - Object[] newMapArr = new Object[oldLen-2]; - System.arraycopy(mapArr, 0, newMapArr, 0, kIdx); - System.arraycopy(mapArr, kIdx+2, newMapArr, kIdx, oldLen - kIdx - 2); - mapArr = newMapArr; - } - - return oldValue; - } - - @Override - public final void putAll(Map otherMap) - { - if ((mapArr == null) || (mapArr.length == 0)) - { - // existing map is empty - mapArr = new Object[otherMap.size() << 1]; - int aIdx = 0; - for (Entry entry : otherMap.entrySet()) - { - if ((entry.getKey() == null) || (entry.getValue() == null)) + } + else + { + int kIdx = findKey(key); + + if (kIdx < 0) { - throw new NullPointerException( "Key or value must not be null."); + // key unknown + int oldLen = mapArr.length; + Object[] newMapArr = new Object[oldLen+2]; + System.arraycopy(mapArr, 0, newMapArr, 0, oldLen); + newMapArr[oldLen] = key; + newMapArr[oldLen+1] = value; + mapArr = newMapArr; + return null; } + else + { + // key exists; replace value + @SuppressWarnings("unchecked") + V oldValue = (V) mapArr[kIdx+1]; + mapArr[kIdx+1] = value; + return oldValue; + } + } + } + + @Override + public V remove(Object key) + { + int kIdx = findKey(key); - mapArr[aIdx++] = entry.getKey(); - mapArr[aIdx++] = entry.getValue(); - } - } - else - { - int oldLen = mapArr.length; - // first increase array size to hold all to put entries as if they have unknown keys - // reduce after adding all to the required size - Object[] newMapArr = new Object[oldLen+(otherMap.size() << 1)]; - System.arraycopy(mapArr, 0, newMapArr, 0, oldLen); - - int newIdx = oldLen; - for (Entry entry : otherMap.entrySet()) - { - if ((entry.getKey() == null) || (entry.getValue() == null)) + if (kIdx < 0) + { + // not found + return null; + } + + @SuppressWarnings("unchecked") + V oldValue = (V) mapArr[kIdx+1]; + int oldLen = mapArr.length; + + if (oldLen == 2) + { + // was last entry + mapArr = null; + } + else + { + Object[] newMapArr = new Object[oldLen-2]; + System.arraycopy(mapArr, 0, newMapArr, 0, kIdx); + System.arraycopy(mapArr, kIdx+2, newMapArr, kIdx, oldLen - kIdx - 2); + mapArr = newMapArr; + } + + return oldValue; + } + + @Override + public final void putAll(Map otherMap) + { + if ((mapArr == null) || (mapArr.length == 0)) + { + // existing map is empty + mapArr = new Object[otherMap.size() << 1]; + int aIdx = 0; + for (Entry entry : otherMap.entrySet()) { - throw new NullPointerException( "Key or value must not be null."); + if ((entry.getKey() == null) || (entry.getValue() == null)) + { + throw new NullPointerException( "Key or value must not be null."); + } + + mapArr[aIdx++] = entry.getKey(); + mapArr[aIdx++] = entry.getValue(); } + } + else + { + int oldLen = mapArr.length; + // first increase array size to hold all to put entries as if they have unknown keys + // reduce after adding all to the required size + Object[] newMapArr = new Object[oldLen+(otherMap.size() << 1)]; + System.arraycopy(mapArr, 0, newMapArr, 0, oldLen); - int existKeyIdx = findKey(entry.getKey()); + int newIdx = oldLen; + for (Entry entry : otherMap.entrySet()) + { + if ((entry.getKey() == null) || (entry.getValue() == null)) + { + throw new NullPointerException( "Key or value must not be null."); + } + + int existKeyIdx = findKey(entry.getKey()); + + if (existKeyIdx >= 0) + { + // existing key + newMapArr[existKeyIdx+1] = entry.getValue(); + } + else + { + // new key + newMapArr[newIdx++] = entry.getKey(); + newMapArr[newIdx++] = entry.getValue(); + } + } - if (existKeyIdx >= 0) + if (newIdx < newMapArr.length) { - // existing key - newMapArr[existKeyIdx+1] = entry.getValue(); + Object[] reducedMapArr = new Object[newIdx]; + System.arraycopy(newMapArr, 0, reducedMapArr, 0, newIdx); + newMapArr = reducedMapArr; } - else + + mapArr = newMapArr; + } + } + + @Override + public void clear() + { + mapArr = null; + } + + /** + * Returns a set view of the keys contained in this map. + * + *

        The current implementation does not allow changes to the + * returned key set (which would have to be reflected in the + * underlying map.

        + */ + @SuppressWarnings("unchecked") + @Override + public Set keySet() + { + if (isEmpty()) + { + return Collections.emptySet(); + } + + Set keys = new LinkedHashSet(mapArr.length >> 1); + for (int kIdx = 0; kIdx < mapArr.length; kIdx+=2) + { + keys.add((K)mapArr[kIdx]); + } + return Collections.unmodifiableSet( keys ); + } + + /** + * Returns a collection of the values contained in this map. + * + *

        The current implementation does not allow changes to the + * returned collection (which would have to be reflected in the + * underlying map.

        + */ + @SuppressWarnings("unchecked") + @Override + public Collection values() + { + if (isEmpty()) + { + return Collections.emptySet(); + } + + List values = new ArrayList(mapArr.length >> 1); + for (int vIdx = 1; vIdx < mapArr.length; vIdx+=2) + { + values.add((V)mapArr[vIdx]); + } + return Collections.unmodifiableList( values ); + } + + private class SmallMapEntry implements Entry + { + private final int keyIdx; + + SmallMapEntry(int keyInMapIdx) + { + keyIdx = keyInMapIdx; + } + + @SuppressWarnings("unchecked") + @Override + public K getKey() + { + return (K)mapArr[keyIdx]; + } + + @SuppressWarnings("unchecked") + @Override + public V getValue() + { + return (V)mapArr[keyIdx+1]; + } + + @Override + public V setValue(V value) + { + if (value == null) { - // new key - newMapArr[newIdx++] = entry.getKey(); - newMapArr[newIdx++] = entry.getValue(); + throw new NullPointerException( "Key or value must not be null."); } - } - - if (newIdx < newMapArr.length) - { - Object[] reducedMapArr = new Object[newIdx]; - System.arraycopy(newMapArr, 0, reducedMapArr, 0, newIdx); - newMapArr = reducedMapArr; - } - - mapArr = newMapArr; - } - } - - @Override - public void clear() - { - mapArr = null; - } - - /** - * Returns a set view of the keys contained in this map. - * - *

        The current implementation does not allow changes to the - * returned key set (which would have to be reflected in the - * underlying map.

        - */ - @SuppressWarnings("unchecked") - @Override - public Set keySet() - { - if (isEmpty()) - { - return Collections.emptySet(); - } - - Set keys = new LinkedHashSet(); - for (int kIdx = 0; kIdx < mapArr.length; kIdx+=2) - { - keys.add((K)mapArr[kIdx]); - } - return Collections.unmodifiableSet( keys ); - } - - /** - * Returns a collection of the values contained in this map. - * - *

        The current implementation does not allow changes to the - * returned collection (which would have to be reflected in the - * underlying map.

        - */ - @SuppressWarnings("unchecked") - @Override - public Collection values() - { - if (isEmpty()) - { - return Collections.emptySet(); - } - - List values = new ArrayList(mapArr.length >> 1); - for (int vIdx = 1; vIdx < mapArr.length; vIdx+=2) - { - values.add((V)mapArr[vIdx]); - } - return Collections.unmodifiableList( values ); - } - - private class SmallMapEntry implements Entry - { - private final int keyIdx; - - SmallMapEntry(int keyInMapIdx) - { - keyIdx = keyInMapIdx; - } - - @SuppressWarnings("unchecked") - @Override - public K getKey() - { - return (K)mapArr[keyIdx]; - } - - @SuppressWarnings("unchecked") - @Override - public V getValue() - { - return (V)mapArr[keyIdx+1]; - } - - @Override - public V setValue(V value) - { - if (value == null) - { - throw new NullPointerException( "Key or value must not be null."); - } - - V oldValue = getValue(); - mapArr[keyIdx+1] = value; - return oldValue; - } - - @Override - public int hashCode() - { - return getKey().hashCode(); - } - - @Override - public boolean equals(Object obj) - { - if (!(obj instanceof SmallMap.SmallMapEntry)) - { - return false; - } - @SuppressWarnings("unchecked") - SmallMapEntry other = (SmallMapEntry) obj; - - return getKey().equals(other.getKey()) && getValue().equals(other.getValue()); - } - } - - @Override - public Set> entrySet() - { - if (isEmpty()) - { - return Collections.emptySet(); - } - - Set> entries = new LinkedHashSet>(); - for (int kIdx = 0; kIdx < mapArr.length; kIdx+=2) - { - entries.add(new SmallMapEntry(kIdx)); - } - return Collections.unmodifiableSet( entries ); - } + + V oldValue = getValue(); + mapArr[keyIdx+1] = value; + return oldValue; + } + + @Override + public int hashCode() + { + return getKey().hashCode(); + } + + @Override + public boolean equals(Object obj) + { + if (!(obj instanceof SmallMap.SmallMapEntry)) + { + return false; + } + @SuppressWarnings("unchecked") + SmallMapEntry other = (SmallMapEntry) obj; + + return getKey().equals(other.getKey()) && getValue().equals(other.getValue()); + } + } + + @Override + public Set> entrySet() + { + if (isEmpty()) + { + return Collections.emptySet(); + } + + Set> entries = new LinkedHashSet>(mapArr.length >> 1); + for (int kIdx = 0; kIdx < mapArr.length; kIdx+=2) + { + entries.add(new SmallMapEntry(kIdx)); + } + return Collections.unmodifiableSet( entries ); + } } diff --git a/library/src/main/java/com/tom_roush/pdfbox/util/StringUtil.java b/library/src/main/java/com/tom_roush/pdfbox/util/StringUtil.java new file mode 100644 index 000000000..1c0d03278 --- /dev/null +++ b/library/src/main/java/com/tom_roush/pdfbox/util/StringUtil.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tom_roush.pdfbox.util; + +import java.util.regex.Pattern; +public final class StringUtil +{ + public static final Pattern PATTERN_SPACE = Pattern.compile("\\s"); + + public static String[] splitOnSpace(String s) + { + return PATTERN_SPACE.split(s); + } +} diff --git a/library/src/main/java/com/tom_roush/pdfbox/util/filetypedetector/FileTypeDetector.java b/library/src/main/java/com/tom_roush/pdfbox/util/filetypedetector/FileTypeDetector.java index ddd678657..31e2fe281 100644 --- a/library/src/main/java/com/tom_roush/pdfbox/util/filetypedetector/FileTypeDetector.java +++ b/library/src/main/java/com/tom_roush/pdfbox/util/filetypedetector/FileTypeDetector.java @@ -20,17 +20,16 @@ import java.io.IOException; import com.tom_roush.pdfbox.util.Charsets; - /** * @author Drew Noakes * - * code taken from https://github.com/drewnoakes/metadata-extractor + * code taken from metadata-extractor * * 2016-01-04 * * latest commit number 73f1a48 * - * Examines the a file's first bytes and estimates the file's type. + * Examines the file's first bytes and estimates the file's type. */ public final class FileTypeDetector { @@ -43,9 +42,11 @@ public final class FileTypeDetector // https://en.wikipedia.org/wiki/List_of_file_signatures + byte[] iiBytes = "II".getBytes(Charsets.ISO_8859_1); + byte[] mmBytes = "MM".getBytes(Charsets.ISO_8859_1); root.addPath(FileType.JPEG, new byte[]{(byte)0xff, (byte)0xd8}); - root.addPath(FileType.TIFF, "II".getBytes(Charsets.ISO_8859_1), new byte[]{0x2a, 0x00}); - root.addPath(FileType.TIFF, "MM".getBytes(Charsets.ISO_8859_1), new byte[]{0x00, 0x2a}); + root.addPath(FileType.TIFF, iiBytes, new byte[]{0x2a, 0x00}); + root.addPath(FileType.TIFF, mmBytes, new byte[]{0x00, 0x2a}); root.addPath(FileType.PSD, "8BPS".getBytes(Charsets.ISO_8859_1)); root.addPath(FileType.PNG, new byte[]{(byte)0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52}); // TODO technically there are other very rare magic numbers for OS/2 BMP files... @@ -62,13 +63,13 @@ public final class FileTypeDetector // https://github.com/drewnoakes/metadata-extractor/issues/217 //root.addPath(FileType.ARW, "II".getBytes(Charsets.ISO_8859_1), new byte[]{0x2a, 0x00, 0x08, 0x00}) - root.addPath(FileType.CRW, "II".getBytes(Charsets.ISO_8859_1), new byte[]{0x1a, 0x00, 0x00, 0x00}, "HEAPCCDR".getBytes(Charsets.ISO_8859_1)); - root.addPath(FileType.CR2, "II".getBytes(Charsets.ISO_8859_1), new byte[]{0x2a, 0x00, 0x10, 0x00, 0x00, 0x00, 0x43, 0x52}); - root.addPath(FileType.NEF, "MM".getBytes(Charsets.ISO_8859_1), new byte[]{0x00, 0x2a, 0x00, 0x00, 0x00, (byte)0x80, 0x00}); + root.addPath(FileType.CRW, iiBytes, new byte[]{0x1a, 0x00, 0x00, 0x00}, "HEAPCCDR".getBytes(Charsets.ISO_8859_1)); + root.addPath(FileType.CR2, iiBytes, new byte[]{0x2a, 0x00, 0x10, 0x00, 0x00, 0x00, 0x43, 0x52}); + root.addPath(FileType.NEF, mmBytes, new byte[]{0x00, 0x2a, 0x00, 0x00, 0x00, (byte)0x80, 0x00}); root.addPath(FileType.ORF, "IIRO".getBytes(Charsets.ISO_8859_1), new byte[]{(byte)0x08, 0x00}); root.addPath(FileType.ORF, "IIRS".getBytes(Charsets.ISO_8859_1), new byte[]{(byte)0x08, 0x00}); root.addPath(FileType.RAF, "FUJIFILMCCD-RAW".getBytes(Charsets.ISO_8859_1)); - root.addPath(FileType.RW2, "II".getBytes(Charsets.ISO_8859_1), new byte[]{0x55, 0x00}); + root.addPath(FileType.RW2, iiBytes, new byte[]{0x55, 0x00}); } private FileTypeDetector() @@ -76,7 +77,7 @@ private FileTypeDetector() } /** - * Examines the a file's first bytes and estimates the file's type. + * Examines the file's first bytes and estimates the file's type. *

        * Requires a {@link BufferedInputStream} in order to mark and reset the stream to the position * at which it was provided to this method once completed. diff --git a/library/src/test/java/com/tom_roush/pdfbox/android/PDFBoxResourceLoaderTest.java b/library/src/test/java/com/tom_roush/pdfbox/android/PDFBoxResourceLoaderTest.java new file mode 100644 index 000000000..178056db3 --- /dev/null +++ b/library/src/test/java/com/tom_roush/pdfbox/android/PDFBoxResourceLoaderTest.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tom_roush.pdfbox.android; + +import java.io.IOException; + +import junit.framework.TestCase; + +/** + * Unit tests for PDFBoxResourceLoader initialization and error handling. + */ +public class PDFBoxResourceLoaderTest extends TestCase +{ + public void testGetStreamThrowsIOExceptionWhenUninitialized() + { + // When not initialized, getStream must throw IOException, not NullPointerException + if (!PDFBoxResourceLoader.isReady()) + { + try + { + PDFBoxResourceLoader.getStream("test/resource.dat"); + fail("Expected IOException when uninitialized"); + } + catch (IOException expected) + { + assertTrue(expected.getMessage().contains("PDFBoxResourceLoader is not initialized")); + } + } + } +} diff --git a/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDColorSpaceTest.java b/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDColorSpaceTest.java new file mode 100644 index 000000000..8e026ae38 --- /dev/null +++ b/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDColorSpaceTest.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tom_roush.pdfbox.pdmodel.graphics.color; + +import java.io.IOException; + +import junit.framework.TestCase; + +import com.tom_roush.pdfbox.cos.COSArray; +import com.tom_roush.pdfbox.cos.COSName; + +/** + * Unit tests for PDColorSpace creation and fallbacks. + */ +public class PDColorSpaceTest extends TestCase +{ + public void testBuiltInColorSpaces() throws IOException + { + assertEquals(PDDeviceRGB.INSTANCE, PDColorSpace.create(COSName.DEVICERGB)); + assertEquals(3, PDColorSpace.create(COSName.DEVICERGB).getNumberOfComponents()); + + assertEquals(PDDeviceCMYK.INSTANCE, PDColorSpace.create(COSName.DEVICECMYK)); + assertEquals(4, PDColorSpace.create(COSName.DEVICECMYK).getNumberOfComponents()); + + assertEquals(PDDeviceGray.INSTANCE, PDColorSpace.create(COSName.DEVICEGRAY)); + assertEquals(1, PDColorSpace.create(COSName.DEVICEGRAY).getNumberOfComponents()); + } + + public void testCalGrayFallbackPreservesSingleComponent() throws IOException + { + // CalGray as direct COSName + PDColorSpace direct = PDColorSpace.create(COSName.CALGRAY); + assertEquals(PDDeviceGray.INSTANCE, direct); + assertEquals(1, direct.getNumberOfComponents()); + + // CalGray as COSArray: [/CalGray <<...>>] + COSArray array = new COSArray(); + array.add(COSName.CALGRAY); + PDColorSpace fromArray = PDColorSpace.create(array); + assertEquals(PDDeviceGray.INSTANCE, fromArray); + assertEquals(1, fromArray.getNumberOfComponents()); + } + + public void testSeparationFallbackPreservesSingleComponent() throws IOException + { + // Separation as COSArray: [/Separation /Name /DeviceCMYK /Function] + COSArray array = new COSArray(); + array.add(COSName.SEPARATION); + array.add(COSName.getPDFName("Spot1")); + array.add(COSName.DEVICECMYK); + PDColorSpace cs = PDColorSpace.create(array); + assertEquals(PDDeviceGray.INSTANCE, cs); + assertEquals(1, cs.getNumberOfComponents()); + } +} diff --git a/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDDeviceCMYKTest.java b/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDDeviceCMYKTest.java index 29a48800e..e2987069f 100644 --- a/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDDeviceCMYKTest.java +++ b/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDDeviceCMYKTest.java @@ -30,6 +30,33 @@ public class PDDeviceCMYKTest extends TestCase { public void testCMYK() throws IOException { + PDDeviceCMYK cmyk = PDDeviceCMYK.INSTANCE; + + float[] white = cmyk.toRGB(new float[] { 0, 0, 0, 0 }); + assertEquals(1.0f, white[0], 0.001f); + assertEquals(1.0f, white[1], 0.001f); + assertEquals(1.0f, white[2], 0.001f); + + float[] black = cmyk.toRGB(new float[] { 0, 0, 0, 1 }); + assertEquals(0.0f, black[0], 0.001f); + assertEquals(0.0f, black[1], 0.001f); + assertEquals(0.0f, black[2], 0.001f); + + float[] cyan = cmyk.toRGB(new float[] { 1, 0, 0, 0 }); + assertEquals(0.0f, cyan[0], 0.001f); + assertEquals(1.0f, cyan[1], 0.001f); + assertEquals(1.0f, cyan[2], 0.001f); + + float[] magenta = cmyk.toRGB(new float[] { 0, 1, 0, 0 }); + assertEquals(1.0f, magenta[0], 0.001f); + assertEquals(0.0f, magenta[1], 0.001f); + assertEquals(1.0f, magenta[2], 0.001f); + + float[] yellow = cmyk.toRGB(new float[] { 0, 0, 1, 0 }); + assertEquals(1.0f, yellow[0], 0.001f); + assertEquals(1.0f, yellow[1], 0.001f); + assertEquals(0.0f, yellow[2], 0.001f); + PDDeviceCMYK.INSTANCE = new CustomDeviceCMYK(); } diff --git a/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDIndexedTest.java b/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDIndexedTest.java new file mode 100644 index 000000000..96a474dc6 --- /dev/null +++ b/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/color/PDIndexedTest.java @@ -0,0 +1,142 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tom_roush.pdfbox.pdmodel.graphics.color; + +import java.io.IOException; +import java.io.OutputStream; + +import junit.framework.TestCase; + +import com.tom_roush.pdfbox.cos.COSArray; +import com.tom_roush.pdfbox.cos.COSInteger; +import com.tom_roush.pdfbox.cos.COSName; +import com.tom_roush.pdfbox.cos.COSStream; +import com.tom_roush.pdfbox.cos.COSString; + +/** + * Unit tests for PDIndexed color space. + */ +public class PDIndexedTest extends TestCase +{ + public void testDefaultConstructor() + { + PDIndexed indexed = new PDIndexed(); + assertEquals("Indexed", indexed.getName()); + assertEquals(1, indexed.getNumberOfComponents()); + float[] decode = indexed.getDefaultDecode(8); + assertEquals(0.0f, decode[0]); + assertEquals(255.0f, decode[1]); + } + + public void testRgbLookupFromString() throws IOException + { + // 3 entries: Red (255, 0, 0), Green (0, 255, 0), Blue (0, 0, 255) + byte[] lookup = new byte[] { + (byte) 255, 0, 0, + 0, (byte) 255, 0, + 0, 0, (byte) 255 + }; + + COSArray array = new COSArray(); + array.add(COSName.INDEXED); + array.add(COSName.DEVICERGB); + array.add(COSInteger.get(2)); // hival = 2 (3 colors: 0, 1, 2) + array.add(new COSString(lookup)); + + PDColorSpace cs = PDColorSpace.create(array); + assertTrue(cs instanceof PDIndexed); + + PDIndexed indexed = (PDIndexed) cs; + assertEquals(1, indexed.getNumberOfComponents()); + assertEquals(PDDeviceRGB.INSTANCE, indexed.getBaseColorSpace()); + + int[] rgbTable = indexed.getRgbColorTable(); + assertNotNull(rgbTable); + assertEquals(3, rgbTable.length); + + // Check packed ARGB values + assertEquals(0xFFFF0000, indexed.getRgbColor(0)); + assertEquals(0xFF00FF00, indexed.getRgbColor(1)); + assertEquals(0xFF0000FF, indexed.getRgbColor(2)); + + // Test toRGB + float[] rgb0 = indexed.toRGB(new float[] { 0 }); + assertEquals(1.0f, rgb0[0], 0.001f); + assertEquals(0.0f, rgb0[1], 0.001f); + assertEquals(0.0f, rgb0[2], 0.001f); + + float[] rgb1 = indexed.toRGB(new float[] { 1 }); + assertEquals(0.0f, rgb1[0], 0.001f); + assertEquals(1.0f, rgb1[1], 0.001f); + assertEquals(0.0f, rgb1[2], 0.001f); + + float[] rgb2 = indexed.toRGB(new float[] { 2 }); + assertEquals(0.0f, rgb2[0], 0.001f); + assertEquals(0.0f, rgb2[1], 0.001f); + assertEquals(1.0f, rgb2[2], 0.001f); + + // Test index clamping (out-of-bounds index should clamp safely) + assertEquals(0xFF0000FF, indexed.getRgbColor(10)); + assertEquals(0xFFFF0000, indexed.getRgbColor(-5)); + } + + public void testGrayLookupFromString() throws IOException + { + // 2 entries: Black (0), White (255) + byte[] lookup = new byte[] { 0, (byte) 255 }; + + COSArray array = new COSArray(); + array.add(COSName.INDEXED); + array.add(COSName.DEVICEGRAY); + array.add(COSInteger.get(1)); // hival = 1 (2 colors) + array.add(new COSString(lookup)); + + PDIndexed indexed = new PDIndexed(array); + assertEquals(2, indexed.getRgbColorTable().length); + + // Black (0) -> ARGB 0xFF000000 + assertEquals(0xFF000000, indexed.getRgbColor(0)); + // White (255) -> ARGB 0xFFFFFFFF + assertEquals(0xFFFFFFFF, indexed.getRgbColor(1)); + } + + public void testLookupFromStream() throws IOException + { + byte[] lookup = new byte[] { + (byte) 200, (byte) 100, 50 + }; + + COSStream stream = new COSStream(); + OutputStream out = stream.createOutputStream(); + out.write(lookup); + out.close(); + + COSArray array = new COSArray(); + array.add(COSName.INDEXED); + array.add(COSName.DEVICERGB); + array.add(COSInteger.get(0)); // hival = 0 (1 color) + array.add(stream); + + PDIndexed indexed = new PDIndexed(array); + assertEquals(1, indexed.getRgbColorTable().length); + + int color = indexed.getRgbColor(0); + assertEquals(200, (color >> 16) & 0xFF); + assertEquals(100, (color >> 8) & 0xFF); + assertEquals(50, color & 0xFF); + } +} diff --git a/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/image/JPEGFactoryTest.java b/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/image/JPEGFactoryTest.java new file mode 100644 index 000000000..fe0dbbe5e --- /dev/null +++ b/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/image/JPEGFactoryTest.java @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tom_roush.pdfbox.pdmodel.graphics.image; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import junit.framework.TestCase; + +import com.tom_roush.pdfbox.pdmodel.PDDocument; +import com.tom_roush.pdfbox.pdmodel.graphics.color.PDDeviceCMYK; +import com.tom_roush.pdfbox.pdmodel.graphics.color.PDDeviceGray; +import com.tom_roush.pdfbox.pdmodel.graphics.color.PDDeviceRGB; + +/** + * Unit tests for JPEGFactory SOF component detection and color space selection. + */ +public class JPEGFactoryTest extends TestCase +{ + private byte[] createSyntheticJpeg(int width, int height, int numComponents) throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + // SOI + out.write(0xFF); + out.write(0xD8); + + // SOF0 (Baseline DCT) + out.write(0xFF); + out.write(0xC0); + + int length = 8 + (3 * numComponents); + out.write((length >> 8) & 0xFF); + out.write(length & 0xFF); + + out.write(8); // Precision: 8 bits + out.write((height >> 8) & 0xFF); + out.write(height & 0xFF); + out.write((width >> 8) & 0xFF); + out.write(width & 0xFF); + out.write(numComponents); + + for (int i = 1; i <= numComponents; i++) + { + out.write(i); // Component ID + out.write(0x11); // Sampling factors + out.write(0); // Quantization table number + } + + // EOI + out.write(0xFF); + out.write(0xD9); + + return out.toByteArray(); + } + + public void testGrayscaleJpegDetectedAsDeviceGray() throws IOException + { + PDDocument doc = new PDDocument(); + byte[] grayJpeg = createSyntheticJpeg(40, 25, 1); + + PDImageXObject image = JPEGFactory.createFromByteArray(doc, grayJpeg); + assertNotNull(image); + assertEquals(40, image.getWidth()); + assertEquals(25, image.getHeight()); + assertEquals(PDDeviceGray.INSTANCE, image.getColorSpace()); + assertEquals(1, image.getColorSpace().getNumberOfComponents()); + + doc.close(); + } + + public void testRgbJpegDetectedAsDeviceRGB() throws IOException + { + PDDocument doc = new PDDocument(); + byte[] rgbJpeg = createSyntheticJpeg(50, 30, 3); + + PDImageXObject image = JPEGFactory.createFromByteArray(doc, rgbJpeg); + assertNotNull(image); + assertEquals(50, image.getWidth()); + assertEquals(30, image.getHeight()); + assertEquals(PDDeviceRGB.INSTANCE, image.getColorSpace()); + assertEquals(3, image.getColorSpace().getNumberOfComponents()); + + doc.close(); + } + + public void testCmykJpegDetectedAsDeviceCMYK() throws IOException + { + PDDocument doc = new PDDocument(); + byte[] cmykJpeg = createSyntheticJpeg(60, 35, 4); + + PDImageXObject image = JPEGFactory.createFromByteArray(doc, cmykJpeg); + assertNotNull(image); + assertEquals(60, image.getWidth()); + assertEquals(35, image.getHeight()); + assertEquals(PDDeviceCMYK.INSTANCE, image.getColorSpace()); + assertEquals(4, image.getColorSpace().getNumberOfComponents()); + + doc.close(); + } +} diff --git a/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImageXObjectTest.java b/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImageXObjectTest.java new file mode 100644 index 000000000..5da510406 --- /dev/null +++ b/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/image/PDImageXObjectTest.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tom_roush.pdfbox.pdmodel.graphics.image; + +import android.graphics.Bitmap; + +import java.io.IOException; + +import junit.framework.TestCase; +import org.mockito.Mockito; + +import com.tom_roush.pdfbox.cos.COSName; +import com.tom_roush.pdfbox.cos.COSStream; +import com.tom_roush.pdfbox.pdmodel.PDDocument; +import com.tom_roush.pdfbox.pdmodel.PDResources; +import com.tom_roush.pdfbox.pdmodel.common.PDStream; + +/** + * Unit tests for PDImageXObject mask handling and resource inheritance. + */ +public class PDImageXObjectTest extends TestCase +{ + public void testApplyMaskNullSafety() throws IOException + { + PDDocument doc = new PDDocument(); + PDImageXObject imageXObject = new PDImageXObject(doc); + + Bitmap mockImage = Mockito.mock(Bitmap.class); + Bitmap mockMask = Mockito.mock(Bitmap.class); + + // When image is null, applyMask must return null without throwing NullPointerException + assertNull(imageXObject.applyMask(null, mockMask, false, false, null)); + assertNull(imageXObject.applyMask(null, null, false, false, null)); + + // When mask is null, applyMask must return the original image + assertSame(mockImage, imageXObject.applyMask(mockImage, null, false, false, null)); + + doc.close(); + } + + public void testMaskAndSoftMaskInheritResources() throws IOException + { + PDDocument doc = new PDDocument(); + PDResources resources = new PDResources(); + COSStream mainStream = new COSStream(); + + COSStream maskStream = new COSStream(); + mainStream.setItem(COSName.MASK, maskStream); + + COSStream smaskStream = new COSStream(); + mainStream.setItem(COSName.SMASK, smaskStream); + + PDStream pdStream = new PDStream(mainStream); + PDImageXObject image = new PDImageXObject(pdStream, resources); + assertSame(resources, image.getResources()); + + PDImageXObject mask = image.getMask(); + assertNotNull(mask); + assertSame("Mask must inherit parent image resources", resources, mask.getResources()); + + PDImageXObject smask = image.getSoftMask(); + assertNotNull(smask); + assertSame("Soft mask must inherit parent image resources", resources, smask.getResources()); + + doc.close(); + } +} diff --git a/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/image/SampledImageReaderTest.java b/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/image/SampledImageReaderTest.java new file mode 100644 index 000000000..e2b765901 --- /dev/null +++ b/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/image/SampledImageReaderTest.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tom_roush.pdfbox.pdmodel.graphics.image; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import junit.framework.TestCase; + +import com.tom_roush.pdfbox.pdmodel.graphics.color.PDDeviceRGB; + +/** + * Unit tests for SampledImageReader bounds checks and stream validation. + */ +public class SampledImageReaderTest extends TestCase +{ + public void testZeroWidthThrowsIOException() + { + byte[] data = new byte[] { 1, 2, 3 }; + try + { + SampledImageReader.createBitmapFromRawStream( + new ByteArrayInputStream(data), 0, 3, 1, PDDeviceRGB.INSTANCE); + fail("Expected IOException for zero width"); + } + catch (IOException expected) + { + assertTrue(expected.getMessage().contains("Invalid image dimensions")); + } + } + + public void testZeroComponentsThrowsIOException() + { + byte[] data = new byte[] { 1, 2, 3 }; + try + { + SampledImageReader.createBitmapFromRawStream( + new ByteArrayInputStream(data), 10, 0, 1, PDDeviceRGB.INSTANCE); + fail("Expected IOException for zero components"); + } + catch (IOException expected) + { + assertTrue(expected.getMessage().contains("Invalid image dimensions")); + } + } + + public void testInsufficientStreamDataThrowsIOException() + { + // 10 width * 3 components = 30 bytes required for a single row, but only 5 provided + byte[] truncated = new byte[5]; + try + { + SampledImageReader.createBitmapFromRawStream( + new ByteArrayInputStream(truncated), 10, 3, 1, PDDeviceRGB.INSTANCE); + fail("Expected IOException for insufficient stream data"); + } + catch (IOException expected) + { + assertTrue(expected.getMessage().contains("insufficient data")); + } + } + + public void testEmptyStreamThrowsIOException() + { + byte[] empty = new byte[0]; + try + { + SampledImageReader.createBitmapFromRawStream( + new ByteArrayInputStream(empty), 10, 3, 1, PDDeviceRGB.INSTANCE); + fail("Expected IOException for empty stream"); + } + catch (IOException expected) + { + assertTrue(expected.getMessage().contains("insufficient data")); + } + } + + public void testUnsupportedComponentsThrowsIOException() + { + // 5 components (not 1, 3, or 4 and not indexed) + byte[] data = new byte[100]; + try + { + SampledImageReader.createBitmapFromRawStream( + new ByteArrayInputStream(data), 2, 5, 1, PDDeviceRGB.INSTANCE); + fail("Expected IOException for unsupported components"); + } + catch (IOException expected) + { + assertTrue(expected.getMessage().contains("Unsupported number of components")); + } + } +} diff --git a/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/optionalcontent/TestOptionalContentGroups.java b/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/optionalcontent/TestOptionalContentGroups.java index cec3c0274..d44a39274 100644 --- a/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/optionalcontent/TestOptionalContentGroups.java +++ b/library/src/test/java/com/tom_roush/pdfbox/pdmodel/graphics/optionalcontent/TestOptionalContentGroups.java @@ -32,6 +32,7 @@ import com.tom_roush.pdfbox.pdmodel.PDPageContentStream.AppendMode; import com.tom_roush.pdfbox.pdmodel.PDResources; import com.tom_roush.pdfbox.pdmodel.documentinterchange.markedcontent.PDMarkedContent; +import com.tom_roush.pdfbox.pdmodel.documentinterchange.markedcontent.PDPropertyList; import com.tom_roush.pdfbox.pdmodel.font.PDFont; import com.tom_roush.pdfbox.pdmodel.font.PDType1Font; import com.tom_roush.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentProperties.BaseState; @@ -206,14 +207,23 @@ public void testOCGConsumption() throws Exception PDFMarkedContentExtractor extractor = new PDFMarkedContentExtractor(); extractor.processPage(page); List markedContents = extractor.getMarkedContents(); - assertEquals("oc1", markedContents.get(0).getTag()); + assertEquals("OC", markedContents.get(0).getTag()); + PDOptionalContentGroup ocg1 = + (PDOptionalContentGroup) PDPropertyList.create(markedContents.get(0).getProperties()); + assertEquals("background", ocg1.getName()); assertEquals("PDF 1.5: Optional Content Groups" + "You should see a green textline, but no red text line.", textPositionListToString(markedContents.get(0).getContents())); - assertEquals("oc2", markedContents.get(1).getTag()); + assertEquals("OC", markedContents.get(1).getTag()); + PDOptionalContentGroup ocg2 = + (PDOptionalContentGroup) PDPropertyList.create(markedContents.get(1).getProperties()); + assertEquals("enabled", ocg2.getName()); assertEquals("This is from an enabled layer. If you see this, that's good.", textPositionListToString(markedContents.get(1).getContents())); - assertEquals("oc3", markedContents.get(2).getTag()); + assertEquals("OC", markedContents.get(2).getTag()); + PDOptionalContentGroup ocg3 = + (PDOptionalContentGroup) PDPropertyList.create(markedContents.get(2).getProperties()); + assertEquals("disabled", ocg3.getName()); assertEquals("This is from a disabled layer. If you see this, that's NOT good!", textPositionListToString(markedContents.get(2).getContents())); } diff --git a/library/src/test/resources/pdfbox/com/tom_roush/pdfbox/pdmodel/interactive/form/PDFBOX-5784.pdf b/library/src/test/resources/pdfbox/com/tom_roush/pdfbox/pdmodel/interactive/form/PDFBOX-5784.pdf new file mode 100644 index 000000000..fe62d0671 Binary files /dev/null and b/library/src/test/resources/pdfbox/com/tom_roush/pdfbox/pdmodel/interactive/form/PDFBOX-5784.pdf differ diff --git a/library/src/test/resources/pdfbox/input/merge/PDFBOX-5762-722238.pdf b/library/src/test/resources/pdfbox/input/merge/PDFBOX-5762-722238.pdf new file mode 100644 index 000000000..a6ceb7021 Binary files /dev/null and b/library/src/test/resources/pdfbox/input/merge/PDFBOX-5762-722238.pdf differ diff --git a/library/src/test/resources/pdfbox/input/merge/PDFBOX-5792-240045.pdf b/library/src/test/resources/pdfbox/input/merge/PDFBOX-5792-240045.pdf new file mode 100644 index 000000000..17d77df5e Binary files /dev/null and b/library/src/test/resources/pdfbox/input/merge/PDFBOX-5792-240045.pdf differ diff --git a/library/src/test/resources/pdfbox/input/merge/PDFBOX-5809-509329.pdf b/library/src/test/resources/pdfbox/input/merge/PDFBOX-5809-509329.pdf new file mode 100644 index 000000000..6f24c4270 Binary files /dev/null and b/library/src/test/resources/pdfbox/input/merge/PDFBOX-5809-509329.pdf differ diff --git a/library/src/test/resources/pdfbox/input/merge/PDFBOX-5811-362972.pdf b/library/src/test/resources/pdfbox/input/merge/PDFBOX-5811-362972.pdf new file mode 100644 index 000000000..fd55975dc Binary files /dev/null and b/library/src/test/resources/pdfbox/input/merge/PDFBOX-5811-362972.pdf differ diff --git a/library/src/test/resources/pdfbox/input/merge/PDFBOX-5840-410609.pdf b/library/src/test/resources/pdfbox/input/merge/PDFBOX-5840-410609.pdf new file mode 100644 index 000000000..a7937dcaa Binary files /dev/null and b/library/src/test/resources/pdfbox/input/merge/PDFBOX-5840-410609.pdf differ diff --git a/library/src/test/resources/pdfbox/input/merge/PDFBOX-6018-099267-p9-OrphanPopups.pdf b/library/src/test/resources/pdfbox/input/merge/PDFBOX-6018-099267-p9-OrphanPopups.pdf new file mode 100644 index 000000000..bfc137f0b Binary files /dev/null and b/library/src/test/resources/pdfbox/input/merge/PDFBOX-6018-099267-p9-OrphanPopups.pdf differ diff --git a/sample/build.gradle b/sample/build.gradle deleted file mode 100644 index abc4e1e6e..000000000 --- a/sample/build.gradle +++ /dev/null @@ -1,42 +0,0 @@ -apply plugin: 'com.android.application' - -android { - compileSdkVersion Integer.parseInt(project.ANDROID_BUILD_SDK_VERSION) - - defaultConfig { - applicationId 'com.tom_roush.pdfbox.sample' - minSdkVersion 14 - targetSdkVersion Integer.parseInt(project.ANDROID_BUILD_TARGET_SDK_VERSION) - versionName project.VERSION_NAME - versionCode Integer.parseInt(project.VERSION_CODE) - multiDexEnabled true - } - - buildTypes { - release { - minifyEnabled false - } - } - - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - lint { - abortOnError false - } - namespace 'com.tom_roush.pdfbox.sample' -} - -dependencies { - implementation project(':library') -// implementation "com.tom-roush:pdfbox-android:${project.VERSION_NAME}" - implementation "androidx.multidex:multidex:2.0.1" - implementation 'androidx.appcompat:appcompat:1.6.1' - - // Optional dependencies - - // Read JPX images - implementation 'com.gemalto.jp2:jp2-android:1.0.3' -} diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts new file mode 100644 index 000000000..788507b1b --- /dev/null +++ b/sample/build.gradle.kts @@ -0,0 +1,57 @@ +plugins { + alias(libs.plugins.android.application) +} + +val versionNameProp = project.findProperty("VERSION_NAME") as String? ?: "2.0.37.1" +val versionCodeProp = (project.findProperty("VERSION_CODE") as String?)?.toInt() ?: 1 + +android { + namespace = "com.tom_roush.pdfbox.sample" + compileSdk = (project.findProperty("ANDROID_BUILD_SDK_VERSION") as String?)?.toInt() ?: 37 + + defaultConfig { + applicationId = "com.tom_roush.pdfbox.sample" + minSdk = (project.findProperty("ANDROID_BUILD_MIN_SDK_VERSION") as String?)?.toInt() ?: 23 + targetSdk = (project.findProperty("ANDROID_BUILD_TARGET_SDK_VERSION") as String?)?.toInt() ?: 37 + versionName = versionNameProp + versionCode = versionCodeProp + } + + buildTypes { + release { + isMinifyEnabled = false + signingConfig = signingConfigs.getByName("debug") + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + packaging { + jniLibs { + useLegacyPackaging = false + } + } + + lint { + abortOnError = false + } +} + +// BouncyCastle 1.85 packaging bug: bcprov and bcutil both ship the same +// org.bouncycastle.asn1.iana.* classes, causing checkDuplicateClasses to fail. +configurations.all { + exclude(group = "org.bouncycastle", module = "bcutil-jdk15to18") + exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-jdk7") + exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-jdk8") +} + +dependencies { + implementation(project(":library")) + implementation(libs.androidx.appcompat) + + // Read JPX images + implementation(libs.jp2.android) +} diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index 0200eceb4..64cab2a1e 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -2,7 +2,6 @@