diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..4bb443cc25a15a065d556c3343958ca6f3183e6f --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.pdf binary \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000000000000000000000000000000000..0c5e7deb24e37d94fa9b58a031eb5e20f10ec957 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +github: hhrutter +open_collective: horst-rutter + diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000000000000000000000000000000000..2686570c242964b05a3d6dd33d983f2a812f423e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,16 @@ +--- +name: 'Bug report' +about: Help us improve the overall pdfcpu experience +labels: 'investigate' +assignees: 'hhrutter' +--- + + + +### Thank you for submitting a possible bug! + +Please ensure the following: + +* Your issue is based on the latest commit +* State your OS and OS version +* When reporting a problem with a specific PDF input file please avoid stating the organization responsible for the PDFWriter - just refer to the *PDFWriter* \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000000000000000000000000000000000..9ebf87fc485dee4703a728963d49a8df07074a3c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,4 @@ +blank_issues_enabled: false +contact_links: + - name: SUPPORT, ISSUES, TROUBLESHOOTING + about: Please visit \#pdfcpu on the Gopher slack for support! \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000000000000000000000000000000000..221dab3aa60d25ef2105db13953e1ef0b375822b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,7 @@ +--- +name: Feature Request +about: Suggest a new feature or an enhancement +title: '' +labels: 'feature request' +assignees: 'hhrutter' +--- \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000000000000000000000000000000000..a23013c42c4835eb8b0ea51b856ad07fbe64e7dd --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,9 @@ +## Thank you for your contribution! + +1. Please do not create a Pull Request without creating an issue first. + +2. **Any** change needs to be discussed before proceeding. + +3. Please provide enough information for PR review. + +4. Fixes # ? diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000000000000000000000000000000000000..da08ccaaa0613b8575255c7306d7727c7aee261a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,50 @@ +on: [push, pull_request] +name: Test +jobs: + test: + strategy: + fail-fast: false + matrix: + target: + - goos: js + goarch: wasm + - goos: darwin + goarch: amd64 + - goos: darwin + goarch: arm64 + - goos: linux + goarch: amd64 + - goos: windows + goarch: amd64 + go: + - '1.22.x' + - '1.23.x' + runs-on: ubuntu-latest + + steps: + - name: Set up Go ${{ matrix.go }} + uses: actions/setup-go@v4 + with: + go-version: ${{ matrix.go }} + + - run: go version + + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Go vet + run: go vet -v ./... + + - name: Check coverage + uses: shogo82148/actions-goveralls@v1 + with: + flag-name: Go-${{ matrix.go }} + parallel: true + + finish: + needs: test + runs-on: ubuntu-latest + steps: + - uses: shogo82148/actions-goveralls@v1 + with: + parallel-finished: true \ No newline at end of file diff --git a/.gitignore b/.gitignore index 66fd13c903cac02eb9657cd53fb227823484401d..457dca2cc673fe7b501501fd650644eaf2690fbc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,15 @@ -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib +# Mac +**/.DS_Store +**/._.DS_Store -# Test binary, built with `go test -c` -*.test +# VSCode +.vscode/* -# Output of the go coverage tool, specifically when used with LiteIDE -*.out +# Coverage +c.out -# Dependency directories (remove the comment below to include it) -# vendor/ +# Stats +_scripts/*.csv + +# GoReleaser +dist/* diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000000000000000000000000000000000000..14178854bbc1f9ae076e130c5a9c1afad8aff715 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,44 @@ +builds: +- main: ./cmd/pdfcpu + env: + - CGO_ENABLED=0 + ldflags: + - '-s -w -X main.version={{.Version}} -X github.com/pdfcpu/pdfcpu/pkg/pdfcpu.VersionStr={{.Version}} -X main.commit={{.ShortCommit}} -X main.date={{.Date}} -X main.builtBy=goreleaser' + goos: + - js + - linux + - darwin + - windows + goarch: + - "386" + - arm64 + - wasm + - amd64 +dist: ./dist +archives: + - + format: tar.xz + format_overrides: + - goos: windows + format: zip + name_template: >- + {{- .ProjectName }}_ + {{- .Version }}_ + {{- title .Os }}_ + {{- if eq .Arch "linux" }}Linux + {{- else if eq .Arch "windows" }}Windows + {{- else if eq .Arch "386" }}i386 + {{- else if eq .Arch "amd64" }}x86_64 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end -}} + wrap_in_directory: true +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ .Tag }}-next" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..1c2fda565b94d0f2b94cb65ba7cca866e7a25478 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/BUG_FIX_RECORD.MD b/BUG_FIX_RECORD.MD new file mode 100644 index 0000000000000000000000000000000000000000..076cc7fca89b9f8f6bea141999a09c2672a28497 --- /dev/null +++ b/BUG_FIX_RECORD.MD @@ -0,0 +1,100 @@ +# 基于pdfcpu v0.81版本修改代码 + +## pdf-bug fix + +### 01 无法寻找报错信息了 + +修改 xxxx/go/pkg/mod/github.com/pdfcpu/pdfcpu@v0.3.13/pkg/pdfcpu/validate/structTree.go +89 行 + +``` + // obj, err := xRefTable.Dereference(*ir) + // if err != nil { + // return err + // } + + // if obj == nil { + // return errors.New("pdfcpu: validateObjectReferenceDict: missing required entry \"Obj\"") + // } + + // ignore obj is empty + _, err := xRefTable.Dereference(*ir) + if err != nil { + return err + } +``` + +### 02 错误:validateAnnotationDictConcrete: unsupported annotation subtype:BJCA:Annot + +### 03 错误:validateAnnotationDictConcrete: unsupported annotation subtype:GoldGrid:AddSeal + +修改 xxxx/go/pkg/mod/github.com/pdfcpu/pdfcpu@v0.3.13/pkg/pdfcpu/validate/annotations.go 1676 行 + +增加代码 + +··· +"GoldGrid:AddSeal": {validateAnnotationDictTrapNet, model.V13, true}, +"BJCA:Annot": {validateAnnotationDictTrapNet, model.V13, true}, +··· + +#### 04 错误:pdfcpu: insufficient access permissions +修改 xxx/go\bin\pkg\mod\github.com\pdfcpu\pdfcpu@v0.3.13\pkg\pdfcpu\read.go 2090 行 + +搜索关键字: hasNeededPermissions + +··· +注释 return errors.New("pdfcpu: insufficient access permissions") +添加 return nil +··· + +#### 05 错误 pdfcpu: dereferenceDict: wrong type %T <%v>", o, o +修改 xxx\go\bin\pkg\mod\github.com\pdfcpu\pdfcpu@v0.3.13\pkg\pdfcpu\dereference.go 335行 +··· +注释 return nil, errors.Errorf("pdfcpu: dereferenceDict: wrong type %T <%v>", o, o) +添加return nil,nil +··· +#### 06 错误 Unescape: illegal escape sequence \P detected: +``` +pkg/pdfcpu/string.go 172行 +// Relax for issue 305 and also accept "\ ". +if !strings.ContainsRune(" nrtbf()01234567", rune(c)) { +continue +//return nil, errors.Errorf("Unescape: illegal escape sequence \\%c detected: <%s>", c, s) +} +``` +这一部分代码源代码已经注释了,不用修改 + +... +#### 07 错误 dereferenceBoolean: wrong type <(\057False)> +``` +pkg/pdfcpu/dereference.go 60行 +b, ok := o.(Boolean) +if !ok { +return nil, errors.Errorf("pdfcpu: dereferenceBoolean: wrong type <%v>", o) +} +改成: +b, _ := o.(Boolean) +//if !ok { +// return nil, errors.Errorf("pdfcpu: dereferenceBoolean: wrong type <%v>", o) +//} +``` +... + +这个bug只是做了记录还没有修改 + + +#### 08 错误 pdfcpu: can't find last xref section +``` +pkg/pdfcpu/read.go 206行 +if err != nil { + //continue + return nil, errors.New("pdfcpu: can't find last xref section") +} +改成: +if err != nil { + continue + //return nil, errors.New("pdfcpu: can't find last xref section") +} +``` + +这个bug只是做了记录还没有修改 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000000000000000000000000000000000..0fad7ce884a687ae5324de2840f01a7deb392813 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,75 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at hhrutter@gmail.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..32e352e3efe5e20c487d1b76b266025743bb3985 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,47 @@ +# Dockerfile References: https://docs.docker.com/engine/reference/builder/ +# +# Usage: +# +# docker build -t pdfcpu . +# +# Simple one off container: +# docker run pdfcpu +# +# One off container with dir binding: +# docker run -v $(pwd):/data -it --rm pdfcpu pdfcpu val test.pdf +# +# Create & run reusable container with dir binding: +# docker run --name pdfcpu -v $(pwd):/data -it pdfcpu /bin/sh +# /data # ... // run pdfcpu commands against your data +# /data # exit // exit container +# +# docker start -i pdfcpu // restart container with dir binding +# /data # ... // run pdfcpu commands against your data +# /data # exit // exit container + +# Start from the latest golang base image +FROM golang:latest as builder + +# install +RUN go install github.com/pdfcpu/pdfcpu/cmd/pdfcpu@latest + +######## Start a new stage from scratch ####### + +FROM alpine:latest + +RUN apk --no-cache add ca-certificates gcompat + +WORKDIR /root + +# Copy the pre-built binary file from the previous stage +COPY --from=builder /go/bin ./ + +# Export path of executable +ENV PATH="${PATH}:/root" + +WORKDIR /data + +# Command to run executable +CMD pdfcpu && echo && pdfcpu version -v + + diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000000000000000000000000000000000000..d645695673349e3947e8e5ae42332d0ac3164cd7 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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 + + 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. diff --git a/README.md b/README.md index 841f7188e1246e4c0e5a1175913d03db6fca5d0a..c1190673246c5f2ed78d9fa96f8da5778be355f4 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,232 @@ -# golang-pdfcpu +# pdfcpu: a Go PDF processor and CLI -#### 介绍 -{**以下是 Gitee 平台说明,您可以替换此简介** -Gitee 是 OSCHINA 推出的基于 Git 的代码托管平台(同时支持 SVN)。专为开发者提供稳定、高效、安全的云端软件开发协作平台 -无论是个人、团队、或是企业,都能够用 Gitee 实现代码托管、项目管理、协作开发。企业项目请看 [https://gitee.com/enterprises](https://gitee.com/enterprises)} +[![Test](https://github.com/pdfcpu/pdfcpu/workflows/Test/badge.svg)](https://github.com/pdfcpu/pdfcpu/actions) +[![Coverage Status](https://coveralls.io/repos/github/pdfcpu/pdfcpu/badge.svg?branch=master)](https://coveralls.io/github/pdfcpu/pdfcpu?branch=master) +[![GoDoc](https://godoc.org/github.com/pdfcpu/pdfcpu?status.svg)](https://pkg.go.dev/github.com/pdfcpu/pdfcpu) +[![Go Report Card](https://goreportcard.com/badge/github.com/pdfcpu/pdfcpu)](https://goreportcard.com/report/github.com/pdfcpu/pdfcpu) +[![Hex.pm](https://img.shields.io/hexpm/l/plug.svg)](https://opensource.org/licenses/Apache-2.0) +[![Latest release](https://img.shields.io/github/release/pdfcpu/pdfcpu.svg)](https://github.com/pdfcpu/pdfcpu/releases) +[![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/hhrutter) -#### 软件架构 -软件架构说明 + + +pdfcpu is a PDF processing library written in [Go](http://golang.org) that supports encryption and offers both an API and a command-line interface (CLI). It is compatible with all PDF versions with basic support and ongoing improvement for PDF 2.0 (ISO-32000-2). -#### 安装教程 -1. xxxx -2. xxxx -3. xxxx +## Motivation -#### 使用说明 +This is an effort to build a comprehensive PDF processing library from the ground up written in Go. Over time pdfcpu aims to support the standard range of PDF processing features and also any interesting use cases that may present themselves along the way. -1. xxxx -2. xxxx -3. xxxx +

+   +   +   +   +   +

+   +   +   +   +  

+ +   + + + +

-#### 参与贡献 +## Focus -1. Fork 本仓库 -2. 新建 Feat_xxx 分支 -3. 提交代码 -4. 新建 Pull Request +The primary emphasis is on providing robust assistance for batch processing and scripting through a comprehensive command-line interface. +Simultaneously, pdfcpu aims to simplify the integration of PDF processing into your Go-based backend system by offering a versatile set of commands. +## Command Set -#### 特技 +* [annotations](https://pdfcpu.io/annot/annot) +* [attachments](https://pdfcpu.io/attach/attach) +* [booklet](https://pdfcpu.io/generate/booklet) +* [bookmarks](https://pdfcpu.io/bookmarks/bookmarks) +* [boxes](https://pdfcpu.io/boxes/boxes) +* [change owner password](https://pdfcpu.io/encrypt/change_opw) +* [change user password](https://pdfcpu.io/encrypt/change_upw) +* [collect](https://pdfcpu.io/core/collect) +* [create](https://pdfcpu.io/generate/create) +* [crop](https://pdfcpu.io/core/crop) +* [cut](https://pdfcpu.io/generate/cut) +* [decrypt](https://pdfcpu.io/encrypt/decryptPDF) +* [encrypt](https://pdfcpu.io/encrypt/encryptPDF) +* [extract](https://pdfcpu.io/extract/extract) +* [fonts](https://pdfcpu.io/fonts/fonts) +* [form](https://pdfcpu.io/form/form) +* [grid](https://pdfcpu.io/generate/grid) +* [images](https://pdfcpu.io/images/images) +* [import](https://pdfcpu.io/generate/import) +* [info](https://pdfcpu.io/info) +* [keywords](https://pdfcpu.io/keywords/keywords) +* [merge](https://pdfcpu.io/core/merge) +* [ndown](https://pdfcpu.io/generate/ndown) +* [nup](https://pdfcpu.io/generate/nup) +* [optimize](https://pdfcpu.io/core/optimize) +* [pagelayout](https://pdfcpu.io/pagelayout/pagelayout) +* [pagemode](https://pdfcpu.io/pagemode/pagemode) +* [pages](https://pdfcpu.io/pages/pages) +* [permissions](https://pdfcpu.io/encrypt/perm_add) +* [portfolio](https://pdfcpu.io/portfolio/portfolio) +* [poster](https://pdfcpu.io/generate/poster) +* [properties](https://pdfcpu.io/properties/properties) +* [resize](https://pdfcpu.io/core/resize) +* [rotate](https://pdfcpu.io/core/rotate) +* [split](https://pdfcpu.io/core/split) +* [stamp](https://pdfcpu.io/core/stamp) +* [trim](https://pdfcpu.io/core/trim) +* [validate](https://pdfcpu.io/core/validate) +* [viewerpref](https://pdfcpu.io/viewerpref/viewerpref) +* [watermark](https://pdfcpu.io/core/watermark) +* [zoom](https://pdfcpu.io/core/zoom) + +## Documentation + +* [pdfcpu.io](https://pdfcpu.io) +* [API tests](https://github.com/pdfcpu/pdfcpu/tree/master/pkg/api/test) +* [API samples](https://github.com/pdfcpu/pdfcpu/tree/master/pkg/samples) +* CLI usage: `$ pdfcpu help cmd` + +### GoDoc + +* [pdfcpu package](https://pkg.go.dev/github.com/pdfcpu/pdfcpu) +* [pdfcpu API](https://pkg.go.dev/github.com/pdfcpu/pdfcpu/pkg/api) +* [pdfcpu CLI](https://pkg.go.dev/github.com/pdfcpu/pdfcpu/pkg/cli) + +## Reminder + +* Always make sure your work is based on the latest commit!
+* pdfcpu is still *Alpha* - bugfixes are committed on the fly and will be mentioned in the next release notes.
+* Follow [pdfcpu](https://twitter.com/pdfcpu) for news and release announcements. +* For quick questions or discussions get in touch on the [Gopher Slack](https://invite.slack.golangbridge.org/) in the #pdfcpu channel. + + +## Demo Screencast + +(using older version with a smaller command set) + +[![asciicast](resources/demo.png)](https://asciinema.org/a/P5jaAo9kgZXKj2iSA1OqIdLAU) + +## Installation + +### Download +Get the latest binary [here](https://github.com/pdfcpu/pdfcpu/releases). + + +### Using Go Modules + +``` +$ git clone https://github.com/pdfcpu/pdfcpu +$ cd pdfcpu/cmd/pdfcpu +$ go install +$ pdfcpu version +``` +or directly through Go install: + +``` +$ go install github.com/pdfcpu/pdfcpu/cmd/pdfcpu@latest +``` + +### Using Homebrew (macOS) +``` +$ brew install pdfcpu +$ pdfcpu version +``` + +### Using DNF/YUM (Fedora) +``` +$ sudo dnf install golang-github-pdfcpu +$ pdfcpu version +``` + +### Run in a Docker container + +``` +$ docker build -t pdfcpu . +# mount current folder into container to process local files +$ docker run -it --mount type=bind,source="$(pwd)",target=/app pdfcpu ./pdfcpu validate /app/pdfs/a.pdf +``` + +## Contributing + +### What + +* Please [create](https://github.com/pdfcpu/pdfcpu/issues/new/choose) an issue if you find a bug or want to propose a change. +* Feature requests - always welcome! +* Bug fixes - always welcome! +* PRs - let's [discuss](https://github.com/pdfcpu/pdfcpu/discussions) first or [create](https://github.com/pdfcpu/pdfcpu/issues/new/choose) an issue. +* pdfcpu is stable but still *Alpha* and occasionally undergoing heavy changes. + +### How + +* The pdfcpu [discussion board](https://github.com/pdfcpu/pdfcpu/discussions) is open! Please engage in any form helpful for the community. +* If you want to report a bug please attach the *very verbose* (`pdfcpu cmd -vv ...`) output and ideally a test PDF that you can share. +* Always make sure your contribution is based on the latest commit. +* Please sign your commits. + +### Reporting Crashes + +Unfortunately crashes do happen :( +For the majority of the cases this is due to a diverse pool of PDF Writers out there and millions of PDF files using different versions waiting to be processed by pdfcpu. Sometimes these PDFs were written more than 20(!) years ago. Often there is an issue with validation - sometimes a bug in the parser. Many times even using relaxed validation with pdfcpu does not work. In these cases we need to extend relaxed validation and for this we are relying on your help. By reporting crashes you are helping to improve the stability of pdfcpu. If you happen to crash on any pdfcpu operation be it on the command line or in your Go backend these are the steps to report this: + +Regardless of the pdfcpu operation, please start using the pdfcpu command line to validate your file: + +``` sh +$ pdfcpu validate -v &> crash.log +``` + + or to produce very verbose output + + ``` sh + $ pdfcpu validate -vv &> crash.log + ``` + +will produce what's needed to investigate a crash. Then open an issue and post `crash.log` or its contents. Ideally post a test PDF you can share to reproduce this. You can also email to hhrutter@gmail.com or if you prefer Slack you can get in touch on the Gopher slack #pdfcpu channel. + +If processing your PDF with pdfcpu crashes during validation and can be opened by Adobe Reader and Mac Preview chances are we can extend relaxed validation and provide a fix. If the file in question cannot be opened by both Adobe Reader and Mac Preview we cannot help you! + +## Contributors + +Thanks 💚 goes to these wonderful people: + + +|||||||| +| :---: | :---: | :---: | :---: | :---: | :---: | :---: | +| [
Horst Rutter](https://github.com/hhrutter) | [
haldyr](https://github.com/haldyr) | [
Vyacheslav](https://github.com/SimePel) | [
Erik Unger](https://github.com/ungerik) | [
Richard Wilkes](https://github.com/richardwilkes) | [
minenok-tutu](https://github.com/minenok-tutu) | [
Mateusz Burniak](https://github.com/matbur) | +| [
Dmitry Harnitski](https://github.com/dharnitski) | [
ryarnyah](https://github.com/ryarnyah) | [
Sam Giffney](https://github.com/s01ipsist) | [
Carlos Eduardo Witte](https://github.com/cewitte) | [
minusworld](https://github.com/minusworld) | [
Witold Konior](https://github.com/jozuenoon) | [
joonas.fi](https://github.com/joonas-fi) | +| [
Henrik Reinstädtler](https://github.com/henrixapp) | [
VMorozov-wh](https://github.com/VMorozov-wh) | [
Benoit KUGLER](https://github.com/benoitkugler) | [
Adam Greenhall](https://github.com/adamgreenhall) | [
moritamori](https://github.com/moritamori) | [
JanBaryla](https://github.com/JanBaryla) | [
TheDiscordian](https://github.com/TheDiscordian) | +| [
Rafael Garcia Argente](https://github.com/rgargente) | [
truyet](https://github.com/truyet) | [
Christian Nicola](https://github.com/christiannicola) | [
Benjamin Krill](https://github.com/kben) | [
Peter Wyatt](https://github.com/petervwyatt) | [
Kroum Tzanev](https://github.com/kpym) | [
Stefan Huber](https://github.com/signalwerk) | +| [
Juan Iscar](https://github.com/juaismar) | [
Eng Zer Jun](https://github.com/Juneezee) | [
Dmitry Ivanov](https://github.com/hant0508)|[
Rene Kaufmann](https://github.com/HeavyHorst)|[
Christian Heusel](https://github.com/christian-heusel) | [
Chris](https://github.com/freshteapot) | [
Lukasz Czaplinski](https://github.com/scoiatael) | +[
Joel Silva Schutz](https://github.com/joelschutz) | [
semvis123](https://github.com/semvis123) | [
guangwu](https://github.com/testwill) | [
Yoshiki Nakagawa](https://github.com/yyoshiki41) | [
Steve van Loben Sels](https://github.com/stevevls) | [
Yaofu](https://github.com/mygityf) | [
vsenko](https://github.com/vsenko) | +[
Alexis Hildebrandt](https://github.com/afh) | [
Sivukhin Nikita](https://github.com/sivukhin) | [
Joachim Bauch](https://github.com/fancycode)| [
kalimit](https://github.com/kalimit) | [
Andreas Erhard](https://github.com/xelan) | | + + + + + + + + + + + + + +## Code of Conduct + +Please note that this project is released with a Contributor [Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms. + +## Disclaimer + +Usage of pdfcpu assumes you know about and respect all copyrights of any PDF content you may be processing. This applies to the PDF files as such, their content and in particular all embedded resources like font files or images. Credit goes to [Renee French](https://instagram.com/reneefrench) for creating our beloved Gopher. + +## License + +Apache-2.0 -1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md -2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com) -3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目 -4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目 -5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help) -6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) diff --git a/_scripts/encryptDir.sh b/_scripts/encryptDir.sh new file mode 100644 index 0000000000000000000000000000000000000000..45082e5835ec255deb702514f05ef873214cc4eb --- /dev/null +++ b/_scripts/encryptDir.sh @@ -0,0 +1,117 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./encryptDir.sh ~/pdf/big ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./encryptDir.sh inDir outDir" + exit 1 +fi + +out=$2 + +#rm -drf $out/* + +#set -e + +new=_new + +for pdf in $1/*.pdf +do + #echo $pdf + + f=${pdf##*/} + #echo f = $f + + f1=${f%.*} + #echo f1 = $f1 + + cp $pdf $out/$f + + out1=$out/$f1$new.pdf + pdfcpu encrypt -verbose -upw=upw -opw=opw $out/$f $out1 &> $out/$f1.log + if [ $? -eq 1 ]; then + echo "encryption error: $pdf -> $out1" + echo + continue + else + echo "encryption success: $pdf -> $out1" + fi + + pdfcpu validate -verbose -mode=relaxed -upw=upw -opw=opw $out1 &> $out/$f1$new.log + if [ $? -eq 1 ]; then + echo "validation error: $out1" + echo + continue + else + echo "validation success: $out1" + fi + + pdfcpu changeupw -opw opw -verbose $out1 upw upwNew &> $out/$f1$new.log + if [ $? -eq 1 ]; then + echo "changeupw error: $1 -> $out1" + echo + continue + else + echo "changeupw success: $1 -> $out1" + fi + + pdfcpu validate -verbose -mode=relaxed -upw upwNew -opw opw $out1 &> $out/$f1$new.log + if [ $? -eq 1 ]; then + echo "validation error: $out1" + echo + continue + else + echo "validation success: $out1" + fi + + pdfcpu changeopw -upw upwNew -verbose $out1 opw opwNew &> $out/$f1$new.log + if [ $? -eq 1 ]; then + echo "changeopw error: $1 -> $out1" + echo + continue + else + echo "changeopw success: $1 -> $out1" + fi + + pdfcpu validate -verbose -mode=relaxed -upw upwNew -opw opwNew $out1 &> $out/$f1$new.log + if [ $? -eq 1 ]; then + echo "validation error: $out1" + echo + continue + else + echo "validation success: $out1" + fi + + pdfcpu decrypt -verbose -upw=upwNew -opw=opwNew $out1 $out1 &> $out/$f1.log + if [ $? -eq 1 ]; then + echo "decryption error: $out1 -> $out1" + echo + continue + else + echo "decryption success: $out1 -> $out1" + fi + + pdfcpu validate -verbose -mode=relaxed $out1 &> $out/$f1$new.log + if [ $? -eq 1 ]; then + echo "validation error: $out1" + else + echo "validation success: $out1" + fi + + echo + +done diff --git a/_scripts/encryptFile.sh b/_scripts/encryptFile.sh new file mode 100644 index 0000000000000000000000000000000000000000..3f6514ce1c257ef402e1fd310c58d580111fbabf --- /dev/null +++ b/_scripts/encryptFile.sh @@ -0,0 +1,101 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./encryptFile.sh ~/pdf/1mb/a.pdf ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./encryptFile.sh inFile outDir" + exit 1 +fi + +new=_new + +f=${1##*/} +f1=${f%.*} +out=$2 + +#rm -drf $out/* + +#set -e + +cp $1 $out/$f + +out1=$out/$f1$new.pdf +pdfcpu encrypt -verbose -upw upw -opw opw $out/$f $out1 &> $out/$f1.log +if [ $? -eq 1 ]; then + echo "encryption error: $1 -> $out1" + exit $? +else + echo "encryption success: $1 -> $out1" +fi + +pdfcpu validate -verbose -mode=relaxed -upw upw -opw opw $out1 &> $out/$f1$new.log +if [ $? -eq 1 ]; then + echo "validation error: $out1" + exit $? +else + echo "validation success: $out1" +fi + +pdfcpu changeupw -opw opw -verbose $out1 upw upwNew &> $out/$f1$new.log +if [ $? -eq 1 ]; then + echo "changeupw error: $1 -> $out1" + exit $? +else + echo "changeupw success: $1 -> $out1" +fi + +pdfcpu validate -verbose -mode=relaxed -upw upwNew -opw opw $out1 &> $out/$f1$new.log +if [ $? -eq 1 ]; then + echo "validation error: $out1" + exit $? +else + echo "validation success: $out1" +fi + +pdfcpu changeopw -upw upwNew -verbose $out1 opw opwNew &> $out/$f1$new.log +if [ $? -eq 1 ]; then + echo "changeopw error: $1 -> $out1" + exit $? +else + echo "changeopw success: $1 -> $out1" +fi + +pdfcpu validate -verbose -mode=relaxed -upw upwNew -opw opwNew $out1 &> $out/$f1$new.log +if [ $? -eq 1 ]; then + echo "validation error: $out1" + exit $? +else + echo "validation success: $out1" +fi + +pdfcpu decrypt -verbose -upw upwNew -opw opwNew $out1 $out1 &> $out/$f1.log +if [ $? -eq 1 ]; then + echo "decryption error: $out1 -> $out1" + exit $? +else + echo "decryption success: $out1 -> $out1" +fi + +pdfcpu validate -verbose -mode=relaxed $out1 &> $out/$f1$new.log +if [ $? -eq 1 ]; then + echo "validation error: $out1" + exit $? +else + echo "validation success: $out1" +fi + + diff --git a/_scripts/extractContentDir.sh b/_scripts/extractContentDir.sh new file mode 100644 index 0000000000000000000000000000000000000000..5c11d499d20f6a7cf90d5da34308d563c0a20d83 --- /dev/null +++ b/_scripts/extractContentDir.sh @@ -0,0 +1,51 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./extractContentDir.sh ~/pdf/big ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./extractContentDir.sh inDir outDir" + exit 1 +fi + +out=$2 + +#rm -drf $out/* + +#set -e + +for pdf in $1/*.pdf +do + + f=${pdf##*/} + #echo f = $f + + f1=${f%.*} + #echo f1 = $f1 + + mkdir $out/$f1 + cp $pdf $out/$f1 + + pdfcpu extract -verbose -mode=content $out/$f1/$f $out/$f1 &> $out/$f1/$f1.log + if [ $? -eq 1 ]; then + echo "extraction error: $pdf -> $out/$f1" + echo + continue + else + echo "extraction success: $pdf -> $out/$f1" + fi + +done diff --git a/_scripts/extractContentFile.sh b/_scripts/extractContentFile.sh new file mode 100644 index 0000000000000000000000000000000000000000..e6a9ba283c5572f8ad729867d93d4c62d2711f7c --- /dev/null +++ b/_scripts/extractContentFile.sh @@ -0,0 +1,44 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./extractContentFile.sh ~/pdf/1mb/a.pdf ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./extractContentFile.sh inFile outDir" + exit 1 +fi + +f=${1##*/} +f1=${f%.*} +out=$2 + +#rm -drf $out/* + +#set -e + +mkdir $out/$f1 +cp $1 $out/$f1 + +pdfcpu extract -verbose -mode=content $out/$f1/$f $out/$f1 &> $out/$f1/$f1.log +if [ $? -eq 1 ]; then + echo "content extraction error: $1 -> $out/$f1" + exit $? +else + echo "content extraction success: $1 -> $out/$f1" +fi + + + diff --git a/_scripts/extractFontsDir.sh b/_scripts/extractFontsDir.sh new file mode 100644 index 0000000000000000000000000000000000000000..92a90640df773e90f9a8910588c5d1218469f26b --- /dev/null +++ b/_scripts/extractFontsDir.sh @@ -0,0 +1,51 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./extractFontsDir.sh ~/pdf/big ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./extractFontsDir.sh inDir outDir" + exit 1 +fi + +out=$2 + +#rm -drf $out/* + +#set -e + +for pdf in $1/*.pdf +do + + f=${pdf##*/} + #echo f = $f + + f1=${f%.*} + #echo f1 = $f1 + + mkdir $out/$f1 + cp $pdf $out/$f1 + + pdfcpu extract -verbose -mode=font $out/$f1/$f $out/$f1 &> $out/$f1/$f1.log + if [ $? -eq 1 ]; then + echo "font extraction error: $pdf -> $out/$f1" + echo + continue + else + echo "font extraction success: $pdf -> $out/$f1" + fi + +done diff --git a/_scripts/extractFontsFile.sh b/_scripts/extractFontsFile.sh new file mode 100644 index 0000000000000000000000000000000000000000..f23e2f6adf7229686113089a605e532fd3fb12e1 --- /dev/null +++ b/_scripts/extractFontsFile.sh @@ -0,0 +1,44 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./extractFontsFile.sh ~/pdf/1mb/a.pdf ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./extractFontsFile.sh inFile outDir" + exit 1 +fi + +f=${1##*/} +f1=${f%.*} +out=$2 + +#rm -drf $out/* + +#set -e + +mkdir $out/$f1 +cp $1 $out/$f1 + +pdfcpu extract -verbose -mode=font $out/$f1/$f $out/$f1 &> $out/$f1/$f1.log +if [ $? -eq 1 ]; then + echo "font extraction error: $1 -> $out/$f1" + exit $? +else + echo "font extraction success: $1 -> $out/$f1" +fi + + + diff --git a/_scripts/extractImagesDir.sh b/_scripts/extractImagesDir.sh new file mode 100644 index 0000000000000000000000000000000000000000..d9f787e3462c7172ee368ba4ce91f7bd7a9cf9fc --- /dev/null +++ b/_scripts/extractImagesDir.sh @@ -0,0 +1,47 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./extractImagesDir.sh ~/pdf/big ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./extractImagesDir.sh inDir outDir" + exit 1 +fi + +out=$2 + +for pdf in $1/*.pdf +do + + f=${pdf##*/} + #echo f = $f + + f1=${f%.*} + #echo f1 = $f1 + + mkdir $out/$f1 + cp $pdf $out/$f1 + + pdfcpu extract -verbose -mode=image $out/$f1/$f $out/$f1 &> $out/$f1/$f1.log + if [ $? -eq 1 ]; then + echo "image extraction error: $pdf -> $out/$f1" + echo + continue + else + echo "image extraction success: $pdf -> $out/$f1" + fi + +done diff --git a/_scripts/extractImagesFile.sh b/_scripts/extractImagesFile.sh new file mode 100644 index 0000000000000000000000000000000000000000..efee20f6edd32959576a616398732d802ed15519 --- /dev/null +++ b/_scripts/extractImagesFile.sh @@ -0,0 +1,40 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./extractImagesFile.sh ~/pdf/1mb/a.pdf ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./extractImagesFile.sh inFile outDir" + exit 1 +fi + +f=${1##*/} +f1=${f%.*} +out=$2 + +mkdir $out/$f1 +cp $1 $out/$f1 + +pdfcpu extract -verbose -mode=image $out/$f1/$f $out/$f1 &> $out/$f1/$f1.log +if [ $? -eq 1 ]; then + echo "image extraction error: $1 -> $out/$f1" + exit $? +else + echo "image extraction success: $1 -> $out/$f1" +fi + + + diff --git a/_scripts/extractMetadataDir.sh b/_scripts/extractMetadataDir.sh new file mode 100644 index 0000000000000000000000000000000000000000..bbe3214fa3d13916b9fa1eec4af88e5eababb690 --- /dev/null +++ b/_scripts/extractMetadataDir.sh @@ -0,0 +1,48 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./extractMetadataDir.sh ~/pdf/big ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./extractMetadataDir.sh inDir outDir" + echo "extracts XML metadata into corresponding dirs." + exit 1 +fi + +out=$2 + +for pdf in $1/*.pdf +do + + f=${pdf##*/} + #echo f = $f + + f1=${f%.*} + #echo f1 = $f1 + + mkdir $out/$f1 + cp $pdf $out/$f1 + + pdfcpu extract -verbose -mode=meta $out/$f1/$f $out/$f1 &> $out/$f1/$f1.log + if [ $? -eq 1 ]; then + echo "metadata extraction error: $pdf -> $out/$f1" + echo + continue + else + echo "metadata extraction success: $pdf -> $out/$f1" + fi + +done diff --git a/_scripts/extractMetadataFile.sh b/_scripts/extractMetadataFile.sh new file mode 100644 index 0000000000000000000000000000000000000000..82f50a1a3101557f9244f9ee977b94107c0c7fb9 --- /dev/null +++ b/_scripts/extractMetadataFile.sh @@ -0,0 +1,41 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./extractMetadataFile.sh ~/pdf/1mb/a.pdf ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./extractMetadataFile.sh inFile outDir" + echo "extracts XML metadata as text files into outDir." + exit 1 +fi + +f=${1##*/} +f1=${f%.*} +out=$2 + +mkdir $out/$f1 +cp $1 $out/$f1 + +pdfcpu extract -verbose -mode=meta $out/$f1/$f $out/$f1 &> $out/$f1/$f1.log +if [ $? -eq 1 ]; then + echo "metadata extraction error: $1 -> $out/$f1" + exit $? +else + echo "metadata extraction success: $1 -> $out/$f1" +fi + + + diff --git a/_scripts/extractPagesDir.sh b/_scripts/extractPagesDir.sh new file mode 100644 index 0000000000000000000000000000000000000000..7ade74534b7276eb1b106875175ff090327b1312 --- /dev/null +++ b/_scripts/extractPagesDir.sh @@ -0,0 +1,63 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./extractPagesDir.sh ~/pdf/big ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./extractPagesDir.sh inDir outDir" + echo "generates single-page PDFs for the first 5 pages." + exit 1 +fi + +out=$2 + +#rm -drf $out/* + +#set -e + +for pdf in $1/*.pdf +do + + f=${pdf##*/} + #echo f = $f + + f1=${f%.*} + #echo f1 = $f1 + + mkdir $out/$f1 + cp $pdf $out/$f1 + + # extract first 5 pages + pdfcpu extract -verbose -mode=page -pages=-5 $out/$f1/$f $out/$f1 &> $out/$f1/$f1.log + if [ $? -eq 1 ]; then + echo "extraction error: $pdf -> $out/$f1" + echo + continue + else + echo "extraction success: $pdf -> $out/$f1" + for subpdf in $out/$f1/*_?.pdf + do + pdfcpu validate -verbose -mode=relaxed $subpdf >> $out/$f1/$f1.log 2>&1 + if [ $? -eq 1 ]; then + echo "validation error: $subpdf" + exit $? + #else + #echo "validation success: $subpdf" + fi + done + fi + +done diff --git a/_scripts/extractPagesFile.sh b/_scripts/extractPagesFile.sh new file mode 100644 index 0000000000000000000000000000000000000000..99df515f616daf14f3079128fd31ed39d8d085cf --- /dev/null +++ b/_scripts/extractPagesFile.sh @@ -0,0 +1,56 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./extractPagesFile.sh ~/pdf/1mb/a.pdf ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./extractPagesFile.sh inFile outDir" + echo "generates single-page PDFs for the first 5 pages." + exit 1 +fi + +f=${1##*/} +f1=${f%.*} +out=$2 + +#rm -drf $out/* + +#set -e + +mkdir $out/$f1 +cp $1 $out/$f1 + +# extract first 5 pages +pdfcpu extract -verbose -mode=page -pages=-5 $out/$f1/$f $out/$f1 &> $out/$f1/$f1.log +if [ $? -eq 1 ]; then + echo "extraction error: $1 -> $out" + exit $? +else + echo "extraction success: $1 -> $out" + for pdf in $out/$f1/*_?.pdf + do + pdfcpu validate -verbose -mode=relaxed $pdf >> $out/$f1/$f1.log 2>&1 + if [ $? -eq 1 ]; then + echo "validation error: $pdf" + exit $? + #else + #echo "validation success: $pdf" + fi + done +fi + + + diff --git a/_scripts/gridDir.sh b/_scripts/gridDir.sh new file mode 100644 index 0000000000000000000000000000000000000000..4d68884daa0ada8db172a5488d55a82814af9049 --- /dev/null +++ b/_scripts/gridDir.sh @@ -0,0 +1,57 @@ +#!/bin/sh + +# Copyright 2019 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./gridDir.sh ~/pdf/big ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./gridDir.sh inDir outDir" + echo "rearrange all pages into 1x3 page grids" + exit 1 +fi + +out=$2 + +new=_grid + +for pdf in $1/*.pdf +do + + f=${pdf##*/} + + f1=${f%.*} + + cp $pdf $out/$f + + out1=$out/$f1$new.pdf + pdfcpu grid -verbose $out1 1 3 $out/$f &> $out/$f1.log + if [ $? -eq 1 ]; then + echo "grid error: $pdf -> $out1" + echo + continue + else + echo "grid success: $pdf -> $out1" + pdfcpu validate -verbose -mode=relaxed $out1 >> $out/$f1.log 2>&1 + if [ $? -eq 1 ]; then + echo "validation error: $out" + exit $? + else + echo "validation success: $out" + fi + fi + + echo + +done diff --git a/_scripts/gridFile.sh b/_scripts/gridFile.sh new file mode 100644 index 0000000000000000000000000000000000000000..a5bfa6ad230e5e4a3f3c5da672a6bf050eefda1c --- /dev/null +++ b/_scripts/gridFile.sh @@ -0,0 +1,50 @@ +#!/bin/sh + +# Copyright 2019 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./gridFile.sh ~/pdf/1mb/a.pdf ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./gridFile.sh inFile outDir" + echo "rearrange all pages into 1x3 page grids" + exit 1 +fi + +new=_grid + +f=${1##*/} +f1=${f%.*} +out=$2 + +cp $1 $out/$f + +out1=$out/$f1$new.pdf +pdfcpu grid -verbose $out1 1 3 $out/$f &> $out/$f1.log +if [ $? -eq 1 ]; then + echo "grid error: $1 -> $out1" + exit $? +else + echo "grid success: $1 -> $out1" + pdfcpu validate -verbose -mode=relaxed $out1 >> $out/$f1.log 2>&1 + if [ $? -eq 1 ]; then + echo "validation error: $out1" + exit $? + else + echo "validation success: $out1" + fi +fi + + + diff --git a/_scripts/mergeDir.sh b/_scripts/mergeDir.sh new file mode 100644 index 0000000000000000000000000000000000000000..97fd0a66c3c40430bc22916980116ae42759f76d --- /dev/null +++ b/_scripts/mergeDir.sh @@ -0,0 +1,43 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./mergeDir.sh ~/pdf/big ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./mergeDir.sh inDir outDir" + exit 1 +fi + +out=$2 + +#rm -drf $out/* + +#set -e + +for pdf in $1/*.pdf +do + f=${pdf##*/} + cp $pdf $out/$f +done + +pdfcpu merge -verbose $out/merged.pdf $out/*.pdf &> $out/merged.log +if [ $? -eq 1 ]; then + echo "merge error: $1/*.pdf -> $out/merged.pdf" + echo + continue +else + echo "merge success: $1/*.pdf -> $out/merged.pdf" +fi \ No newline at end of file diff --git a/_scripts/nupDir.sh b/_scripts/nupDir.sh new file mode 100644 index 0000000000000000000000000000000000000000..8869ca0853eba5698a5604189bac4fe713089ddc --- /dev/null +++ b/_scripts/nupDir.sh @@ -0,0 +1,57 @@ +#!/bin/sh + +# Copyright 2019 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./nupDir.sh ~/pdf/big ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./nupDir.sh inDir outDir" + echo "nup all pages into new pages showing 4 original pages on each page" + exit 1 +fi + +out=$2 + +new=_nup + +for pdf in $1/*.pdf +do + + f=${pdf##*/} + + f1=${f%.*} + + cp $pdf $out/$f + + out1=$out/$f1$new.pdf + pdfcpu nup -verbose $out1 4 $out/$f &> $out/$f1.log + if [ $? -eq 1 ]; then + echo "nup error: $pdf -> $out1" + echo + continue + else + echo "nup success: $pdf -> $out1" + pdfcpu validate -verbose -mode=relaxed $out1 >> $out/$f1.log 2>&1 + if [ $? -eq 1 ]; then + echo "validation error: $out" + exit $? + else + echo "validation success: $out" + fi + fi + + echo + +done diff --git a/_scripts/nupFile.sh b/_scripts/nupFile.sh new file mode 100644 index 0000000000000000000000000000000000000000..f53765d1340b5834e416990540e6223282371b2d --- /dev/null +++ b/_scripts/nupFile.sh @@ -0,0 +1,50 @@ +#!/bin/sh + +# Copyright 2019 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./nupFile.sh ~/pdf/1mb/a.pdf ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./nupFile.sh inFile outDir" + echo "nup all pages into new pages showing 4 original pages on each page" + exit 1 +fi + +new=_nup + +f=${1##*/} +f1=${f%.*} +out=$2 + +cp $1 $out/$f + +out1=$out/$f1$new.pdf +pdfcpu nup -verbose $out1 4 $out/$f &> $out/$f1.log +if [ $? -eq 1 ]; then + echo "nup error: $1 -> $out1" + exit $? +else + echo "nup success: $1 -> $out1" + pdfcpu validate -verbose -mode=relaxed $out1 >> $out/$f1.log 2>&1 + if [ $? -eq 1 ]; then + echo "validation error: $out1" + exit $? + else + echo "validation success: $out1" + fi +fi + + + diff --git a/_scripts/optimizeDir.sh b/_scripts/optimizeDir.sh new file mode 100644 index 0000000000000000000000000000000000000000..8a5b353a5c11768c5dc9868e0ae3a9e31c743526 --- /dev/null +++ b/_scripts/optimizeDir.sh @@ -0,0 +1,64 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./optimizeDir.sh ~/pdf/big ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./optimizeDir.sh inDir outDir" + exit 1 +fi + +out=$2 + +#rm -drf $out/* + +#set -e + +new=_new + +for pdf in $1/*.pdf +do + #echo $pdf + + f=${pdf##*/} + #echo f = $f + + f1=${f%.*} + #echo f1 = $f1 + + cp $pdf $out/$f + + out1=$out/$f1$new.pdf + pdfcpu optimize -verbose -stats=stats.csv $out/$f $out1 &> $out/$f1.log + if [ $? -eq 1 ]; then + echo "optimization error: $pdf -> $out1" + echo + continue + else + echo "optimization success: $pdf -> $out1" + fi + + out2=$out/$f1$new$new.pdf + pdfcpu optimize -verbose -stats=statsNew.csv $out1 $out2 &> $out/$f1$new.log + if [ $? -eq 1 ]; then + echo "optimization error: $out1 -> $out2" + else + echo "optimization success: $out1 -> $out2" + fi + + echo + +done diff --git a/_scripts/optimizeFile.sh b/_scripts/optimizeFile.sh new file mode 100644 index 0000000000000000000000000000000000000000..1d91380f5e926bc0e539dad381143d47c5c6eae4 --- /dev/null +++ b/_scripts/optimizeFile.sh @@ -0,0 +1,54 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./optimizeFile.sh ~/pdf/1mb/a.pdf ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./optimizeFile.sh inFile outDir" + exit 1 +fi + +new=_new + +f=${1##*/} +f1=${f%.*} +out=$2 + +#rm -drf $out/* + +#set -e + +cp $1 $out/$f + +out1=$out/$f1$new.pdf +pdfcpu optimize -verbose $out/$f $out1 &> $out/$f1.log +if [ $? -eq 1 ]; then + echo "optimization error: $1 -> $out1" + exit $? +else + echo "optimization success: $1 -> $out1" +fi + +out2=$out/$f1$new$new.pdf +pdfcpu optimize -verbose $out1 $out2 &> $out/$f1$new.log +if [ $? -eq 1 ]; then + echo "optimization error: $out1 -> $out2" + exit $? +else + echo "optimization success: $out1 -> $out2" +fi + + diff --git a/_scripts/resources/GC2018.png b/_scripts/resources/GC2018.png new file mode 100644 index 0000000000000000000000000000000000000000..85acf9edfa4d6ba99224e54cbedb53c38ac433b0 Binary files /dev/null and b/_scripts/resources/GC2018.png differ diff --git a/_scripts/runAll.sh b/_scripts/runAll.sh new file mode 100644 index 0000000000000000000000000000000000000000..883981980f0238ce324bd3a5ff6911478dd72f5f --- /dev/null +++ b/_scripts/runAll.sh @@ -0,0 +1,40 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +if [ $# -lt 2 ]; then + echo "usage: ./runAll.sh outDir dir..." + exit 1 +fi + +out=$1 + +rm -drf $out/* +for dir in $*; do + if [ $dir = $1 ]; then + continue + fi + echo $dir + ./validateDir.sh $dir $out +done + +rm -drf $out/* +for dir in $*; do + if [ $dir = $1 ]; then + continue + fi + echo $dir + ./optimizeDir.sh $dir $out +done \ No newline at end of file diff --git a/_scripts/splitDir.sh b/_scripts/splitDir.sh new file mode 100644 index 0000000000000000000000000000000000000000..c50d493c78f9585790fb03dfde080fcde2297c01 --- /dev/null +++ b/_scripts/splitDir.sh @@ -0,0 +1,61 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./splitDir.sh ~/pdf/big ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./splitDir.sh inDir outDir" + exit 1 +fi + +out=$2 + +#rm -drf $out/* + +#set -e + +for pdf in $1/*.pdf +do + + f=${pdf##*/} + #echo f = $f + + f1=${f%.*} + #echo f1 = $f1 + + mkdir $out/$f1 + cp $pdf $out/$f1 + + pdfcpu split -verbose $out/$f1/$f $out/$f1 &> $out/$f1/$f1.log + if [ $? -eq 1 ]; then + echo "split error: $pdf -> $out/$f1" + echo + continue + else + echo "split success: $pdf -> $out/$f1" + for subpdf in $out/$f1/*_*.pdf + do + pdfcpu validate -verbose -mode=relaxed $subpdf >> $out/$f1/$f1.log 2>&1 + if [ $? -eq 1 ]; then + echo "validation error: $subpdf" + exit $? + #else + #echo "validation success: $subpdf" + fi + done + fi + +done diff --git a/_scripts/splitFile.sh b/_scripts/splitFile.sh new file mode 100644 index 0000000000000000000000000000000000000000..601fbb3dea8f28c39aa80c86804a0d3d2607822b --- /dev/null +++ b/_scripts/splitFile.sh @@ -0,0 +1,55 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./splitFile.sh ~/pdf/1mb/a.pdf ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./splitFile.sh inFile outDir" + exit 1 +fi + +f=${1##*/} +f1=${f%.*} +out=$2 + +#rm -drf $out/* + +#set -e + +mkdir $out/$f1 +cp $1 $out/$f1 + +pdfcpu split -verbose $out/$f1/$f $out/$f1 &> $out/$f1/$f1.log +if [ $? -eq 1 ]; then + echo "split error: $1 -> $out" + exit $? +else + echo "split success: $1 -> $out" + for pdf in $out/$f1/*_*.pdf + do + echo "validating: $pdf" + pdfcpu validate -verbose -mode=relaxed $pdf >> $out/$f1/$f1.log 2>&1 + if [ $? -eq 1 ]; then + echo "validation error: $pdf" + exit $? + #else + #echo "validation success: $pdf" + fi + done +fi + + + diff --git a/_scripts/splitSpanDir.sh b/_scripts/splitSpanDir.sh new file mode 100644 index 0000000000000000000000000000000000000000..b0825462ae2ec6607c678790b0317e30549584bd --- /dev/null +++ b/_scripts/splitSpanDir.sh @@ -0,0 +1,64 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./splitDir.sh ~/pdf/big ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./splitDir.sh inDir outDir" + exit 1 +fi + +out=$2 + +#rm -drf $out/* + +#set -e + +# Split all files up by generating a new PDF for every 2 pages. +span=2 + +for pdf in $1/*.pdf +do + + f=${pdf##*/} + #echo f = $f + + f1=${f%.*} + #echo f1 = $f1 + + mkdir $out/$f1 + cp $pdf $out/$f1 + + pdfcpu split -verbose $out/$f1/$f $out/$f1 $span &> $out/$f1/$f1.log + if [ $? -eq 1 ]; then + echo "split error: $pdf -> $out/$f1" + echo + continue + else + echo "split success: $pdf -> $out/$f1" + for subpdf in $out/$f1/*_*.pdf + do + pdfcpu validate -verbose -mode=relaxed $subpdf >> $out/$f1/$f1.log 2>&1 + if [ $? -eq 1 ]; then + echo "validation error: $subpdf" + exit $? + #else + #echo "validation success: $subpdf" + fi + done + fi + +done diff --git a/_scripts/splitSpanFile.sh b/_scripts/splitSpanFile.sh new file mode 100644 index 0000000000000000000000000000000000000000..166371b4933e788f623a5e0d89313340c2dcad4c --- /dev/null +++ b/_scripts/splitSpanFile.sh @@ -0,0 +1,58 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./splitSpanFile.sh ~/pdf/1mb/a.pdf ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./splitSpanFile.sh inFile outDir" + exit 1 +fi + +f=${1##*/} +f1=${f%.*} +out=$2 + +#rm -drf $out/* + +#set -e + +mkdir $out/$f1 +cp $1 $out/$f1 + +# Split this file up by generating a new PDF for every 2 pages. +span=2 + +pdfcpu split -verbose $out/$f1/$f $out/$f1 $span &> $out/$f1/$f1.log +if [ $? -eq 1 ]; then + echo "split error: $1 -> $out" + exit $? +else + echo "split success: $1 -> $out" + for pdf in $out/$f1/*_*.pdf + do + echo "validating: $pdf" + pdfcpu validate -verbose -mode=relaxed $pdf >> $out/$f1/$f1.log 2>&1 + if [ $? -eq 1 ]; then + echo "validation error: $pdf" + exit $? + #else + #echo "validation success: $pdf" + fi + done +fi + + + diff --git a/_scripts/stampImageDir.sh b/_scripts/stampImageDir.sh new file mode 100644 index 0000000000000000000000000000000000000000..f56003420bd4049d092053a8be2c46d51e166bd8 --- /dev/null +++ b/_scripts/stampImageDir.sh @@ -0,0 +1,64 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./stampImageDir.sh ~/pdf/big ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./stampImageDir.sh inDir outDir" + echo "adds image stamps with rotation angle of 0 degrees" + exit 1 +fi + +out=$2 + +#rm -drf $out/* + +#set -e + +new=_wm + +for pdf in $1/*.pdf +do + #echo $pdf + + f=${pdf##*/} + #echo f = $f + + f1=${f%.*} + #echo f1 = $f1 + + cp $pdf $out/$f + + out1=$out/$f1$new.pdf + pdfcpu stamp add -verbose "resources/GC2018.png, rot:0" $out/$f $out1 &> $out/$f1.log + if [ $? -eq 1 ]; then + echo "stamp error: $pdf -> $out1" + echo + continue + else + echo "stamp success: $pdf -> $out1" + pdfcpu validate -verbose -mode=relaxed $out1 >> $out/$f1.log 2>&1 + if [ $? -eq 1 ]; then + echo "validation error: $out" + exit $? + else + echo "validation success: $out" + fi + fi + + echo + +done diff --git a/_scripts/stampImageFile.sh b/_scripts/stampImageFile.sh new file mode 100644 index 0000000000000000000000000000000000000000..faef1c926e034ec94cf38f4160dfe6360a66e778 --- /dev/null +++ b/_scripts/stampImageFile.sh @@ -0,0 +1,53 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./stampImageFile.sh ~/pdf/1mb/a.pdf ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./stampImageFile.sh inFile outDir" + echo "stamp all pages with an image" + exit 1 +fi + +new=_st + +f=${1##*/} +f1=${f%.*} +out=$2 + +#rm -drf $out/* + +#set -e + +cp $1 $out/$f + +out1=$out/$f1$new.pdf +pdfcpu stamp add -verbose "resources/GC2018.png:1" $out/$f $out1 &> $out/$f1.log +if [ $? -eq 1 ]; then + echo "stamp error: $1 -> $out1" + exit $? +else + echo "stamp success: $1 -> $out1" + pdfcpu validate -verbose -mode=relaxed $out1 >> $out/$f1.log 2>&1 + if [ $? -eq 1 ]; then + echo "validation error: $out1" + exit $? + else + echo "validation success: $out1" + fi +fi + + diff --git a/_scripts/stampPDFDir.sh b/_scripts/stampPDFDir.sh new file mode 100644 index 0000000000000000000000000000000000000000..746d6910af2d3209b1a286193d53b5c6a0bceccc --- /dev/null +++ b/_scripts/stampPDFDir.sh @@ -0,0 +1,64 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./stampPDFDir.sh ~/pdf/big ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./stampPDFDir.sh inDir outDir" + echo "stamp each file with its first page." + exit 1 +fi + +out=$2 + +#rm -drf $out/* + +#set -e + +new=_st + +for pdf in $1/*.pdf +do + #echo $pdf + + f=${pdf##*/} + #echo f = $f + + f1=${f%.*} + #echo f1 = $f1 + + cp $pdf $out/$f + + out1=$out/$f1$new.pdf + pdfcpu stamp add -verbose "$out/$f, op:.9" $out/$f $out1 &> $out/$f1.log + if [ $? -eq 1 ]; then + echo "stamp error: $pdf -> $out1" + echo + continue + else + echo "stamp success: $pdf -> $out1" + pdfcpu validate -verbose -mode=relaxed $out1 >> $out/$f1.log 2>&1 + if [ $? -eq 1 ]; then + echo "validation error: $out" + exit $? + else + echo "validation success: $out" + fi + fi + + echo + +done diff --git a/_scripts/stampPDFFile.sh b/_scripts/stampPDFFile.sh new file mode 100644 index 0000000000000000000000000000000000000000..a6a55cfac35f7571c09e1bc3d0f10431dab2d1c6 --- /dev/null +++ b/_scripts/stampPDFFile.sh @@ -0,0 +1,53 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./stampPDFFile.sh ~/pdf/1mb/a.pdf ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./stampPDFFile.sh inFile outDir" + echo "stamp all pages with the first pageof inFile" + exit 1 +fi + +new=_st + +f=${1##*/} +f1=${f%.*} +out=$2 + +#rm -drf $out/* + +#set -e + +cp $1 $out/$f + +out1=$out/$f1$new.pdf +pdfcpu stamp add -verbose "$out/$f" $out/$f $out1 &> $out/$f1.log +if [ $? -eq 1 ]; then + echo "stamp error: $1 -> $out1" + exit $? +else + echo "stamp success: $1 -> $out1" + pdfcpu validate -verbose -mode=relaxed $out1 >> $out/$f1.log 2>&1 + if [ $? -eq 1 ]; then + echo "validation error: $out1" + exit $? + else + echo "validation success: $out1" + fi +fi + + diff --git a/_scripts/stampTextDir.sh b/_scripts/stampTextDir.sh new file mode 100644 index 0000000000000000000000000000000000000000..213ee7e348daadd7ebf75b4c156f5766eabeac91 --- /dev/null +++ b/_scripts/stampTextDir.sh @@ -0,0 +1,64 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./stampTextDir.sh ~/pdf/big ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./stampTextDir.sh inDir outDir" + echo "stamp all files as Draft with opacity 0.9" + exit 1 +fi + +out=$2 + +#rm -drf $out/* + +#set -e + +new=_st + +for pdf in $1/*.pdf +do + #echo $pdf + + f=${pdf##*/} + #echo f = $f + + f1=${f%.*} + #echo f1 = $f1 + + cp $pdf $out/$f + + out1=$out/$f1$new.pdf + pdfcpu stamp add -verbose "Draft, op:.9" $out/$f $out1 &> $out/$f1.log + if [ $? -eq 1 ]; then + echo "stamp error: $pdf -> $out1" + echo + continue + else + echo "stamp success: $pdf -> $out1" + pdfcpu validate -verbose -mode=relaxed $out1 >> $out/$f1.log 2>&1 + if [ $? -eq 1 ]; then + echo "validation error: $out" + exit $? + else + echo "validation success: $out" + fi + fi + + echo + +done diff --git a/_scripts/stampTextFile.sh b/_scripts/stampTextFile.sh new file mode 100644 index 0000000000000000000000000000000000000000..3eeaa08691284dbbd6eb8991230935e56dda3fa0 --- /dev/null +++ b/_scripts/stampTextFile.sh @@ -0,0 +1,53 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./stampTextFile.sh ~/pdf/1mb/a.pdf ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./stampTextFile.sh inFile outDir" + echo "stamp all pages as Draft" + exit 1 +fi + +new=_st + +f=${1##*/} +f1=${f%.*} +out=$2 + +#rm -drf $out/* + +#set -e + +cp $1 $out/$f + +out1=$out/$f1$new.pdf +pdfcpu stamp add -verbose "Draft" $out/$f $out1 &> $out/$f1.log +if [ $? -eq 1 ]; then + echo "stamp error: $1 -> $out1" + exit $? +else + echo "stamp success: $1 -> $out1" + pdfcpu validate -verbose -mode=relaxed $out1 >> $out/$f1.log 2>&1 + if [ $? -eq 1 ]; then + echo "validation error: $out1" + exit $? + else + echo "validation success: $out1" + fi +fi + + diff --git a/_scripts/trimDir.sh b/_scripts/trimDir.sh new file mode 100644 index 0000000000000000000000000000000000000000..65038a847b76a46c9feffeeb461433b1cba84b8e --- /dev/null +++ b/_scripts/trimDir.sh @@ -0,0 +1,64 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./trimDir.sh ~/pdf/big ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./trimDir.sh inDir outDir" + echo "generate PDFs with the first 5 pages" + exit 1 +fi + +out=$2 + +#rm -drf $out/* + +#set -e + +new=_trim + +for pdf in $1/*.pdf +do + #echo $pdf + + f=${pdf##*/} + #echo f = $f + + f1=${f%.*} + #echo f1 = $f1 + + cp $pdf $out/$f + + out1=$out/$f1$new.pdf + pdfcpu trim -verbose -pages=-5 $out/$f $out1 &> $out/$f1.log + if [ $? -eq 1 ]; then + echo "trim error: $pdf -> $out1" + echo + continue + else + echo "trim success: $pdf -> $out1" + pdfcpu validate -verbose -mode=relaxed $out1 >> $out/$f1.log 2>&1 + if [ $? -eq 1 ]; then + echo "validation error: $out" + exit $? + else + echo "validation success: $out" + fi + fi + + echo + +done diff --git a/_scripts/trimFile.sh b/_scripts/trimFile.sh new file mode 100644 index 0000000000000000000000000000000000000000..435a0be5a3ac1df4f23f970fc64f3c046dab7e25 --- /dev/null +++ b/_scripts/trimFile.sh @@ -0,0 +1,54 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./trimFile.sh ~/pdf/1mb/a.pdf ~/pdf/1mb/a. + +if [ $# -ne 2 ]; then + echo "usage: ./trimFile.sh inFile outFile" + echo "generate a PDF with the first 5 pages" + exit 1 +fi + +new=_trim + +f=${1##*/} +f1=${f%.*} +out=$2 + +#rm -drf $out/* + +#set -e + +cp $1 $out/$f + +out1=$out/$f1$new.pdf +pdfcpu trim -verbose -pages=-5 $out/$f $out1 &> $out/$f1.log +if [ $? -eq 1 ]; then + echo "trim error: $1 -> $out1" + exit $? +else + echo "trim success: $1 -> $out1" + pdfcpu validate -verbose -mode=relaxed $out1 >> $out/$f1.log 2>&1 + if [ $? -eq 1 ]; then + echo "validation error: $out1" + exit $? + else + echo "validation success: $out1" + fi +fi + + + diff --git a/_scripts/validateDir.sh b/_scripts/validateDir.sh new file mode 100644 index 0000000000000000000000000000000000000000..508391a146391d88bbc3154b36663596eed85105 --- /dev/null +++ b/_scripts/validateDir.sh @@ -0,0 +1,55 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./validateDir.sh ~/pdf/big ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./validateDir.sh inDir outDir" + exit 1 +fi + +out=$2 + +#rm -drf $out/* + +#set -e + +new=_new + +for pdf in $1/*.pdf +do + #echo $pdf + + f=${pdf##*/} + #echo f = $f + + f1=${f%.*} + #echo f1 = $f1 + + cp $pdf $out/$f + + out1=$out/$f1$new.pdf + + pdfcpu validate -verbose -mode=relaxed $out/$f &> $out/$f1.log + + if [ $? -eq 1 ]; then + echo "validation error: $pdf" + #exit $? + else + echo "validation success: $pdf" + fi + +done diff --git a/_scripts/validateFile.sh b/_scripts/validateFile.sh new file mode 100644 index 0000000000000000000000000000000000000000..5799e538c9bb63bb9f6e10e7caa91be73b9f3813 --- /dev/null +++ b/_scripts/validateFile.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./validateFile.sh ~/pdf/1mb/a.pdf ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./validateFile.sh inFile logDir" + exit 1 +fi + +f=${1##*/} +f1=${f%.*} +out=$2 + +#rm -drf $out/* + +cp $1 $out/$f + +pdfcpu validate -verbose -mode=relaxed $out/$f &> $out/$f1.log + +if [ $? -eq 1 ]; then + echo "validation error: $out/$f" + exit $? +else + echo "validation success: $out/$f" +fi \ No newline at end of file diff --git a/_scripts/watermarkTextDir.sh b/_scripts/watermarkTextDir.sh new file mode 100644 index 0000000000000000000000000000000000000000..ddb9fd30a6460b47161ecbcae6e2ab201a8e1211 --- /dev/null +++ b/_scripts/watermarkTextDir.sh @@ -0,0 +1,64 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./watermarkTextDir.sh ~/pdf/big ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./watermarkTextDir.sh inDir outDir" + echo "adds a text watermark" + exit 1 +fi + +out=$2 + +#rm -drf $out/* + +#set -e + +new=_wm + +for pdf in $1/*.pdf +do + #echo $pdf + + f=${pdf##*/} + #echo f = $f + + f1=${f%.*} + #echo f1 = $f1 + + cp $pdf $out/$f + + out1=$out/$f1$new.pdf + pdfcpu watermark add -verbose "Draft, mode:1" $out/$f $out1 &> $out/$f1.log + if [ $? -eq 1 ]; then + echo "watermark error: $pdf -> $out1" + echo + continue + else + echo "watermark success: $pdf -> $out1" + pdfcpu validate -verbose -mode=relaxed $out1 >> $out/$f1.log 2>&1 + if [ $? -eq 1 ]; then + echo "validation error: $out" + exit $? + else + echo "validation success: $out" + fi + fi + + echo + +done diff --git a/_scripts/watermarkTextFile.sh b/_scripts/watermarkTextFile.sh new file mode 100644 index 0000000000000000000000000000000000000000..c19f59ffc90f1700d9bc0967dcaa249ca67cac00 --- /dev/null +++ b/_scripts/watermarkTextFile.sh @@ -0,0 +1,53 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +# eg: ./watermarkTextFile.sh ~/pdf/1mb/a.pdf ~/pdf/out + +if [ $# -ne 2 ]; then + echo "usage: ./watermarkTextFile.sh inFile outDir" + echo "add a Draft watermark to all pages" + exit 1 +fi + +new=_wm + +f=${1##*/} +f1=${f%.*} +out=$2 + +#rm -drf $out/* + +#set -e + +cp $1 $out/$f + +out1=$out/$f1$new.pdf +pdfcpu watermark add -verbose "Draft" $out/$f $out1 &> $out/$f1.log +if [ $? -eq 1 ]; then + echo "watermark error: $1 -> $out1" + exit $? +else + echo "watermark success: $1 -> $out1" + pdfcpu validate -verbose -mode=relaxed $out1 >> $out/$f1.log 2>&1 + if [ $? -eq 1 ]; then + echo "validation error: $out1" + exit $? + else + echo "validation success: $out1" + fi +fi + + diff --git a/cmd/pdfcpu/cmd.go b/cmd/pdfcpu/cmd.go new file mode 100644 index 0000000000000000000000000000000000000000..703d22595389cce1249fbbb6005db5a810ba7213 --- /dev/null +++ b/cmd/pdfcpu/cmd.go @@ -0,0 +1,182 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 main + +import ( + "errors" + "flag" + "fmt" + "os" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +var ( + errUnknownCmd = errors.New("pdfcpu: unknown command") + errAmbiguousCmd = errors.New("pdfcpu: ambiguous command") +) + +// Command represents command meta information and details. +type command struct { + handler func(conf *model.Configuration) + cmdMap commandMap // Optional map of sub commands. + usageShort string // Short command description. + usageLong string // Long command description. +} + +func (c command) String() string { + return fmt.Sprintf("cmd: <%s> <%s>\n", c.usageShort, c.usageLong) +} + +type commandMap map[string]*command + +func newCommandMap() commandMap { + return map[string]*command{} +} + +func (m commandMap) register(cmdStr string, cmd command) { + m[cmdStr] = &cmd +} + +func parseFlags(cmd *command) { + // Execute after command completion. + i := 2 + + // This command uses a subcommand and is therefore a special case => start flag processing after 3rd argument. + if cmd.handler == nil { + if len(os.Args) == 2 { + fmt.Fprintln(os.Stderr, cmd.usageShort) + os.Exit(1) + } + i = 3 + } + + // Parse commandline flags. + if !flag.CommandLine.Parsed() { + err := flag.CommandLine.Parse(os.Args[i:]) + if err != nil { + os.Exit(1) + } + initLogging(verbose, veryVerbose) + } +} + +func validateConfigDirFlag() { + if len(conf) > 0 && conf != "disable" { + info, err := os.Stat(conf) + if err != nil { + if os.IsNotExist(err) { + fmt.Fprintf(os.Stderr, "conf: %s does not exist\n\n", conf) + os.Exit(1) + } + fmt.Fprintf(os.Stderr, "conf: %s %v\n\n", conf, err) + os.Exit(1) + } + if !info.IsDir() { + fmt.Fprintf(os.Stderr, "conf: %s not a directory\n\n", conf) + os.Exit(1) + } + model.ConfigPath = conf + return + } + if conf == "disable" { + model.ConfigPath = "disable" + } +} + +func ensureDefaultConfig() (*model.Configuration, error) { + validateConfigDirFlag() + if !types.MemberOf(model.ConfigPath, []string{"default", "disable"}) { + if err := model.EnsureDefaultConfigAt(model.ConfigPath); err != nil { + return nil, err + } + } + return model.NewDefaultConfiguration(), nil +} + +// process applies command completion and if successful processes the resulting command. +func (m commandMap) process(cmdPrefix string, command string) (string, error) { + var cmdStr string + + // Support command completion. + for k := range m { + if !strings.HasPrefix(k, cmdPrefix) { + continue + } + if len(cmdStr) > 0 { + return command, errAmbiguousCmd + } + cmdStr = k + } + + if cmdStr == "" { + return command, errUnknownCmd + } + + parseFlags(m[cmdStr]) + + conf, err := ensureDefaultConfig() + if err != nil { + return command, err + } + + conf.OwnerPW = opw + conf.UserPW = upw + + if m[cmdStr].handler != nil { + m[cmdStr].handler(conf) + return command, nil + } + + if len(os.Args) == 2 { + fmt.Fprintln(os.Stderr, m[cmdStr].usageShort) + os.Exit(1) + } + + return m[cmdStr].cmdMap.process(os.Args[2], cmdStr) +} + +// HelpString returns documentation for a topic. +func (m commandMap) HelpString(topic string) (string, error) { + topicStr := "" + for k := range m { + if !strings.HasPrefix(k, topic) { + continue + } + if len(topicStr) > 0 { + return topic, errAmbiguousCmd + } + topicStr = k + } + + cmd, ok := m[topicStr] + if !ok || cmd.usageShort == "" { + return fmt.Sprintf("Unknown help topic `%s`. Run 'pdfcpu help'.\n", topic), nil + } + + return fmt.Sprintf("%s\n\n%s\n", cmd.usageShort, cmd.usageLong), nil +} + +func (m commandMap) String() string { + logStr := []string{} + for k, v := range m { + logStr = append(logStr, fmt.Sprintf("%s: %v\n", k, v)) + } + return strings.Join(logStr, "") +} diff --git a/cmd/pdfcpu/init.go b/cmd/pdfcpu/init.go new file mode 100644 index 0000000000000000000000000000000000000000..6c1cc6d5bf7b03dcd1ba8ec5be5e2b8d926da31f --- /dev/null +++ b/cmd/pdfcpu/init.go @@ -0,0 +1,395 @@ +/* +Copyright 2019 The pdfcpu Authors. + +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 + + 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 main + +import ( + "flag" + + "github.com/pdfcpu/pdfcpu/pkg/log" +) + +func initAnnotsCmdMap() commandMap { + m := newCommandMap() + for k, v := range map[string]command{ + "list": {processListAnnotationsCommand, nil, "", ""}, + "remove": {processRemoveAnnotationsCommand, nil, "", ""}, + } { + m.register(k, v) + } + return m +} + +func initAttachCmdMap() commandMap { + m := newCommandMap() + for k, v := range map[string]command{ + "list": {processListAttachmentsCommand, nil, "", ""}, + "add": {processAddAttachmentsCommand, nil, "", ""}, + "remove": {processRemoveAttachmentsCommand, nil, "", ""}, + "extract": {processExtractAttachmentsCommand, nil, "", ""}, + } { + m.register(k, v) + } + return m +} + +func initBookmarksCmdMap() commandMap { + m := newCommandMap() + for k, v := range map[string]command{ + "list": {processListBookmarksCommand, nil, "", ""}, + "import": {processImportBookmarksCommand, nil, "", ""}, + "export": {processExportBookmarksCommand, nil, "", ""}, + "remove": {processRemoveBookmarksCommand, nil, "", ""}, + } { + m.register(k, v) + } + return m +} + +func initBoxesCmdMap() commandMap { + m := newCommandMap() + for k, v := range map[string]command{ + "list": {processListBoxesCommand, nil, "", ""}, + "add": {processAddBoxesCommand, nil, "", ""}, + "remove": {processRemoveBoxesCommand, nil, "", ""}, + } { + m.register(k, v) + } + return m +} + +func initFontsCmdMap() commandMap { + m := newCommandMap() + for k, v := range map[string]command{ + "cheatsheet": {processCreateCheatSheetFontsCommand, nil, "", ""}, + "install": {processInstallFontsCommand, nil, "", ""}, + "list": {processListFontsCommand, nil, "", ""}, + } { + m.register(k, v) + } + return m +} + +func initFormCmdMap() commandMap { + m := newCommandMap() + for k, v := range map[string]command{ + "list": {processListFormFieldsCommand, nil, "", ""}, + "remove": {processRemoveFormFieldsCommand, nil, "", ""}, + "lock": {processLockFormCommand, nil, "", ""}, + "unlock": {processUnlockFormCommand, nil, "", ""}, + "reset": {processResetFormCommand, nil, "", ""}, + "export": {processExportFormCommand, nil, "", ""}, + "fill": {processFillFormCommand, nil, "", ""}, + "multifill": {processMultiFillFormCommand, nil, "", ""}, + } { + m.register(k, v) + } + return m +} + +func initImagesCmdMap() commandMap { + m := newCommandMap() + for k, v := range map[string]command{ + "list": {processListImagesCommand, nil, "", ""}, + } { + m.register(k, v) + } + return m +} + +func initKeywordsCmdMap() commandMap { + m := newCommandMap() + for k, v := range map[string]command{ + "list": {processListKeywordsCommand, nil, "", ""}, + "add": {processAddKeywordsCommand, nil, "", ""}, + "remove": {processRemoveKeywordsCommand, nil, "", ""}, + } { + m.register(k, v) + } + return m +} + +func initPagesCmdMap() commandMap { + m := newCommandMap() + for k, v := range map[string]command{ + "insert": {processInsertPagesCommand, nil, "", ""}, + "remove": {processRemovePagesCommand, nil, "", ""}, + } { + m.register(k, v) + } + return m +} + +func initPermissionsCmdMap() commandMap { + m := newCommandMap() + for k, v := range map[string]command{ + "list": {processListPermissionsCommand, nil, "", ""}, + "set": {processSetPermissionsCommand, nil, "", ""}, + } { + m.register(k, v) + } + return m +} + +func initPortfolioCmdMap() commandMap { + m := newCommandMap() + for k, v := range map[string]command{ + "list": {processListAttachmentsCommand, nil, "", ""}, + "add": {processAddAttachmentsPortfolioCommand, nil, "", ""}, + "remove": {processRemoveAttachmentsCommand, nil, "", ""}, + "extract": {processExtractAttachmentsCommand, nil, "", ""}, + } { + m.register(k, v) + } + return m +} + +func initPropertiesCmdMap() commandMap { + m := newCommandMap() + for k, v := range map[string]command{ + "list": {processListPropertiesCommand, nil, "", ""}, + "add": {processAddPropertiesCommand, nil, "", ""}, + "remove": {processRemovePropertiesCommand, nil, "", ""}, + } { + m.register(k, v) + } + return m +} + +func initStampCmdMap() commandMap { + m := newCommandMap() + for k, v := range map[string]command{ + "add": {processAddStampsCommand, nil, "", ""}, + "remove": {processRemoveStampsCommand, nil, "", ""}, + "update": {processUpdateStampsCommand, nil, "", ""}, + } { + m.register(k, v) + } + return m +} + +func initWatermarkCmdMap() commandMap { + m := newCommandMap() + for k, v := range map[string]command{ + "add": {processAddWatermarksCommand, nil, "", ""}, + "remove": {processRemoveWatermarksCommand, nil, "", ""}, + "update": {processUpdateWatermarksCommand, nil, "", ""}, + } { + m.register(k, v) + } + return m +} + +func initPageModeCmdMap() commandMap { + m := newCommandMap() + for k, v := range map[string]command{ + "list": {processListPageModeCommand, nil, "", ""}, + "set": {processSetPageModeCommand, nil, "", ""}, + "reset": {processResetPageModeCommand, nil, "", ""}, + } { + m.register(k, v) + } + return m +} + +func initPageLayoutCmdMap() commandMap { + m := newCommandMap() + for k, v := range map[string]command{ + "list": {processListPageLayoutCommand, nil, "", ""}, + "set": {processSetPageLayoutCommand, nil, "", ""}, + "reset": {processResetPageLayoutCommand, nil, "", ""}, + } { + m.register(k, v) + } + return m +} + +func initViewerPreferencesCmdMap() commandMap { + m := newCommandMap() + for k, v := range map[string]command{ + "list": {processListViewerPreferencesCommand, nil, "", ""}, + "set": {processSetViewerPreferencesCommand, nil, "", ""}, + "reset": {processResetViewerPreferencesCommand, nil, "", ""}, + } { + m.register(k, v) + } + return m +} + +func initCommandMap() { + annotsCmdMap := initAnnotsCmdMap() + attachCmdMap := initAttachCmdMap() + bookmarksCmdMap := initBookmarksCmdMap() + boxesCmdMap := initBoxesCmdMap() + fontsCmdMap := initFontsCmdMap() + formCmdMap := initFormCmdMap() + imagesCmdMap := initImagesCmdMap() + keywordsCmdMap := initKeywordsCmdMap() + pagesCmdMap := initPagesCmdMap() + permissionsCmdMap := initPermissionsCmdMap() + portfolioCmdMap := initPortfolioCmdMap() + propertiesCmdMap := initPropertiesCmdMap() + stampCmdMap := initStampCmdMap() + watermarkCmdMap := initWatermarkCmdMap() + pageModeCmdMap := initPageModeCmdMap() + pageLayoutCmdMap := initPageLayoutCmdMap() + viewerPrefsCmdMap := initViewerPreferencesCmdMap() + + cmdMap = newCommandMap() + + for k, v := range map[string]command{ + "annotations": {nil, annotsCmdMap, usageAnnots, usageLongAnnots}, + "attachments": {nil, attachCmdMap, usageAttach, usageLongAttach}, + "bookmarks": {nil, bookmarksCmdMap, usageBookmarks, usageLongBookmarks}, + "booklet": {processBookletCommand, nil, usageBooklet, usageLongBooklet}, + "boxes": {nil, boxesCmdMap, usageBoxes, usageLongBoxes}, + "changeopw": {processChangeOwnerPasswordCommand, nil, usageChangeOwnerPW, usageLongChangeOwnerPW}, + "changeupw": {processChangeUserPasswordCommand, nil, usageChangeUserPW, usageLongChangeUserPW}, + "collect": {processCollectCommand, nil, usageCollect, usageLongCollect}, + "config": {printConfiguration, nil, usageConfig, usageLongConfig}, + "create": {processCreateCommand, nil, usageCreate, usageLongCreate}, + "crop": {processCropCommand, nil, usageCrop, usageLongCrop}, + "cut": {processCutCommand, nil, usageCut, usageLongCut}, + "decrypt": {processDecryptCommand, nil, usageDecrypt, usageLongDecrypt}, + "dump": {processDumpCommand, nil, "", ""}, + "encrypt": {processEncryptCommand, nil, usageEncrypt, usageLongEncrypt}, + "extract": {processExtractCommand, nil, usageExtract, usageLongExtract}, + "fonts": {nil, fontsCmdMap, usageFonts, usageLongFonts}, + "form": {nil, formCmdMap, usageForm, usageLongForm}, + "grid": {processGridCommand, nil, usageGrid, usageLongGrid}, + "help": {printHelp, nil, "", ""}, + "images": {nil, imagesCmdMap, usageImages, usageLongImages}, + "import": {processImportImagesCommand, nil, usageImportImages, usageLongImportImages}, + "info": {processInfoCommand, nil, usageInfo, usageLongInfo}, + "keywords": {nil, keywordsCmdMap, usageKeywords, usageLongKeywords}, + "merge": {processMergeCommand, nil, usageMerge, usageLongMerge}, + "ndown": {processNDownCommand, nil, usageNDown, usageLongNDown}, + "nup": {processNUpCommand, nil, usageNUp, usageLongNUp}, + "optimize": {processOptimizeCommand, nil, usageOptimize, usageLongOptimize}, + "pagelayout": {nil, pageLayoutCmdMap, usagePageLayout, usageLongPageLayout}, + "pagemode": {nil, pageModeCmdMap, usagePageMode, usageLongPageMode}, + "pages": {nil, pagesCmdMap, usagePages, usageLongPages}, + "paper": {printPaperSizes, nil, usagePaper, usageLongPaper}, + "permissions": {nil, permissionsCmdMap, usagePerm, usageLongPerm}, + "portfolio": {nil, portfolioCmdMap, usagePortfolio, usageLongPortfolio}, + "poster": {processPosterCommand, nil, usagePoster, usageLongPoster}, + "properties": {nil, propertiesCmdMap, usageProperties, usageLongProperties}, + "resize": {processResizeCommand, nil, usageResize, usageLongResize}, + "rotate": {processRotateCommand, nil, usageRotate, usageLongRotate}, + "selectedpages": {printSelectedPages, nil, usageSelectedPages, usageLongSelectedPages}, + "split": {processSplitCommand, nil, usageSplit, usageLongSplit}, + "stamp": {nil, stampCmdMap, usageStamp, usageLongStamp}, + "trim": {processTrimCommand, nil, usageTrim, usageLongTrim}, + "validate": {processValidateCommand, nil, usageValidate, usageLongValidate}, + "watermark": {nil, watermarkCmdMap, usageWatermark, usageLongWatermark}, + "version": {printVersion, nil, usageVersion, usageLongVersion}, + "viewerpref": {nil, viewerPrefsCmdMap, usageViewerPreferences, usageLongViewerPreferences}, + "zoom": {processZoomCommand, nil, usageZoom, usageLongZoom}, + } { + cmdMap.register(k, v) + } +} + +func initFlags() { + flag.BoolVar(&all, "all", false, "") + flag.BoolVar(&all, "a", false, "") + + bookmarksUsage := "create bookmarks while merging" + flag.BoolVar(&bookmarks, "bookmarks", true, bookmarksUsage) + flag.BoolVar(&bookmarks, "b", true, bookmarksUsage) + + confUsage := "the config directory path | skip | none" + flag.StringVar(&conf, "config", "", confUsage) + flag.StringVar(&conf, "conf", "", confUsage) + flag.StringVar(&conf, "c", "", confUsage) + + dividerPageUsage := "create divider pages while merging" + flag.BoolVar(÷rPage, "dividerPage", false, dividerPageUsage) + flag.BoolVar(÷rPage, "d", false, dividerPageUsage) + + jsonUsage := "produce JSON output" + flag.BoolVar(&json, "json", false, jsonUsage) + flag.BoolVar(&json, "j", false, jsonUsage) + + keyUsage := "encrypt: 40|128|256" + flag.StringVar(&key, "key", "256", keyUsage) + flag.StringVar(&key, "k", "256", keyUsage) + + linksUsage := "check for broken links" + flag.BoolVar(&links, "links", false, linksUsage) + flag.BoolVar(&links, "l", false, linksUsage) + + modeUsage := "validate: strict|relaxed; extract: image|font|content|page|meta; encrypt: rc4|aes, stamp:text|image/pdf" + flag.StringVar(&mode, "mode", "", modeUsage) + flag.StringVar(&mode, "m", "", modeUsage) + + selectedPagesUsage := "a comma separated list of pages or page ranges, see pdfcpu selectedpages" + flag.StringVar(&selectedPages, "pages", "", selectedPagesUsage) + flag.StringVar(&selectedPages, "p", "", selectedPagesUsage) + + permUsage := "encrypt, perm set: none|all" + flag.StringVar(&perm, "perm", "none", permUsage) + + flag.BoolVar(&quiet, "quiet", false, "") + flag.BoolVar(&quiet, "q", false, "") + + replaceUsage := "replace existing bookmarks" + flag.BoolVar(&replaceBookmarks, "replace", false, replaceUsage) + flag.BoolVar(&replaceBookmarks, "r", false, replaceUsage) + + sortUsage := "sort files before merging" + flag.BoolVar(&sorted, "sort", false, sortUsage) + flag.BoolVar(&sorted, "s", false, sortUsage) + + statsUsage := "optimize: create a csv file for stats" + flag.StringVar(&fileStats, "stats", "", statsUsage) + + unitUsage := "info: po|in|cm|mm" + flag.StringVar(&unit, "unit", "", unitUsage) + flag.StringVar(&unit, "u", "", unitUsage) + + flag.StringVar(&upw, "upw", "", "user password") + flag.StringVar(&opw, "opw", "", "owner password") + + flag.BoolVar(&verbose, "verbose", false, "") + flag.BoolVar(&verbose, "v", false, "") + flag.BoolVar(&veryVerbose, "vv", false, "") +} + +func initLogging(verbose, veryVerbose bool) { + needStackTrace = verbose || veryVerbose + if quiet { + // TODO Need separate logger for command result output. + return + } + + log.SetDefaultCLILogger() + + if verbose || veryVerbose { + log.SetDefaultDebugLogger() + log.SetDefaultInfoLogger() + log.SetDefaultStatsLogger() + } + + if veryVerbose { + log.SetDefaultTraceLogger() + //log.SetDefaultParseLogger() + log.SetDefaultReadLogger() + log.SetDefaultValidateLogger() + log.SetDefaultOptimizeLogger() + log.SetDefaultWriteLogger() + } +} diff --git a/cmd/pdfcpu/main.go b/cmd/pdfcpu/main.go new file mode 100644 index 0000000000000000000000000000000000000000..dc9e643c0303b181d8e63a55d80b751aeefeceab --- /dev/null +++ b/cmd/pdfcpu/main.go @@ -0,0 +1,67 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 main provides the command line for interacting with pdfcpu. +package main + +import ( + "fmt" + "os" +) + +var ( + fileStats, mode, selectedPages string + upw, opw, key, perm, unit, conf string + verbose, veryVerbose bool + links, quiet, sorted, bookmarks bool + all, dividerPage, json, replaceBookmarks bool + needStackTrace = true + cmdMap commandMap +) + +// Set by Goreleaser. +var ( + commit = "?" + date = "?" +) + +func init() { + initFlags() + initCommandMap() +} + +func main() { + if len(os.Args) == 1 { + fmt.Fprintln(os.Stderr, usage) + os.Exit(0) + } + + // The first argument is the pdfcpu command string. + cmdStr := os.Args[1] + + // Process command string for given configuration. + str, err := cmdMap.process(cmdStr, "") + if err != nil { + if len(str) > 0 { + cmdStr = fmt.Sprintf("%s %s", str, os.Args[2]) + } + fmt.Fprintf(os.Stderr, "%v \"%s\"\n", err, cmdStr) + fmt.Fprintln(os.Stderr, "Run 'pdfcpu help' for usage.") + os.Exit(1) + } + + os.Exit(0) +} diff --git a/cmd/pdfcpu/process.go b/cmd/pdfcpu/process.go new file mode 100644 index 0000000000000000000000000000000000000000..5bbf2f57ed70fd21f0715e1ef93328a19b171fa6 --- /dev/null +++ b/cmd/pdfcpu/process.go @@ -0,0 +1,2649 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 main + +import ( + "bytes" + "flag" + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "runtime" + "runtime/debug" + "sort" + "strconv" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/cli" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/validate" + "github.com/pkg/errors" +) + +func hasPDFExtension(filename string) bool { + return strings.HasSuffix(strings.ToLower(filename), ".pdf") +} + +func ensurePDFExtension(filename string) { + if !hasPDFExtension(filename) { + fmt.Fprintf(os.Stderr, "%s needs extension \".pdf\".\n", filename) + os.Exit(1) + } +} + +func hasJSONExtension(filename string) bool { + return strings.HasSuffix(strings.ToLower(filename), ".json") +} + +func ensureJSONExtension(filename string) { + if !hasJSONExtension(filename) { + fmt.Fprintf(os.Stderr, "%s needs extension \".json\".\n", filename) + os.Exit(1) + } +} + +func hasCSVExtension(filename string) bool { + return strings.HasSuffix(strings.ToLower(filename), ".csv") +} + +func ensureCSVExtension(filename string) { + if !hasCSVExtension(filename) { + fmt.Fprintf(os.Stderr, "%s needs extension \".csv\".\n", filename) + os.Exit(1) + } +} + +func printHelp(conf *model.Configuration) { + switch len(flag.Args()) { + + case 0: + fmt.Fprintln(os.Stderr, usage) + + case 1: + s, err := cmdMap.HelpString(flag.Arg(0)) + if err != nil { + fmt.Fprintln(os.Stderr, err) + } + fmt.Fprintln(os.Stderr, s) + + default: + fmt.Fprintln(os.Stderr, "usage: pdfcpu help command\n\nToo many arguments.") + + } +} + +func printConfiguration(conf *model.Configuration) { + fmt.Fprintf(os.Stdout, "config: %s\n", conf.Path) + f, err := os.Open(conf.Path) + if err != nil { + fmt.Fprintf(os.Stderr, "can't open %s", conf.Path) + os.Exit(1) + } + defer f.Close() + + var buf bytes.Buffer + if _, err := io.Copy(&buf, f); err != nil { + fmt.Fprintf(os.Stderr, "can't read %s", conf.Path) + os.Exit(1) + } + + fmt.Print(string(buf.String())) +} + +func printPaperSizes(conf *model.Configuration) { + fmt.Fprintln(os.Stderr, paperSizes) +} + +func printSelectedPages(conf *model.Configuration) { + fmt.Fprintln(os.Stderr, usagePageSelection) +} + +func printVersion(conf *model.Configuration) { + if len(flag.Args()) != 0 { + fmt.Fprintf(os.Stderr, "%s\n\n", usageVersion) + os.Exit(1) + } + + fmt.Fprintf(os.Stdout, "pdfcpu: %s\n", model.VersionStr) + + if date == "?" { + if info, ok := debug.ReadBuildInfo(); ok { + for _, setting := range info.Settings { + if setting.Key == "vcs.revision" { + commit = setting.Value + if len(commit) >= 8 { + commit = commit[:8] + } + } + if setting.Key == "vcs.time" { + date = setting.Value + } + } + } + } + + fmt.Fprintf(os.Stdout, "commit: %s (%s)\n", commit, date) + fmt.Fprintf(os.Stdout, "base : %s\n", runtime.Version()) + fmt.Fprintf(os.Stdout, "config: %s\n", conf.Path) +} + +func process(cmd *cli.Command) { + out, err := cli.Process(cmd) + if err != nil { + if needStackTrace { + fmt.Fprintf(os.Stderr, "Fatal: %+v\n", err) + } else { + fmt.Fprintf(os.Stderr, "%v\n", err) + } + os.Exit(1) + } + + if out != nil && !quiet { + for _, s := range out { + fmt.Fprintln(os.Stdout, s) + } + } + //os.Exit(0) +} + +func processValidateCommand(conf *model.Configuration) { + if len(flag.Args()) == 0 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "%s\n\n", usageValidate) + os.Exit(1) + } + + inFiles := []string{} + for _, arg := range flag.Args() { + if strings.Contains(arg, "*") { + matches, err := filepath.Glob(arg) + if err != nil { + fmt.Fprintf(os.Stderr, "%s", err) + os.Exit(1) + } + inFiles = append(inFiles, matches...) + continue + } + if conf.CheckFileNameExt { + ensurePDFExtension(arg) + } + inFiles = append(inFiles, arg) + } + + if mode != "" && mode != "strict" && mode != "s" && mode != "relaxed" && mode != "r" { + fmt.Fprintf(os.Stderr, "%s\n\n", usageValidate) + os.Exit(1) + } + + switch mode { + case "strict", "s": + conf.ValidationMode = model.ValidationStrict + case "relaxed", "r": + conf.ValidationMode = model.ValidationRelaxed + } + + if links { + conf.ValidateLinks = true + } + + process(cli.ValidateCommand(inFiles, conf)) +} + +func processOptimizeCommand(conf *model.Configuration) { + if len(flag.Args()) == 0 || len(flag.Args()) > 2 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "%s\n\n", usageOptimize) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + outFile := inFile + if len(flag.Args()) == 2 { + outFile = flag.Arg(1) + ensurePDFExtension(outFile) + } + + conf.StatsFileName = fileStats + if len(fileStats) > 0 { + fmt.Fprintf(os.Stdout, "stats will be appended to %s\n", fileStats) + } + + process(cli.OptimizeCommand(inFile, outFile, conf)) +} + +func processSplitByPageNumberCommand(inFile, outDir string, conf *model.Configuration) { + if len(flag.Args()) == 2 { + fmt.Fprintln(os.Stderr, "split: missing page numbers") + os.Exit(1) + } + + ii := types.IntSet{} + for i := 2; i < len(flag.Args()); i++ { + p, err := strconv.Atoi(flag.Arg(i)) + if err != nil || p < 2 { + fmt.Fprintln(os.Stderr, "split: pageNr is a numeric value >= 2") + os.Exit(1) + } + ii[p] = true + } + + pageNrs := make([]int, 0, len(ii)) + for k := range ii { + pageNrs = append(pageNrs, k) + } + sort.Ints(pageNrs) + + process(cli.SplitByPageNrCommand(inFile, outDir, pageNrs, conf)) +} + +func processSplitCommand(conf *model.Configuration) { + if mode == "" { + mode = "span" + } + mode = modeCompletion(mode, []string{"span", "bookmark", "page"}) + if mode == "" || len(flag.Args()) < 2 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "%s\n\n", usageSplit) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + outDir := flag.Arg(1) + + if mode == "page" { + processSplitByPageNumberCommand(inFile, outDir, conf) + return + } + + span := 0 + + if mode == "span" { + span = 1 + var err error + if len(flag.Args()) == 3 { + span, err = strconv.Atoi(flag.Arg(2)) + if err != nil || span < 1 { + fmt.Fprintln(os.Stderr, "split: span is a numeric value >= 1") + os.Exit(1) + } + } + } + + process(cli.SplitCommand(inFile, outDir, span, conf)) +} + +func sortFiles(inFiles []string) { + + // See PR #631 + + re := regexp.MustCompile(`\d+`) + + sort.Slice( + inFiles, + func(i, j int) bool { + ssi := re.FindAllString(inFiles[i], 1) + ssj := re.FindAllString(inFiles[j], 1) + if len(ssi) == 0 || len(ssj) == 0 { + return inFiles[i] <= inFiles[j] + } + i1, _ := strconv.Atoi(ssi[0]) + i2, _ := strconv.Atoi(ssj[0]) + return i1 < i2 + }) +} + +func processArgsForMerge(conf *model.Configuration) ([]string, string) { + inFiles := []string{} + outFile := "" + for i, arg := range flag.Args() { + if i == 0 { + ensurePDFExtension(arg) + outFile = arg + continue + } + if arg == outFile { + fmt.Fprintf(os.Stderr, "%s may appear as inFile or outFile only\n", outFile) + os.Exit(1) + } + if mode != "zip" && strings.Contains(arg, "*") { + matches, err := filepath.Glob(arg) + if err != nil { + fmt.Fprintf(os.Stderr, "%s", err) + os.Exit(1) + } + inFiles = append(inFiles, matches...) + continue + } + if conf.CheckFileNameExt { + ensurePDFExtension(arg) + } + inFiles = append(inFiles, arg) + } + return inFiles, outFile +} + +func processMergeCommand(conf *model.Configuration) { + if mode == "" { + mode = "create" + } + mode = modeCompletion(mode, []string{"create", "append", "zip"}) + if mode == "" { + fmt.Fprintf(os.Stderr, "%s\n\n", usageMerge) + os.Exit(1) + } + + if len(flag.Args()) < 2 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "%s\n\n", usageMerge) + os.Exit(1) + } + + if mode == "zip" && len(flag.Args()) != 3 { + fmt.Fprintf(os.Stderr, "merge zip: expecting outFile inFile1 inFile2\n") + os.Exit(1) + } + + if mode == "zip" && dividerPage { + fmt.Fprintf(os.Stderr, "merge zip: -d(ivider) not applicable and will be ignored\n") + } + + inFiles, outFile := processArgsForMerge(conf) + + if sorted { + sortFiles(inFiles) + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + conf.CreateBookmarks = bookmarks + } + + conf.CreateBookmarks = bookmarks + + var cmd *cli.Command + + switch mode { + + case "create": + cmd = cli.MergeCreateCommand(inFiles, outFile, dividerPage, conf) + + case "zip": + cmd = cli.MergeCreateZipCommand(inFiles, outFile, conf) + + case "append": + cmd = cli.MergeAppendCommand(inFiles, outFile, dividerPage, conf) + + } + + process(cmd) +} + +func modeCompletion(modePrefix string, modes []string) string { + var modeStr string + for _, mode := range modes { + if !strings.HasPrefix(mode, modePrefix) { + continue + } + if len(modeStr) > 0 { + return "" + } + modeStr = mode + } + return modeStr +} + +func processExtractCommand(conf *model.Configuration) { + mode = modeCompletion(mode, []string{"image", "font", "page", "content", "meta"}) + if len(flag.Args()) != 2 || mode == "" { + fmt.Fprintf(os.Stderr, "%s\n\n", usageExtract) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + outDir := flag.Arg(1) + + pages, err := api.ParsePageSelection(selectedPages) + if err != nil { + fmt.Fprintf(os.Stderr, "problem with flag selectedPages: %v\n", err) + os.Exit(1) + } + + var cmd *cli.Command + + switch mode { + + case "image": + cmd = cli.ExtractImagesCommand(inFile, outDir, pages, conf) + + case "font": + cmd = cli.ExtractFontsCommand(inFile, outDir, pages, conf) + + case "page": + cmd = cli.ExtractPagesCommand(inFile, outDir, pages, conf) + + case "content": + cmd = cli.ExtractContentCommand(inFile, outDir, pages, conf) + + case "meta": + cmd = cli.ExtractMetadataCommand(inFile, outDir, conf) + + default: + fmt.Fprintf(os.Stderr, "unknown extract mode: %s\n", mode) + os.Exit(1) + + } + + process(cmd) +} + +func processTrimCommand(conf *model.Configuration) { + if len(flag.Args()) == 0 || len(flag.Args()) > 2 || selectedPages == "" { + fmt.Fprintf(os.Stderr, "%s\n\n", usageTrim) + os.Exit(1) + } + + pages, err := api.ParsePageSelection(selectedPages) + if err != nil { + fmt.Fprintf(os.Stderr, "problem with flag selectedPages: %v\n", err) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + outFile := "" + if len(flag.Args()) == 2 { + outFile = flag.Arg(1) + ensurePDFExtension(outFile) + } + + process(cli.TrimCommand(inFile, outFile, pages, conf)) +} + +func processListAttachmentsCommand(conf *model.Configuration) { + if len(flag.Args()) != 1 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n", usageAttachList) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + process(cli.ListAttachmentsCommand(inFile, conf)) +} + +func processAddAttachmentsCommand(conf *model.Configuration) { + if len(flag.Args()) < 2 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usageAttachAdd) + os.Exit(1) + } + + var inFile string + fileNames := []string{} + + for i, arg := range flag.Args() { + if i == 0 { + inFile = arg + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + continue + } + if strings.Contains(arg, "*") { + matches, err := filepath.Glob(arg) + if err != nil { + fmt.Fprintf(os.Stderr, "%s", err) + os.Exit(1) + } + fileNames = append(fileNames, matches...) + continue + } + fileNames = append(fileNames, arg) + } + + process(cli.AddAttachmentsCommand(inFile, "", fileNames, conf)) +} + +func processAddAttachmentsPortfolioCommand(conf *model.Configuration) { + if len(flag.Args()) < 2 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usageAttachAdd) + os.Exit(1) + } + + var inFile string + fileNames := []string{} + + for i, arg := range flag.Args() { + if i == 0 { + inFile = arg + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + continue + } + if strings.Contains(arg, "*") { + matches, err := filepath.Glob(arg) + if err != nil { + fmt.Fprintf(os.Stderr, "%s", err) + os.Exit(1) + } + fileNames = append(fileNames, matches...) + continue + } + fileNames = append(fileNames, arg) + } + + process(cli.AddAttachmentsPortfolioCommand(inFile, "", fileNames, conf)) +} + +func processRemoveAttachmentsCommand(conf *model.Configuration) { + if len(flag.Args()) < 1 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usageAttachRemove) + os.Exit(1) + } + + var inFile string + fileNames := []string{} + + for i, arg := range flag.Args() { + if i == 0 { + inFile = arg + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + continue + } + fileNames = append(fileNames, arg) + } + + process(cli.RemoveAttachmentsCommand(inFile, "", fileNames, conf)) +} + +func processExtractAttachmentsCommand(conf *model.Configuration) { + if len(flag.Args()) < 2 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usageAttachExtract) + os.Exit(1) + } + + var inFile string + fileNames := []string{} + var outDir string + + for i, arg := range flag.Args() { + if i == 0 { + inFile = arg + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + continue + } + if i == 1 { + outDir = arg + continue + } + fileNames = append(fileNames, arg) + } + + process(cli.ExtractAttachmentsCommand(inFile, outDir, fileNames, conf)) +} + +func processListPermissionsCommand(conf *model.Configuration) { + if len(flag.Args()) == 0 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n", usagePermList) + os.Exit(1) + } + + inFiles := []string{} + for _, arg := range flag.Args() { + if strings.Contains(arg, "*") { + matches, err := filepath.Glob(arg) + if err != nil { + fmt.Fprintf(os.Stderr, "%s", err) + os.Exit(1) + } + inFiles = append(inFiles, matches...) + continue + } + if conf.CheckFileNameExt { + ensurePDFExtension(arg) + } + inFiles = append(inFiles, arg) + } + + process(cli.ListPermissionsCommand(inFiles, conf)) +} + +func permCompletion(permPrefix string) string { + for _, perm := range []string{"none", "print", "all"} { + if !strings.HasPrefix(perm, permPrefix) { + continue + } + return perm + } + + return permPrefix +} + +func isBinary(s string) bool { + _, err := strconv.ParseUint(s, 2, 12) + return err == nil +} + +func isHex(s string) bool { + if s[0] != 'x' { + return false + } + s = s[1:] + _, err := strconv.ParseUint(s, 16, 16) + return err == nil +} + +func configPerm(perm string, conf *model.Configuration) { + if perm != "" { + switch perm { + case "none": + conf.Permissions = model.PermissionsNone + case "print": + conf.Permissions = model.PermissionsPrint + case "all": + conf.Permissions = model.PermissionsAll + default: + var p uint64 + if perm[0] == 'x' { + p, _ = strconv.ParseUint(perm[1:], 16, 16) + } else { + p, _ = strconv.ParseUint(perm, 2, 12) + } + conf.Permissions = model.PermissionFlags(p) + } + } +} + +func processSetPermissionsCommand(conf *model.Configuration) { + if perm != "" { + perm = permCompletion(perm) + } + if len(flag.Args()) != 1 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usagePermSet) + os.Exit(1) + } + if perm != "" && perm != "none" && perm != "print" && perm != "all" && !isBinary(perm) && !isHex(perm) { + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usagePermSet) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + configPerm(perm, conf) + + process(cli.SetPermissionsCommand(inFile, "", conf)) +} + +func processDecryptCommand(conf *model.Configuration) { + if len(flag.Args()) == 0 || len(flag.Args()) > 2 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "%s\n\n", usageDecrypt) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + outFile := inFile + if len(flag.Args()) == 2 { + outFile = flag.Arg(1) + ensurePDFExtension(outFile) + } + + process(cli.DecryptCommand(inFile, outFile, conf)) +} + +func validateEncryptModeFlag() { + if !types.MemberOf(mode, []string{"rc4", "aes", ""}) { + fmt.Fprintf(os.Stderr, "%s\n\n", "valid modes: rc4,aes default:aes") + os.Exit(1) + } + + // Default to AES encryption. + if mode == "" { + mode = "aes" + } + + if key == "256" && mode == "rc4" { + key = "128" + } + + if mode == "rc4" { + if key != "40" && key != "128" && key != "" { + fmt.Fprintf(os.Stderr, "%s\n\n", "supported RC4 key lengths: 40,128 default:128") + os.Exit(1) + } + } + + if mode == "aes" { + if key != "40" && key != "128" && key != "256" && key != "" { + fmt.Fprintf(os.Stderr, "%s\n\n", "supported AES key lengths: 40,128,256 default:256") + os.Exit(1) + } + } + +} + +func validateEncryptFlags() { + validateEncryptModeFlag() + if perm != "none" && perm != "print" && perm != "all" && perm != "" { + fmt.Fprintf(os.Stderr, "%s\n\n", "supported permissions: none,print,all default:none (viewing always allowed!)") + os.Exit(1) + } +} + +func processEncryptCommand(conf *model.Configuration) { + if perm != "" { + perm = permCompletion(perm) + } + if len(flag.Args()) == 0 || len(flag.Args()) > 2 || + !(perm == "none" || perm == "print" || perm == "all") { + fmt.Fprintf(os.Stderr, "%s\n\n", usageEncrypt) + os.Exit(1) + } + + if conf.OwnerPW == "" { + fmt.Fprintln(os.Stderr, "missing non-empty owner password!") + fmt.Fprintf(os.Stderr, "%s\n\n", usageEncrypt) + os.Exit(1) + } + + validateEncryptFlags() + if perm != "" { + perm = permCompletion(perm) + } + + conf.EncryptUsingAES = mode != "rc4" + + kl, _ := strconv.Atoi(key) + conf.EncryptKeyLength = kl + + if perm == "all" { + conf.Permissions = model.PermissionsAll + } + + if perm == "print" { + conf.Permissions = model.PermissionsPrint + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + outFile := inFile + if len(flag.Args()) == 2 { + outFile = flag.Arg(1) + ensurePDFExtension(outFile) + } + + process(cli.EncryptCommand(inFile, outFile, conf)) +} + +func processChangeUserPasswordCommand(conf *model.Configuration) { + if len(flag.Args()) != 3 { + fmt.Fprintf(os.Stderr, "%s\n\n", usageChangeUserPW) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + outFile := inFile + if len(flag.Args()) == 2 { + outFile = flag.Arg(1) + ensurePDFExtension(outFile) + } + + pwOld := flag.Arg(1) + pwNew := flag.Arg(2) + + process(cli.ChangeUserPWCommand(inFile, outFile, &pwOld, &pwNew, conf)) +} + +func processChangeOwnerPasswordCommand(conf *model.Configuration) { + if len(flag.Args()) != 3 { + fmt.Fprintf(os.Stderr, "%s\n\n", usageChangeOwnerPW) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + outFile := inFile + if len(flag.Args()) == 2 { + outFile = flag.Arg(1) + ensurePDFExtension(outFile) + } + + pwOld := flag.Arg(1) + pwNew := flag.Arg(2) + if pwNew == "" { + fmt.Fprintf(os.Stderr, "owner password cannot be empty") + fmt.Fprintf(os.Stderr, "%s\n\n", usageChangeOwnerPW) + os.Exit(1) + } + + process(cli.ChangeOwnerPWCommand(inFile, outFile, &pwOld, &pwNew, conf)) +} + +func addWatermarks(conf *model.Configuration, onTop bool) { + u := usageWatermarkAdd + if onTop { + u = usageStampAdd + } + + if len(flag.Args()) < 3 || len(flag.Args()) > 4 { + fmt.Fprintf(os.Stderr, "usage: %s\n\n", u) + os.Exit(1) + } + + if mode != "text" && mode != "image" && mode != "pdf" { + fmt.Fprintln(os.Stderr, "mode has to be one of: text, image or pdf") + os.Exit(1) + } + + processDiplayUnit(conf) + + var ( + wm *model.Watermark + err error + ) + + switch mode { + case "text": + wm, err = pdfcpu.ParseTextWatermarkDetails(flag.Arg(0), flag.Arg(1), onTop, conf.Unit) + + case "image": + wm, err = pdfcpu.ParseImageWatermarkDetails(flag.Arg(0), flag.Arg(1), onTop, conf.Unit) + + case "pdf": + wm, err = pdfcpu.ParsePDFWatermarkDetails(flag.Arg(0), flag.Arg(1), onTop, conf.Unit) + default: + err = errors.Errorf("unsupported wm type: %s\n", mode) + } + + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + + selectedPages, err := api.ParsePageSelection(selectedPages) + if err != nil { + fmt.Fprintf(os.Stderr, "problem with flag selectedPages: %v", err) + os.Exit(1) + } + + inFile := flag.Arg(2) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + outFile := "" + if len(flag.Args()) == 4 { + outFile = flag.Arg(3) + ensurePDFExtension(outFile) + } + + process(cli.AddWatermarksCommand(inFile, outFile, selectedPages, wm, conf)) +} + +func processAddStampsCommand(conf *model.Configuration) { + addWatermarks(conf, true) +} + +func processAddWatermarksCommand(conf *model.Configuration) { + addWatermarks(conf, false) +} + +func updateWatermarks(conf *model.Configuration, onTop bool) { + u := usageWatermarkUpdate + if onTop { + u = usageStampUpdate + } + + if len(flag.Args()) < 3 || len(flag.Args()) > 4 { + fmt.Fprintf(os.Stderr, "%s\n\n", u) + os.Exit(1) + } + + if mode != "text" && mode != "image" && mode != "pdf" { + fmt.Fprintf(os.Stderr, "%s\n\n", u) + os.Exit(1) + } + + processDiplayUnit(conf) + + var ( + wm *model.Watermark + err error + ) + + switch mode { + case "text": + wm, err = pdfcpu.ParseTextWatermarkDetails(flag.Arg(0), flag.Arg(1), onTop, conf.Unit) + + case "image": + wm, err = pdfcpu.ParseImageWatermarkDetails(flag.Arg(0), flag.Arg(1), onTop, conf.Unit) + + case "pdf": + wm, err = pdfcpu.ParsePDFWatermarkDetails(flag.Arg(0), flag.Arg(1), onTop, conf.Unit) + default: + err = errors.Errorf("unsupported wm type: %s\n", mode) + } + + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + + selectedPages, err := api.ParsePageSelection(selectedPages) + if err != nil { + fmt.Fprintf(os.Stderr, "problem with flag selectedPages: %v", err) + os.Exit(1) + } + + wm.Update = true + + inFile := flag.Arg(2) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + outFile := "" + if len(flag.Args()) == 4 { + outFile = flag.Arg(3) + ensurePDFExtension(outFile) + } + + process(cli.AddWatermarksCommand(inFile, outFile, selectedPages, wm, conf)) +} + +func processUpdateStampsCommand(conf *model.Configuration) { + updateWatermarks(conf, true) +} + +func processUpdateWatermarksCommand(conf *model.Configuration) { + updateWatermarks(conf, false) +} + +func removeWatermarks(conf *model.Configuration, onTop bool) { + if len(flag.Args()) < 1 || len(flag.Args()) > 2 { + s := usageWatermarkRemove + if onTop { + s = usageStampRemove + } + fmt.Fprintf(os.Stderr, "%s\n\n", s) + os.Exit(1) + } + + selectedPages, err := api.ParsePageSelection(selectedPages) + if err != nil { + fmt.Fprintf(os.Stderr, "problem with flag selectedPages: %v", err) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + outFile := "" + if len(flag.Args()) == 2 { + outFile = flag.Arg(1) + ensurePDFExtension(outFile) + } + + process(cli.RemoveWatermarksCommand(inFile, outFile, selectedPages, conf)) +} + +func processRemoveStampsCommand(conf *model.Configuration) { + removeWatermarks(conf, true) +} + +func processRemoveWatermarksCommand(conf *model.Configuration) { + removeWatermarks(conf, false) +} + +func ensureImageExtension(filename string) { + if !model.ImageFileName(filename) { + fmt.Fprintf(os.Stderr, "%s needs an image extension (.jpg, .jpeg, .png, .tif, .tiff, .webp)\n", filename) + os.Exit(1) + } +} + +func parseArgsForImageFileNames(startInd int) []string { + imageFileNames := []string{} + for i := startInd; i < len(flag.Args()); i++ { + arg := flag.Arg(i) + if strings.Contains(arg, "*") { + matches, err := filepath.Glob(arg) + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + for _, fn := range matches { + ensureImageExtension(fn) + imageFileNames = append(imageFileNames, fn) + } + continue + } + ensureImageExtension(arg) + imageFileNames = append(imageFileNames, arg) + } + return imageFileNames +} + +func processImportImagesCommand(conf *model.Configuration) { + if len(flag.Args()) < 2 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "%s\n\n", usageImportImages) + os.Exit(1) + } + + processDiplayUnit(conf) + + var outFile string + outFile = flag.Arg(0) + if hasPDFExtension(outFile) { + // pdfcpu import outFile imageFile... + imp := pdfcpu.DefaultImportConfig() + imageFileNames := parseArgsForImageFileNames(1) + process(cli.ImportImagesCommand(imageFileNames, outFile, imp, conf)) + return + } + + // pdfcpu import description outFile imageFile... + imp, err := pdfcpu.ParseImportDetails(flag.Arg(0), conf.Unit) + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + if imp == nil { + fmt.Fprintf(os.Stderr, "missing import description\n") + os.Exit(1) + } + + outFile = flag.Arg(1) + ensurePDFExtension(outFile) + imageFileNames := parseArgsForImageFileNames(2) + process(cli.ImportImagesCommand(imageFileNames, outFile, imp, conf)) +} + +func processInsertPagesCommand(conf *model.Configuration) { + if len(flag.Args()) == 0 || len(flag.Args()) > 3 { + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usagePagesInsert) + os.Exit(1) + } + + pages, err := api.ParsePageSelection(selectedPages) + if err != nil { + fmt.Fprintf(os.Stderr, "problem with flag selectedPages: %v\n", err) + os.Exit(1) + } + + // Set default to insert pages before selected pages. + if mode != "" && mode != "before" && mode != "after" { + fmt.Fprintf(os.Stderr, "%s\n\n", usagePagesInsert) + os.Exit(1) + } + + inFile := flag.Arg(0) + if hasPDFExtension(inFile) { + // pdfcpu pages insert inFile [outFile] + + outFile := "" + if len(flag.Args()) == 2 { + outFile = flag.Arg(1) + ensurePDFExtension(outFile) + } + + process(cli.InsertPagesCommand(inFile, outFile, pages, conf, mode, nil)) + + return + } + + // pdfcpu pages insert description inFile [outFile] + + pageConf, err := pdfcpu.ParsePageConfiguration(flag.Arg(0), conf.Unit) + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + if pageConf == nil { + fmt.Fprintf(os.Stderr, "missing page configuration\n") + os.Exit(1) + } + + inFile = flag.Arg(1) + ensurePDFExtension(inFile) + + outFile := "" + if len(flag.Args()) == 3 { + outFile = flag.Arg(2) + ensurePDFExtension(outFile) + } + + process(cli.InsertPagesCommand(inFile, outFile, pages, conf, mode, pageConf)) +} + +func processRemovePagesCommand(conf *model.Configuration) { + if len(flag.Args()) == 0 || len(flag.Args()) > 2 || selectedPages == "" { + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usagePagesRemove) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + outFile := "" + if len(flag.Args()) == 2 { + outFile = flag.Arg(1) + ensurePDFExtension(outFile) + } + + pages, err := api.ParsePageSelection(selectedPages) + if err != nil { + fmt.Fprintf(os.Stderr, "problem with flag selectedPages: %v\n", err) + os.Exit(1) + } + if pages == nil { + fmt.Fprintf(os.Stderr, "missing page selection\n") + os.Exit(1) + } + + process(cli.RemovePagesCommand(inFile, outFile, pages, conf)) +} + +func abs(i int) int { + if i < 0 { + return -i + } + return i +} + +func processRotateCommand(conf *model.Configuration) { + if len(flag.Args()) < 2 || len(flag.Args()) > 3 { + fmt.Fprintf(os.Stderr, "%s\n\n", usageRotate) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + rotation, err := strconv.Atoi(flag.Arg(1)) + if err != nil || abs(rotation)%90 > 0 { + fmt.Fprintf(os.Stderr, "rotation must be a multiple of 90: %s\n", flag.Arg(1)) + os.Exit(1) + } + + outFile := "" + if len(flag.Args()) == 3 { + outFile = flag.Arg(2) + ensurePDFExtension(outFile) + } + + selectedPages, err := api.ParsePageSelection(selectedPages) + if err != nil { + fmt.Fprintf(os.Stderr, "problem with flag selectedPages: %v\n", err) + os.Exit(1) + } + + process(cli.RotateCommand(inFile, outFile, rotation, selectedPages, conf)) +} + +func parseForGrid(nup *model.NUp, argInd *int) { + cols, err := strconv.Atoi(flag.Arg(*argInd)) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) + } + rows, err := strconv.Atoi(flag.Arg(*argInd + 1)) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) + } + if err = pdfcpu.ParseNUpGridDefinition(cols, rows, nup); err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) + } + *argInd += 2 +} + +func parseForNUp(nup *model.NUp, argInd *int, nUpValues []int) { + n, err := strconv.Atoi(flag.Arg(*argInd)) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) + } + if !types.IntMemberOf(n, nUpValues) { + ss := make([]string, len(nUpValues)) + for i, v := range nUpValues { + ss[i] = strconv.Itoa(v) + } + err := errors.Errorf("pdfcpu: n must be one of %s", strings.Join(ss, ", ")) + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) + } + if err = pdfcpu.ParseNUpValue(n, nup); err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) + } + *argInd++ +} + +func parseAfterNUpDetails(nup *model.NUp, argInd int, nUpValues []int, filenameOut string) []string { + if nup.PageGrid { + parseForGrid(nup, &argInd) + } else { + parseForNUp(nup, &argInd, nUpValues) + } + + filenameIn := flag.Arg(argInd) + if !hasPDFExtension(filenameIn) && !model.ImageFileName(filenameIn) { + fmt.Fprintf(os.Stderr, "inFile has to be a PDF or one or a sequence of image files: %s\n", filenameIn) + os.Exit(1) + } + + filenamesIn := []string{filenameIn} + + if hasPDFExtension(filenameIn) { + if len(flag.Args()) > argInd+1 { + usage := usageNUp + if nup.PageGrid { + usage = usageGrid + } + fmt.Fprintf(os.Stderr, "%s\n\n", usage) + os.Exit(1) + } + if filenameIn == filenameOut { + fmt.Fprintln(os.Stderr, "inFile and outFile can't be the same.") + os.Exit(1) + } + } else { + nup.ImgInputFile = true + for i := argInd + 1; i < len(flag.Args()); i++ { + arg := flag.Args()[i] + ensureImageExtension(arg) + filenamesIn = append(filenamesIn, arg) + } + } + + return filenamesIn +} + +func processNUpCommand(conf *model.Configuration) { + if len(flag.Args()) < 3 { + fmt.Fprintf(os.Stderr, "%s\n\n", usageNUp) + os.Exit(1) + } + + processDiplayUnit(conf) + + pages, err := api.ParsePageSelection(selectedPages) + if err != nil { + fmt.Fprintf(os.Stderr, "problem with flag selectedPages: %v\n", err) + os.Exit(1) + } + + nup := model.DefaultNUpConfig() + nup.InpUnit = conf.Unit + argInd := 1 + + outFile := flag.Arg(0) + if !hasPDFExtension(outFile) { + // pdfcpu nup description outFile n inFile|imageFiles... + if err = pdfcpu.ParseNUpDetails(flag.Arg(0), nup); err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) + } + outFile = flag.Arg(1) + ensurePDFExtension(outFile) + argInd = 2 + } // else first argument is outFile. + + // pdfcpu nup outFile n inFile|imageFiles... + // If no optional 'description' argument provided use default nup configuration. + + inFiles := parseAfterNUpDetails(nup, argInd, pdfcpu.NUpValues, outFile) + process(cli.NUpCommand(inFiles, outFile, pages, nup, conf)) +} + +func processGridCommand(conf *model.Configuration) { + if len(flag.Args()) < 4 { + fmt.Fprintf(os.Stderr, "%s\n\n", usageGrid) + os.Exit(1) + } + + processDiplayUnit(conf) + + pages, err := api.ParsePageSelection(selectedPages) + if err != nil { + fmt.Fprintf(os.Stderr, "problem with flag selectedPages: %v\n", err) + os.Exit(1) + } + + nup := model.DefaultNUpConfig() + nup.InpUnit = conf.Unit + nup.PageGrid = true + argInd := 1 + + outFile := flag.Arg(0) + if !hasPDFExtension(outFile) { + // pdfcpu grid description outFile m n inFile|imageFiles... + if err = pdfcpu.ParseNUpDetails(flag.Arg(0), nup); err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) + } + outFile = flag.Arg(1) + ensurePDFExtension(outFile) + argInd = 2 + } // else first argument is outFile. + + // pdfcpu grid outFile m n inFile|imageFiles... + // If no optional 'description' argument provided use default nup configuration. + + inFiles := parseAfterNUpDetails(nup, argInd, nil, outFile) + process(cli.NUpCommand(inFiles, outFile, pages, nup, conf)) +} + +func processBookletCommand(conf *model.Configuration) { + if len(flag.Args()) < 3 { + fmt.Fprintf(os.Stderr, "%s\n\n", usageBooklet) + os.Exit(1) + } + + processDiplayUnit(conf) + + pages, err := api.ParsePageSelection(selectedPages) + if err != nil { + fmt.Fprintf(os.Stderr, "problem with flag selectedPages: %v\n", err) + os.Exit(1) + } + + nup := pdfcpu.DefaultBookletConfig() + nup.InpUnit = conf.Unit + argInd := 1 + + // First argument may be outFile or description. + outFile := flag.Arg(0) + if !hasPDFExtension(outFile) { + // pdfcpu booklet description outFile n inFile|imageFiles... + if err = pdfcpu.ParseNUpDetails(flag.Arg(0), nup); err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) + } + outFile = flag.Arg(1) + ensurePDFExtension(outFile) + argInd = 2 + } // else first argument is outFile. + + // pdfcpu booklet outFile n inFile|imageFiles... + // If no optional 'description' argument provided use default nup configuration. + + inFiles := parseAfterNUpDetails(nup, argInd, pdfcpu.NUpValuesForBooklets, outFile) + process(cli.BookletCommand(inFiles, outFile, pages, nup, conf)) +} + +func processDiplayUnit(conf *model.Configuration) { + if !types.MemberOf(unit, []string{"", "points", "po", "inches", "in", "cm", "mm"}) { + fmt.Fprintf(os.Stderr, "%s\n\n", "supported units: (po)ints, (in)ches, cm, mm") + os.Exit(1) + } + + switch unit { + case "points", "po": + conf.Unit = types.POINTS + case "inches", "in": + conf.Unit = types.INCHES + case "cm": + conf.Unit = types.CENTIMETRES + case "mm": + conf.Unit = types.MILLIMETRES + } +} + +func processInfoCommand(conf *model.Configuration) { + if len(flag.Args()) < 1 { + fmt.Fprintf(os.Stderr, "%s\n\n", usageInfo) + os.Exit(1) + } + + inFiles := []string{} + for _, arg := range flag.Args() { + if strings.Contains(arg, "*") { + matches, err := filepath.Glob(arg) + if err != nil { + fmt.Fprintf(os.Stderr, "%s", err) + os.Exit(1) + } + inFiles = append(inFiles, matches...) + continue + } + if conf.CheckFileNameExt { + ensurePDFExtension(arg) + } + inFiles = append(inFiles, arg) + } + + selectedPages, err := api.ParsePageSelection(selectedPages) + if err != nil { + fmt.Fprintf(os.Stderr, "problem with flag selectedPages: %v\n", err) + os.Exit(1) + } + + processDiplayUnit(conf) + + process(cli.InfoCommand(inFiles, selectedPages, json, conf)) +} + +func processListFontsCommand(conf *model.Configuration) { + process(cli.ListFontsCommand(conf)) +} + +func processInstallFontsCommand(conf *model.Configuration) { + fileNames := []string{} + if len(flag.Args()) == 0 { + fmt.Fprintf(os.Stderr, "%s\n\n", "expecting a list of TrueType filenames (.ttf, .ttc) for installation.") + os.Exit(1) + } + for _, arg := range flag.Args() { + if !types.MemberOf(filepath.Ext(arg), []string{".ttf", ".ttc"}) { + continue + } + fileNames = append(fileNames, arg) + } + if len(fileNames) == 0 { + fmt.Fprintln(os.Stderr, "Please supply a *.ttf or *.tcc fontname!") + os.Exit(1) + } + process(cli.InstallFontsCommand(fileNames, conf)) +} + +func processCreateCheatSheetFontsCommand(conf *model.Configuration) { + fileNames := []string{} + if len(flag.Args()) > 0 { + fileNames = append(fileNames, flag.Args()...) + } + process(cli.CreateCheatSheetsFontsCommand(fileNames, conf)) +} + +func processListKeywordsCommand(conf *model.Configuration) { + if len(flag.Args()) != 1 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n", usageKeywordsList) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + process(cli.ListKeywordsCommand(inFile, conf)) +} + +func processAddKeywordsCommand(conf *model.Configuration) { + if len(flag.Args()) < 2 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usageKeywordsAdd) + os.Exit(1) + } + + var inFile string + keywords := []string{} + + for i, arg := range flag.Args() { + if i == 0 { + inFile = arg + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + continue + } + keywords = append(keywords, arg) + } + + process(cli.AddKeywordsCommand(inFile, "", keywords, conf)) +} + +func processRemoveKeywordsCommand(conf *model.Configuration) { + if len(flag.Args()) < 1 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usageKeywordsRemove) + os.Exit(1) + } + + var inFile string + keywords := []string{} + + for i, arg := range flag.Args() { + if i == 0 { + inFile = arg + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + continue + } + keywords = append(keywords, arg) + } + + process(cli.RemoveKeywordsCommand(inFile, "", keywords, conf)) +} + +func processListPropertiesCommand(conf *model.Configuration) { + if len(flag.Args()) != 1 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n", usagePropertiesList) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + process(cli.ListPropertiesCommand(inFile, conf)) +} + +func processAddPropertiesCommand(conf *model.Configuration) { + if len(flag.Args()) < 2 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usagePropertiesAdd) + os.Exit(1) + } + + var inFile string + properties := map[string]string{} + + for i, arg := range flag.Args() { + if i == 0 { + inFile = arg + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + continue + } + // Ensure key value pair. + ss := strings.SplitN(arg, "=", 2) + if len(ss) != 2 { + fmt.Fprintf(os.Stderr, "keyValuePair = 'key = value'\n") + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usagePropertiesAdd) + os.Exit(1) + } + k := strings.TrimSpace(ss[0]) + if !validate.DocumentProperty(k) { + fmt.Fprintf(os.Stderr, "property name \"%s\" not allowed!\n", k) + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usagePropertiesAdd) + os.Exit(1) + } + v := strings.TrimSpace(ss[1]) + properties[k] = v + } + + process(cli.AddPropertiesCommand(inFile, "", properties, conf)) +} + +func processRemovePropertiesCommand(conf *model.Configuration) { + if len(flag.Args()) < 1 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usagePropertiesRemove) + os.Exit(1) + } + + var inFile string + keys := []string{} + + for i, arg := range flag.Args() { + if i == 0 { + inFile = arg + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + continue + } + + if !validate.DocumentProperty(arg) { + fmt.Fprintf(os.Stderr, "property name \"%s\" not allowed!\n", arg) + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usagePropertiesRemove) + os.Exit(1) + } + + keys = append(keys, arg) + } + + process(cli.RemovePropertiesCommand(inFile, "", keys, conf)) +} + +func processCollectCommand(conf *model.Configuration) { + if len(flag.Args()) < 1 || len(flag.Args()) > 2 || selectedPages == "" { + fmt.Fprintf(os.Stderr, "%s\n\n", usageCollect) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + outFile := "" + if len(flag.Args()) == 2 { + outFile = flag.Arg(1) + ensurePDFExtension(outFile) + } + + selectedPages, err := api.ParsePageSelection(selectedPages) + if err != nil { + fmt.Fprintf(os.Stderr, "problem with flag selectedPages: %v\n", err) + os.Exit(1) + } + + process(cli.CollectCommand(inFile, outFile, selectedPages, conf)) +} + +func processListBoxesCommand(conf *model.Configuration) { + if len(flag.Args()) < 1 || len(flag.Args()) > 2 { + fmt.Fprintf(os.Stderr, "usage: %s\n", usageBoxesList) + os.Exit(1) + } + + processDiplayUnit(conf) + + selectedPages, err := api.ParsePageSelection(selectedPages) + if err != nil { + fmt.Fprintf(os.Stderr, "problem with flag selectedPages: %v\n", err) + os.Exit(1) + } + + if len(flag.Args()) == 1 { + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + process(cli.ListBoxesCommand(inFile, selectedPages, nil, conf)) + return + } + + pb, err := api.PageBoundariesFromBoxList(flag.Arg(0)) + if err != nil { + fmt.Fprintf(os.Stderr, "problem parsing box list: %v\n", err) + os.Exit(1) + } + + inFile := flag.Arg(1) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + process(cli.ListBoxesCommand(inFile, selectedPages, pb, conf)) +} + +func processAddBoxesCommand(conf *model.Configuration) { + if len(flag.Args()) < 1 || len(flag.Args()) > 3 { + fmt.Fprintf(os.Stderr, "usage: %s\n", usageBoxesAdd) + os.Exit(1) + } + + processDiplayUnit(conf) + + pb, err := api.PageBoundaries(flag.Arg(0), conf.Unit) + if err != nil { + fmt.Fprintf(os.Stderr, "problem parsing page boundaries: %v\n", err) + os.Exit(1) + } + + inFile := flag.Arg(1) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + outFile := "" + if len(flag.Args()) == 3 { + outFile = flag.Arg(2) + ensurePDFExtension(outFile) + } + + selectedPages, err := api.ParsePageSelection(selectedPages) + if err != nil { + fmt.Fprintf(os.Stderr, "problem with flag selectedPages: %v\n", err) + os.Exit(1) + } + + process(cli.AddBoxesCommand(inFile, outFile, selectedPages, pb, conf)) +} + +func processRemoveBoxesCommand(conf *model.Configuration) { + if len(flag.Args()) < 1 || len(flag.Args()) > 3 { + fmt.Fprintf(os.Stderr, "usage: %s\n", usageBoxesRemove) + os.Exit(1) + } + + pb, err := api.PageBoundariesFromBoxList(flag.Arg(0)) + if err != nil { + fmt.Fprintf(os.Stderr, "problem parsing box list: %v\n", err) + os.Exit(1) + } + if pb == nil { + fmt.Fprintln(os.Stderr, "please supply a list of box types to be removed") + os.Exit(1) + } + + if pb.Media != nil { + fmt.Fprintf(os.Stderr, "cannot remove media box\n") + os.Exit(1) + } + + inFile := flag.Arg(1) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + outFile := "" + if len(flag.Args()) == 3 { + outFile = flag.Arg(2) + ensurePDFExtension(outFile) + } + + selectedPages, err := api.ParsePageSelection(selectedPages) + if err != nil { + fmt.Fprintf(os.Stderr, "problem with flag selectedPages: %v\n", err) + os.Exit(1) + } + + process(cli.RemoveBoxesCommand(inFile, outFile, selectedPages, pb, conf)) +} + +func processCropCommand(conf *model.Configuration) { + if len(flag.Args()) < 1 || len(flag.Args()) > 3 { + fmt.Fprintf(os.Stderr, "%s\n", usageCrop) + os.Exit(1) + } + + processDiplayUnit(conf) + + box, err := api.Box(flag.Arg(0), conf.Unit) + if err != nil { + fmt.Fprintf(os.Stderr, "problem parsing box definition: %v\n", err) + os.Exit(1) + } + + inFile := flag.Arg(1) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + outFile := "" + if len(flag.Args()) == 3 { + outFile = flag.Arg(2) + ensurePDFExtension(outFile) + } + + selectedPages, err := api.ParsePageSelection(selectedPages) + if err != nil { + fmt.Fprintf(os.Stderr, "problem with flag selectedPages: %v\n", err) + os.Exit(1) + } + + process(cli.CropCommand(inFile, outFile, selectedPages, box, conf)) +} + +func processListAnnotationsCommand(conf *model.Configuration) { + if len(flag.Args()) != 1 { + fmt.Fprintf(os.Stderr, "usage: %s\n", usageAnnotsList) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + selectedPages, err := api.ParsePageSelection(selectedPages) + if err != nil { + fmt.Fprintf(os.Stderr, "problem with flag selectedPages: %v\n", err) + os.Exit(1) + } + + process(cli.ListAnnotationsCommand(inFile, selectedPages, conf)) +} + +func processRemoveAnnotationsCommand(conf *model.Configuration) { + if len(flag.Args()) < 1 { + fmt.Fprintf(os.Stderr, "usage: %s\n", usageAnnotsRemove) + os.Exit(1) + } + + selectedPages, err := api.ParsePageSelection(selectedPages) + if err != nil { + fmt.Fprintf(os.Stderr, "problem with flag selectedPages: %v\n", err) + os.Exit(1) + } + + inFile, outFile := "", "" + + var ( + idsAndTypes []string + objNrs []int + ) + + for i, arg := range flag.Args() { + if i == 0 { + inFile = arg + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + continue + } + if i == 1 { + if hasPDFExtension(arg) { + outFile = arg + continue + } + } + + j, err := strconv.Atoi(arg) + if err != nil { + // strings args may be and id or annotType + idsAndTypes = append(idsAndTypes, arg) + continue + } + objNrs = append(objNrs, j) + } + + process(cli.RemoveAnnotationsCommand(inFile, outFile, selectedPages, idsAndTypes, objNrs, conf)) +} + +func processListImagesCommand(conf *model.Configuration) { + if len(flag.Args()) < 1 { + fmt.Fprintf(os.Stderr, "usage: %s\n", usageImagesList) + os.Exit(1) + } + + inFiles := []string{} + for _, arg := range flag.Args() { + if strings.Contains(arg, "*") { + matches, err := filepath.Glob(arg) + if err != nil { + fmt.Fprintf(os.Stderr, "%s", err) + os.Exit(1) + } + inFiles = append(inFiles, matches...) + continue + } + if conf.CheckFileNameExt { + ensurePDFExtension(arg) + } + inFiles = append(inFiles, arg) + } + + selectedPages, err := api.ParsePageSelection(selectedPages) + if err != nil { + fmt.Fprintf(os.Stderr, "problem with flag selectedPages: %v\n", err) + os.Exit(1) + } + + process(cli.ListImagesCommand(inFiles, selectedPages, conf)) +} + +func processDumpCommand(conf *model.Configuration) { + s := "No dump for you! - One year!\n\n" + if len(flag.Args()) != 3 { + fmt.Fprintln(os.Stderr, s) + os.Exit(1) + } + + vals := []int{0, 0} + + mode := strings.ToLower(flag.Arg(0)) + + switch mode[0] { + case 'a': + vals[0] = 1 + case 'h': + vals[0] = 2 + } + + objNr, err := strconv.Atoi(flag.Arg(1)) + if err != nil { + fmt.Fprintln(os.Stderr, s) + os.Exit(1) + } + vals[1] = objNr + + inFile := flag.Arg(2) + ensurePDFExtension(inFile) + + conf.ValidationMode = model.ValidationRelaxed + + process(cli.DumpCommand(inFile, vals, conf)) +} + +func processCreateCommand(conf *model.Configuration) { + if len(flag.Args()) <= 1 || len(flag.Args()) > 3 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "%s\n\n", usageCreate) + os.Exit(1) + } + + inFileJSON := flag.Arg(0) + ensureJSONExtension(inFileJSON) + + inFile, outFile := "", "" + if len(flag.Args()) == 2 { + outFile = flag.Arg(1) + ensurePDFExtension(outFile) + } else { + inFile = flag.Arg(1) + ensurePDFExtension(inFile) + outFile = flag.Arg(2) + ensurePDFExtension(outFile) + } + + process(cli.CreateCommand(inFile, inFileJSON, outFile, conf)) +} + +func processListFormFieldsCommand(conf *model.Configuration) { + if len(flag.Args()) < 1 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usageFormListFields) + os.Exit(1) + } + + inFiles := []string{} + for _, arg := range flag.Args() { + if strings.Contains(arg, "*") { + matches, err := filepath.Glob(arg) + if err != nil { + fmt.Fprintf(os.Stderr, "%s", err) + os.Exit(1) + } + inFiles = append(inFiles, matches...) + continue + } + if conf.CheckFileNameExt { + ensurePDFExtension(arg) + } + inFiles = append(inFiles, arg) + } + + process(cli.ListFormFieldsCommand(inFiles, conf)) +} + +func processRemoveFormFieldsCommand(conf *model.Configuration) { + if len(flag.Args()) < 2 { + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usageFormRemoveFields) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + var fieldIDs []string + outFile := inFile + + if len(flag.Args()) == 2 { + s := flag.Arg(1) + if hasPDFExtension(s) { + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usageFormRemoveFields) + os.Exit(1) + } + fieldIDs = append(fieldIDs, s) + } else { + s := flag.Arg(1) + if hasPDFExtension(s) { + outFile = s + } else { + fieldIDs = append(fieldIDs, s) + } + for i := 2; i < len(flag.Args()); i++ { + fieldIDs = append(fieldIDs, flag.Arg(i)) + } + } + + process(cli.RemoveFormFieldsCommand(inFile, outFile, fieldIDs, conf)) +} + +func processLockFormCommand(conf *model.Configuration) { + if len(flag.Args()) == 0 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usageFormLock) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + var fieldIDs []string + outFile := inFile + + if len(flag.Args()) > 1 { + s := flag.Arg(1) + if hasPDFExtension(s) { + outFile = s + } else { + fieldIDs = append(fieldIDs, s) + } + } + + if len(flag.Args()) > 2 { + for i := 2; i < len(flag.Args()); i++ { + fieldIDs = append(fieldIDs, flag.Arg(i)) + } + } + + process(cli.LockFormCommand(inFile, outFile, fieldIDs, conf)) +} + +func processUnlockFormCommand(conf *model.Configuration) { + if len(flag.Args()) == 0 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usageFormUnlock) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + var fieldIDs []string + outFile := inFile + + if len(flag.Args()) > 1 { + s := flag.Arg(1) + if hasPDFExtension(s) { + outFile = s + } else { + fieldIDs = append(fieldIDs, s) + } + } + + if len(flag.Args()) > 2 { + for i := 2; i < len(flag.Args()); i++ { + fieldIDs = append(fieldIDs, flag.Arg(i)) + } + } + + process(cli.UnlockFormCommand(inFile, outFile, fieldIDs, conf)) +} + +func processResetFormCommand(conf *model.Configuration) { + if len(flag.Args()) == 0 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usageFormReset) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + var fieldIDs []string + outFile := inFile + + if len(flag.Args()) > 1 { + s := flag.Arg(1) + if hasPDFExtension(s) { + outFile = s + } else { + fieldIDs = append(fieldIDs, s) + } + } + + if len(flag.Args()) > 2 { + for i := 2; i < len(flag.Args()); i++ { + fieldIDs = append(fieldIDs, flag.Arg(i)) + } + } + + process(cli.ResetFormCommand(inFile, outFile, fieldIDs, conf)) +} + +func processExportFormCommand(conf *model.Configuration) { + if len(flag.Args()) == 0 || len(flag.Args()) > 2 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usageFormExport) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + // TODO inFile.json + outFileJSON := "out.json" + if len(flag.Args()) == 2 { + outFileJSON = flag.Arg(1) + ensureJSONExtension(outFileJSON) + } + ensureJSONExtension(outFileJSON) + + process(cli.ExportFormCommand(inFile, outFileJSON, conf)) +} + +func processFillFormCommand(conf *model.Configuration) { + if len(flag.Args()) < 2 || len(flag.Args()) > 3 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usageFormFill) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + inFileJSON := flag.Arg(1) + ensureJSONExtension(inFileJSON) + + outFile := inFile + if len(flag.Args()) == 3 { + outFile = flag.Arg(2) + ensurePDFExtension(outFile) + } + + process(cli.FillFormCommand(inFile, inFileJSON, outFile, conf)) +} + +func processMultiFillFormCommand(conf *model.Configuration) { + if mode == "" { + mode = "single" + } + mode = modeCompletion(mode, []string{"single", "merge"}) + if mode == "" { + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usageFormMultiFill) + os.Exit(1) + } + + if len(flag.Args()) < 3 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usageFormMultiFill) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + inFileData := flag.Arg(1) + if !hasJSONExtension(inFileData) && !hasCSVExtension(inFileData) { + fmt.Fprintf(os.Stderr, "%s needs extension \".json\" or \".csv\".\n", inFileData) + os.Exit(1) + } + + outDir := flag.Arg(2) + + outFile := inFile + if len(flag.Args()) == 4 { + outFile = flag.Arg(3) + ensurePDFExtension(outFile) + } + + process(cli.MultiFillFormCommand(inFile, inFileData, outDir, outFile, mode == "merge", conf)) +} + +func processResizeCommand(conf *model.Configuration) { + if len(flag.Args()) < 2 || len(flag.Args()) > 3 { + fmt.Fprintf(os.Stderr, "%s\n", usageResize) + os.Exit(1) + } + + processDiplayUnit(conf) + + rc, err := pdfcpu.ParseResizeConfig(flag.Arg(0), conf.Unit) + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + + inFile := flag.Arg(1) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + outFile := "" + if len(flag.Args()) == 3 { + outFile = flag.Arg(2) + ensurePDFExtension(outFile) + } + + selectedPages, err := api.ParsePageSelection(selectedPages) + if err != nil { + fmt.Fprintf(os.Stderr, "problem with flag selectedPages: %v\n", err) + os.Exit(1) + } + + process(cli.ResizeCommand(inFile, outFile, selectedPages, rc, conf)) +} + +func processPosterCommand(conf *model.Configuration) { + if len(flag.Args()) < 3 || len(flag.Args()) > 4 { + fmt.Fprintf(os.Stderr, "%s\n", usagePoster) + os.Exit(1) + } + + processDiplayUnit(conf) + + // formsize(=papersize) or dimensions, optionally: scalefactor, border, margin, bgcolor + cut, err := pdfcpu.ParseCutConfigForPoster(flag.Arg(0), conf.Unit) + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + + inFile := flag.Arg(1) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + outDir := flag.Arg(2) + + selectedPages, err := api.ParsePageSelection(selectedPages) + if err != nil { + fmt.Fprintf(os.Stderr, "problem with flag selectedPages: %v\n", err) + os.Exit(1) + } + + var outFile string + if len(flag.Args()) == 4 { + outFile = flag.Arg(3) + } + + process(cli.PosterCommand(inFile, outDir, outFile, selectedPages, cut, conf)) +} + +func processNDownCommand(conf *model.Configuration) { + if len(flag.Args()) < 3 || len(flag.Args()) > 5 { + fmt.Fprintf(os.Stderr, "%s\n", usageNDown) + os.Exit(1) + } + + processDiplayUnit(conf) + + selectedPages, err := api.ParsePageSelection(selectedPages) + if err != nil { + fmt.Fprintf(os.Stderr, "problem with flag selectedPages: %v\n", err) + os.Exit(1) + } + + var inFile, outDir string + + n, err := strconv.Atoi(flag.Arg(0)) + if err == nil { + // pdfcpu ndown n inFile outDir outFile + + // Optionally: border, margin, bgcolor + cut, err := pdfcpu.ParseCutConfigForN(n, "", conf.Unit) + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + inFile = flag.Arg(1) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + outDir = flag.Arg(2) + + var outFile string + if len(flag.Args()) == 4 { + outFile = flag.Arg(3) + } + + process(cli.NDownCommand(inFile, outDir, outFile, selectedPages, n, cut, conf)) + return + } + + // pdfcpu ndown description n inFile outDir outFile + + n, err = strconv.Atoi(flag.Arg(1)) + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + + // Optionally: border, margin, bgcolor + cut, err := pdfcpu.ParseCutConfigForN(n, flag.Arg(0), conf.Unit) + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + + inFile = flag.Arg(2) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + outDir = flag.Arg(3) + + var outFile string + if len(flag.Args()) == 5 { + outFile = flag.Arg(4) + } + + process(cli.NDownCommand(inFile, outDir, outFile, selectedPages, n, cut, conf)) +} + +func processCutCommand(conf *model.Configuration) { + if len(flag.Args()) < 3 || len(flag.Args()) > 4 { + fmt.Fprintf(os.Stderr, "%s\n", usageCut) + os.Exit(1) + } + + processDiplayUnit(conf) + + // required: at least one of horizontalCut, verticalCut + // optionally: border, margin, bgcolor + cut, err := pdfcpu.ParseCutConfig(flag.Arg(0), conf.Unit) + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + + inFile := flag.Arg(1) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + outDir := flag.Arg(2) + + selectedPages, err := api.ParsePageSelection(selectedPages) + if err != nil { + fmt.Fprintf(os.Stderr, "problem with flag selectedPages: %v\n", err) + os.Exit(1) + } + + var outFile string + if len(flag.Args()) >= 4 { + outFile = flag.Arg(3) + } + + process(cli.CutCommand(inFile, outDir, outFile, selectedPages, cut, conf)) +} + +func processListBookmarksCommand(conf *model.Configuration) { + if len(flag.Args()) < 1 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usageBookmarksList) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + process(cli.ListBookmarksCommand(inFile, conf)) +} + +func processExportBookmarksCommand(conf *model.Configuration) { + if len(flag.Args()) == 0 || len(flag.Args()) > 2 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usageBookmarksExport) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + outFileJSON := "out.json" + if len(flag.Args()) == 2 { + outFileJSON = flag.Arg(1) + ensureJSONExtension(outFileJSON) + } + + process(cli.ExportBookmarksCommand(inFile, outFileJSON, conf)) +} + +func processImportBookmarksCommand(conf *model.Configuration) { + if len(flag.Args()) == 0 || len(flag.Args()) > 3 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usageBookmarksImport) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + inFileJSON := flag.Arg(1) + ensureJSONExtension(inFileJSON) + + outFile := "" + if len(flag.Args()) == 3 { + outFile = flag.Arg(2) + ensurePDFExtension(outFile) + } + + process(cli.ImportBookmarksCommand(inFile, inFileJSON, outFile, replaceBookmarks, conf)) +} + +func processRemoveBookmarksCommand(conf *model.Configuration) { + if len(flag.Args()) == 0 || len(flag.Args()) > 2 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usageBookmarksExport) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + outFile := "" + if len(flag.Args()) == 2 { + outFile = flag.Arg(1) + ensurePDFExtension(outFile) + } + + process(cli.RemoveBookmarksCommand(inFile, outFile, conf)) +} + +func processListPageLayoutCommand(conf *model.Configuration) { + if len(flag.Args()) != 1 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n", usagePageLayoutList) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + process(cli.ListPageLayoutCommand(inFile, conf)) +} + +func processSetPageLayoutCommand(conf *model.Configuration) { + if len(flag.Args()) != 2 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n", usagePageLayoutSet) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + v := flag.Arg(1) + + if !validate.DocumentPageLayout(v) { + fmt.Fprintln(os.Stderr, "invalid page layout, use one of: SinglePage, TwoColumnLeft, TwoColumnRight, TwoPageLeft, TwoPageRight") + os.Exit(1) + } + + process(cli.SetPageLayoutCommand(inFile, "", v, conf)) +} + +func processResetPageLayoutCommand(conf *model.Configuration) { + if len(flag.Args()) != 1 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n", usagePageLayoutReset) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + process(cli.ResetPageLayoutCommand(inFile, "", conf)) +} + +func processListPageModeCommand(conf *model.Configuration) { + if len(flag.Args()) != 1 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n", usagePageModeList) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + process(cli.ListPageModeCommand(inFile, conf)) +} + +func processSetPageModeCommand(conf *model.Configuration) { + if len(flag.Args()) != 2 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n", usagePageModeSet) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + v := flag.Arg(1) + + if !validate.DocumentPageMode(v) { + fmt.Fprintln(os.Stderr, "invalid page mode, use one of: UseNone, UseOutlines, UseThumbs, FullScreen, UseOC, UseAttachments") + os.Exit(1) + } + + process(cli.SetPageModeCommand(inFile, "", v, conf)) +} + +func processResetPageModeCommand(conf *model.Configuration) { + if len(flag.Args()) != 1 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n", usagePageModeReset) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + process(cli.ResetPageModeCommand(inFile, "", conf)) +} + +func processListViewerPreferencesCommand(conf *model.Configuration) { + if len(flag.Args()) != 1 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n", usageViewerPreferencesList) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + process(cli.ListViewerPreferencesCommand(inFile, all, json, conf)) +} + +func processSetViewerPreferencesCommand(conf *model.Configuration) { + if len(flag.Args()) != 2 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n", usageViewerPreferencesSet) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + inFileJSON, stringJSON := "", "" + + s := flag.Arg(1) + if hasJSONExtension(s) { + inFileJSON = s + } else { + stringJSON = s + } + + process(cli.SetViewerPreferencesCommand(inFile, inFileJSON, "", stringJSON, conf)) +} + +func processResetViewerPreferencesCommand(conf *model.Configuration) { + if len(flag.Args()) != 1 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n", usageViewerPreferencesReset) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + process(cli.ResetViewerPreferencesCommand(inFile, "", conf)) +} + +func processZoomCommand(conf *model.Configuration) { + if len(flag.Args()) < 2 || len(flag.Args()) > 3 { + fmt.Fprintf(os.Stderr, "%s\n", usageZoom) + os.Exit(1) + } + + processDiplayUnit(conf) + + zc, err := pdfcpu.ParseZoomConfig(flag.Arg(0), conf.Unit) + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + + inFile := flag.Arg(1) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + outFile := "" + if len(flag.Args()) == 3 { + outFile = flag.Arg(2) + ensurePDFExtension(outFile) + } + + selectedPages, err := api.ParsePageSelection(selectedPages) + if err != nil { + fmt.Fprintf(os.Stderr, "problem with flag selectedPages: %v\n", err) + os.Exit(1) + } + + process(cli.ZoomCommand(inFile, outFile, selectedPages, zc, conf)) +} diff --git a/cmd/pdfcpu/usage.go b/cmd/pdfcpu/usage.go new file mode 100644 index 0000000000000000000000000000000000000000..44bca14a35e9886959d31281884a4e6637c905d9 --- /dev/null +++ b/cmd/pdfcpu/usage.go @@ -0,0 +1,1677 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 main + +const ( + usage = `pdfcpu is a tool for PDF manipulation written in Go. + +Usage: + + pdfcpu command [arguments] + +The commands are: + + annotations list, remove page annotations + attachments list, add, remove, extract embedded file attachments + booklet arrange pages onto larger sheets of paper to make a booklet or zine + bookmarks list, import, export, remove bookmarks + boxes list, add, remove page boundaries for selected pages + changeopw change owner password + changeupw change user password + collect create custom sequence of selected pages + config print configuration + create create PDF content including forms via JSON + crop set cropbox for selected pages + cut custom cut pages horizontally or vertically + decrypt remove password protection + encrypt set password protection + extract extract images, fonts, content, pages or metadata + fonts install, list supported fonts, create cheat sheets + form list, remove fields, lock, unlock, reset, export, fill form via JSON or CSV + grid rearrange pages or images for enhanced browsing experience + images list images for selected pages + import import/convert images to PDF + info print file info + keywords list, add, remove keywords + merge concatenate PDFs + ndown cut selected pages into n pages symmetrically + nup rearrange pages or images for reduced number of pages + optimize optimize PDF by getting rid of redundant page resources + pagelayout list, set, reset page layout for opened document + pagemode list, set, reset page mode for opened document + pages insert, remove selected pages + paper print list of supported paper sizes + permissions list, set user access permissions + portfolio list, add, remove, extract portfolio entries with optional description + poster cut selected pages into poster by paper size or dimensions + properties list, add, remove document properties + resize scale selected pages + rotate rotate selected pages + selectedpages print definition of the -pages flag + split split up a PDF by span or bookmark + stamp add, remove, update Unicode text, image or PDF stamps for selected pages + trim create trimmed version of selected pages + validate validate PDF against PDF 32000-1:2008 (PDF 1.7) + basic PDF 2.0 validation + version print version + viewerpref list, set, reset viewer preferences for opened document + watermark add, remove, update Unicode text, image or PDF watermarks for selected pages + zoom zoom in/out of selected pages by magnification factor or corresponding margin + + All instantly recognizable command prefixes are supported eg. val for validation + One letter Unix style abbreviations supported for flags and command parameters. + +Use "pdfcpu help [command]" for more information about a command.` + + generalFlags = ` + +common flags: -v(erbose) ... turn on logging + -vv ... verbose logging + -q(uiet) ... disable output + -c(onf) ... set or disable config dir: $path|disable + -opw ... owner password + -upw ... user password + -u(nit) ... display unit: po(ints) ... points + in(ches) ... inches + cm ... centimetres + mm ... millimetres` + + usageValidate = "usage: pdfcpu validate [-m(ode) strict|relaxed] [-l(inks)] inFile..." + generalFlags + + usageLongValidate = `Check inFile for specification compliance. + + mode ... validation mode + links ... check for broken links + inFile ... input PDF file + +The validation modes are: + + strict ... validates against PDF 32000-1:2008 (PDF 1.7) and rudimentary against PDF 32000:2 (PDF 2.0) +relaxed ... (default) like strict but doesn't complain about common seen spec violations.` + + usageOptimize = "usage: pdfcpu optimize [-stats csvFile] inFile [outFile]" + generalFlags + usageLongOptimize = `Read inFile, remove redundant page resources like embedded fonts and images and write the result to outFile. + + stats ... appends a stats line to a csv file with information about the usage of root and page entries. + useful for batch optimization and debugging PDFs. + inFile ... input PDF file + outFile ... output PDF file` + + usageSplit = "usage: pdfcpu split [-m(ode) span|bookmark|page] inFile outDir [span|pageNr...]" + generalFlags + usageLongSplit = `Generate a set of PDFs for the input file in outDir according to given span value or along bookmarks or page numbers. + + mode ... split mode (defaults to span) + inFile ... input PDF file + outDir ... output directory + span ... split span in pages (default: 1) for mode "span" + pageNr ... split before a specific page number for mode "page" + +The split modes are: + + span ... Split into PDF files with span pages each (default). + span itself defaults to 1 resulting in single page PDF files. + + bookmark ... Split into PDF files representing sections defined by existing bookmarks. + Assumption: inFile contains an outline dictionary. + + page ... Split before specific page numbers. + +Eg. pdfcpu split test.pdf . (= pdfcpu split -m span test.pdf . 1) + generates: + test_1.pdf + test_2.pdf + etc. + + pdfcpu split test.pdf . 2 (= pdfcpu split -m span test.pdf . 2) + generates: + test_1-2.pdf + test_3-4.pdf + etc. + + pdfcpu split -m bookmark test.pdf . + generates: + test_bm1Title_1-4.pdf + test_bm2Title.5-7-pdf + etc. + + pdfcpu split -m page test.pdf . 2 4 10 + generates: + test_1.pdf + test_2-3.pdf + test_4-9.pdf + test_10-20.pdf` + + usageMerge = "usage: pdfcpu merge [-m(ode) create|append|zip] [ -s(ort) -b(ookmarks) -d(ivider)] outFile inFile..." + generalFlags + usageLongMerge = `Concatenate a sequence of PDFs/inFiles into outFile. + + mode ... merge mode (defaults to create) + sort ... sort inFiles by file name + bookmarks ... create bookmarks + divider ... insert blank page between merged documents + outFile ... output PDF file + inFile ... a list of PDF files subject to concatenation. + +The merge modes are: + + create ... outFile will be created and possibly overwritten (default). + + append ... if outFile does not exist, it will be created (like in default mode). + if outFile already exists, inFiles will be appended to outFile. + + zip ... zip inFile1 and inFile2 into outFile (which will be created and possibly overwritten). + +Skip bookmark creation like so: -bookmarks=false` + + usagePageSelection = `'-pages' selects pages for processing and is a comma separated list of expressions: + + Valid expressions are: + + even ... include even pages odd ... include odd pages + # ... include page # #-# ... include page range + !# ... exclude page # !#-# ... exclude page range + n# ... exclude page # n#-# ... exclude page range + + #- ... include page # - last page -# ... include first page - page # + !#- ... exclude page # - last page !-# ... exclude first page - page # + n#- ... exclude page # - last page n-# ... exclude first page - page # + + l-3- ... include last 3 pages l-3 ... include page # last-3 + -l-3 ... include all, but last 3 2-l-1 ... pages 2 up to "last-1" + + n serves as an alternative for !, since ! needs to be escaped with single quotes on the cmd line. + + e.g. -3,5,7- or 4-7,!6 or 1-,!5 or odd,n1` + + usageExtract = "usage: pdfcpu extract -m(ode) i(mage)|f(ont)|c(ontent)|p(age)|m(eta) [-p(ages) selectedPages] inFile outDir" + generalFlags + usageLongExtract = `Export inFile's images, fonts, content or pages into outDir. + + mode ... extraction mode + pages ... Please refer to "pdfcpu selectedpages" + inFile ... input PDF file + outDir ... output directory + + The extraction modes are: + + image ... extract images + font ... extract font files (supported font types: TrueType) +content ... extract raw page content + page ... extract single page PDFs + meta ... extract all metadata (page selection does not apply) + +` + + usageTrim = "usage: pdfcpu trim -p(ages) selectedPages inFile [outFile]" + generalFlags + usageLongTrim = `Generate a trimmed version of inFile for selected pages. + + pages ... Please refer to "pdfcpu selectedpages" + inFile ... input PDF file + outFile ... output PDF file + +` + + usageAttachList = "pdfcpu attachments list inFile" + usageAttachAdd = "pdfcpu attachments add inFile file..." + usageAttachRemove = "pdfcpu attachments remove inFile [file...]" + usageAttachExtract = "pdfcpu attachments extract inFile outDir [file...]" + + usageAttach = "usage: " + usageAttachList + + "\n " + usageAttachAdd + + "\n " + usageAttachRemove + + "\n " + usageAttachExtract + generalFlags + + usageLongAttach = `Manage embedded file attachments. + + inFile ... input PDF file + file ... attachment + outDir ... output directory + + Remove all attachments: pdfcpu attach remove test.pdf + ` + + usagePortfolioList = "pdfcpu portfolio list inFile" + usagePortfolioAdd = "pdfcpu portfolio add inFile file[,desc]..." + usagePortfolioRemove = "pdfcpu portfolio remove inFile [file...]" + usagePortfolioExtract = "pdfcpu portfolio extract inFile outDir [file...]" + + usagePortfolio = "usage: " + usagePortfolioList + + "\n " + usagePortfolioAdd + + "\n " + usagePortfolioRemove + + "\n " + usagePortfolioExtract + generalFlags + + usageLongPortfolio = `Manage portfolio entries. + + inFile ... input PDF file + file ... attachment + desc ... description (optional) + outDir ... output directory + + Adding attachments to portfolio: + pdfcpu portfolio add test.pdf test.mp3 test.mkv + + Adding attachments to portfolio with description: + pdfcpu portfolio add test.pdf "test.mp3, Test sound file" "test.mkv, Test video file" + ` + + usagePermList = "pdfcpu permissions list [-upw userpw] [-opw ownerpw] inFile..." + usagePermSet = "pdfcpu permissions set [-perm none|print|all|max4Hex|max12Bits] [-upw userpw] -opw ownerpw inFile" + + usagePerm = "usage: " + usagePermList + + "\n " + usagePermSet + generalFlags + + usageLongPerm = `Manage user access permissions. + + perm ... user access permissions + inFile ... input PDF file + + perm modes: + + none: 000000000000 (x000) + print: 100000000100 (x804) + all: 111100111100 (xF3C) + max4Hex: x + max. 3 hex digits + max12Bits: max. 12 binary digits + + using the permission bits: + + 1: - + 2: - + 3: Print (security handlers rev.2), draft print (security handlers >= rev.3) + 4: Modify contents by operations other than controlled by bits 6, 9, 11. + 5: Copy, extract text & graphics + 6: Add or modify annotations, fill form fields, in conjunction with bit 4 create/mod form fields. + 7: - + 8: - + 9: Fill form fields (security handlers >= rev.3) + 10: Copy, extract text & graphics (security handlers >= rev.3) (unused since PDF 2.0) + 11: Assemble document (security handlers >= rev.3) + 12: Print (security handlers >= rev.3)` + + usageEncrypt = "usage: pdfcpu encrypt [-m(ode) rc4|aes] [-key 40|128|256] [-perm none|print|all] [-upw userpw] -opw ownerpw inFile [outFile]" + generalFlags + usageLongEncrypt = `Setup password protection based on user and owner password. + + mode ... algorithm (default=aes) + key ... key length in bits (default=256) + perm ... user access permissions + inFile ... input PDF file + outFile ... output PDF file + + PDF 2.0 files have to be encrypted using aes/256.` + + usageDecrypt = "usage: pdfcpu decrypt [-upw userpw] [-opw ownerpw] inFile [outFile]" + generalFlags + usageLongDecrypt = `Remove password protection and reset permissions. + + inFile ... input PDF file + outFile ... output PDF file` + + usageChangeUserPW = "usage: pdfcpu changeupw [-opw ownerpw] inFile upwOld upwNew" + generalFlags + usageLongChangeUserPW = `Change the user password also known as the open doc password. + + opw ... owner password, required unless = "" + inFile ... input PDF file + upwOld ... old user password + upwNew ... new user password` + + usageChangeOwnerPW = "usage: pdfcpu changeopw [-upw userpw] inFile opwOld opwNew" + generalFlags + usageLongChangeOwnerPW = `Change the owner password also known as the set permissions password. + + upw ... user password, required unless = "" + inFile ... input PDF file + opwOld ... old owner password (provide user password on initial changeopw) + opwNew ... new owner password` + + usageStampMode = `There are 3 different kinds of stamps: + + 1) text based: + -mode text string + eg. pdfcpu stamp add -mode text -- "Hello gopher!" "" in.pdf out.pdf + Use the following format strings: + %p ... current page number + %P ... total pages + eg. pdfcpu stamp add -mode text -- "Page %p of %P" "scale:1.0 abs, pos:bc, rot:0" in.pdf out.pdf + + 2) image based + -mode image imageFileName + supported extensions: .jpg, .jpeg, .png, .tif, .tiff, .webp + eg. pdfcpu stamp add -mode image -- "logo.png" "" in.pdf out.pdf + + 3) PDF based + -mode pdf PDFFileName:page# + Stamp selected pages of infile with one specific page of a stamp PDF file. + Eg: pdfcpu stamp add -mode pdf -- "stamp.pdf:3" "" in.pdf out.pdf ... stamp each page of in.pdf with page 3 of stamp.pdf + + -mode pdf PDFFileName + Multistamp your file, meaning apply all pages of a stamp PDF file one by one to ascending pages of inFile. + Eg: pdfcpu stamp add -mode pdf -- "stamp.pdf" "" in.pdf out.pdf ... multistamp all pages of in.pdf with ascending pages of stamp.pdf + + -mode pdf PDFFileName:startPage#Src:startPage#Dest + Customize your multistamp by starting with startPage#Src of a stamp PDF file. + Apply repeatedly pages of the stamp file to inFile starting at startPage#Dest. + Eg: pdfcpu stamp add -mode pdf -- "stamp.pdf:2:3" "" in.pdf out.pdf ... multistamp starting with page 2 of stamp.pdf onto page 3 of in.pdf + ` + + usageWatermarkMode = `There are 3 different kinds of watermarks: + + 1) text based: + -mode text string + eg. pdfcpu watermark add -mode text -- "Hello gopher!" "" in.pdf out.pdf + Use the following format strings: + %p ... current page number + %P ... total pages + eg. pdfcpu watermark add -mode text -- "Page %p of %P" "scale:1.0 abs, pos:bc, rot:0" in.pdf out.pdf + + 2) image based + -mode image imageFileName + supported extensions: .jpg, .jpeg, .png, .tif, .tiff, .webp + eg. pdfcpu watermark add -mode image -- "logo.png" "" in.pdf out.pdf + + 3) PDF based + -mode pdf PDFFileName:page# + Watermark selected pages of infile with one specific page of a watermark PDF file. + Eg: pdfcpu watermark add -mode pdf -- "watermark.pdf:3" "" in.pdf out.pdf ... watermark each page of in.pdf with page 3 of watermark.pdf + + -mode pdf PDFFileName + Multiwatermark your file, meaning apply all pages of a watermark PDF file one by one to ascending pages of inFile. + Eg: pdfcpu watermark add -mode pdf -- "watermark.pdf" "" in.pdf out.pdf ... multiwatermark all pages of in.pdf with ascending pages of watermark.pdf + + -mode pdf PDFFileName:startPage#Src:startPage#Dest + Customize your multiwatermark by starting with startPage#Src of a watermark PDF file. + Apply repeatedly pages of the watermark file to inFile starting at startPage#Dest. + Eg: pdfcpu watermark add -mode pdf -- "watermark.pdf:2:3" "" in.pdf out.pdf ... multiwatermark starting with page 2 of watermark.pdf onto page 3 of in.pdf + + A watermark is the first content that gets rendered for a page. + The visibility of the watermark depends on the transparency of all layers rendered on top. +` + usageWMDescription = ` + + is a comma separated configuration string containing these optional entries: + + (defaults: "font:Helvetica, points:24, rtl:off, pos:c, off:0,0 scale:0.5 rel, rot:0, d:1, op:1, m:0 and for all colors: 0.5 0.5 0.5") + + fontname: Please refer to "pdfcpu fonts list" + + scriptname: to avoid embedding of big font files + + ISO-15924 code CID System Info + Hans UniGB-UTF16-H / GB1 + Hant UniCNS-UTF16-H / CNS1 + Hira, Kana, Jpan UniJIS-UTF16-H / Japan1 + Hang, Kore UniKS-UTF16-H / KR + + points: fontsize in points, in combination with absolute scaling only. + + rtl: render right to left (on/off, true/false, t/f) + + position: one of the anchors: + + tl|top-left tc|top-center tr|top-right + l|left c|center r|right + bl|bottom-left bc|bottom-center br|bottom-right + + offset: (dx dy) in given display unit eg. '15 20' + + scalefactor: 0.0 < i <= 1.0 {r|rel} | 0.0 < i {a|abs} + + aligntext: l|left, c|center, r|right, j|justified (for text watermarks only) + + fillcolor: color value to be used when rendering text, see also rendermode + for backwards compatibility "color" is also accepted. + + strokecolor: color value to be used when rendering text, see also rendermode + + backgroundcolor: color value for visualization of the bounding box background for text. + "bgcolor" is also accepted. + + rotation: -180.0 <= x <= 180.0 + + diagonal: render along diagonal + 1..lower left to upper right + 2..upper left to lower right (if present overrules r!) + Only one of rotation and diagonal is allowed! + + opacity: where 0.0 <= x <= 1.0 + + mode, rendermode: 0 ... fill (applies fill color) + 1 ... stroke (applies stroke color) + 2 ... fill & stroke (applies both fill and stroke colors) + + margins: Set bounding box margins for text (requires background color) i >= 0 + i ... set all four margins + i j ... set top/bottom margins to i + set left/right margins to j + i j k ... set top margin to i + set left/right margins to j + set bottom margins to k + i j k l ... set top, right, bottom, left margins + + border: Set bounding box border for text (requires background color) + i {color} {round} + i ... border width > 0 + color ... border color + round ... set round bounding box corners + + url: Add link annotation for stamps only (omit https://) + +A color value: 3 color intensities, where 0.0 < i < 1.0, eg 1.0, + or the hex RGB value: #RRGGBB, eg #FF0000 = red + +All configuration string parameters support completion. + +e.g. "pos:bl, off: 20 5" "rot:45" "op:0.5, scale:0.5 abs, rot:0" + "d:2" "scale:.75 abs, points:48" "rot:-90, scale:0.75 rel" + "f:Courier, scale:0.75, str: 0.5 0.0 0.0, rot:20" + + +` + + usageStampAdd = "pdfcpu stamp add [-p(ages) selectedPages] -m(ode) text|image|pdf -- string|file description inFile [outFile]" + usageStampUpdate = "pdfcpu stamp update [-p(ages) selectedPages] -m(ode) text|image|pdf -- string|file description inFile [outFile]" + usageStampRemove = "pdfcpu stamp remove [-p(ages) selectedPages] inFile [outFile]" + + usageStamp = "usage: " + usageStampAdd + + "\n " + usageStampUpdate + + "\n " + usageStampRemove + generalFlags + + usageLongStamp = `Process stamping for selected pages. + + pages ... Please refer to "pdfcpu selectedpages" + upw ... user password + opw ... owner password + mode ... text, image, PDF + string ... display string for text based watermarks + file ... image or PDF file +description ... fontname, points, position, offset, scalefactor, aligntext, rotation, + diagonal, opacity, rendermode, strokecolor, fillcolor, bgcolor, margins, border + inFile ... input PDF file + outFile ... output PDF file + +` + usageStampMode + usageWMDescription + + usageWatermarkAdd = "pdfcpu watermark add [-p(ages) selectedPages] -m(ode) text|image|pdf -- string|file description inFile [outFile]" + usageWatermarkUpdate = "pdfcpu watermark update [-p(ages) selectedPages] -m(ode) text|image|pdf -- string|file description inFile [outFile]" + usageWatermarkRemove = "pdfcpu watermark remove [-p(ages) selectedPages] inFile [outFile]" + + usageWatermark = "usage: " + usageWatermarkAdd + + "\n " + usageWatermarkUpdate + + "\n " + usageWatermarkRemove + generalFlags + + usageLongWatermark = `Process watermarking for selected pages. + + pages ... Please refer to "pdfcpu selectedpages" + mode ... text, image, PDF + string ... display string for text based watermarks + file ... image or PDF file +description ... fontname, points, position, offset, scalefactor, aligntext, rotation, + diagonal, opacity, rendermode, strokecolor, fillcolor, bgcolor, margins, border + inFile ... input PDF file + outFile ... output PDF file + +` + usageWatermarkMode + usageWMDescription + + usageImportImages = "usage: pdfcpu import -- [description] outFile imageFile..." + generalFlags + usageLongImportImages = `Turn image files into a PDF page sequence and write the result to outFile. +If outFile already exists the page sequence will be appended. +Each imageFile will be rendered to a separate page. +In its simplest form this converts an image into a PDF: "pdfcpu import img.pdf img.jpg" + +description ... dimensions, formsize, position, offset, scale factor, boxes + outFile ... output PDF file + imageFile ... a list of image files + + is a comma separated configuration string containing: + + optional entries: + + (defaults: "dim:595 842, f:A4, pos:full, off:0 0, sc:0.5 rel, dpi:72, gray:off, sepia:off") + + dimensions: (width height) in given display unit eg. '400 200' setting the media box + + formsize: eg. A4, Letter, Legal... + Append 'L' to enforce landscape mode. (eg. A3L) + Append 'P' to enforce portrait mode. (eg. TabloidP) + Please refer to "pdfcpu paper" for a comprehensive list of defined paper sizes. + "papersize" is also accepted. + + position: one of 'full' or the anchors: + + tl|top-left tc|top-center tr|top-right + l|left c|center r|right + bl|bottom-left bc|bottom-center br|bottom-right + + offset: (dx dy) in given display unit eg. '15 20' + + scalefactor: 0.0 <= x <= 1.0 followed by optional 'abs|rel' or 'a|r' + + dpi: apply desired dpi + + gray: Convert to grayscale (on/off, true/false, t/f) + + sepia: Apply sepia effect (on/off, true/false, t/f) + + backgroundcolor: "bgcolor" is also accepted. + + Only one of dimensions or formsize is allowed. + position: full => image dimensions equal page dimensions. + + All configuration string parameters support completion. + + e.g. "f:A5, pos:c" ... render the image centered on A5 with relative scaling 0.5.' + "dim:300 600, pos:bl, off:20 20, sc:1.0 abs" ... render the image anchored to bottom left corner with offset 20,20 and abs. scaling 1.0. + "pos:full" ... render the image to a page with corresponding dimensions. + "f:A4, pos:c, dpi:300" ... render the image centered on A4 respecting a destination resolution of 300 dpi.` + + usagePagesInsert = "pdfcpu pages insert [-p(ages) selectedPages] [-m(ode) before|after] [description] inFile [outFile]" + usagePagesRemove = "pdfcpu pages remove -p(ages) selectedPages inFile [outFile]" + usagePages = "usage: " + usagePagesInsert + + "\n " + usagePagesRemove + generalFlags + + usageLongPages = `Manage pages. + + pages ... Please refer to "pdfcpu selectedpages" + mode ... before, after (default: before) +description ... dimensions, formsize + inFile ... input PDF file + outFile ... output PDF file + + is a comma separated configuration string containing: + + optional entries: + + (defaults: "dim:595 842, f:A4") + + dimensions: (width height) in given display unit eg. '400 200' setting the media box + + formsize: eg. A4, Letter, Legal... + Append 'L' to enforce landscape mode. (eg. A3L) + Append 'P' to enforce portrait mode. (eg. TabloidP) + Please refer to "pdfcpu paper" for a comprehensive list of defined paper sizes. + "papersize" is also accepted. + + All configuration string parameters support completion. + + Examples: pdfcpu pages insert in.pdf + Insert one blank page before each page using the form size imposed internally by the current media box. + + pdfcpu pages insert -pages 3 "f:A5L" in.pdf + Insert one blank A5 page in landscape mode before page 3. + + pdfcpu pages insert "dim: 10 5" -u cm in.pdf + Insert one blank 10 x 5 cm separator page for all pages. + + pdfcpu pages remove -p odd in.pdf out.pdf + pdfcpu pages remove -pages=odd in.pdf out.pdf + Remove all odd pages. +` + + usageRotate = "usage: pdfcpu rotate [-p(ages) selectedPages] inFile rotation [outFile]" + generalFlags + usageLongRotate = `Rotate selected pages by a multiple of 90 degrees. + + pages ... Please refer to "pdfcpu selectedpages" + inFile ... input PDF file + rotation ... a multiple of 90 degrees for clockwise rotation + outFile ... output PDF file + +` + + usageNUp = "usage: pdfcpu nup [-p(ages) selectedPages] -- [description] outFile n inFile|imageFiles..." + generalFlags + usageLongNUp = `Rearrange existing PDF pages or images into a sequence of page grids. +This reduces the number of pages and therefore the required print time. +If the input is one imageFile a single page n-up PDF gets generated. + + pages ... inFile only, please refer to "pdfcpu selectedpages" +description ... dimensions, formsize, orientation + outFile ... output PDF file + n ... the n-Up value (see below for details) + inFile ... input PDF file + imageFiles ... input image file(s) + + portrait landscape + Supported values for n: 2 ... 1x2 2x1 + 3 ... 1x3 3x1 + 4 ... 2x2 + 8 ... 2x4 4x2 + 9 ... 3x3 + 12 ... 3x4 4x3 + 16 ... 4x4 + + is a comma separated configuration string containing: + + optional entries: + + (defaults: "di:595 842, form:A4, or:rd, bo:on, ma:3, enforce:on") + + dimensions: (width,height) in given display unit eg. '400 200' + + formsize: The output sheet size, eg. A4, Letter, Legal... + Append 'L' to enforce landscape mode. (eg. A3L) + Append 'P' to enforce portrait mode. (eg. TabloidP) + Only one of dimensions or formsize is allowed. + Please refer to "pdfcpu paper" for a comprehensive list of defined paper sizes. + "papersize" is also accepted. + + orientation: one of rd ... right down (=default) + dr ... down right + ld ... left down + dl ... down left + Orientation applies to PDF input files only. + + enforce: enforce best-fit orientation of individual content (on/off, true/false, t/f). + + border: Print border (on/off, true/false, t/f) + + margin: for n-up content: float >= 0 in given display unit + + backgroundcolor: backgound color for margin > 0. + "bgcolor" is also accepted. + +All configuration string parameters support completion. + +Examples: pdfcpu nup out.pdf 4 in.pdf + Rearrange pages of in.pdf into 2x2 grids and write result to out.pdf using the default orientation + and default paper size A4. in.pdf's page size will be preserved. + + pdfcpu nup -pages=3- -- out.pdf 6 in.pdf + Rearrange selected pages of in.pdf (all pages starting with page 3) into 3x2 grids and + write result to out.pdf using the default orientation and default paper size A4. + in.pdf's page size will be preserved. + + pdfcpu nup out.pdf 9 logo.jpg + Arrange instances of logo.jpg into a 3x3 grid and write result to out.pdf using the A4 default form size. + + pdfcpu nup -- "form:Tabloid" out.pdf 4 *.jpg + Rearrange all jpg files into 2x2 grids and write result to out.pdf using the Tabloid form size + and the default orientation. + +` + + usageBooklet = "usage: pdfcpu booklet [-p(ages) selectedPages] -- [description] outFile n inFile|imageFiles..." + generalFlags + usageLongBooklet = `Arrange a sequence of pages onto larger sheets of paper for a small book or zine. + + pages ... for inFile only, please refer to "pdfcpu selectedpages" + description ... dimensions, formsize, border, margin + outFile ... output PDF file + n ... booklet style (2, 4, 6, 8) + inFile ... input PDF file + imageFiles ... input image file(s) + +There are several styles of booklet, depending on your page/input and sheet/output size, +the edge along which your booklet will be bound, +and your preferred method for creating the booklet. + +For assembly instructions for each type, see: https://pdfcpu.io/generate/booklet + +n=2: This is the simplest case and the most common for those printing at home. +Two of your pages fit on one side of a sheet (eg statement on letter, A5 on A4) +Assemble by printing on both sides (odd pages on the front and even pages on the back) and folding down the middle. + +n=4: Four of your pages fit on one side of a sheet (eg statement on ledger, A5 on A3, A6 on A4). + +When printing 4-up, your booklet can be bound either along the long-edge (for portrait this is the left side of the paper, for landscape the top) +or the short-edge (for portrait this is the top of the paper, for landscape the left side). +Using a different binding will change the ordering of the pages on the sheet. +You can set long or short-edge with the 'binding' option. + +In 4-up printing, the sets of pages on the bottom of the sheet are rotated so that the cut side of the +paper is on the bottom of the booklet for every page (for the default portrait, long-edge binding case. +Similar rotation logic applies for the other three orientations). +Having the cut edge always on bottom makes for more uniform pages within the book and less work in trimming. + +The btype=advanced is a special method for assembling, only for 4-up booklets. +Printers that are used to collating first and then cutting may prefer this method. + +n=6: Six of your pages fit on one side of a sheet. This produces an unusual sized booklet. + +Only available for portrait, long-edge orientation. + +n=8: Eight of your pages fit on one side of a sheet (eg A6 on A3). + +Only available for portrait, long-edge orientation. + +Perfect binding is a special type of booklet. The main difference is that the binding is glued into a spine, +meaning that the pages are cut along the binding and not folded as in the other forms of booklet. +This results in a different page ordering on the sheet than the other methods. If you intend to perfect bind your booklet, +use btype=perfectbound. + +There is also an option to use signatures, a bookbinding method useful for books with higher page counts. +In this method of binding, you arrange your folios (sheets folded in half) in groups of 'foliosize'. +Each group is called a signature. You then stack the signatures together to form the book. +For example, you can bind your paper in groups of eight sheets (foliosize=8), so that each signature containing 32 pages of your book. +For such a multi folio booklet set 'multifolio:on' and 'foliosize', which defaults to 8. +The last signature may be shorter, e.g. for a booklet of 120 pages with signature size=16 (foliosize=4) will have 7 complete signatures and a final signature of only 8 pages. + + + portrait landscape + Possible values for n: 2 ... 1x2 -- + 4 ... 2x2 2x2 + 6 ... 2x3 -- + 8 ... 2x4 -- + + is a comma separated configuration string containing these optional entries: + + (defaults: "dim:595 842, formsize:A4, btype: booklet, binding: long, multifolio: false, border:off, guides:off, margin:0") + + dimensions: (width,height) of the output sheet in given display unit eg. '400 200' + formsize: The output sheet size, eg. A4, Letter, Legal... + Append 'L' to enforce landscape mode. (eg. A3L) + Append 'P' to enforce portrait mode. (eg. TabloidP) + Only one of dimensions or formsize is allowed. + Please refer to "pdfcpu paper" for a comprehensive list of defined paper sizes. + "papersize" is also accepted. + btype: The method for arranging pages into a booklet. (booklet, bookletadvanced, perfectbound) + binding: The edge of the paper which has the binding. (long, short) + multifolio: Generate multi folio booklet (on/off, true/false, t/f) for n=2 and PDF input only. + foliosize: folio size for multi folio booklets only (default:8) + border: Print border (on/off, true/false, t/f) + guides: Print folding and cutting lines (on/off, true/false, t/f) + margin: Apply content margin (float >= 0 in given display unit) + backgroundcolor: sheet backgound color for margin > 0. + "bgcolor" is also accepted. + +All configuration string parameters support completion. + +Examples: + + pdfcpu booklet -- "formsize:Letter" out.pdf 2 in.pdf + Arrange pages of in.pdf 2 per sheet side (4 per sheet, back and front) onto out.pdf + + pdfcpu booklet -- "formsize:Ledger" out.pdf 4 in.pdf + Arrange pages of in.pdf 4 per sheet side (8 per sheet, back and front) onto out.pdf + + pdfcpu booklet -- "formsize:Ledger" out.pdf 6 in.pdf + Arrange pages of in.pdf 6 per sheet side (12 per sheet, back and front) onto out.pdf + + pdfcpu booklet -- "formsize:A3" out.pdf 8 in.pdf + Arrange pages of in.pdf 8 per sheet side (16 per sheet, back and front) onto out.pdf + + pdfcpu booklet -- "formsize:A3, binding:short" out.pdf 4 in.pdf + Arrange pages of in.pdf 4 per sheet side, with short-edge binding onto out.pdf + + pdfcpu booklet -- "formsize:A4, multifolio:on" hardbackbook.pdf 2 in.pdf + Arrange pages of in.pdf 2 per sheetside as sequence of folios covering 4*foliosize pages each. + See also: https://www.instructables.com/How-to-bind-your-own-Hardback-Book/ + + pdfcpu booklet -- "formsize:A4, btype:perfectbound" out.pdf 2 in.pdf + Arrange pages of in.pdf 2 per sheet side, arranged for perfect binding, onto out.pdf + + pdfcpu booklet -- "formsize:A3, btype:bookletadvanced" out.pdf 4 in.pdf + Arrange pages of in.pdf 4 per sheet side, arranged for advanced binding, onto out.pdf +` + + usageGrid = "usage: pdfcpu grid [-p(ages) selectedPages] -- [description] outFile m n inFile|imageFiles..." + generalFlags + usageLongGrid = `Rearrange PDF pages or images for enhanced browsing experience. +For a PDF inputfile each output page represents a grid of input pages. +For image inputfiles each output page shows all images laid out onto grids of given paper size. +This command produces poster like PDF pages convenient for page and image browsing. + + pages ... Please refer to "pdfcpu selectedpages" +description ... dimensions, formsize, orientation, enforce + outFile ... output PDF file + m ... grid lines + n ... grid columns + inFile ... input PDF file + imageFiles ... input image file(s) + + is a comma separated configuration string containing: + + optional entries: + + (defaults: "d:595 842, form:A4, o:rd, bo:on, ma:3, enforce:on") + + dimensions: (width height) in given display unit eg. '400 200' + + formsize: The output sheet size, eg. A4, Letter, Legal... + Append 'L' to enforce landscape mode. (eg. A3L) + Append 'P' to enforce portrait mode. (eg. TabloidP) + Only one of dimensions or formsize is allowed. + Please refer to "pdfcpu paper" for a comprehensive list of defined paper sizes. + "papersize" is also accepted. + + orientation: one of rd ... right down (=default) + dr ... down right + ld ... left down + dl ... down left + Orientation applies to PDF input files only. + + enforce: enforce best-fit orientation of individual content (on/off, true/false, t/f). + + border: Print border (on/off, true/false, t/f) + + margin: Apply content margin (float >= 0 in given display unit) + +All configuration string parameters support completion. + +Examples: pdfcpu grid out.pdf 1 10 in.pdf + Rearrange pages of in.pdf into 1x10 grids and write result to out.pdf using the default orientation. + The output page size is the result of a 1(vert)x10(hor) page grid using in.pdf's page size. + + pdfcpu grid -- "p:LegalL" out.pdf 2 2 in.pdf + Rearrange pages of in.pdf into 2x2 grids and write result to out.pdf using the default orientation. + The output page size is the result of a 2(vert)x2(hor) page grid using page size Legal in landscape mode. + + pdfcpu grid -- "o:rd" out.pdf 3 2 in.pdf + Rearrange pages of in.pdf into 3x2 grids and write result to out.pdf using orientation 'right down'. + The output page size is the result of a 3(vert)x2(hor) page grid using in.pdf's page size. + + pdfcpu grid -- "d:400 400" out.pdf 8 6 *.jpg + Arrange imagefiles onto a 8x6 page grid and write result to out.pdf using a grid cell size of 400x400. + +` + + paperSizes = `This is a list of predefined paper sizes: + + ISO 216:1975 A: + 4A0, 2A0, A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10 + + ISO 216:1975 B: + B0+, B0, B1+, B1, B2+, B2, B3, B4, B5, B6, B7, B8, B9, B10 + + ISO 269:1985 C: + C0, C1, C2, C3, C4, C5, C6, C7, C8, C9, C10 + + ISO 217:2013 untrimmed: + RA0, RA1, RA2, RA3, RA4, SRA0, SRA1, SRA2, SRA3, SRA4, SRA1+, SRA2+, SRA3+, SRA3++ + + American: + SuperB(=B+), + Tabloid (=ANSIB, DobleCarta), Ledger(=ANSIB, DobleCarta), + Legal, GovLegal(=Oficio, Folio), + Letter (=ANSIA, Carta, AmericanQuarto), GovLetter, Executive, + HalfLetter (=Memo, Statement, Stationary), + JuniorLegal (=IndexCard), + Photo + + ANSI/ASME Y14.1: + ANSIA (=Letter, Carta, AmericanQuarto), + ANSIB (=Ledger, Tabloid, DobleCarta), + ANSIC, ANSID, ANSIE, ANSIF + + ANSI/ASME Y14.1 Architectural series: + ARCHA (=ARCH1), + ARCHB (=ARCH2, ExtraTabloide), + ARCHC (=ARCH3), + ARCHD (=ARCH4), + ARCHE (=ARCH6), + ARCHE1 (=ARCH5), + ARCHE2, + ARCHE3 + + American uncut: + Bond, Book, Cover, Index, NewsPrint (=Tissue), Offset (=Text) + + English uncut: + Crown, DoubleCrown, Quad, Demy, DoubleDemy, Medium, Royal, SuperRoyal, + DoublePott, DoublePost, Foolscap, DoubleFoolscap + + F4 + + China GB/T 148-1997 D Series: + D0, D1, D2, D3, D4, D5, D6, + RD0, RD1, RD2, RD3, RD4, RD5, RD6 + + Japan: + + B-series variant: + JIS-B0, JIS-B1, JIS-B2, JIS-B3, JIS-B4, JIS-B5, JIS-B6, + JIS-B7, JIS-B8, JIS-B9, JIS-B10, JIS-B11, JIS-B12 + + Shirokuban4, Shirokuban5, Shirokuban6 + Kiku4, Kiku5 + AB, B40, Shikisen` + + usageVersion = "usage: pdfcpu version" + usageLongVersion = "Print the pdfcpu version & build info." + + usagePaper = "usage: pdfcpu paper" + usageLongPaper = "Print a list of supported paper sizes." + + usageConfig = "usage: pdfcpu config" + usageLongConfig = "Print configuration." + + usageSelectedPages = "usage: pdfcpu selectedpages" + usageLongSelectedPages = "Print definition of the -pages flag." + + usageInfo = "usage: pdfcpu info [-p(ages) selectedPages] [-j(son)] inFile..." + generalFlags + usageLongInfo = `Print info about a PDF file. + + pages ... Please refer to "pdfcpu selectedpages" + json ... output JSON + inFile ... a list of PDF input files` + + usageFontsList = "pdfcpu fonts list" + usageFontsInstall = "pdfcpu fonts install fontFiles..." + usageFontsCheatSheet = "pdfcpu fonts cheatsheet fontFiles..." + + usageFonts = "usage: " + usageFontsList + + "\n " + usageFontsInstall + + "\n " + usageFontsCheatSheet + usageLongFonts = `Print a list of supported fonts (includes the 14 PDF core fonts). +Install given True Type fonts(.ttf) or True Type collections(.ttc) for usage in stamps/watermarks. +Create single page PDF cheat sheets in current dir.` + + usageKeywordsList = "pdfcpu keywords list inFile" + usageKeywordsAdd = "pdfcpu keywords add inFile keyword..." + usageKeywordsRemove = "pdfcpu keywords remove inFile [keyword...]" + + usageKeywords = "usage: " + usageKeywordsList + + "\n " + usageKeywordsAdd + + "\n " + usageKeywordsRemove + generalFlags + + usageLongKeywords = `Manage keywords. + + inFile ... input PDF file + keyword ... search keyword + + Eg. adding two keywords: + pdfcpu keywords add test.pdf music 'virtual instruments' + + remove all keywords: + pdfcpu keywords remove test.pdf + ` + + usagePropertiesList = "pdfcpu properties list inFile" + usagePropertiesAdd = "pdfcpu properties add inFile nameValuePair..." + usagePropertiesRemove = "pdfcpu properties remove inFile [name...]" + + usageProperties = "usage: " + usagePropertiesList + + "\n " + usagePropertiesAdd + + "\n " + usagePropertiesRemove + generalFlags + + usageLongProperties = `Manage document properties. + + inFile ... input PDF file +nameValuePair ... 'name = value' + name ... property name + + Eg. adding one property: pdfcpu properties add test.pdf 'key = value' + adding two properties: pdfcpu properties add test.pdf 'key1 = val1' 'key2 = val2' + + remove all properties: pdfcpu properties remove test.pdf + ` + usageCollect = "usage: pdfcpu collect -p(ages) selectedPages inFile [outFile]" + generalFlags + usageLongCollect = `Create custom sequence of selected pages. + + pages ... Please refer to "pdfcpu selectedpages" + inFile ... input PDF file + outFile ... output PDF file + + ` + + usageBoxDescription = ` +box: + + A rectangular region in user space describing one of: + + media box: boundaries of the physical medium on which the page is to be printed. + crop box: region to which the contents of the page shall be clipped (cropped) when displayed or printed. + bleed box: region to which the contents of the page shall be clipped when output in a production environment. + trim box: intended dimensions of the finished page after trimming. + art box: extent of the page’s meaningful content as intended by the page’s creator. + + Please refer to the PDF Specification 14.11.2 Page Boundaries for details. + + All values are in given display unit (po, in, mm, cm) + + General rules: + The media box is mandatory and serves as default for the crop box and is its parent box. + The crop box serves as default for art box, bleed box and trim box and is their parent box. + + Arbitrary rectangular region in user space: + [0 10 200 150] lower left corner at (0/10), upper right corner at (200/150) + or xmin:0 ymin:10 xmax:200 ymax:150 + + Expressed as margins within parent box: + "0.5 0.5 20 20" absolute, top:.5 right:.5 bottom:20 left:20 + "0.5 0.5 .1 .1 abs" absolute, top:.5 right:.5 bottom:.1 left:.1 + "0.5 0.5 .1 .1 rel" relative, top:.5 right:.5 bottom:20 left:20 + "10" absolute, top,right,bottom,left:10 + "10 5" absolute, top,bottom:10 left,right:5 + "10 5 15" absolute, top:10 left,right:5 bottom:15 + "5%" relative, top,right,bottom,left:5% of parent box width/height + ".1 .5" absolute, top,bottom:.1 left,right:.5 + ".1 .3 rel" relative, top,bottom:.1=10% left,right:.3=30% + "-10" absolute, top,right,bottom,left:-10 relative to parent box (for crop box the media box gets expanded) + + Anchored within parent box, use dim and optionally pos, off: + "dim: 200 300 abs" centered, 200x300 display units + "pos:c, off:0 0, dim: 200 300 abs" centered, 200x300 display units + "pos:tl, off:5 5, dim: 50% 50% rel" anchored to top left corner, 50% width/height of parent box, offset by 5/5 display units + "pos:br, off:-5 -5, dim: .5 .5 rel" anchored to bottom right corner, 50% width/height of parent box, offset by -5/-5 display units + + +` + + usageCrop = "usage: pdfcpu crop [-p(ages) selectedPages] -- description inFile [outFile]" + generalFlags + usageLongCrop = `Set crop box for selected pages. + + pages ... Please refer to "pdfcpu selectedpages" + description ... crop box definition abs. or rel. to media box + inFile ... input PDF file + outFile ... output PDF file + +Examples: + pdfcpu crop -- "[0 0 500 500]" in.pdf ... crop a 500x500 points region located in lower left corner + pdfcpu crop -u mm -- "20" in.pdf ... crop relative to media box using a 20mm margin + +` + usageBoxDescription + + usageBoxesList = "pdfcpu boxes list [-p(ages) selectedPages] -- [boxTypes] inFile" + usageBoxesAdd = "pdfcpu boxes add [-p(ages) selectedPages] -- description inFile [outFile]" + usageBoxesRemove = "pdfcpu boxes remove [-p(ages) selectedPages] -- boxTypes inFile [outFile]" + + usageBoxes = "usage: " + usageBoxesList + + "\n " + usageBoxesAdd + + "\n " + usageBoxesRemove + generalFlags + + usageLongBoxes = `Manage page boundaries. + + boxTypes ... comma separated list of box types: m(edia), c(rop), t(rim), b(leed), a(rt) + pages ... Please refer to "pdfcpu selectedpages" + description ... box definitions abs. or rel. to parent box + inFile ... input PDF file + outFile ... output PDF file + + is a sequence of box definitions and assignments: + + m(edia): {box} + c(rop): {box} + a(rt): {box} | m(edia) | c(rop) | b(leed) | t(rim) + b(leed): {box} | m(edia) | c(rop) | a(rt) | t(rim) + t(rim): {box} | m(edia) | c(rop) | a(rt) | b(leed) + +Examples: + pdfcpu box list in.pdf + pdfcpu box l -- "bleed,trim" in.pdf + pdfcpu box add -- "crop:[10 10 200 200], trim:5, bleed:trim" in.pdf + pdfcpu box rem -- "t,b" in.pdf + +` + usageBoxDescription + + usageAnnotsList = "pdfcpu annotations list [-p(ages) selectedPages] inFile" + usageAnnotsRemove = "pdfcpu annotations remove [-p(ages) selectedPages] inFile [outFile] [objNr|annotId|annotType]..." + + usageAnnots = "usage: " + usageAnnotsList + + "\n " + usageAnnotsRemove + generalFlags + + usageLongAnnots = `Manage annotations. + + pages ... Please refer to "pdfcpu selectedpages" + inFile ... input PDF file + objNr ... obj# from "pdfcpu annotations list" + annotId ... id from "pdfcpu annotations list" + annotType ... Text, Link, FreeText, Line, Square, Circle, Polygon, PolyLine, HighLight, Underline, Squiggly, StrikeOut, Stamp, + Caret, Ink, Popup, FileAttachment, Sound, Movie, Widget, Screen, PrinterMark, TrapNet, Watermark, 3D, Redact + + Examples: + + List all annotations: + pdfcpu annot list in.pdf + + List annotation of first two pages: + pdfcpu annot list -pages 1-2 in.pdf + + Remove all page annotations and write to out.pdf: + pdfcpu annot remove in.pdf out.pdf + + Remove annotations for first 10 pages: + pdfcpu annot remove -pages 1-10 in.pdf + + Remove annotations with obj# 37, 38 (see output of pdfcpu annot list) + pdfcpu annot remove in.pdf 37 38 + + Remove all Widget annotations and write to out.pdf: + pdfcpu annot remove in.pdf out.pdf Widget + + Remove all Ink and Widget annotations on page 3: + pdfcpu annot remove -pages 3 in.pdf Ink Widget + + Remove annotations by type, id and obj# and write to out.pdf: + pdfcpu annot remove in.pdf out.pdf Link 30 Text someId + ` + + usageImagesList = "pdfcpu images list [-p(ages) selectedPages] inFile..." + generalFlags + + usageImages = "usage: " + usageImagesList + + usageLongImages = `Manage keywords. + + pages ... Please refer to "pdfcpu selectedpages" + inFile ... input PDF file + + Example: pdfcpu images list -p "1-5" gallery.pdf + ` + + usageCreate = "usage: pdfcpu create inFileJSON [inFile] outFile" + generalFlags + usageLongCreate = `Create page content corresponding to declarations in inFileJSON. +Append new page content to existing page content in inFile and write result to outFile. +If inFile is absent outFile will be overwritten. + + inFileJSON ... input json file + inFile ... optional input PDF file + outFile ... output PDF file + +A minimalistic sample json: +{ + "pages": { + "1": { + "content": { + "text": [ + { + "value": "Hello pdfcpu user!", + "anchor": "center", + "font": { + "name": "Helvetica", + "size": 12 + } + } + ] + } + } + } +} + +For more info on json syntax & samples please refer to : + pdfcpu/pkg/testdata/json/* + pdfcpu/pkg/samples/create/*` + + usageFormListFields = "pdfcpu form list inFile..." + usageFormRemoveFields = "pdfcpu form remove inFile [outFile] ..." + usageFormLock = "pdfcpu form lock inFile [outFile] [fieldID|fieldName]..." + usageFormUnlock = "pdfcpu form unlock inFile [outFile] [fieldID|fieldName]..." + usageFormReset = "pdfcpu form reset inFile [outFile] [fieldID|fieldName]..." + usageFormExport = "pdfcpu form export inFile [outFileJSON]" + usageFormFill = "pdfcpu form fill inFile inFileJSON [outFile]" + usageFormMultiFill = "pdfcpu form multifill [-m(ode) single|merge] inFile inFileData outDir [outName]" + + usageForm = "usage: " + usageFormListFields + + "\n " + usageFormRemoveFields + + "\n " + usageFormLock + + "\n " + usageFormUnlock + + "\n " + usageFormReset + + "\n " + usageFormExport + + "\n\n " + usageFormFill + + "\n " + usageFormMultiFill + generalFlags + + usageLongForm = `Manage PDF forms. + + inFile ... input PDF file + inFileData ... input CSV or JSON file + inFileJSON ... input JSON file + outFile ... output PDF file + outFileJSON ... output JSON file + mode ... output mode (defaults to single) + outDir ... output directory + outName ... base output name + fieldID ... as indicated by "pdfcpu form list" + fieldName ... as indicated by "pdfcpu form list" + +The output modes are: + + single ... each filled form instance gets written to a separate output file. + + merge ... all filled form instances are merged together resulting in one output file. + + +Supported usecases: + + 1) Get a list of form fields: + "pdfcpu form list in.pdf" returns a list of form fields of in.pdf. + Each field is identified by its name and id. + + 2) Remove some form fields: + "pdfcpu form remove in.pdf middleName birthPlace" removes the the two fields "middleName" and "birthPlace". + You may supply a mixed list of field ids and field names. + + 3) Make some or all fields read-only: + "pdfcpu form lock in.pdf dateOfBirth" turns the field "dateOfBirth" into read-only. + "pdfcpu from lock in.pdf" makes the form read-only. + You may supply a mixed list of field ids and field names. + + 4) Make some or all read-only fields writeable: + "pdfcpu form unlock in.pdf dateOfBirth" makes the field "dateOfBirth" writeable. + "pdfcpu form unlock in.pdf" makes all fields of in.pdf writeable. + You may supply a mixed list of field ids and field names. + + 5) Clear some or all fields: + "pdfcpu form reset in.pdf firstName lastName" resets the fields "firstName" and "lastName" to its default values. + "pdfcpu form reset in.pdf" resets the whole form of in.pdf. + You may supply a mixed list of field ids and field names. + + 6) Export all form fields as preparation for form filling: + "pdfcpu form export in.pdf" exports field data into a JSON structure written to in.json. + + 7) Fill a form with data: + a) Export your form into in.json and edit the field values. + b) Optionally trim down each field to id or name and value(s). + c) "pdfcpu form fill in.pdf in.json out.pdf" fills in.pdf with form data from in.json and writes the result to out.pdf. + + or + + 8) Generate a sequence of filled instances of a form: + a) Export your form to in.json and edit the field values. + Extend the JSON Array containing the form by using copy & paste and edit the corresponding form data. + b) Optionally trim down each field to id or name and value(s). + c) "pdfcpu form multifill in.pdf in.json outDir" creates a separate PDF for each filled form instance in outDir. + or + a) Export your form to in.json. + b) Create a CSV file holding form instance data where each CSV line corresponds to one form data tuple. + The first line identifies fields via id or name from in.json. + c) "pdfcpu form multifill in.pdf in.csv outDir" creates a separate PDF for each filled form instance in outDir. + + or + + 9) Generate a sequence of filled instances of a form and merge output: + a) Export your form to in.json and edit the field values. + Extend the JSON Array containing the form by using copy & paste and edit the corresponding form data. + b) Optionally trim down each field to id or name and value(s). + c) "pdfcpu form multifill -m merge in.pdf in.json outDir" creates a single output PDF in outDir. + or + a) Export your form to in.json. + b) Create a CSV file holding form instance data where each CSV line corresponds to one form data tuple. + The first line identifies fields via id or name in in.json. + c) "pdfcpu form multifill -m merge in.pdf in.csv outDir" creates a single output PDF in outDir. + + + (For syntax and details please refer to pdfcpu/pkg/api/test/form_test.go)` + + usageResize = "usage: pdfcpu resize [-p(ages) selectedPages] -- description inFile [outFile]" + generalFlags + usageLongResize = `Resize existing pages. + + pages ... please refer to "pdfcpu selectedpages" +description ... scalefactor, dimensions, formsize, enforce, border, bgcolor + inFile ... input PDF file + outFile ... output PDF file + + is a comma separated configuration string containing: + + scalefactor: Resize page by scale factor. + Use scale < 1 to shrink pages. + Use scale > 1 to enlarge pages. + + formsize: Resize page to form/paper size eg. A4, Letter, Legal... + Append 'L' to enforce landscape mode. (eg. A3L) + Append 'P' to enforce portrait mode. (eg. A4P, TabloidP) + Please refer to "pdfcpu paper" for a comprehensive list of defined paper sizes. + "papersize" is also accepted. + + dimensions: Resize page to custom dimensions. + (width height) in given display unit eg. "400 200" + + enforce: if dimensions set only, enforce orientation (on/off, true/false, t/f). + + border: if dimensions set only, draw content region border (on/off, true/false, t/f). + + bgcolor: if dimensions set only, background color value for unused page regions. + + + Examples: + + pdfcpu resize "scale:2" in.pdf out.pdf + Enlarge pages by doubling the page dimensions, keep orientation. + + pdfcpu resize -pages 1-3 -- "sc:.5" in.pdf out.pdf + Shrink first 3 pages by cutting in half the page dimensions, keep orientation. + + pdfcpu resize -u cm -- "dim:40 0" in.pdf out.pdf + Resize pages to width of 40 cm, keep orientation. + + pdfcpu resize "form:A4" in.pdf out.pdf + Resize pages to A4, keep orientation. + + pdfcpu resize "f:A4P, bgcol:#d0d0d0" in.pdf out.pdf + Resize pages to A4 and enforce orientation(here: portrait mode), apply background color. + + pdfcpu resize "dim:400 200" in.pdf out.pdf + Resize pages to 400 x 200 points, keep orientation. + + pdfcpu resize "dim:400 200, enforce:true" in.pdf out.pdf + Resize pages to 400 x 200 points, enforce orientation. +` + usagePoster = "usage: pdfcpu poster [-p(ages) selectedPages] -- description inFile outDir [outFileName]" + generalFlags + usageLongPoster = `Create a poster using paper size. + + pages ... Please refer to "pdfcpu selectedpages" + description ... formsize(=papersize), dimensions, scalefactor, margin, bgcolor, border + inFile ... input PDF file + outDir ... output directory + outFileName ... output file name + + Optionally scale up your page dimensions then define the poster grid tile size via form size or dimensions. + + is a comma separated configuration string containing: + + scalefactor: Enlarge page by scale factor > 1. + + formsize: Posterize using tiles with form/paper size eg. A4, Letter, Legal... + Append 'L' to enforce landscape mode. (eg. A3L) + Append 'P' to enforce portrait mode. (eg. A4P, TabloidP) + Please refer to "pdfcpu paper" for a comprehensive list of defined paper sizes. + "papersize" is also accepted. + + dimensions: Posterize using tiles with custom dimensions. + (width height) in given display unit eg. "400 200" + + margin: Apply margin / glue area (float >= 0 in given display unit) + + bgcolor: color value for visualization of margin / glue area. + + border: if margin set, draw content region border (on/off, true/false, t/f) + + + Examples: + + pdfcpu poster "f:A4" in.pdf outDir + Page form size is A2, the printer supports A4. + Generate a poster(A2) via a corresponding 2x2 grid of A4 pages. + + pdfcpu poster "f:A4, scale:2.0" in.pdf outDir + Page form size is A2, the printer supports A4. + Generate a poster(A0) via a corresponding 4x4 grid of A4 pages. + + pdfcpu poster -u cm -- "dim:15 10, margin:1, bgcol:DarkGray, border:on" in.pdf outDir + Generate a poster via a corresponding grid with cell size 15x10 cm and provide a glue area of 1 cm. + + See also the related commands: ndown, cut` + + usageNDown = "usage: pdfcpu ndown [-p(ages) selectedPages] -- [description] n inFile outDir [outFileName]" + generalFlags + usageLongNDown = `Cut selected page into n pages symmetrically. + + pages ... Please refer to "pdfcpu selectedpages" + description ... margin, bgcolor, border + n ... the n-Down value (see below for details) + inFile ... input PDF file + outDir ... output directory + outFileName ... output file name + + is a comma separated configuration string containing: + + margin: Apply margin / glue area (float >= 0 in given display unit) + + bgcolor: color value for visualization of margin / glue area. + + border: if margin set, draw content region border (on/off, true/false, t/f) + + + grid Eg. + Supported values for n: 2 ... 1x2 A1 -> 2 x A2 + 3 ... 1x3 + 4 ... 2x2 A1 -> 4 x A3 + 8 ... 2x4 A1 -> 8 x A4 + 9 ... 3x3 + 12 ... 3x4 + 16 ... 4x4 A1 -> 16 x A5 + + + Examples: + + pdfcpu ndown 2 in.pdf outDir + Page form size is A2, the printer supports A3. + Quick cut page into 2 equally sized pages. + + pdfcpu ndown 4 in.pdf outDir + Page form size is A2, the printer supports A4. + Quick cut page into 4 equally (A4) sized pages. + + pdfcpu ndown -u cm -- "margin:1, bgcol:DarkGray, border:on" 4 in.pdf outDir + Page format size is A2, the printer supports A4. + Quick cut page into 4 equally (A4) sized pages and provide a glue area of 1 cm. + + See also the related commands: poster, cut` + + usageCut = "usage: pdfcpu cut [-p(ages) selectedPages] -- description inFile outDir [outFileName]" + generalFlags + usageLongCut = `Custom cut pages horizontally or vertically. + + pages ... Please refer to "pdfcpu selectedpages" + description ... horizontal, vertical, margin, bgcolor, border + inFile ... input PDF file + outDir ... output directory + outFileName ... output file name + + Fine grained custom page cutting. + Apply any number of horizontal or vertical page cuts. + + is a comma separated configuration string containing: + + horizontal: Apply horizontal page cuts at height fraction (origin top left corner) + A sequence of fractions separated by white space. + + vertical: Apply vertical page cuts at width fraction (origin top left corner) + A sequence of fractions separated by white space. + + margin: Apply margin / glue area (float >= 0 in given display unit) + + bgcolor: color value for visualization of margin / glue area. + + border: if margin set, draw content region border (on/off, true/false, t/f) + + + Examples: + + pdfcpu cut -- "hor:.25" inFile outDir + Apply a horizontal page cut at 0.25*height + Results in 2 PDF pages. + + pdfcpu cut -- "hor:.25, vert:.75" inFile outDir + Apply a horizontal page cut at 0.25*height + Apply a vertical page cut at 0.75*width + + pdfcpu cut -- "hor:.33 .66" inFile outDir + Has the same effect as: pdfcpu ndown 3 in.pdf outDir + + pdfcpu cut -- "hor:.5, ver:.5" inFile outDir + Has the same effect as: pdfcpu ndown 4 in.pdf outDir + + See also the related commands: poster, ndown` + + usageBookmarksList = "pdfcpu bookmarks list inFile" + usageBookmarksImport = "pdfcpu bookmarks import [-r(eplace)] inFile inFileJSON [outFile]" + usageBookmarksExport = "pdfcpu bookmarks export inFile [outFileJSON]" + usageBookmarksRemove = "pdfcpu bookmarks remove inFile [outFile]" + + usageBookmarks = "usage: " + usageBookmarksList + + "\n " + usageBookmarksImport + + "\n " + usageBookmarksExport + + "\n " + usageBookmarksRemove + generalFlags + + usageLongBookmarks = `Manage bookmarks. + + inFile ... input PDF file + inFileJSON ... input JSON file + outFile ... output PDF file + outFileJSON ... output PDF file +` + + usagePageLayoutList = "pdfcpu pagelayout list inFile" + usagePageLayoutSet = "pdfcpu pagelayout set inFile value" + usagePageLayoutReset = "pdfcpu pagelayout reset inFile" + + usagePageLayout = "usage: " + usagePageLayoutList + + "\n " + usagePageLayoutSet + + "\n " + usagePageLayoutReset + generalFlags + + usageLongPageLayout = `Manage the page layout which shall be used when the document is opened: + + inFile ... input PDF file + value ... one of: + + SinglePage ... Display one page at a time (default) + TwoColumnLeft ... Display the pages in two columns, with odd- numbered pages on the left + TwoColumnRight ... Display the pages in two columns, with odd- numbered pages on the right + TwoPageLeft ... Display the pages two at a time, with odd-numbered pages on the left + TwoPageRight ... Display the pages two at a time, with odd-numbered pages on the right + + Eg. set page layout: + pdfcpu pagelayout set test.pdf TwoPageLeft + + reset page layout: + pdfcpu pagelayout reset test.pdf +` + + usagePageModeList = "pdfcpu pagemode list inFile" + usagePageModeSet = "pdfcpu pagemode set inFile value" + usagePageModeReset = "pdfcpu pagemode reset inFile" + + usagePageMode = "usage: " + usagePageModeList + + "\n " + usagePageModeSet + + "\n " + usagePageModeReset + generalFlags + + usageLongPageMode = `Manage how the document shall be displayed when opened: + + inFile ... input PDF file + value ... one of: + + UseNone ... Neither document outline nor thumbnail images visible (default) + UseOutlines ... Document outline visible + UseThumbs ... Thumbnail images visible + FullScreen ... Full-screen mode, with no menu bar, window controls, or any other window visible + UseOC ... Optional content group panel visible (since PDF 1.5) + UseAttachments ... Attachments panel visible (since PDF 1.6) + + Eg. set page mode: + pdfcpu pagemode set test.pdf UseOutlines + + reset page mode: + pdfcpu pagemode reset test.pdf + ` + + usageViewerPreferencesList = "pdfcpu viewerpref list [-a(ll)] [-j(son)] inFile" + usageViewerPreferencesSet = "pdfcpu viewerpref set inFile (inFileJSON | JSONstring)" + usageViewerPreferencesReset = "pdfcpu viewerpref reset inFile" + + usageViewerPreferences = "usage: " + usageViewerPreferencesList + + "\n " + usageViewerPreferencesSet + + "\n " + usageViewerPreferencesReset + generalFlags + + usageLongViewerPreferences = `Manage the way the document shall be displayed on the screen and shall be printed: + + all ... output all (including default values) + json ... output JSON + inFile ... input PDF file + inFileJSON ... input JSON file containing viewing preferences + JSONstring ... JSON string containing viewing preferences + + + The preferences are: + + HideToolbar ... Hide tool bars when the document is active (default=false). + HideMenubar ... Hide the menu bar when the document is active (default=false). + HideWindowUI ... Hide user interface elements in the document’s window (default=false). + FitWindow ... Resize the document’s window to fit the size of the first displayed page (default=false). + CenterWindow ... Position the document’s window in the centre of the screen (default=false). + DisplayDocTitle ... true: The window’s title bar should display the document title taken from the dc:title element of the XMP metadata stream. + false: The title bar should display the name of the PDF file containing the document (default=false). + + NonFullScreenPageMode ... How to display the document on exiting full-screen mode: + UseNone = Neither document outline nor thumbnail images visible (=default) + UseOutlines = Document outline visible + UseThumbs = Thumbnail images visible + UseOC = Optional content group panel visible + + Direction ... The predominant logical content order for text + L2R = Left to right (=default) + R2L = Right to left (including vertical writing systems, such as Chinese, Japanese, and Korean) + + ViewArea ... The name of the page boundary representing the area of a page that shall be displayed when viewing the document on the screen. + ViewClip ... The name of the page boundary to which the contents of a page shall be clipped when viewing the document on the screen. + PrintArea ... The name of the page boundary representing the area of a page that shall be rendered when printing the document. + PrintClip ... The name of the page boundary to which the contents of a page shall be clipped when printing the document. + All 4 since PDF 1.4 and deprecated as of PDF 2.0 + Page Boundaries: MediaBox, CropBox(=default), TrimBox, BleedBox, ArtBox + + Duplex ... The paper handling option that shall be used when printing the file from the print dialogue (since PDF 1.7): + Simplex = Print single-sided + DuplexFlipShortEdge = Duplex and flip on the short edge of the sheet + DuplexFlipLongEdge = Duplex and flip on the long edge of the sheet + + PickTrayByPDFSize ... Whether the PDF page size shall be used to select the input paper tray. + + PrintPageRange ... The page numbers used to initialize the print dialogue box when the file is printed (since PDF 1.7). + The array shall contain an even number of integers to be interpreted in pairs, with each pair specifying + the first and last pages in a sub-range of pages to be printed. The first page of the PDF file shall be denoted by 1. + + NumCopies ... The number of copies that shall be printed when the print dialog is opened for this file (since PDF 1.7). + + Enforce ... Array of names of Viewer preference settings that shall be enforced by PDF processors and + that shall not be overridden by subsequent selections in the application user interface (since PDF 2.0). + Possible values: PrintScaling + + Eg. list viewer preferences: + pdfcpu viewerpref list test.pdf + pdfcpu viewerpref list -all test.pdf + pdfcpu viewerpref list -json test.pdf + pdfcpu viewerpref list -all -json test.pdf + + reset viewer preferences: + pdfcpu viewerpref reset test.pdf + + set printer preferences via JSON string (case agnostic): + pdfcpu viewerpref set test.pdf "{\"HideMenuBar\": true, \"CenterWindow\": true}" + pdfcpu viewerpref set test.pdf "{\"duplex\": \"duplexFlipShortEdge\", \"printPageRange\": [1, 4, 10, 12], \"NumCopies\": 3}" + + set viewer preferences via JSON file: + pdfcpu viewerpref set test.pdf viewerpref.json + + and eg. viewerpref.json (each preferences is optional!): + + { + "viewerPreferences": { + "HideToolBar": true, + "HideMenuBar": false, + "HideWindowUI": false, + "FitWindow": true, + "CenterWindow": true, + "DisplayDocTitle": true, + "NonFullScreenPageMode": "UseThumbs", + "Direction": "R2L", + "Duplex": "Simplex", + "PickTrayByPDFSize": false, + "PrintPageRange": [ + 1, 4, + 10, 20 + ], + "NumCopies": 3, + "Enforce": [ + "PrintScaling" + ] + } + } + + ` + + usageZoom = "usage: pdfcpu zoom [-p(ages) selectedPages] -- description inFile [outFile]" + generalFlags + + usageLongZoom = `Zoom in/out of selected pages either by magnification factor or corresponding margin. + + pages ... Please refer to "pdfcpu selectedpages" +description ... factor, hmargin, vmargin, border, bgcolor + inFile ... input PDF file + outFile ... output PDF file + +Examples: + pdfcpu zoom -- "factor: 2" in.pdf out.pdf ... zoom in to magnification of 200% + pdfcpu zoom -- "factor: .5" in.pdf out.pdf ... zoom out to magnification of 50% + + pdfcpu zoom -- "hmargin: -10" in.pdf out.pdf ... zoom in to horizontal margin of -10 points + pdfcpu zoom -- "hmargin: 10" in.pdf out.pdf ... zoom out to horizontal margin of 10 points + + pdfcpu zoom -unit cm -- "hmargin: -1" in.pdf out.pdf ... zoom in to horizontal margin of -1 cm + pdfcpu zoom -unit cm -- "hmargin: 1" in.pdf out.pdf ... zoom out to horizontal margin of 1 cm + + pdfcpu zoom -- "vmargin: -10" in.pdf out.pdf ... zoom in to vertical margin of -10 points + pdfcpu zoom -- "vmargin: 10" in.pdf out.pdf ... zoom out to vertical margin of 10 points + + pdfcpu zoom -unit cm -- "vmargin: -1" in.pdf out.pdf ... zoom in to vertical margin of -1 cm + pdfcpu zoom -unit cm -- "vmargin: 1, border:true, bgcolor:lightgray" in.pdf out.pdf ... zoom out to vertical margin of 1 cm +` +) diff --git a/coverage.sh b/coverage.sh new file mode 100644 index 0000000000000000000000000000000000000000..e3f81954c65e19bc4ac9467609be67914da7e787 --- /dev/null +++ b/coverage.sh @@ -0,0 +1,48 @@ +#!/bin/sh + +# Copyright 2018 The pdfcpu Authors. +# +# 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 +# +# 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. + +rm c.out + +set -e + +echo mode: set > c.out + +function internalDeps { + + for p in $(go list -f '{{.Deps}}' $1) + do + if [[ $p == github.com/pdfcpu/pdfcpu* ]]; then + idep=$idep,$p + fi + done +} + +echo collecting coverage ... + +for q in $(go list ./...) +do + #echo collecting coverage for $q + idep=$q + internalDeps $idep + if [[ $q == */test ]]; then + idep=${idep%/test} + fi + go test -coverprofile=c1.out -coverpkg=$idep $q && tail -n +2 c1.out >> c.out +done + +rm c1.out + +go tool cover -html=c.out \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000000000000000000000000000000000000..b1309ec49b6d73654900ce21cb04edbbb7b45a87 --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module github.com/pdfcpu/pdfcpu + +go 1.20 + +require ( + github.com/hhrutter/lzw v1.0.0 + github.com/hhrutter/tiff v1.0.1 + github.com/mattn/go-runewidth v0.0.16 + github.com/pkg/errors v0.9.1 + golang.org/x/image v0.19.0 + golang.org/x/text v0.17.0 + gopkg.in/yaml.v2 v2.4.0 +) + +require github.com/rivo/uniseg v0.4.7 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000000000000000000000000000000000000..64f71ab01b20bb1cf3c9871e728c172206f9141a --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/hhrutter/lzw v1.0.0/go.mod h1:2HC6DJSn/n6iAZfgM3Pg+cP1KxeWc3ezG8bBqW5+WEo= +github.com/hhrutter/tiff v1.0.1/go.mod h1:zU/dNgDm0cMIa8y8YwcYBeuEEveI4B0owqHyiPpJPHc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +golang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/internal/corefont/Core14_AFMs/Courier-Bold.afm b/internal/corefont/Core14_AFMs/Courier-Bold.afm new file mode 100644 index 0000000000000000000000000000000000000000..eb80542b11fa911243728a2ee962fa430668c64b --- /dev/null +++ b/internal/corefont/Core14_AFMs/Courier-Bold.afm @@ -0,0 +1,342 @@ +StartFontMetrics 4.1 +Comment Copyright (c) 1989, 1990, 1991, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved. +Comment Creation Date: Mon Jun 23 16:28:00 1997 +Comment UniqueID 43048 +Comment VMusage 41139 52164 +FontName Courier-Bold +FullName Courier Bold +FamilyName Courier +Weight Bold +ItalicAngle 0 +IsFixedPitch true +CharacterSet ExtendedRoman +FontBBox -113 -250 749 801 +UnderlinePosition -100 +UnderlineThickness 50 +Version 003.000 +Notice Copyright (c) 1989, 1990, 1991, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved. +EncodingScheme AdobeStandardEncoding +CapHeight 562 +XHeight 439 +Ascender 629 +Descender -157 +StdHW 84 +StdVW 106 +StartCharMetrics 315 +C 32 ; WX 600 ; N space ; B 0 0 0 0 ; +C 33 ; WX 600 ; N exclam ; B 202 -15 398 572 ; +C 34 ; WX 600 ; N quotedbl ; B 135 277 465 562 ; +C 35 ; WX 600 ; N numbersign ; B 56 -45 544 651 ; +C 36 ; WX 600 ; N dollar ; B 82 -126 519 666 ; +C 37 ; WX 600 ; N percent ; B 5 -15 595 616 ; +C 38 ; WX 600 ; N ampersand ; B 36 -15 546 543 ; +C 39 ; WX 600 ; N quoteright ; B 171 277 423 562 ; +C 40 ; WX 600 ; N parenleft ; B 219 -102 461 616 ; +C 41 ; WX 600 ; N parenright ; B 139 -102 381 616 ; +C 42 ; WX 600 ; N asterisk ; B 91 219 509 601 ; +C 43 ; WX 600 ; N plus ; B 71 39 529 478 ; +C 44 ; WX 600 ; N comma ; B 123 -111 393 174 ; +C 45 ; WX 600 ; N hyphen ; B 100 203 500 313 ; +C 46 ; WX 600 ; N period ; B 192 -15 408 171 ; +C 47 ; WX 600 ; N slash ; B 98 -77 502 626 ; +C 48 ; WX 600 ; N zero ; B 87 -15 513 616 ; +C 49 ; WX 600 ; N one ; B 81 0 539 616 ; +C 50 ; WX 600 ; N two ; B 61 0 499 616 ; +C 51 ; WX 600 ; N three ; B 63 -15 501 616 ; +C 52 ; WX 600 ; N four ; B 53 0 507 616 ; +C 53 ; WX 600 ; N five ; B 70 -15 521 601 ; +C 54 ; WX 600 ; N six ; B 90 -15 521 616 ; +C 55 ; WX 600 ; N seven ; B 55 0 494 601 ; +C 56 ; WX 600 ; N eight ; B 83 -15 517 616 ; +C 57 ; WX 600 ; N nine ; B 79 -15 510 616 ; +C 58 ; WX 600 ; N colon ; B 191 -15 407 425 ; +C 59 ; WX 600 ; N semicolon ; B 123 -111 408 425 ; +C 60 ; WX 600 ; N less ; B 66 15 523 501 ; +C 61 ; WX 600 ; N equal ; B 71 118 529 398 ; +C 62 ; WX 600 ; N greater ; B 77 15 534 501 ; +C 63 ; WX 600 ; N question ; B 98 -14 501 580 ; +C 64 ; WX 600 ; N at ; B 16 -15 584 616 ; +C 65 ; WX 600 ; N A ; B -9 0 609 562 ; +C 66 ; WX 600 ; N B ; B 30 0 573 562 ; +C 67 ; WX 600 ; N C ; B 22 -18 560 580 ; +C 68 ; WX 600 ; N D ; B 30 0 594 562 ; +C 69 ; WX 600 ; N E ; B 25 0 560 562 ; +C 70 ; WX 600 ; N F ; B 39 0 570 562 ; +C 71 ; WX 600 ; N G ; B 22 -18 594 580 ; +C 72 ; WX 600 ; N H ; B 20 0 580 562 ; +C 73 ; WX 600 ; N I ; B 77 0 523 562 ; +C 74 ; WX 600 ; N J ; B 37 -18 601 562 ; +C 75 ; WX 600 ; N K ; B 21 0 599 562 ; +C 76 ; WX 600 ; N L ; B 39 0 578 562 ; +C 77 ; WX 600 ; N M ; B -2 0 602 562 ; +C 78 ; WX 600 ; N N ; B 8 -12 610 562 ; +C 79 ; WX 600 ; N O ; B 22 -18 578 580 ; +C 80 ; WX 600 ; N P ; B 48 0 559 562 ; +C 81 ; WX 600 ; N Q ; B 32 -138 578 580 ; +C 82 ; WX 600 ; N R ; B 24 0 599 562 ; +C 83 ; WX 600 ; N S ; B 47 -22 553 582 ; +C 84 ; WX 600 ; N T ; B 21 0 579 562 ; +C 85 ; WX 600 ; N U ; B 4 -18 596 562 ; +C 86 ; WX 600 ; N V ; B -13 0 613 562 ; +C 87 ; WX 600 ; N W ; B -18 0 618 562 ; +C 88 ; WX 600 ; N X ; B 12 0 588 562 ; +C 89 ; WX 600 ; N Y ; B 12 0 589 562 ; +C 90 ; WX 600 ; N Z ; B 62 0 539 562 ; +C 91 ; WX 600 ; N bracketleft ; B 245 -102 475 616 ; +C 92 ; WX 600 ; N backslash ; B 99 -77 503 626 ; +C 93 ; WX 600 ; N bracketright ; B 125 -102 355 616 ; +C 94 ; WX 600 ; N asciicircum ; B 108 250 492 616 ; +C 95 ; WX 600 ; N underscore ; B 0 -125 600 -75 ; +C 96 ; WX 600 ; N quoteleft ; B 178 277 428 562 ; +C 97 ; WX 600 ; N a ; B 35 -15 570 454 ; +C 98 ; WX 600 ; N b ; B 0 -15 584 626 ; +C 99 ; WX 600 ; N c ; B 40 -15 545 459 ; +C 100 ; WX 600 ; N d ; B 20 -15 591 626 ; +C 101 ; WX 600 ; N e ; B 40 -15 563 454 ; +C 102 ; WX 600 ; N f ; B 83 0 547 626 ; L i fi ; L l fl ; +C 103 ; WX 600 ; N g ; B 30 -146 580 454 ; +C 104 ; WX 600 ; N h ; B 5 0 592 626 ; +C 105 ; WX 600 ; N i ; B 77 0 523 658 ; +C 106 ; WX 600 ; N j ; B 63 -146 440 658 ; +C 107 ; WX 600 ; N k ; B 20 0 585 626 ; +C 108 ; WX 600 ; N l ; B 77 0 523 626 ; +C 109 ; WX 600 ; N m ; B -22 0 626 454 ; +C 110 ; WX 600 ; N n ; B 18 0 592 454 ; +C 111 ; WX 600 ; N o ; B 30 -15 570 454 ; +C 112 ; WX 600 ; N p ; B -1 -142 570 454 ; +C 113 ; WX 600 ; N q ; B 20 -142 591 454 ; +C 114 ; WX 600 ; N r ; B 47 0 580 454 ; +C 115 ; WX 600 ; N s ; B 68 -17 535 459 ; +C 116 ; WX 600 ; N t ; B 47 -15 532 562 ; +C 117 ; WX 600 ; N u ; B -1 -15 569 439 ; +C 118 ; WX 600 ; N v ; B -1 0 601 439 ; +C 119 ; WX 600 ; N w ; B -18 0 618 439 ; +C 120 ; WX 600 ; N x ; B 6 0 594 439 ; +C 121 ; WX 600 ; N y ; B -4 -142 601 439 ; +C 122 ; WX 600 ; N z ; B 81 0 520 439 ; +C 123 ; WX 600 ; N braceleft ; B 160 -102 464 616 ; +C 124 ; WX 600 ; N bar ; B 255 -250 345 750 ; +C 125 ; WX 600 ; N braceright ; B 136 -102 440 616 ; +C 126 ; WX 600 ; N asciitilde ; B 71 153 530 356 ; +C 161 ; WX 600 ; N exclamdown ; B 202 -146 398 449 ; +C 162 ; WX 600 ; N cent ; B 66 -49 518 614 ; +C 163 ; WX 600 ; N sterling ; B 72 -28 558 611 ; +C 164 ; WX 600 ; N fraction ; B 25 -60 576 661 ; +C 165 ; WX 600 ; N yen ; B 10 0 590 562 ; +C 166 ; WX 600 ; N florin ; B -30 -131 572 616 ; +C 167 ; WX 600 ; N section ; B 83 -70 517 580 ; +C 168 ; WX 600 ; N currency ; B 54 49 546 517 ; +C 169 ; WX 600 ; N quotesingle ; B 227 277 373 562 ; +C 170 ; WX 600 ; N quotedblleft ; B 71 277 535 562 ; +C 171 ; WX 600 ; N guillemotleft ; B 8 70 553 446 ; +C 172 ; WX 600 ; N guilsinglleft ; B 141 70 459 446 ; +C 173 ; WX 600 ; N guilsinglright ; B 141 70 459 446 ; +C 174 ; WX 600 ; N fi ; B 12 0 593 626 ; +C 175 ; WX 600 ; N fl ; B 12 0 593 626 ; +C 177 ; WX 600 ; N endash ; B 65 203 535 313 ; +C 178 ; WX 600 ; N dagger ; B 106 -70 494 580 ; +C 179 ; WX 600 ; N daggerdbl ; B 106 -70 494 580 ; +C 180 ; WX 600 ; N periodcentered ; B 196 165 404 351 ; +C 182 ; WX 600 ; N paragraph ; B 6 -70 576 580 ; +C 183 ; WX 600 ; N bullet ; B 140 132 460 430 ; +C 184 ; WX 600 ; N quotesinglbase ; B 175 -142 427 143 ; +C 185 ; WX 600 ; N quotedblbase ; B 65 -142 529 143 ; +C 186 ; WX 600 ; N quotedblright ; B 61 277 525 562 ; +C 187 ; WX 600 ; N guillemotright ; B 47 70 592 446 ; +C 188 ; WX 600 ; N ellipsis ; B 26 -15 574 116 ; +C 189 ; WX 600 ; N perthousand ; B -113 -15 713 616 ; +C 191 ; WX 600 ; N questiondown ; B 99 -146 502 449 ; +C 193 ; WX 600 ; N grave ; B 132 508 395 661 ; +C 194 ; WX 600 ; N acute ; B 205 508 468 661 ; +C 195 ; WX 600 ; N circumflex ; B 103 483 497 657 ; +C 196 ; WX 600 ; N tilde ; B 89 493 512 636 ; +C 197 ; WX 600 ; N macron ; B 88 505 512 585 ; +C 198 ; WX 600 ; N breve ; B 83 468 517 631 ; +C 199 ; WX 600 ; N dotaccent ; B 230 498 370 638 ; +C 200 ; WX 600 ; N dieresis ; B 128 498 472 638 ; +C 202 ; WX 600 ; N ring ; B 198 481 402 678 ; +C 203 ; WX 600 ; N cedilla ; B 205 -206 387 0 ; +C 205 ; WX 600 ; N hungarumlaut ; B 68 488 588 661 ; +C 206 ; WX 600 ; N ogonek ; B 169 -199 400 0 ; +C 207 ; WX 600 ; N caron ; B 103 493 497 667 ; +C 208 ; WX 600 ; N emdash ; B -10 203 610 313 ; +C 225 ; WX 600 ; N AE ; B -29 0 602 562 ; +C 227 ; WX 600 ; N ordfeminine ; B 147 196 453 580 ; +C 232 ; WX 600 ; N Lslash ; B 39 0 578 562 ; +C 233 ; WX 600 ; N Oslash ; B 22 -22 578 584 ; +C 234 ; WX 600 ; N OE ; B -25 0 595 562 ; +C 235 ; WX 600 ; N ordmasculine ; B 147 196 453 580 ; +C 241 ; WX 600 ; N ae ; B -4 -15 601 454 ; +C 245 ; WX 600 ; N dotlessi ; B 77 0 523 439 ; +C 248 ; WX 600 ; N lslash ; B 77 0 523 626 ; +C 249 ; WX 600 ; N oslash ; B 30 -24 570 463 ; +C 250 ; WX 600 ; N oe ; B -18 -15 611 454 ; +C 251 ; WX 600 ; N germandbls ; B 22 -15 596 626 ; +C -1 ; WX 600 ; N Idieresis ; B 77 0 523 761 ; +C -1 ; WX 600 ; N eacute ; B 40 -15 563 661 ; +C -1 ; WX 600 ; N abreve ; B 35 -15 570 661 ; +C -1 ; WX 600 ; N uhungarumlaut ; B -1 -15 628 661 ; +C -1 ; WX 600 ; N ecaron ; B 40 -15 563 667 ; +C -1 ; WX 600 ; N Ydieresis ; B 12 0 589 761 ; +C -1 ; WX 600 ; N divide ; B 71 16 529 500 ; +C -1 ; WX 600 ; N Yacute ; B 12 0 589 784 ; +C -1 ; WX 600 ; N Acircumflex ; B -9 0 609 780 ; +C -1 ; WX 600 ; N aacute ; B 35 -15 570 661 ; +C -1 ; WX 600 ; N Ucircumflex ; B 4 -18 596 780 ; +C -1 ; WX 600 ; N yacute ; B -4 -142 601 661 ; +C -1 ; WX 600 ; N scommaaccent ; B 68 -250 535 459 ; +C -1 ; WX 600 ; N ecircumflex ; B 40 -15 563 657 ; +C -1 ; WX 600 ; N Uring ; B 4 -18 596 801 ; +C -1 ; WX 600 ; N Udieresis ; B 4 -18 596 761 ; +C -1 ; WX 600 ; N aogonek ; B 35 -199 586 454 ; +C -1 ; WX 600 ; N Uacute ; B 4 -18 596 784 ; +C -1 ; WX 600 ; N uogonek ; B -1 -199 585 439 ; +C -1 ; WX 600 ; N Edieresis ; B 25 0 560 761 ; +C -1 ; WX 600 ; N Dcroat ; B 30 0 594 562 ; +C -1 ; WX 600 ; N commaaccent ; B 205 -250 397 -57 ; +C -1 ; WX 600 ; N copyright ; B 0 -18 600 580 ; +C -1 ; WX 600 ; N Emacron ; B 25 0 560 708 ; +C -1 ; WX 600 ; N ccaron ; B 40 -15 545 667 ; +C -1 ; WX 600 ; N aring ; B 35 -15 570 678 ; +C -1 ; WX 600 ; N Ncommaaccent ; B 8 -250 610 562 ; +C -1 ; WX 600 ; N lacute ; B 77 0 523 801 ; +C -1 ; WX 600 ; N agrave ; B 35 -15 570 661 ; +C -1 ; WX 600 ; N Tcommaaccent ; B 21 -250 579 562 ; +C -1 ; WX 600 ; N Cacute ; B 22 -18 560 784 ; +C -1 ; WX 600 ; N atilde ; B 35 -15 570 636 ; +C -1 ; WX 600 ; N Edotaccent ; B 25 0 560 761 ; +C -1 ; WX 600 ; N scaron ; B 68 -17 535 667 ; +C -1 ; WX 600 ; N scedilla ; B 68 -206 535 459 ; +C -1 ; WX 600 ; N iacute ; B 77 0 523 661 ; +C -1 ; WX 600 ; N lozenge ; B 66 0 534 740 ; +C -1 ; WX 600 ; N Rcaron ; B 24 0 599 790 ; +C -1 ; WX 600 ; N Gcommaaccent ; B 22 -250 594 580 ; +C -1 ; WX 600 ; N ucircumflex ; B -1 -15 569 657 ; +C -1 ; WX 600 ; N acircumflex ; B 35 -15 570 657 ; +C -1 ; WX 600 ; N Amacron ; B -9 0 609 708 ; +C -1 ; WX 600 ; N rcaron ; B 47 0 580 667 ; +C -1 ; WX 600 ; N ccedilla ; B 40 -206 545 459 ; +C -1 ; WX 600 ; N Zdotaccent ; B 62 0 539 761 ; +C -1 ; WX 600 ; N Thorn ; B 48 0 557 562 ; +C -1 ; WX 600 ; N Omacron ; B 22 -18 578 708 ; +C -1 ; WX 600 ; N Racute ; B 24 0 599 784 ; +C -1 ; WX 600 ; N Sacute ; B 47 -22 553 784 ; +C -1 ; WX 600 ; N dcaron ; B 20 -15 727 626 ; +C -1 ; WX 600 ; N Umacron ; B 4 -18 596 708 ; +C -1 ; WX 600 ; N uring ; B -1 -15 569 678 ; +C -1 ; WX 600 ; N threesuperior ; B 138 222 433 616 ; +C -1 ; WX 600 ; N Ograve ; B 22 -18 578 784 ; +C -1 ; WX 600 ; N Agrave ; B -9 0 609 784 ; +C -1 ; WX 600 ; N Abreve ; B -9 0 609 784 ; +C -1 ; WX 600 ; N multiply ; B 81 39 520 478 ; +C -1 ; WX 600 ; N uacute ; B -1 -15 569 661 ; +C -1 ; WX 600 ; N Tcaron ; B 21 0 579 790 ; +C -1 ; WX 600 ; N partialdiff ; B 63 -38 537 728 ; +C -1 ; WX 600 ; N ydieresis ; B -4 -142 601 638 ; +C -1 ; WX 600 ; N Nacute ; B 8 -12 610 784 ; +C -1 ; WX 600 ; N icircumflex ; B 73 0 523 657 ; +C -1 ; WX 600 ; N Ecircumflex ; B 25 0 560 780 ; +C -1 ; WX 600 ; N adieresis ; B 35 -15 570 638 ; +C -1 ; WX 600 ; N edieresis ; B 40 -15 563 638 ; +C -1 ; WX 600 ; N cacute ; B 40 -15 545 661 ; +C -1 ; WX 600 ; N nacute ; B 18 0 592 661 ; +C -1 ; WX 600 ; N umacron ; B -1 -15 569 585 ; +C -1 ; WX 600 ; N Ncaron ; B 8 -12 610 790 ; +C -1 ; WX 600 ; N Iacute ; B 77 0 523 784 ; +C -1 ; WX 600 ; N plusminus ; B 71 24 529 515 ; +C -1 ; WX 600 ; N brokenbar ; B 255 -175 345 675 ; +C -1 ; WX 600 ; N registered ; B 0 -18 600 580 ; +C -1 ; WX 600 ; N Gbreve ; B 22 -18 594 784 ; +C -1 ; WX 600 ; N Idotaccent ; B 77 0 523 761 ; +C -1 ; WX 600 ; N summation ; B 15 -10 586 706 ; +C -1 ; WX 600 ; N Egrave ; B 25 0 560 784 ; +C -1 ; WX 600 ; N racute ; B 47 0 580 661 ; +C -1 ; WX 600 ; N omacron ; B 30 -15 570 585 ; +C -1 ; WX 600 ; N Zacute ; B 62 0 539 784 ; +C -1 ; WX 600 ; N Zcaron ; B 62 0 539 790 ; +C -1 ; WX 600 ; N greaterequal ; B 26 0 523 696 ; +C -1 ; WX 600 ; N Eth ; B 30 0 594 562 ; +C -1 ; WX 600 ; N Ccedilla ; B 22 -206 560 580 ; +C -1 ; WX 600 ; N lcommaaccent ; B 77 -250 523 626 ; +C -1 ; WX 600 ; N tcaron ; B 47 -15 532 703 ; +C -1 ; WX 600 ; N eogonek ; B 40 -199 563 454 ; +C -1 ; WX 600 ; N Uogonek ; B 4 -199 596 562 ; +C -1 ; WX 600 ; N Aacute ; B -9 0 609 784 ; +C -1 ; WX 600 ; N Adieresis ; B -9 0 609 761 ; +C -1 ; WX 600 ; N egrave ; B 40 -15 563 661 ; +C -1 ; WX 600 ; N zacute ; B 81 0 520 661 ; +C -1 ; WX 600 ; N iogonek ; B 77 -199 523 658 ; +C -1 ; WX 600 ; N Oacute ; B 22 -18 578 784 ; +C -1 ; WX 600 ; N oacute ; B 30 -15 570 661 ; +C -1 ; WX 600 ; N amacron ; B 35 -15 570 585 ; +C -1 ; WX 600 ; N sacute ; B 68 -17 535 661 ; +C -1 ; WX 600 ; N idieresis ; B 77 0 523 618 ; +C -1 ; WX 600 ; N Ocircumflex ; B 22 -18 578 780 ; +C -1 ; WX 600 ; N Ugrave ; B 4 -18 596 784 ; +C -1 ; WX 600 ; N Delta ; B 6 0 594 688 ; +C -1 ; WX 600 ; N thorn ; B -14 -142 570 626 ; +C -1 ; WX 600 ; N twosuperior ; B 143 230 436 616 ; +C -1 ; WX 600 ; N Odieresis ; B 22 -18 578 761 ; +C -1 ; WX 600 ; N mu ; B -1 -142 569 439 ; +C -1 ; WX 600 ; N igrave ; B 77 0 523 661 ; +C -1 ; WX 600 ; N ohungarumlaut ; B 30 -15 668 661 ; +C -1 ; WX 600 ; N Eogonek ; B 25 -199 576 562 ; +C -1 ; WX 600 ; N dcroat ; B 20 -15 591 626 ; +C -1 ; WX 600 ; N threequarters ; B -47 -60 648 661 ; +C -1 ; WX 600 ; N Scedilla ; B 47 -206 553 582 ; +C -1 ; WX 600 ; N lcaron ; B 77 0 597 626 ; +C -1 ; WX 600 ; N Kcommaaccent ; B 21 -250 599 562 ; +C -1 ; WX 600 ; N Lacute ; B 39 0 578 784 ; +C -1 ; WX 600 ; N trademark ; B -9 230 749 562 ; +C -1 ; WX 600 ; N edotaccent ; B 40 -15 563 638 ; +C -1 ; WX 600 ; N Igrave ; B 77 0 523 784 ; +C -1 ; WX 600 ; N Imacron ; B 77 0 523 708 ; +C -1 ; WX 600 ; N Lcaron ; B 39 0 637 562 ; +C -1 ; WX 600 ; N onehalf ; B -47 -60 648 661 ; +C -1 ; WX 600 ; N lessequal ; B 26 0 523 696 ; +C -1 ; WX 600 ; N ocircumflex ; B 30 -15 570 657 ; +C -1 ; WX 600 ; N ntilde ; B 18 0 592 636 ; +C -1 ; WX 600 ; N Uhungarumlaut ; B 4 -18 638 784 ; +C -1 ; WX 600 ; N Eacute ; B 25 0 560 784 ; +C -1 ; WX 600 ; N emacron ; B 40 -15 563 585 ; +C -1 ; WX 600 ; N gbreve ; B 30 -146 580 661 ; +C -1 ; WX 600 ; N onequarter ; B -56 -60 656 661 ; +C -1 ; WX 600 ; N Scaron ; B 47 -22 553 790 ; +C -1 ; WX 600 ; N Scommaaccent ; B 47 -250 553 582 ; +C -1 ; WX 600 ; N Ohungarumlaut ; B 22 -18 628 784 ; +C -1 ; WX 600 ; N degree ; B 86 243 474 616 ; +C -1 ; WX 600 ; N ograve ; B 30 -15 570 661 ; +C -1 ; WX 600 ; N Ccaron ; B 22 -18 560 790 ; +C -1 ; WX 600 ; N ugrave ; B -1 -15 569 661 ; +C -1 ; WX 600 ; N radical ; B -19 -104 473 778 ; +C -1 ; WX 600 ; N Dcaron ; B 30 0 594 790 ; +C -1 ; WX 600 ; N rcommaaccent ; B 47 -250 580 454 ; +C -1 ; WX 600 ; N Ntilde ; B 8 -12 610 759 ; +C -1 ; WX 600 ; N otilde ; B 30 -15 570 636 ; +C -1 ; WX 600 ; N Rcommaaccent ; B 24 -250 599 562 ; +C -1 ; WX 600 ; N Lcommaaccent ; B 39 -250 578 562 ; +C -1 ; WX 600 ; N Atilde ; B -9 0 609 759 ; +C -1 ; WX 600 ; N Aogonek ; B -9 -199 625 562 ; +C -1 ; WX 600 ; N Aring ; B -9 0 609 801 ; +C -1 ; WX 600 ; N Otilde ; B 22 -18 578 759 ; +C -1 ; WX 600 ; N zdotaccent ; B 81 0 520 638 ; +C -1 ; WX 600 ; N Ecaron ; B 25 0 560 790 ; +C -1 ; WX 600 ; N Iogonek ; B 77 -199 523 562 ; +C -1 ; WX 600 ; N kcommaaccent ; B 20 -250 585 626 ; +C -1 ; WX 600 ; N minus ; B 71 203 529 313 ; +C -1 ; WX 600 ; N Icircumflex ; B 77 0 523 780 ; +C -1 ; WX 600 ; N ncaron ; B 18 0 592 667 ; +C -1 ; WX 600 ; N tcommaaccent ; B 47 -250 532 562 ; +C -1 ; WX 600 ; N logicalnot ; B 71 103 529 413 ; +C -1 ; WX 600 ; N odieresis ; B 30 -15 570 638 ; +C -1 ; WX 600 ; N udieresis ; B -1 -15 569 638 ; +C -1 ; WX 600 ; N notequal ; B 12 -47 537 563 ; +C -1 ; WX 600 ; N gcommaaccent ; B 30 -146 580 714 ; +C -1 ; WX 600 ; N eth ; B 58 -27 543 626 ; +C -1 ; WX 600 ; N zcaron ; B 81 0 520 667 ; +C -1 ; WX 600 ; N ncommaaccent ; B 18 -250 592 454 ; +C -1 ; WX 600 ; N onesuperior ; B 153 230 447 616 ; +C -1 ; WX 600 ; N imacron ; B 77 0 523 585 ; +C -1 ; WX 600 ; N Euro ; B 0 0 0 0 ; +EndCharMetrics +EndFontMetrics diff --git a/internal/corefont/Core14_AFMs/Courier-BoldOblique.afm b/internal/corefont/Core14_AFMs/Courier-BoldOblique.afm new file mode 100644 index 0000000000000000000000000000000000000000..29d3b8b10ec4807eca647057669f027e93943527 --- /dev/null +++ b/internal/corefont/Core14_AFMs/Courier-BoldOblique.afm @@ -0,0 +1,342 @@ +StartFontMetrics 4.1 +Comment Copyright (c) 1989, 1990, 1991, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved. +Comment Creation Date: Mon Jun 23 16:28:46 1997 +Comment UniqueID 43049 +Comment VMusage 17529 79244 +FontName Courier-BoldOblique +FullName Courier Bold Oblique +FamilyName Courier +Weight Bold +ItalicAngle -12 +IsFixedPitch true +CharacterSet ExtendedRoman +FontBBox -57 -250 869 801 +UnderlinePosition -100 +UnderlineThickness 50 +Version 003.000 +Notice Copyright (c) 1989, 1990, 1991, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved. +EncodingScheme AdobeStandardEncoding +CapHeight 562 +XHeight 439 +Ascender 629 +Descender -157 +StdHW 84 +StdVW 106 +StartCharMetrics 315 +C 32 ; WX 600 ; N space ; B 0 0 0 0 ; +C 33 ; WX 600 ; N exclam ; B 215 -15 495 572 ; +C 34 ; WX 600 ; N quotedbl ; B 211 277 585 562 ; +C 35 ; WX 600 ; N numbersign ; B 88 -45 641 651 ; +C 36 ; WX 600 ; N dollar ; B 87 -126 630 666 ; +C 37 ; WX 600 ; N percent ; B 101 -15 625 616 ; +C 38 ; WX 600 ; N ampersand ; B 61 -15 595 543 ; +C 39 ; WX 600 ; N quoteright ; B 229 277 543 562 ; +C 40 ; WX 600 ; N parenleft ; B 265 -102 592 616 ; +C 41 ; WX 600 ; N parenright ; B 117 -102 444 616 ; +C 42 ; WX 600 ; N asterisk ; B 179 219 598 601 ; +C 43 ; WX 600 ; N plus ; B 114 39 596 478 ; +C 44 ; WX 600 ; N comma ; B 99 -111 430 174 ; +C 45 ; WX 600 ; N hyphen ; B 143 203 567 313 ; +C 46 ; WX 600 ; N period ; B 206 -15 427 171 ; +C 47 ; WX 600 ; N slash ; B 90 -77 626 626 ; +C 48 ; WX 600 ; N zero ; B 135 -15 593 616 ; +C 49 ; WX 600 ; N one ; B 93 0 562 616 ; +C 50 ; WX 600 ; N two ; B 61 0 594 616 ; +C 51 ; WX 600 ; N three ; B 71 -15 571 616 ; +C 52 ; WX 600 ; N four ; B 81 0 559 616 ; +C 53 ; WX 600 ; N five ; B 77 -15 621 601 ; +C 54 ; WX 600 ; N six ; B 135 -15 652 616 ; +C 55 ; WX 600 ; N seven ; B 147 0 622 601 ; +C 56 ; WX 600 ; N eight ; B 115 -15 604 616 ; +C 57 ; WX 600 ; N nine ; B 75 -15 592 616 ; +C 58 ; WX 600 ; N colon ; B 205 -15 480 425 ; +C 59 ; WX 600 ; N semicolon ; B 99 -111 481 425 ; +C 60 ; WX 600 ; N less ; B 120 15 613 501 ; +C 61 ; WX 600 ; N equal ; B 96 118 614 398 ; +C 62 ; WX 600 ; N greater ; B 97 15 589 501 ; +C 63 ; WX 600 ; N question ; B 183 -14 592 580 ; +C 64 ; WX 600 ; N at ; B 65 -15 642 616 ; +C 65 ; WX 600 ; N A ; B -9 0 632 562 ; +C 66 ; WX 600 ; N B ; B 30 0 630 562 ; +C 67 ; WX 600 ; N C ; B 74 -18 675 580 ; +C 68 ; WX 600 ; N D ; B 30 0 664 562 ; +C 69 ; WX 600 ; N E ; B 25 0 670 562 ; +C 70 ; WX 600 ; N F ; B 39 0 684 562 ; +C 71 ; WX 600 ; N G ; B 74 -18 675 580 ; +C 72 ; WX 600 ; N H ; B 20 0 700 562 ; +C 73 ; WX 600 ; N I ; B 77 0 643 562 ; +C 74 ; WX 600 ; N J ; B 58 -18 721 562 ; +C 75 ; WX 600 ; N K ; B 21 0 692 562 ; +C 76 ; WX 600 ; N L ; B 39 0 636 562 ; +C 77 ; WX 600 ; N M ; B -2 0 722 562 ; +C 78 ; WX 600 ; N N ; B 8 -12 730 562 ; +C 79 ; WX 600 ; N O ; B 74 -18 645 580 ; +C 80 ; WX 600 ; N P ; B 48 0 643 562 ; +C 81 ; WX 600 ; N Q ; B 83 -138 636 580 ; +C 82 ; WX 600 ; N R ; B 24 0 617 562 ; +C 83 ; WX 600 ; N S ; B 54 -22 673 582 ; +C 84 ; WX 600 ; N T ; B 86 0 679 562 ; +C 85 ; WX 600 ; N U ; B 101 -18 716 562 ; +C 86 ; WX 600 ; N V ; B 84 0 733 562 ; +C 87 ; WX 600 ; N W ; B 79 0 738 562 ; +C 88 ; WX 600 ; N X ; B 12 0 690 562 ; +C 89 ; WX 600 ; N Y ; B 109 0 709 562 ; +C 90 ; WX 600 ; N Z ; B 62 0 637 562 ; +C 91 ; WX 600 ; N bracketleft ; B 223 -102 606 616 ; +C 92 ; WX 600 ; N backslash ; B 222 -77 496 626 ; +C 93 ; WX 600 ; N bracketright ; B 103 -102 486 616 ; +C 94 ; WX 600 ; N asciicircum ; B 171 250 556 616 ; +C 95 ; WX 600 ; N underscore ; B -27 -125 585 -75 ; +C 96 ; WX 600 ; N quoteleft ; B 297 277 487 562 ; +C 97 ; WX 600 ; N a ; B 61 -15 593 454 ; +C 98 ; WX 600 ; N b ; B 13 -15 636 626 ; +C 99 ; WX 600 ; N c ; B 81 -15 631 459 ; +C 100 ; WX 600 ; N d ; B 60 -15 645 626 ; +C 101 ; WX 600 ; N e ; B 81 -15 605 454 ; +C 102 ; WX 600 ; N f ; B 83 0 677 626 ; L i fi ; L l fl ; +C 103 ; WX 600 ; N g ; B 40 -146 674 454 ; +C 104 ; WX 600 ; N h ; B 18 0 615 626 ; +C 105 ; WX 600 ; N i ; B 77 0 546 658 ; +C 106 ; WX 600 ; N j ; B 36 -146 580 658 ; +C 107 ; WX 600 ; N k ; B 33 0 643 626 ; +C 108 ; WX 600 ; N l ; B 77 0 546 626 ; +C 109 ; WX 600 ; N m ; B -22 0 649 454 ; +C 110 ; WX 600 ; N n ; B 18 0 615 454 ; +C 111 ; WX 600 ; N o ; B 71 -15 622 454 ; +C 112 ; WX 600 ; N p ; B -32 -142 622 454 ; +C 113 ; WX 600 ; N q ; B 60 -142 685 454 ; +C 114 ; WX 600 ; N r ; B 47 0 655 454 ; +C 115 ; WX 600 ; N s ; B 66 -17 608 459 ; +C 116 ; WX 600 ; N t ; B 118 -15 567 562 ; +C 117 ; WX 600 ; N u ; B 70 -15 592 439 ; +C 118 ; WX 600 ; N v ; B 70 0 695 439 ; +C 119 ; WX 600 ; N w ; B 53 0 712 439 ; +C 120 ; WX 600 ; N x ; B 6 0 671 439 ; +C 121 ; WX 600 ; N y ; B -21 -142 695 439 ; +C 122 ; WX 600 ; N z ; B 81 0 614 439 ; +C 123 ; WX 600 ; N braceleft ; B 203 -102 595 616 ; +C 124 ; WX 600 ; N bar ; B 201 -250 505 750 ; +C 125 ; WX 600 ; N braceright ; B 114 -102 506 616 ; +C 126 ; WX 600 ; N asciitilde ; B 120 153 590 356 ; +C 161 ; WX 600 ; N exclamdown ; B 196 -146 477 449 ; +C 162 ; WX 600 ; N cent ; B 121 -49 605 614 ; +C 163 ; WX 600 ; N sterling ; B 106 -28 650 611 ; +C 164 ; WX 600 ; N fraction ; B 22 -60 708 661 ; +C 165 ; WX 600 ; N yen ; B 98 0 710 562 ; +C 166 ; WX 600 ; N florin ; B -57 -131 702 616 ; +C 167 ; WX 600 ; N section ; B 74 -70 620 580 ; +C 168 ; WX 600 ; N currency ; B 77 49 644 517 ; +C 169 ; WX 600 ; N quotesingle ; B 303 277 493 562 ; +C 170 ; WX 600 ; N quotedblleft ; B 190 277 594 562 ; +C 171 ; WX 600 ; N guillemotleft ; B 62 70 639 446 ; +C 172 ; WX 600 ; N guilsinglleft ; B 195 70 545 446 ; +C 173 ; WX 600 ; N guilsinglright ; B 165 70 514 446 ; +C 174 ; WX 600 ; N fi ; B 12 0 644 626 ; +C 175 ; WX 600 ; N fl ; B 12 0 644 626 ; +C 177 ; WX 600 ; N endash ; B 108 203 602 313 ; +C 178 ; WX 600 ; N dagger ; B 175 -70 586 580 ; +C 179 ; WX 600 ; N daggerdbl ; B 121 -70 587 580 ; +C 180 ; WX 600 ; N periodcentered ; B 248 165 461 351 ; +C 182 ; WX 600 ; N paragraph ; B 61 -70 700 580 ; +C 183 ; WX 600 ; N bullet ; B 196 132 523 430 ; +C 184 ; WX 600 ; N quotesinglbase ; B 144 -142 458 143 ; +C 185 ; WX 600 ; N quotedblbase ; B 34 -142 560 143 ; +C 186 ; WX 600 ; N quotedblright ; B 119 277 645 562 ; +C 187 ; WX 600 ; N guillemotright ; B 71 70 647 446 ; +C 188 ; WX 600 ; N ellipsis ; B 35 -15 587 116 ; +C 189 ; WX 600 ; N perthousand ; B -45 -15 743 616 ; +C 191 ; WX 600 ; N questiondown ; B 100 -146 509 449 ; +C 193 ; WX 600 ; N grave ; B 272 508 503 661 ; +C 194 ; WX 600 ; N acute ; B 312 508 609 661 ; +C 195 ; WX 600 ; N circumflex ; B 212 483 607 657 ; +C 196 ; WX 600 ; N tilde ; B 199 493 643 636 ; +C 197 ; WX 600 ; N macron ; B 195 505 637 585 ; +C 198 ; WX 600 ; N breve ; B 217 468 652 631 ; +C 199 ; WX 600 ; N dotaccent ; B 348 498 493 638 ; +C 200 ; WX 600 ; N dieresis ; B 246 498 595 638 ; +C 202 ; WX 600 ; N ring ; B 319 481 528 678 ; +C 203 ; WX 600 ; N cedilla ; B 168 -206 368 0 ; +C 205 ; WX 600 ; N hungarumlaut ; B 171 488 729 661 ; +C 206 ; WX 600 ; N ogonek ; B 143 -199 367 0 ; +C 207 ; WX 600 ; N caron ; B 238 493 633 667 ; +C 208 ; WX 600 ; N emdash ; B 33 203 677 313 ; +C 225 ; WX 600 ; N AE ; B -29 0 708 562 ; +C 227 ; WX 600 ; N ordfeminine ; B 188 196 526 580 ; +C 232 ; WX 600 ; N Lslash ; B 39 0 636 562 ; +C 233 ; WX 600 ; N Oslash ; B 48 -22 673 584 ; +C 234 ; WX 600 ; N OE ; B 26 0 701 562 ; +C 235 ; WX 600 ; N ordmasculine ; B 188 196 543 580 ; +C 241 ; WX 600 ; N ae ; B 21 -15 652 454 ; +C 245 ; WX 600 ; N dotlessi ; B 77 0 546 439 ; +C 248 ; WX 600 ; N lslash ; B 77 0 587 626 ; +C 249 ; WX 600 ; N oslash ; B 54 -24 638 463 ; +C 250 ; WX 600 ; N oe ; B 18 -15 662 454 ; +C 251 ; WX 600 ; N germandbls ; B 22 -15 629 626 ; +C -1 ; WX 600 ; N Idieresis ; B 77 0 643 761 ; +C -1 ; WX 600 ; N eacute ; B 81 -15 609 661 ; +C -1 ; WX 600 ; N abreve ; B 61 -15 658 661 ; +C -1 ; WX 600 ; N uhungarumlaut ; B 70 -15 769 661 ; +C -1 ; WX 600 ; N ecaron ; B 81 -15 633 667 ; +C -1 ; WX 600 ; N Ydieresis ; B 109 0 709 761 ; +C -1 ; WX 600 ; N divide ; B 114 16 596 500 ; +C -1 ; WX 600 ; N Yacute ; B 109 0 709 784 ; +C -1 ; WX 600 ; N Acircumflex ; B -9 0 632 780 ; +C -1 ; WX 600 ; N aacute ; B 61 -15 609 661 ; +C -1 ; WX 600 ; N Ucircumflex ; B 101 -18 716 780 ; +C -1 ; WX 600 ; N yacute ; B -21 -142 695 661 ; +C -1 ; WX 600 ; N scommaaccent ; B 66 -250 608 459 ; +C -1 ; WX 600 ; N ecircumflex ; B 81 -15 607 657 ; +C -1 ; WX 600 ; N Uring ; B 101 -18 716 801 ; +C -1 ; WX 600 ; N Udieresis ; B 101 -18 716 761 ; +C -1 ; WX 600 ; N aogonek ; B 61 -199 593 454 ; +C -1 ; WX 600 ; N Uacute ; B 101 -18 716 784 ; +C -1 ; WX 600 ; N uogonek ; B 70 -199 592 439 ; +C -1 ; WX 600 ; N Edieresis ; B 25 0 670 761 ; +C -1 ; WX 600 ; N Dcroat ; B 30 0 664 562 ; +C -1 ; WX 600 ; N commaaccent ; B 151 -250 385 -57 ; +C -1 ; WX 600 ; N copyright ; B 53 -18 667 580 ; +C -1 ; WX 600 ; N Emacron ; B 25 0 670 708 ; +C -1 ; WX 600 ; N ccaron ; B 81 -15 633 667 ; +C -1 ; WX 600 ; N aring ; B 61 -15 593 678 ; +C -1 ; WX 600 ; N Ncommaaccent ; B 8 -250 730 562 ; +C -1 ; WX 600 ; N lacute ; B 77 0 639 801 ; +C -1 ; WX 600 ; N agrave ; B 61 -15 593 661 ; +C -1 ; WX 600 ; N Tcommaaccent ; B 86 -250 679 562 ; +C -1 ; WX 600 ; N Cacute ; B 74 -18 675 784 ; +C -1 ; WX 600 ; N atilde ; B 61 -15 643 636 ; +C -1 ; WX 600 ; N Edotaccent ; B 25 0 670 761 ; +C -1 ; WX 600 ; N scaron ; B 66 -17 633 667 ; +C -1 ; WX 600 ; N scedilla ; B 66 -206 608 459 ; +C -1 ; WX 600 ; N iacute ; B 77 0 609 661 ; +C -1 ; WX 600 ; N lozenge ; B 145 0 614 740 ; +C -1 ; WX 600 ; N Rcaron ; B 24 0 659 790 ; +C -1 ; WX 600 ; N Gcommaaccent ; B 74 -250 675 580 ; +C -1 ; WX 600 ; N ucircumflex ; B 70 -15 597 657 ; +C -1 ; WX 600 ; N acircumflex ; B 61 -15 607 657 ; +C -1 ; WX 600 ; N Amacron ; B -9 0 633 708 ; +C -1 ; WX 600 ; N rcaron ; B 47 0 655 667 ; +C -1 ; WX 600 ; N ccedilla ; B 81 -206 631 459 ; +C -1 ; WX 600 ; N Zdotaccent ; B 62 0 637 761 ; +C -1 ; WX 600 ; N Thorn ; B 48 0 620 562 ; +C -1 ; WX 600 ; N Omacron ; B 74 -18 663 708 ; +C -1 ; WX 600 ; N Racute ; B 24 0 665 784 ; +C -1 ; WX 600 ; N Sacute ; B 54 -22 673 784 ; +C -1 ; WX 600 ; N dcaron ; B 60 -15 861 626 ; +C -1 ; WX 600 ; N Umacron ; B 101 -18 716 708 ; +C -1 ; WX 600 ; N uring ; B 70 -15 592 678 ; +C -1 ; WX 600 ; N threesuperior ; B 193 222 526 616 ; +C -1 ; WX 600 ; N Ograve ; B 74 -18 645 784 ; +C -1 ; WX 600 ; N Agrave ; B -9 0 632 784 ; +C -1 ; WX 600 ; N Abreve ; B -9 0 684 784 ; +C -1 ; WX 600 ; N multiply ; B 104 39 606 478 ; +C -1 ; WX 600 ; N uacute ; B 70 -15 599 661 ; +C -1 ; WX 600 ; N Tcaron ; B 86 0 679 790 ; +C -1 ; WX 600 ; N partialdiff ; B 91 -38 627 728 ; +C -1 ; WX 600 ; N ydieresis ; B -21 -142 695 638 ; +C -1 ; WX 600 ; N Nacute ; B 8 -12 730 784 ; +C -1 ; WX 600 ; N icircumflex ; B 77 0 577 657 ; +C -1 ; WX 600 ; N Ecircumflex ; B 25 0 670 780 ; +C -1 ; WX 600 ; N adieresis ; B 61 -15 595 638 ; +C -1 ; WX 600 ; N edieresis ; B 81 -15 605 638 ; +C -1 ; WX 600 ; N cacute ; B 81 -15 649 661 ; +C -1 ; WX 600 ; N nacute ; B 18 0 639 661 ; +C -1 ; WX 600 ; N umacron ; B 70 -15 637 585 ; +C -1 ; WX 600 ; N Ncaron ; B 8 -12 730 790 ; +C -1 ; WX 600 ; N Iacute ; B 77 0 643 784 ; +C -1 ; WX 600 ; N plusminus ; B 76 24 614 515 ; +C -1 ; WX 600 ; N brokenbar ; B 217 -175 489 675 ; +C -1 ; WX 600 ; N registered ; B 53 -18 667 580 ; +C -1 ; WX 600 ; N Gbreve ; B 74 -18 684 784 ; +C -1 ; WX 600 ; N Idotaccent ; B 77 0 643 761 ; +C -1 ; WX 600 ; N summation ; B 15 -10 672 706 ; +C -1 ; WX 600 ; N Egrave ; B 25 0 670 784 ; +C -1 ; WX 600 ; N racute ; B 47 0 655 661 ; +C -1 ; WX 600 ; N omacron ; B 71 -15 637 585 ; +C -1 ; WX 600 ; N Zacute ; B 62 0 665 784 ; +C -1 ; WX 600 ; N Zcaron ; B 62 0 659 790 ; +C -1 ; WX 600 ; N greaterequal ; B 26 0 627 696 ; +C -1 ; WX 600 ; N Eth ; B 30 0 664 562 ; +C -1 ; WX 600 ; N Ccedilla ; B 74 -206 675 580 ; +C -1 ; WX 600 ; N lcommaaccent ; B 77 -250 546 626 ; +C -1 ; WX 600 ; N tcaron ; B 118 -15 627 703 ; +C -1 ; WX 600 ; N eogonek ; B 81 -199 605 454 ; +C -1 ; WX 600 ; N Uogonek ; B 101 -199 716 562 ; +C -1 ; WX 600 ; N Aacute ; B -9 0 655 784 ; +C -1 ; WX 600 ; N Adieresis ; B -9 0 632 761 ; +C -1 ; WX 600 ; N egrave ; B 81 -15 605 661 ; +C -1 ; WX 600 ; N zacute ; B 81 0 614 661 ; +C -1 ; WX 600 ; N iogonek ; B 77 -199 546 658 ; +C -1 ; WX 600 ; N Oacute ; B 74 -18 645 784 ; +C -1 ; WX 600 ; N oacute ; B 71 -15 649 661 ; +C -1 ; WX 600 ; N amacron ; B 61 -15 637 585 ; +C -1 ; WX 600 ; N sacute ; B 66 -17 609 661 ; +C -1 ; WX 600 ; N idieresis ; B 77 0 561 618 ; +C -1 ; WX 600 ; N Ocircumflex ; B 74 -18 645 780 ; +C -1 ; WX 600 ; N Ugrave ; B 101 -18 716 784 ; +C -1 ; WX 600 ; N Delta ; B 6 0 594 688 ; +C -1 ; WX 600 ; N thorn ; B -32 -142 622 626 ; +C -1 ; WX 600 ; N twosuperior ; B 191 230 542 616 ; +C -1 ; WX 600 ; N Odieresis ; B 74 -18 645 761 ; +C -1 ; WX 600 ; N mu ; B 49 -142 592 439 ; +C -1 ; WX 600 ; N igrave ; B 77 0 546 661 ; +C -1 ; WX 600 ; N ohungarumlaut ; B 71 -15 809 661 ; +C -1 ; WX 600 ; N Eogonek ; B 25 -199 670 562 ; +C -1 ; WX 600 ; N dcroat ; B 60 -15 712 626 ; +C -1 ; WX 600 ; N threequarters ; B 8 -60 699 661 ; +C -1 ; WX 600 ; N Scedilla ; B 54 -206 673 582 ; +C -1 ; WX 600 ; N lcaron ; B 77 0 731 626 ; +C -1 ; WX 600 ; N Kcommaaccent ; B 21 -250 692 562 ; +C -1 ; WX 600 ; N Lacute ; B 39 0 636 784 ; +C -1 ; WX 600 ; N trademark ; B 86 230 869 562 ; +C -1 ; WX 600 ; N edotaccent ; B 81 -15 605 638 ; +C -1 ; WX 600 ; N Igrave ; B 77 0 643 784 ; +C -1 ; WX 600 ; N Imacron ; B 77 0 663 708 ; +C -1 ; WX 600 ; N Lcaron ; B 39 0 757 562 ; +C -1 ; WX 600 ; N onehalf ; B 22 -60 716 661 ; +C -1 ; WX 600 ; N lessequal ; B 26 0 671 696 ; +C -1 ; WX 600 ; N ocircumflex ; B 71 -15 622 657 ; +C -1 ; WX 600 ; N ntilde ; B 18 0 643 636 ; +C -1 ; WX 600 ; N Uhungarumlaut ; B 101 -18 805 784 ; +C -1 ; WX 600 ; N Eacute ; B 25 0 670 784 ; +C -1 ; WX 600 ; N emacron ; B 81 -15 637 585 ; +C -1 ; WX 600 ; N gbreve ; B 40 -146 674 661 ; +C -1 ; WX 600 ; N onequarter ; B 13 -60 707 661 ; +C -1 ; WX 600 ; N Scaron ; B 54 -22 689 790 ; +C -1 ; WX 600 ; N Scommaaccent ; B 54 -250 673 582 ; +C -1 ; WX 600 ; N Ohungarumlaut ; B 74 -18 795 784 ; +C -1 ; WX 600 ; N degree ; B 173 243 570 616 ; +C -1 ; WX 600 ; N ograve ; B 71 -15 622 661 ; +C -1 ; WX 600 ; N Ccaron ; B 74 -18 689 790 ; +C -1 ; WX 600 ; N ugrave ; B 70 -15 592 661 ; +C -1 ; WX 600 ; N radical ; B 67 -104 635 778 ; +C -1 ; WX 600 ; N Dcaron ; B 30 0 664 790 ; +C -1 ; WX 600 ; N rcommaaccent ; B 47 -250 655 454 ; +C -1 ; WX 600 ; N Ntilde ; B 8 -12 730 759 ; +C -1 ; WX 600 ; N otilde ; B 71 -15 643 636 ; +C -1 ; WX 600 ; N Rcommaaccent ; B 24 -250 617 562 ; +C -1 ; WX 600 ; N Lcommaaccent ; B 39 -250 636 562 ; +C -1 ; WX 600 ; N Atilde ; B -9 0 669 759 ; +C -1 ; WX 600 ; N Aogonek ; B -9 -199 632 562 ; +C -1 ; WX 600 ; N Aring ; B -9 0 632 801 ; +C -1 ; WX 600 ; N Otilde ; B 74 -18 669 759 ; +C -1 ; WX 600 ; N zdotaccent ; B 81 0 614 638 ; +C -1 ; WX 600 ; N Ecaron ; B 25 0 670 790 ; +C -1 ; WX 600 ; N Iogonek ; B 77 -199 643 562 ; +C -1 ; WX 600 ; N kcommaaccent ; B 33 -250 643 626 ; +C -1 ; WX 600 ; N minus ; B 114 203 596 313 ; +C -1 ; WX 600 ; N Icircumflex ; B 77 0 643 780 ; +C -1 ; WX 600 ; N ncaron ; B 18 0 633 667 ; +C -1 ; WX 600 ; N tcommaaccent ; B 118 -250 567 562 ; +C -1 ; WX 600 ; N logicalnot ; B 135 103 617 413 ; +C -1 ; WX 600 ; N odieresis ; B 71 -15 622 638 ; +C -1 ; WX 600 ; N udieresis ; B 70 -15 595 638 ; +C -1 ; WX 600 ; N notequal ; B 30 -47 626 563 ; +C -1 ; WX 600 ; N gcommaaccent ; B 40 -146 674 714 ; +C -1 ; WX 600 ; N eth ; B 93 -27 661 626 ; +C -1 ; WX 600 ; N zcaron ; B 81 0 643 667 ; +C -1 ; WX 600 ; N ncommaaccent ; B 18 -250 615 454 ; +C -1 ; WX 600 ; N onesuperior ; B 212 230 514 616 ; +C -1 ; WX 600 ; N imacron ; B 77 0 575 585 ; +C -1 ; WX 600 ; N Euro ; B 0 0 0 0 ; +EndCharMetrics +EndFontMetrics diff --git a/internal/corefont/Core14_AFMs/Courier-Oblique.afm b/internal/corefont/Core14_AFMs/Courier-Oblique.afm new file mode 100644 index 0000000000000000000000000000000000000000..3dc163f771a7a5bfd4978f6cb09b3f9eb29c55c6 --- /dev/null +++ b/internal/corefont/Core14_AFMs/Courier-Oblique.afm @@ -0,0 +1,342 @@ +StartFontMetrics 4.1 +Comment Copyright (c) 1989, 1990, 1991, 1992, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved. +Comment Creation Date: Thu May 1 17:37:52 1997 +Comment UniqueID 43051 +Comment VMusage 16248 75829 +FontName Courier-Oblique +FullName Courier Oblique +FamilyName Courier +Weight Medium +ItalicAngle -12 +IsFixedPitch true +CharacterSet ExtendedRoman +FontBBox -27 -250 849 805 +UnderlinePosition -100 +UnderlineThickness 50 +Version 003.000 +Notice Copyright (c) 1989, 1990, 1991, 1992, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved. +EncodingScheme AdobeStandardEncoding +CapHeight 562 +XHeight 426 +Ascender 629 +Descender -157 +StdHW 51 +StdVW 51 +StartCharMetrics 315 +C 32 ; WX 600 ; N space ; B 0 0 0 0 ; +C 33 ; WX 600 ; N exclam ; B 243 -15 464 572 ; +C 34 ; WX 600 ; N quotedbl ; B 273 328 532 562 ; +C 35 ; WX 600 ; N numbersign ; B 133 -32 596 639 ; +C 36 ; WX 600 ; N dollar ; B 108 -126 596 662 ; +C 37 ; WX 600 ; N percent ; B 134 -15 599 622 ; +C 38 ; WX 600 ; N ampersand ; B 87 -15 580 543 ; +C 39 ; WX 600 ; N quoteright ; B 283 328 495 562 ; +C 40 ; WX 600 ; N parenleft ; B 313 -108 572 622 ; +C 41 ; WX 600 ; N parenright ; B 137 -108 396 622 ; +C 42 ; WX 600 ; N asterisk ; B 212 257 580 607 ; +C 43 ; WX 600 ; N plus ; B 129 44 580 470 ; +C 44 ; WX 600 ; N comma ; B 157 -112 370 122 ; +C 45 ; WX 600 ; N hyphen ; B 152 231 558 285 ; +C 46 ; WX 600 ; N period ; B 238 -15 382 109 ; +C 47 ; WX 600 ; N slash ; B 112 -80 604 629 ; +C 48 ; WX 600 ; N zero ; B 154 -15 575 622 ; +C 49 ; WX 600 ; N one ; B 98 0 515 622 ; +C 50 ; WX 600 ; N two ; B 70 0 568 622 ; +C 51 ; WX 600 ; N three ; B 82 -15 538 622 ; +C 52 ; WX 600 ; N four ; B 108 0 541 622 ; +C 53 ; WX 600 ; N five ; B 99 -15 589 607 ; +C 54 ; WX 600 ; N six ; B 155 -15 629 622 ; +C 55 ; WX 600 ; N seven ; B 182 0 612 607 ; +C 56 ; WX 600 ; N eight ; B 132 -15 588 622 ; +C 57 ; WX 600 ; N nine ; B 93 -15 574 622 ; +C 58 ; WX 600 ; N colon ; B 238 -15 441 385 ; +C 59 ; WX 600 ; N semicolon ; B 157 -112 441 385 ; +C 60 ; WX 600 ; N less ; B 96 42 610 472 ; +C 61 ; WX 600 ; N equal ; B 109 138 600 376 ; +C 62 ; WX 600 ; N greater ; B 85 42 599 472 ; +C 63 ; WX 600 ; N question ; B 222 -15 583 572 ; +C 64 ; WX 600 ; N at ; B 127 -15 582 622 ; +C 65 ; WX 600 ; N A ; B 3 0 607 562 ; +C 66 ; WX 600 ; N B ; B 43 0 616 562 ; +C 67 ; WX 600 ; N C ; B 93 -18 655 580 ; +C 68 ; WX 600 ; N D ; B 43 0 645 562 ; +C 69 ; WX 600 ; N E ; B 53 0 660 562 ; +C 70 ; WX 600 ; N F ; B 53 0 660 562 ; +C 71 ; WX 600 ; N G ; B 83 -18 645 580 ; +C 72 ; WX 600 ; N H ; B 32 0 687 562 ; +C 73 ; WX 600 ; N I ; B 96 0 623 562 ; +C 74 ; WX 600 ; N J ; B 52 -18 685 562 ; +C 75 ; WX 600 ; N K ; B 38 0 671 562 ; +C 76 ; WX 600 ; N L ; B 47 0 607 562 ; +C 77 ; WX 600 ; N M ; B 4 0 715 562 ; +C 78 ; WX 600 ; N N ; B 7 -13 712 562 ; +C 79 ; WX 600 ; N O ; B 94 -18 625 580 ; +C 80 ; WX 600 ; N P ; B 79 0 644 562 ; +C 81 ; WX 600 ; N Q ; B 95 -138 625 580 ; +C 82 ; WX 600 ; N R ; B 38 0 598 562 ; +C 83 ; WX 600 ; N S ; B 76 -20 650 580 ; +C 84 ; WX 600 ; N T ; B 108 0 665 562 ; +C 85 ; WX 600 ; N U ; B 125 -18 702 562 ; +C 86 ; WX 600 ; N V ; B 105 -13 723 562 ; +C 87 ; WX 600 ; N W ; B 106 -13 722 562 ; +C 88 ; WX 600 ; N X ; B 23 0 675 562 ; +C 89 ; WX 600 ; N Y ; B 133 0 695 562 ; +C 90 ; WX 600 ; N Z ; B 86 0 610 562 ; +C 91 ; WX 600 ; N bracketleft ; B 246 -108 574 622 ; +C 92 ; WX 600 ; N backslash ; B 249 -80 468 629 ; +C 93 ; WX 600 ; N bracketright ; B 135 -108 463 622 ; +C 94 ; WX 600 ; N asciicircum ; B 175 354 587 622 ; +C 95 ; WX 600 ; N underscore ; B -27 -125 584 -75 ; +C 96 ; WX 600 ; N quoteleft ; B 343 328 457 562 ; +C 97 ; WX 600 ; N a ; B 76 -15 569 441 ; +C 98 ; WX 600 ; N b ; B 29 -15 625 629 ; +C 99 ; WX 600 ; N c ; B 106 -15 608 441 ; +C 100 ; WX 600 ; N d ; B 85 -15 640 629 ; +C 101 ; WX 600 ; N e ; B 106 -15 598 441 ; +C 102 ; WX 600 ; N f ; B 114 0 662 629 ; L i fi ; L l fl ; +C 103 ; WX 600 ; N g ; B 61 -157 657 441 ; +C 104 ; WX 600 ; N h ; B 33 0 592 629 ; +C 105 ; WX 600 ; N i ; B 95 0 515 657 ; +C 106 ; WX 600 ; N j ; B 52 -157 550 657 ; +C 107 ; WX 600 ; N k ; B 58 0 633 629 ; +C 108 ; WX 600 ; N l ; B 95 0 515 629 ; +C 109 ; WX 600 ; N m ; B -5 0 615 441 ; +C 110 ; WX 600 ; N n ; B 26 0 585 441 ; +C 111 ; WX 600 ; N o ; B 102 -15 588 441 ; +C 112 ; WX 600 ; N p ; B -24 -157 605 441 ; +C 113 ; WX 600 ; N q ; B 85 -157 682 441 ; +C 114 ; WX 600 ; N r ; B 60 0 636 441 ; +C 115 ; WX 600 ; N s ; B 78 -15 584 441 ; +C 116 ; WX 600 ; N t ; B 167 -15 561 561 ; +C 117 ; WX 600 ; N u ; B 101 -15 572 426 ; +C 118 ; WX 600 ; N v ; B 90 -10 681 426 ; +C 119 ; WX 600 ; N w ; B 76 -10 695 426 ; +C 120 ; WX 600 ; N x ; B 20 0 655 426 ; +C 121 ; WX 600 ; N y ; B -4 -157 683 426 ; +C 122 ; WX 600 ; N z ; B 99 0 593 426 ; +C 123 ; WX 600 ; N braceleft ; B 233 -108 569 622 ; +C 124 ; WX 600 ; N bar ; B 222 -250 485 750 ; +C 125 ; WX 600 ; N braceright ; B 140 -108 477 622 ; +C 126 ; WX 600 ; N asciitilde ; B 116 197 600 320 ; +C 161 ; WX 600 ; N exclamdown ; B 225 -157 445 430 ; +C 162 ; WX 600 ; N cent ; B 151 -49 588 614 ; +C 163 ; WX 600 ; N sterling ; B 124 -21 621 611 ; +C 164 ; WX 600 ; N fraction ; B 84 -57 646 665 ; +C 165 ; WX 600 ; N yen ; B 120 0 693 562 ; +C 166 ; WX 600 ; N florin ; B -26 -143 671 622 ; +C 167 ; WX 600 ; N section ; B 104 -78 590 580 ; +C 168 ; WX 600 ; N currency ; B 94 58 628 506 ; +C 169 ; WX 600 ; N quotesingle ; B 345 328 460 562 ; +C 170 ; WX 600 ; N quotedblleft ; B 262 328 541 562 ; +C 171 ; WX 600 ; N guillemotleft ; B 92 70 652 446 ; +C 172 ; WX 600 ; N guilsinglleft ; B 204 70 540 446 ; +C 173 ; WX 600 ; N guilsinglright ; B 170 70 506 446 ; +C 174 ; WX 600 ; N fi ; B 3 0 619 629 ; +C 175 ; WX 600 ; N fl ; B 3 0 619 629 ; +C 177 ; WX 600 ; N endash ; B 124 231 586 285 ; +C 178 ; WX 600 ; N dagger ; B 217 -78 546 580 ; +C 179 ; WX 600 ; N daggerdbl ; B 163 -78 546 580 ; +C 180 ; WX 600 ; N periodcentered ; B 275 189 434 327 ; +C 182 ; WX 600 ; N paragraph ; B 100 -78 630 562 ; +C 183 ; WX 600 ; N bullet ; B 224 130 485 383 ; +C 184 ; WX 600 ; N quotesinglbase ; B 185 -134 397 100 ; +C 185 ; WX 600 ; N quotedblbase ; B 115 -134 478 100 ; +C 186 ; WX 600 ; N quotedblright ; B 213 328 576 562 ; +C 187 ; WX 600 ; N guillemotright ; B 58 70 618 446 ; +C 188 ; WX 600 ; N ellipsis ; B 46 -15 575 111 ; +C 189 ; WX 600 ; N perthousand ; B 59 -15 627 622 ; +C 191 ; WX 600 ; N questiondown ; B 105 -157 466 430 ; +C 193 ; WX 600 ; N grave ; B 294 497 484 672 ; +C 194 ; WX 600 ; N acute ; B 348 497 612 672 ; +C 195 ; WX 600 ; N circumflex ; B 229 477 581 654 ; +C 196 ; WX 600 ; N tilde ; B 212 489 629 606 ; +C 197 ; WX 600 ; N macron ; B 232 525 600 565 ; +C 198 ; WX 600 ; N breve ; B 279 501 576 609 ; +C 199 ; WX 600 ; N dotaccent ; B 373 537 478 640 ; +C 200 ; WX 600 ; N dieresis ; B 272 537 579 640 ; +C 202 ; WX 600 ; N ring ; B 332 463 500 627 ; +C 203 ; WX 600 ; N cedilla ; B 197 -151 344 10 ; +C 205 ; WX 600 ; N hungarumlaut ; B 239 497 683 672 ; +C 206 ; WX 600 ; N ogonek ; B 189 -172 377 4 ; +C 207 ; WX 600 ; N caron ; B 262 492 614 669 ; +C 208 ; WX 600 ; N emdash ; B 49 231 661 285 ; +C 225 ; WX 600 ; N AE ; B 3 0 655 562 ; +C 227 ; WX 600 ; N ordfeminine ; B 209 249 512 580 ; +C 232 ; WX 600 ; N Lslash ; B 47 0 607 562 ; +C 233 ; WX 600 ; N Oslash ; B 94 -80 625 629 ; +C 234 ; WX 600 ; N OE ; B 59 0 672 562 ; +C 235 ; WX 600 ; N ordmasculine ; B 210 249 535 580 ; +C 241 ; WX 600 ; N ae ; B 41 -15 626 441 ; +C 245 ; WX 600 ; N dotlessi ; B 95 0 515 426 ; +C 248 ; WX 600 ; N lslash ; B 95 0 587 629 ; +C 249 ; WX 600 ; N oslash ; B 102 -80 588 506 ; +C 250 ; WX 600 ; N oe ; B 54 -15 615 441 ; +C 251 ; WX 600 ; N germandbls ; B 48 -15 617 629 ; +C -1 ; WX 600 ; N Idieresis ; B 96 0 623 753 ; +C -1 ; WX 600 ; N eacute ; B 106 -15 612 672 ; +C -1 ; WX 600 ; N abreve ; B 76 -15 576 609 ; +C -1 ; WX 600 ; N uhungarumlaut ; B 101 -15 723 672 ; +C -1 ; WX 600 ; N ecaron ; B 106 -15 614 669 ; +C -1 ; WX 600 ; N Ydieresis ; B 133 0 695 753 ; +C -1 ; WX 600 ; N divide ; B 136 48 573 467 ; +C -1 ; WX 600 ; N Yacute ; B 133 0 695 805 ; +C -1 ; WX 600 ; N Acircumflex ; B 3 0 607 787 ; +C -1 ; WX 600 ; N aacute ; B 76 -15 612 672 ; +C -1 ; WX 600 ; N Ucircumflex ; B 125 -18 702 787 ; +C -1 ; WX 600 ; N yacute ; B -4 -157 683 672 ; +C -1 ; WX 600 ; N scommaaccent ; B 78 -250 584 441 ; +C -1 ; WX 600 ; N ecircumflex ; B 106 -15 598 654 ; +C -1 ; WX 600 ; N Uring ; B 125 -18 702 760 ; +C -1 ; WX 600 ; N Udieresis ; B 125 -18 702 753 ; +C -1 ; WX 600 ; N aogonek ; B 76 -172 569 441 ; +C -1 ; WX 600 ; N Uacute ; B 125 -18 702 805 ; +C -1 ; WX 600 ; N uogonek ; B 101 -172 572 426 ; +C -1 ; WX 600 ; N Edieresis ; B 53 0 660 753 ; +C -1 ; WX 600 ; N Dcroat ; B 43 0 645 562 ; +C -1 ; WX 600 ; N commaaccent ; B 145 -250 323 -58 ; +C -1 ; WX 600 ; N copyright ; B 53 -18 667 580 ; +C -1 ; WX 600 ; N Emacron ; B 53 0 660 698 ; +C -1 ; WX 600 ; N ccaron ; B 106 -15 614 669 ; +C -1 ; WX 600 ; N aring ; B 76 -15 569 627 ; +C -1 ; WX 600 ; N Ncommaaccent ; B 7 -250 712 562 ; +C -1 ; WX 600 ; N lacute ; B 95 0 640 805 ; +C -1 ; WX 600 ; N agrave ; B 76 -15 569 672 ; +C -1 ; WX 600 ; N Tcommaaccent ; B 108 -250 665 562 ; +C -1 ; WX 600 ; N Cacute ; B 93 -18 655 805 ; +C -1 ; WX 600 ; N atilde ; B 76 -15 629 606 ; +C -1 ; WX 600 ; N Edotaccent ; B 53 0 660 753 ; +C -1 ; WX 600 ; N scaron ; B 78 -15 614 669 ; +C -1 ; WX 600 ; N scedilla ; B 78 -151 584 441 ; +C -1 ; WX 600 ; N iacute ; B 95 0 612 672 ; +C -1 ; WX 600 ; N lozenge ; B 94 0 519 706 ; +C -1 ; WX 600 ; N Rcaron ; B 38 0 642 802 ; +C -1 ; WX 600 ; N Gcommaaccent ; B 83 -250 645 580 ; +C -1 ; WX 600 ; N ucircumflex ; B 101 -15 572 654 ; +C -1 ; WX 600 ; N acircumflex ; B 76 -15 581 654 ; +C -1 ; WX 600 ; N Amacron ; B 3 0 607 698 ; +C -1 ; WX 600 ; N rcaron ; B 60 0 636 669 ; +C -1 ; WX 600 ; N ccedilla ; B 106 -151 614 441 ; +C -1 ; WX 600 ; N Zdotaccent ; B 86 0 610 753 ; +C -1 ; WX 600 ; N Thorn ; B 79 0 606 562 ; +C -1 ; WX 600 ; N Omacron ; B 94 -18 628 698 ; +C -1 ; WX 600 ; N Racute ; B 38 0 670 805 ; +C -1 ; WX 600 ; N Sacute ; B 76 -20 650 805 ; +C -1 ; WX 600 ; N dcaron ; B 85 -15 849 629 ; +C -1 ; WX 600 ; N Umacron ; B 125 -18 702 698 ; +C -1 ; WX 600 ; N uring ; B 101 -15 572 627 ; +C -1 ; WX 600 ; N threesuperior ; B 213 240 501 622 ; +C -1 ; WX 600 ; N Ograve ; B 94 -18 625 805 ; +C -1 ; WX 600 ; N Agrave ; B 3 0 607 805 ; +C -1 ; WX 600 ; N Abreve ; B 3 0 607 732 ; +C -1 ; WX 600 ; N multiply ; B 103 43 607 470 ; +C -1 ; WX 600 ; N uacute ; B 101 -15 602 672 ; +C -1 ; WX 600 ; N Tcaron ; B 108 0 665 802 ; +C -1 ; WX 600 ; N partialdiff ; B 45 -38 546 710 ; +C -1 ; WX 600 ; N ydieresis ; B -4 -157 683 620 ; +C -1 ; WX 600 ; N Nacute ; B 7 -13 712 805 ; +C -1 ; WX 600 ; N icircumflex ; B 95 0 551 654 ; +C -1 ; WX 600 ; N Ecircumflex ; B 53 0 660 787 ; +C -1 ; WX 600 ; N adieresis ; B 76 -15 575 620 ; +C -1 ; WX 600 ; N edieresis ; B 106 -15 598 620 ; +C -1 ; WX 600 ; N cacute ; B 106 -15 612 672 ; +C -1 ; WX 600 ; N nacute ; B 26 0 602 672 ; +C -1 ; WX 600 ; N umacron ; B 101 -15 600 565 ; +C -1 ; WX 600 ; N Ncaron ; B 7 -13 712 802 ; +C -1 ; WX 600 ; N Iacute ; B 96 0 640 805 ; +C -1 ; WX 600 ; N plusminus ; B 96 44 594 558 ; +C -1 ; WX 600 ; N brokenbar ; B 238 -175 469 675 ; +C -1 ; WX 600 ; N registered ; B 53 -18 667 580 ; +C -1 ; WX 600 ; N Gbreve ; B 83 -18 645 732 ; +C -1 ; WX 600 ; N Idotaccent ; B 96 0 623 753 ; +C -1 ; WX 600 ; N summation ; B 15 -10 670 706 ; +C -1 ; WX 600 ; N Egrave ; B 53 0 660 805 ; +C -1 ; WX 600 ; N racute ; B 60 0 636 672 ; +C -1 ; WX 600 ; N omacron ; B 102 -15 600 565 ; +C -1 ; WX 600 ; N Zacute ; B 86 0 670 805 ; +C -1 ; WX 600 ; N Zcaron ; B 86 0 642 802 ; +C -1 ; WX 600 ; N greaterequal ; B 98 0 594 710 ; +C -1 ; WX 600 ; N Eth ; B 43 0 645 562 ; +C -1 ; WX 600 ; N Ccedilla ; B 93 -151 658 580 ; +C -1 ; WX 600 ; N lcommaaccent ; B 95 -250 515 629 ; +C -1 ; WX 600 ; N tcaron ; B 167 -15 587 717 ; +C -1 ; WX 600 ; N eogonek ; B 106 -172 598 441 ; +C -1 ; WX 600 ; N Uogonek ; B 124 -172 702 562 ; +C -1 ; WX 600 ; N Aacute ; B 3 0 660 805 ; +C -1 ; WX 600 ; N Adieresis ; B 3 0 607 753 ; +C -1 ; WX 600 ; N egrave ; B 106 -15 598 672 ; +C -1 ; WX 600 ; N zacute ; B 99 0 612 672 ; +C -1 ; WX 600 ; N iogonek ; B 95 -172 515 657 ; +C -1 ; WX 600 ; N Oacute ; B 94 -18 640 805 ; +C -1 ; WX 600 ; N oacute ; B 102 -15 612 672 ; +C -1 ; WX 600 ; N amacron ; B 76 -15 600 565 ; +C -1 ; WX 600 ; N sacute ; B 78 -15 612 672 ; +C -1 ; WX 600 ; N idieresis ; B 95 0 545 620 ; +C -1 ; WX 600 ; N Ocircumflex ; B 94 -18 625 787 ; +C -1 ; WX 600 ; N Ugrave ; B 125 -18 702 805 ; +C -1 ; WX 600 ; N Delta ; B 6 0 598 688 ; +C -1 ; WX 600 ; N thorn ; B -24 -157 605 629 ; +C -1 ; WX 600 ; N twosuperior ; B 230 249 535 622 ; +C -1 ; WX 600 ; N Odieresis ; B 94 -18 625 753 ; +C -1 ; WX 600 ; N mu ; B 72 -157 572 426 ; +C -1 ; WX 600 ; N igrave ; B 95 0 515 672 ; +C -1 ; WX 600 ; N ohungarumlaut ; B 102 -15 723 672 ; +C -1 ; WX 600 ; N Eogonek ; B 53 -172 660 562 ; +C -1 ; WX 600 ; N dcroat ; B 85 -15 704 629 ; +C -1 ; WX 600 ; N threequarters ; B 73 -56 659 666 ; +C -1 ; WX 600 ; N Scedilla ; B 76 -151 650 580 ; +C -1 ; WX 600 ; N lcaron ; B 95 0 667 629 ; +C -1 ; WX 600 ; N Kcommaaccent ; B 38 -250 671 562 ; +C -1 ; WX 600 ; N Lacute ; B 47 0 607 805 ; +C -1 ; WX 600 ; N trademark ; B 75 263 742 562 ; +C -1 ; WX 600 ; N edotaccent ; B 106 -15 598 620 ; +C -1 ; WX 600 ; N Igrave ; B 96 0 623 805 ; +C -1 ; WX 600 ; N Imacron ; B 96 0 628 698 ; +C -1 ; WX 600 ; N Lcaron ; B 47 0 632 562 ; +C -1 ; WX 600 ; N onehalf ; B 65 -57 669 665 ; +C -1 ; WX 600 ; N lessequal ; B 98 0 645 710 ; +C -1 ; WX 600 ; N ocircumflex ; B 102 -15 588 654 ; +C -1 ; WX 600 ; N ntilde ; B 26 0 629 606 ; +C -1 ; WX 600 ; N Uhungarumlaut ; B 125 -18 761 805 ; +C -1 ; WX 600 ; N Eacute ; B 53 0 670 805 ; +C -1 ; WX 600 ; N emacron ; B 106 -15 600 565 ; +C -1 ; WX 600 ; N gbreve ; B 61 -157 657 609 ; +C -1 ; WX 600 ; N onequarter ; B 65 -57 674 665 ; +C -1 ; WX 600 ; N Scaron ; B 76 -20 672 802 ; +C -1 ; WX 600 ; N Scommaaccent ; B 76 -250 650 580 ; +C -1 ; WX 600 ; N Ohungarumlaut ; B 94 -18 751 805 ; +C -1 ; WX 600 ; N degree ; B 214 269 576 622 ; +C -1 ; WX 600 ; N ograve ; B 102 -15 588 672 ; +C -1 ; WX 600 ; N Ccaron ; B 93 -18 672 802 ; +C -1 ; WX 600 ; N ugrave ; B 101 -15 572 672 ; +C -1 ; WX 600 ; N radical ; B 85 -15 765 792 ; +C -1 ; WX 600 ; N Dcaron ; B 43 0 645 802 ; +C -1 ; WX 600 ; N rcommaaccent ; B 60 -250 636 441 ; +C -1 ; WX 600 ; N Ntilde ; B 7 -13 712 729 ; +C -1 ; WX 600 ; N otilde ; B 102 -15 629 606 ; +C -1 ; WX 600 ; N Rcommaaccent ; B 38 -250 598 562 ; +C -1 ; WX 600 ; N Lcommaaccent ; B 47 -250 607 562 ; +C -1 ; WX 600 ; N Atilde ; B 3 0 655 729 ; +C -1 ; WX 600 ; N Aogonek ; B 3 -172 607 562 ; +C -1 ; WX 600 ; N Aring ; B 3 0 607 750 ; +C -1 ; WX 600 ; N Otilde ; B 94 -18 655 729 ; +C -1 ; WX 600 ; N zdotaccent ; B 99 0 593 620 ; +C -1 ; WX 600 ; N Ecaron ; B 53 0 660 802 ; +C -1 ; WX 600 ; N Iogonek ; B 96 -172 623 562 ; +C -1 ; WX 600 ; N kcommaaccent ; B 58 -250 633 629 ; +C -1 ; WX 600 ; N minus ; B 129 232 580 283 ; +C -1 ; WX 600 ; N Icircumflex ; B 96 0 623 787 ; +C -1 ; WX 600 ; N ncaron ; B 26 0 614 669 ; +C -1 ; WX 600 ; N tcommaaccent ; B 165 -250 561 561 ; +C -1 ; WX 600 ; N logicalnot ; B 155 108 591 369 ; +C -1 ; WX 600 ; N odieresis ; B 102 -15 588 620 ; +C -1 ; WX 600 ; N udieresis ; B 101 -15 575 620 ; +C -1 ; WX 600 ; N notequal ; B 43 -16 621 529 ; +C -1 ; WX 600 ; N gcommaaccent ; B 61 -157 657 708 ; +C -1 ; WX 600 ; N eth ; B 102 -15 639 629 ; +C -1 ; WX 600 ; N zcaron ; B 99 0 624 669 ; +C -1 ; WX 600 ; N ncommaaccent ; B 26 -250 585 441 ; +C -1 ; WX 600 ; N onesuperior ; B 231 249 491 622 ; +C -1 ; WX 600 ; N imacron ; B 95 0 543 565 ; +C -1 ; WX 600 ; N Euro ; B 0 0 0 0 ; +EndCharMetrics +EndFontMetrics diff --git a/internal/corefont/Core14_AFMs/Courier.afm b/internal/corefont/Core14_AFMs/Courier.afm new file mode 100644 index 0000000000000000000000000000000000000000..2f7be81d583f0f95cda1c73f5604d0ee5425019c --- /dev/null +++ b/internal/corefont/Core14_AFMs/Courier.afm @@ -0,0 +1,342 @@ +StartFontMetrics 4.1 +Comment Copyright (c) 1989, 1990, 1991, 1992, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved. +Comment Creation Date: Thu May 1 17:27:09 1997 +Comment UniqueID 43050 +Comment VMusage 39754 50779 +FontName Courier +FullName Courier +FamilyName Courier +Weight Medium +ItalicAngle 0 +IsFixedPitch true +CharacterSet ExtendedRoman +FontBBox -23 -250 715 805 +UnderlinePosition -100 +UnderlineThickness 50 +Version 003.000 +Notice Copyright (c) 1989, 1990, 1991, 1992, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved. +EncodingScheme AdobeStandardEncoding +CapHeight 562 +XHeight 426 +Ascender 629 +Descender -157 +StdHW 51 +StdVW 51 +StartCharMetrics 315 +C 32 ; WX 600 ; N space ; B 0 0 0 0 ; +C 33 ; WX 600 ; N exclam ; B 236 -15 364 572 ; +C 34 ; WX 600 ; N quotedbl ; B 187 328 413 562 ; +C 35 ; WX 600 ; N numbersign ; B 93 -32 507 639 ; +C 36 ; WX 600 ; N dollar ; B 105 -126 496 662 ; +C 37 ; WX 600 ; N percent ; B 81 -15 518 622 ; +C 38 ; WX 600 ; N ampersand ; B 63 -15 538 543 ; +C 39 ; WX 600 ; N quoteright ; B 213 328 376 562 ; +C 40 ; WX 600 ; N parenleft ; B 269 -108 440 622 ; +C 41 ; WX 600 ; N parenright ; B 160 -108 331 622 ; +C 42 ; WX 600 ; N asterisk ; B 116 257 484 607 ; +C 43 ; WX 600 ; N plus ; B 80 44 520 470 ; +C 44 ; WX 600 ; N comma ; B 181 -112 344 122 ; +C 45 ; WX 600 ; N hyphen ; B 103 231 497 285 ; +C 46 ; WX 600 ; N period ; B 229 -15 371 109 ; +C 47 ; WX 600 ; N slash ; B 125 -80 475 629 ; +C 48 ; WX 600 ; N zero ; B 106 -15 494 622 ; +C 49 ; WX 600 ; N one ; B 96 0 505 622 ; +C 50 ; WX 600 ; N two ; B 70 0 471 622 ; +C 51 ; WX 600 ; N three ; B 75 -15 466 622 ; +C 52 ; WX 600 ; N four ; B 78 0 500 622 ; +C 53 ; WX 600 ; N five ; B 92 -15 497 607 ; +C 54 ; WX 600 ; N six ; B 111 -15 497 622 ; +C 55 ; WX 600 ; N seven ; B 82 0 483 607 ; +C 56 ; WX 600 ; N eight ; B 102 -15 498 622 ; +C 57 ; WX 600 ; N nine ; B 96 -15 489 622 ; +C 58 ; WX 600 ; N colon ; B 229 -15 371 385 ; +C 59 ; WX 600 ; N semicolon ; B 181 -112 371 385 ; +C 60 ; WX 600 ; N less ; B 41 42 519 472 ; +C 61 ; WX 600 ; N equal ; B 80 138 520 376 ; +C 62 ; WX 600 ; N greater ; B 66 42 544 472 ; +C 63 ; WX 600 ; N question ; B 129 -15 492 572 ; +C 64 ; WX 600 ; N at ; B 77 -15 533 622 ; +C 65 ; WX 600 ; N A ; B 3 0 597 562 ; +C 66 ; WX 600 ; N B ; B 43 0 559 562 ; +C 67 ; WX 600 ; N C ; B 41 -18 540 580 ; +C 68 ; WX 600 ; N D ; B 43 0 574 562 ; +C 69 ; WX 600 ; N E ; B 53 0 550 562 ; +C 70 ; WX 600 ; N F ; B 53 0 545 562 ; +C 71 ; WX 600 ; N G ; B 31 -18 575 580 ; +C 72 ; WX 600 ; N H ; B 32 0 568 562 ; +C 73 ; WX 600 ; N I ; B 96 0 504 562 ; +C 74 ; WX 600 ; N J ; B 34 -18 566 562 ; +C 75 ; WX 600 ; N K ; B 38 0 582 562 ; +C 76 ; WX 600 ; N L ; B 47 0 554 562 ; +C 77 ; WX 600 ; N M ; B 4 0 596 562 ; +C 78 ; WX 600 ; N N ; B 7 -13 593 562 ; +C 79 ; WX 600 ; N O ; B 43 -18 557 580 ; +C 80 ; WX 600 ; N P ; B 79 0 558 562 ; +C 81 ; WX 600 ; N Q ; B 43 -138 557 580 ; +C 82 ; WX 600 ; N R ; B 38 0 588 562 ; +C 83 ; WX 600 ; N S ; B 72 -20 529 580 ; +C 84 ; WX 600 ; N T ; B 38 0 563 562 ; +C 85 ; WX 600 ; N U ; B 17 -18 583 562 ; +C 86 ; WX 600 ; N V ; B -4 -13 604 562 ; +C 87 ; WX 600 ; N W ; B -3 -13 603 562 ; +C 88 ; WX 600 ; N X ; B 23 0 577 562 ; +C 89 ; WX 600 ; N Y ; B 24 0 576 562 ; +C 90 ; WX 600 ; N Z ; B 86 0 514 562 ; +C 91 ; WX 600 ; N bracketleft ; B 269 -108 442 622 ; +C 92 ; WX 600 ; N backslash ; B 118 -80 482 629 ; +C 93 ; WX 600 ; N bracketright ; B 158 -108 331 622 ; +C 94 ; WX 600 ; N asciicircum ; B 94 354 506 622 ; +C 95 ; WX 600 ; N underscore ; B 0 -125 600 -75 ; +C 96 ; WX 600 ; N quoteleft ; B 224 328 387 562 ; +C 97 ; WX 600 ; N a ; B 53 -15 559 441 ; +C 98 ; WX 600 ; N b ; B 14 -15 575 629 ; +C 99 ; WX 600 ; N c ; B 66 -15 529 441 ; +C 100 ; WX 600 ; N d ; B 45 -15 591 629 ; +C 101 ; WX 600 ; N e ; B 66 -15 548 441 ; +C 102 ; WX 600 ; N f ; B 114 0 531 629 ; L i fi ; L l fl ; +C 103 ; WX 600 ; N g ; B 45 -157 566 441 ; +C 104 ; WX 600 ; N h ; B 18 0 582 629 ; +C 105 ; WX 600 ; N i ; B 95 0 505 657 ; +C 106 ; WX 600 ; N j ; B 82 -157 410 657 ; +C 107 ; WX 600 ; N k ; B 43 0 580 629 ; +C 108 ; WX 600 ; N l ; B 95 0 505 629 ; +C 109 ; WX 600 ; N m ; B -5 0 605 441 ; +C 110 ; WX 600 ; N n ; B 26 0 575 441 ; +C 111 ; WX 600 ; N o ; B 62 -15 538 441 ; +C 112 ; WX 600 ; N p ; B 9 -157 555 441 ; +C 113 ; WX 600 ; N q ; B 45 -157 591 441 ; +C 114 ; WX 600 ; N r ; B 60 0 559 441 ; +C 115 ; WX 600 ; N s ; B 80 -15 513 441 ; +C 116 ; WX 600 ; N t ; B 87 -15 530 561 ; +C 117 ; WX 600 ; N u ; B 21 -15 562 426 ; +C 118 ; WX 600 ; N v ; B 10 -10 590 426 ; +C 119 ; WX 600 ; N w ; B -4 -10 604 426 ; +C 120 ; WX 600 ; N x ; B 20 0 580 426 ; +C 121 ; WX 600 ; N y ; B 7 -157 592 426 ; +C 122 ; WX 600 ; N z ; B 99 0 502 426 ; +C 123 ; WX 600 ; N braceleft ; B 182 -108 437 622 ; +C 124 ; WX 600 ; N bar ; B 275 -250 326 750 ; +C 125 ; WX 600 ; N braceright ; B 163 -108 418 622 ; +C 126 ; WX 600 ; N asciitilde ; B 63 197 540 320 ; +C 161 ; WX 600 ; N exclamdown ; B 236 -157 364 430 ; +C 162 ; WX 600 ; N cent ; B 96 -49 500 614 ; +C 163 ; WX 600 ; N sterling ; B 84 -21 521 611 ; +C 164 ; WX 600 ; N fraction ; B 92 -57 509 665 ; +C 165 ; WX 600 ; N yen ; B 26 0 574 562 ; +C 166 ; WX 600 ; N florin ; B 4 -143 539 622 ; +C 167 ; WX 600 ; N section ; B 113 -78 488 580 ; +C 168 ; WX 600 ; N currency ; B 73 58 527 506 ; +C 169 ; WX 600 ; N quotesingle ; B 259 328 341 562 ; +C 170 ; WX 600 ; N quotedblleft ; B 143 328 471 562 ; +C 171 ; WX 600 ; N guillemotleft ; B 37 70 563 446 ; +C 172 ; WX 600 ; N guilsinglleft ; B 149 70 451 446 ; +C 173 ; WX 600 ; N guilsinglright ; B 149 70 451 446 ; +C 174 ; WX 600 ; N fi ; B 3 0 597 629 ; +C 175 ; WX 600 ; N fl ; B 3 0 597 629 ; +C 177 ; WX 600 ; N endash ; B 75 231 525 285 ; +C 178 ; WX 600 ; N dagger ; B 141 -78 459 580 ; +C 179 ; WX 600 ; N daggerdbl ; B 141 -78 459 580 ; +C 180 ; WX 600 ; N periodcentered ; B 222 189 378 327 ; +C 182 ; WX 600 ; N paragraph ; B 50 -78 511 562 ; +C 183 ; WX 600 ; N bullet ; B 172 130 428 383 ; +C 184 ; WX 600 ; N quotesinglbase ; B 213 -134 376 100 ; +C 185 ; WX 600 ; N quotedblbase ; B 143 -134 457 100 ; +C 186 ; WX 600 ; N quotedblright ; B 143 328 457 562 ; +C 187 ; WX 600 ; N guillemotright ; B 37 70 563 446 ; +C 188 ; WX 600 ; N ellipsis ; B 37 -15 563 111 ; +C 189 ; WX 600 ; N perthousand ; B 3 -15 600 622 ; +C 191 ; WX 600 ; N questiondown ; B 108 -157 471 430 ; +C 193 ; WX 600 ; N grave ; B 151 497 378 672 ; +C 194 ; WX 600 ; N acute ; B 242 497 469 672 ; +C 195 ; WX 600 ; N circumflex ; B 124 477 476 654 ; +C 196 ; WX 600 ; N tilde ; B 105 489 503 606 ; +C 197 ; WX 600 ; N macron ; B 120 525 480 565 ; +C 198 ; WX 600 ; N breve ; B 153 501 447 609 ; +C 199 ; WX 600 ; N dotaccent ; B 249 537 352 640 ; +C 200 ; WX 600 ; N dieresis ; B 148 537 453 640 ; +C 202 ; WX 600 ; N ring ; B 218 463 382 627 ; +C 203 ; WX 600 ; N cedilla ; B 224 -151 362 10 ; +C 205 ; WX 600 ; N hungarumlaut ; B 133 497 540 672 ; +C 206 ; WX 600 ; N ogonek ; B 211 -172 407 4 ; +C 207 ; WX 600 ; N caron ; B 124 492 476 669 ; +C 208 ; WX 600 ; N emdash ; B 0 231 600 285 ; +C 225 ; WX 600 ; N AE ; B 3 0 550 562 ; +C 227 ; WX 600 ; N ordfeminine ; B 156 249 442 580 ; +C 232 ; WX 600 ; N Lslash ; B 47 0 554 562 ; +C 233 ; WX 600 ; N Oslash ; B 43 -80 557 629 ; +C 234 ; WX 600 ; N OE ; B 7 0 567 562 ; +C 235 ; WX 600 ; N ordmasculine ; B 157 249 443 580 ; +C 241 ; WX 600 ; N ae ; B 19 -15 570 441 ; +C 245 ; WX 600 ; N dotlessi ; B 95 0 505 426 ; +C 248 ; WX 600 ; N lslash ; B 95 0 505 629 ; +C 249 ; WX 600 ; N oslash ; B 62 -80 538 506 ; +C 250 ; WX 600 ; N oe ; B 19 -15 559 441 ; +C 251 ; WX 600 ; N germandbls ; B 48 -15 588 629 ; +C -1 ; WX 600 ; N Idieresis ; B 96 0 504 753 ; +C -1 ; WX 600 ; N eacute ; B 66 -15 548 672 ; +C -1 ; WX 600 ; N abreve ; B 53 -15 559 609 ; +C -1 ; WX 600 ; N uhungarumlaut ; B 21 -15 580 672 ; +C -1 ; WX 600 ; N ecaron ; B 66 -15 548 669 ; +C -1 ; WX 600 ; N Ydieresis ; B 24 0 576 753 ; +C -1 ; WX 600 ; N divide ; B 87 48 513 467 ; +C -1 ; WX 600 ; N Yacute ; B 24 0 576 805 ; +C -1 ; WX 600 ; N Acircumflex ; B 3 0 597 787 ; +C -1 ; WX 600 ; N aacute ; B 53 -15 559 672 ; +C -1 ; WX 600 ; N Ucircumflex ; B 17 -18 583 787 ; +C -1 ; WX 600 ; N yacute ; B 7 -157 592 672 ; +C -1 ; WX 600 ; N scommaaccent ; B 80 -250 513 441 ; +C -1 ; WX 600 ; N ecircumflex ; B 66 -15 548 654 ; +C -1 ; WX 600 ; N Uring ; B 17 -18 583 760 ; +C -1 ; WX 600 ; N Udieresis ; B 17 -18 583 753 ; +C -1 ; WX 600 ; N aogonek ; B 53 -172 587 441 ; +C -1 ; WX 600 ; N Uacute ; B 17 -18 583 805 ; +C -1 ; WX 600 ; N uogonek ; B 21 -172 590 426 ; +C -1 ; WX 600 ; N Edieresis ; B 53 0 550 753 ; +C -1 ; WX 600 ; N Dcroat ; B 30 0 574 562 ; +C -1 ; WX 600 ; N commaaccent ; B 198 -250 335 -58 ; +C -1 ; WX 600 ; N copyright ; B 0 -18 600 580 ; +C -1 ; WX 600 ; N Emacron ; B 53 0 550 698 ; +C -1 ; WX 600 ; N ccaron ; B 66 -15 529 669 ; +C -1 ; WX 600 ; N aring ; B 53 -15 559 627 ; +C -1 ; WX 600 ; N Ncommaaccent ; B 7 -250 593 562 ; +C -1 ; WX 600 ; N lacute ; B 95 0 505 805 ; +C -1 ; WX 600 ; N agrave ; B 53 -15 559 672 ; +C -1 ; WX 600 ; N Tcommaaccent ; B 38 -250 563 562 ; +C -1 ; WX 600 ; N Cacute ; B 41 -18 540 805 ; +C -1 ; WX 600 ; N atilde ; B 53 -15 559 606 ; +C -1 ; WX 600 ; N Edotaccent ; B 53 0 550 753 ; +C -1 ; WX 600 ; N scaron ; B 80 -15 513 669 ; +C -1 ; WX 600 ; N scedilla ; B 80 -151 513 441 ; +C -1 ; WX 600 ; N iacute ; B 95 0 505 672 ; +C -1 ; WX 600 ; N lozenge ; B 18 0 443 706 ; +C -1 ; WX 600 ; N Rcaron ; B 38 0 588 802 ; +C -1 ; WX 600 ; N Gcommaaccent ; B 31 -250 575 580 ; +C -1 ; WX 600 ; N ucircumflex ; B 21 -15 562 654 ; +C -1 ; WX 600 ; N acircumflex ; B 53 -15 559 654 ; +C -1 ; WX 600 ; N Amacron ; B 3 0 597 698 ; +C -1 ; WX 600 ; N rcaron ; B 60 0 559 669 ; +C -1 ; WX 600 ; N ccedilla ; B 66 -151 529 441 ; +C -1 ; WX 600 ; N Zdotaccent ; B 86 0 514 753 ; +C -1 ; WX 600 ; N Thorn ; B 79 0 538 562 ; +C -1 ; WX 600 ; N Omacron ; B 43 -18 557 698 ; +C -1 ; WX 600 ; N Racute ; B 38 0 588 805 ; +C -1 ; WX 600 ; N Sacute ; B 72 -20 529 805 ; +C -1 ; WX 600 ; N dcaron ; B 45 -15 715 629 ; +C -1 ; WX 600 ; N Umacron ; B 17 -18 583 698 ; +C -1 ; WX 600 ; N uring ; B 21 -15 562 627 ; +C -1 ; WX 600 ; N threesuperior ; B 155 240 406 622 ; +C -1 ; WX 600 ; N Ograve ; B 43 -18 557 805 ; +C -1 ; WX 600 ; N Agrave ; B 3 0 597 805 ; +C -1 ; WX 600 ; N Abreve ; B 3 0 597 732 ; +C -1 ; WX 600 ; N multiply ; B 87 43 515 470 ; +C -1 ; WX 600 ; N uacute ; B 21 -15 562 672 ; +C -1 ; WX 600 ; N Tcaron ; B 38 0 563 802 ; +C -1 ; WX 600 ; N partialdiff ; B 17 -38 459 710 ; +C -1 ; WX 600 ; N ydieresis ; B 7 -157 592 620 ; +C -1 ; WX 600 ; N Nacute ; B 7 -13 593 805 ; +C -1 ; WX 600 ; N icircumflex ; B 94 0 505 654 ; +C -1 ; WX 600 ; N Ecircumflex ; B 53 0 550 787 ; +C -1 ; WX 600 ; N adieresis ; B 53 -15 559 620 ; +C -1 ; WX 600 ; N edieresis ; B 66 -15 548 620 ; +C -1 ; WX 600 ; N cacute ; B 66 -15 529 672 ; +C -1 ; WX 600 ; N nacute ; B 26 0 575 672 ; +C -1 ; WX 600 ; N umacron ; B 21 -15 562 565 ; +C -1 ; WX 600 ; N Ncaron ; B 7 -13 593 802 ; +C -1 ; WX 600 ; N Iacute ; B 96 0 504 805 ; +C -1 ; WX 600 ; N plusminus ; B 87 44 513 558 ; +C -1 ; WX 600 ; N brokenbar ; B 275 -175 326 675 ; +C -1 ; WX 600 ; N registered ; B 0 -18 600 580 ; +C -1 ; WX 600 ; N Gbreve ; B 31 -18 575 732 ; +C -1 ; WX 600 ; N Idotaccent ; B 96 0 504 753 ; +C -1 ; WX 600 ; N summation ; B 15 -10 585 706 ; +C -1 ; WX 600 ; N Egrave ; B 53 0 550 805 ; +C -1 ; WX 600 ; N racute ; B 60 0 559 672 ; +C -1 ; WX 600 ; N omacron ; B 62 -15 538 565 ; +C -1 ; WX 600 ; N Zacute ; B 86 0 514 805 ; +C -1 ; WX 600 ; N Zcaron ; B 86 0 514 802 ; +C -1 ; WX 600 ; N greaterequal ; B 98 0 502 710 ; +C -1 ; WX 600 ; N Eth ; B 30 0 574 562 ; +C -1 ; WX 600 ; N Ccedilla ; B 41 -151 540 580 ; +C -1 ; WX 600 ; N lcommaaccent ; B 95 -250 505 629 ; +C -1 ; WX 600 ; N tcaron ; B 87 -15 530 717 ; +C -1 ; WX 600 ; N eogonek ; B 66 -172 548 441 ; +C -1 ; WX 600 ; N Uogonek ; B 17 -172 583 562 ; +C -1 ; WX 600 ; N Aacute ; B 3 0 597 805 ; +C -1 ; WX 600 ; N Adieresis ; B 3 0 597 753 ; +C -1 ; WX 600 ; N egrave ; B 66 -15 548 672 ; +C -1 ; WX 600 ; N zacute ; B 99 0 502 672 ; +C -1 ; WX 600 ; N iogonek ; B 95 -172 505 657 ; +C -1 ; WX 600 ; N Oacute ; B 43 -18 557 805 ; +C -1 ; WX 600 ; N oacute ; B 62 -15 538 672 ; +C -1 ; WX 600 ; N amacron ; B 53 -15 559 565 ; +C -1 ; WX 600 ; N sacute ; B 80 -15 513 672 ; +C -1 ; WX 600 ; N idieresis ; B 95 0 505 620 ; +C -1 ; WX 600 ; N Ocircumflex ; B 43 -18 557 787 ; +C -1 ; WX 600 ; N Ugrave ; B 17 -18 583 805 ; +C -1 ; WX 600 ; N Delta ; B 6 0 598 688 ; +C -1 ; WX 600 ; N thorn ; B -6 -157 555 629 ; +C -1 ; WX 600 ; N twosuperior ; B 177 249 424 622 ; +C -1 ; WX 600 ; N Odieresis ; B 43 -18 557 753 ; +C -1 ; WX 600 ; N mu ; B 21 -157 562 426 ; +C -1 ; WX 600 ; N igrave ; B 95 0 505 672 ; +C -1 ; WX 600 ; N ohungarumlaut ; B 62 -15 580 672 ; +C -1 ; WX 600 ; N Eogonek ; B 53 -172 561 562 ; +C -1 ; WX 600 ; N dcroat ; B 45 -15 591 629 ; +C -1 ; WX 600 ; N threequarters ; B 8 -56 593 666 ; +C -1 ; WX 600 ; N Scedilla ; B 72 -151 529 580 ; +C -1 ; WX 600 ; N lcaron ; B 95 0 533 629 ; +C -1 ; WX 600 ; N Kcommaaccent ; B 38 -250 582 562 ; +C -1 ; WX 600 ; N Lacute ; B 47 0 554 805 ; +C -1 ; WX 600 ; N trademark ; B -23 263 623 562 ; +C -1 ; WX 600 ; N edotaccent ; B 66 -15 548 620 ; +C -1 ; WX 600 ; N Igrave ; B 96 0 504 805 ; +C -1 ; WX 600 ; N Imacron ; B 96 0 504 698 ; +C -1 ; WX 600 ; N Lcaron ; B 47 0 554 562 ; +C -1 ; WX 600 ; N onehalf ; B 0 -57 611 665 ; +C -1 ; WX 600 ; N lessequal ; B 98 0 502 710 ; +C -1 ; WX 600 ; N ocircumflex ; B 62 -15 538 654 ; +C -1 ; WX 600 ; N ntilde ; B 26 0 575 606 ; +C -1 ; WX 600 ; N Uhungarumlaut ; B 17 -18 590 805 ; +C -1 ; WX 600 ; N Eacute ; B 53 0 550 805 ; +C -1 ; WX 600 ; N emacron ; B 66 -15 548 565 ; +C -1 ; WX 600 ; N gbreve ; B 45 -157 566 609 ; +C -1 ; WX 600 ; N onequarter ; B 0 -57 600 665 ; +C -1 ; WX 600 ; N Scaron ; B 72 -20 529 802 ; +C -1 ; WX 600 ; N Scommaaccent ; B 72 -250 529 580 ; +C -1 ; WX 600 ; N Ohungarumlaut ; B 43 -18 580 805 ; +C -1 ; WX 600 ; N degree ; B 123 269 477 622 ; +C -1 ; WX 600 ; N ograve ; B 62 -15 538 672 ; +C -1 ; WX 600 ; N Ccaron ; B 41 -18 540 802 ; +C -1 ; WX 600 ; N ugrave ; B 21 -15 562 672 ; +C -1 ; WX 600 ; N radical ; B 3 -15 597 792 ; +C -1 ; WX 600 ; N Dcaron ; B 43 0 574 802 ; +C -1 ; WX 600 ; N rcommaaccent ; B 60 -250 559 441 ; +C -1 ; WX 600 ; N Ntilde ; B 7 -13 593 729 ; +C -1 ; WX 600 ; N otilde ; B 62 -15 538 606 ; +C -1 ; WX 600 ; N Rcommaaccent ; B 38 -250 588 562 ; +C -1 ; WX 600 ; N Lcommaaccent ; B 47 -250 554 562 ; +C -1 ; WX 600 ; N Atilde ; B 3 0 597 729 ; +C -1 ; WX 600 ; N Aogonek ; B 3 -172 608 562 ; +C -1 ; WX 600 ; N Aring ; B 3 0 597 750 ; +C -1 ; WX 600 ; N Otilde ; B 43 -18 557 729 ; +C -1 ; WX 600 ; N zdotaccent ; B 99 0 502 620 ; +C -1 ; WX 600 ; N Ecaron ; B 53 0 550 802 ; +C -1 ; WX 600 ; N Iogonek ; B 96 -172 504 562 ; +C -1 ; WX 600 ; N kcommaaccent ; B 43 -250 580 629 ; +C -1 ; WX 600 ; N minus ; B 80 232 520 283 ; +C -1 ; WX 600 ; N Icircumflex ; B 96 0 504 787 ; +C -1 ; WX 600 ; N ncaron ; B 26 0 575 669 ; +C -1 ; WX 600 ; N tcommaaccent ; B 87 -250 530 561 ; +C -1 ; WX 600 ; N logicalnot ; B 87 108 513 369 ; +C -1 ; WX 600 ; N odieresis ; B 62 -15 538 620 ; +C -1 ; WX 600 ; N udieresis ; B 21 -15 562 620 ; +C -1 ; WX 600 ; N notequal ; B 15 -16 540 529 ; +C -1 ; WX 600 ; N gcommaaccent ; B 45 -157 566 708 ; +C -1 ; WX 600 ; N eth ; B 62 -15 538 629 ; +C -1 ; WX 600 ; N zcaron ; B 99 0 502 669 ; +C -1 ; WX 600 ; N ncommaaccent ; B 26 -250 575 441 ; +C -1 ; WX 600 ; N onesuperior ; B 172 249 428 622 ; +C -1 ; WX 600 ; N imacron ; B 95 0 505 565 ; +C -1 ; WX 600 ; N Euro ; B 0 0 0 0 ; +EndCharMetrics +EndFontMetrics diff --git a/internal/corefont/Core14_AFMs/Helvetica-Bold.afm b/internal/corefont/Core14_AFMs/Helvetica-Bold.afm new file mode 100644 index 0000000000000000000000000000000000000000..837c594e0e80214c2c9c520b2aa9ae1ce3f18b33 --- /dev/null +++ b/internal/corefont/Core14_AFMs/Helvetica-Bold.afm @@ -0,0 +1,2827 @@ +StartFontMetrics 4.1 +Comment Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems Incorporated. All Rights Reserved. +Comment Creation Date: Thu May 1 12:43:52 1997 +Comment UniqueID 43052 +Comment VMusage 37169 48194 +FontName Helvetica-Bold +FullName Helvetica Bold +FamilyName Helvetica +Weight Bold +ItalicAngle 0 +IsFixedPitch false +CharacterSet ExtendedRoman +FontBBox -170 -228 1003 962 +UnderlinePosition -100 +UnderlineThickness 50 +Version 002.000 +Notice Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems Incorporated. All Rights Reserved.Helvetica is a trademark of Linotype-Hell AG and/or its subsidiaries. +EncodingScheme AdobeStandardEncoding +CapHeight 718 +XHeight 532 +Ascender 718 +Descender -207 +StdHW 118 +StdVW 140 +StartCharMetrics 315 +C 32 ; WX 278 ; N space ; B 0 0 0 0 ; +C 33 ; WX 333 ; N exclam ; B 90 0 244 718 ; +C 34 ; WX 474 ; N quotedbl ; B 98 447 376 718 ; +C 35 ; WX 556 ; N numbersign ; B 18 0 538 698 ; +C 36 ; WX 556 ; N dollar ; B 30 -115 523 775 ; +C 37 ; WX 889 ; N percent ; B 28 -19 861 710 ; +C 38 ; WX 722 ; N ampersand ; B 54 -19 701 718 ; +C 39 ; WX 278 ; N quoteright ; B 69 445 209 718 ; +C 40 ; WX 333 ; N parenleft ; B 35 -208 314 734 ; +C 41 ; WX 333 ; N parenright ; B 19 -208 298 734 ; +C 42 ; WX 389 ; N asterisk ; B 27 387 362 718 ; +C 43 ; WX 584 ; N plus ; B 40 0 544 506 ; +C 44 ; WX 278 ; N comma ; B 64 -168 214 146 ; +C 45 ; WX 333 ; N hyphen ; B 27 215 306 345 ; +C 46 ; WX 278 ; N period ; B 64 0 214 146 ; +C 47 ; WX 278 ; N slash ; B -33 -19 311 737 ; +C 48 ; WX 556 ; N zero ; B 32 -19 524 710 ; +C 49 ; WX 556 ; N one ; B 69 0 378 710 ; +C 50 ; WX 556 ; N two ; B 26 0 511 710 ; +C 51 ; WX 556 ; N three ; B 27 -19 516 710 ; +C 52 ; WX 556 ; N four ; B 27 0 526 710 ; +C 53 ; WX 556 ; N five ; B 27 -19 516 698 ; +C 54 ; WX 556 ; N six ; B 31 -19 520 710 ; +C 55 ; WX 556 ; N seven ; B 25 0 528 698 ; +C 56 ; WX 556 ; N eight ; B 32 -19 524 710 ; +C 57 ; WX 556 ; N nine ; B 30 -19 522 710 ; +C 58 ; WX 333 ; N colon ; B 92 0 242 512 ; +C 59 ; WX 333 ; N semicolon ; B 92 -168 242 512 ; +C 60 ; WX 584 ; N less ; B 38 -8 546 514 ; +C 61 ; WX 584 ; N equal ; B 40 87 544 419 ; +C 62 ; WX 584 ; N greater ; B 38 -8 546 514 ; +C 63 ; WX 611 ; N question ; B 60 0 556 727 ; +C 64 ; WX 975 ; N at ; B 118 -19 856 737 ; +C 65 ; WX 722 ; N A ; B 20 0 702 718 ; +C 66 ; WX 722 ; N B ; B 76 0 669 718 ; +C 67 ; WX 722 ; N C ; B 44 -19 684 737 ; +C 68 ; WX 722 ; N D ; B 76 0 685 718 ; +C 69 ; WX 667 ; N E ; B 76 0 621 718 ; +C 70 ; WX 611 ; N F ; B 76 0 587 718 ; +C 71 ; WX 778 ; N G ; B 44 -19 713 737 ; +C 72 ; WX 722 ; N H ; B 71 0 651 718 ; +C 73 ; WX 278 ; N I ; B 64 0 214 718 ; +C 74 ; WX 556 ; N J ; B 22 -18 484 718 ; +C 75 ; WX 722 ; N K ; B 87 0 722 718 ; +C 76 ; WX 611 ; N L ; B 76 0 583 718 ; +C 77 ; WX 833 ; N M ; B 69 0 765 718 ; +C 78 ; WX 722 ; N N ; B 69 0 654 718 ; +C 79 ; WX 778 ; N O ; B 44 -19 734 737 ; +C 80 ; WX 667 ; N P ; B 76 0 627 718 ; +C 81 ; WX 778 ; N Q ; B 44 -52 737 737 ; +C 82 ; WX 722 ; N R ; B 76 0 677 718 ; +C 83 ; WX 667 ; N S ; B 39 -19 629 737 ; +C 84 ; WX 611 ; N T ; B 14 0 598 718 ; +C 85 ; WX 722 ; N U ; B 72 -19 651 718 ; +C 86 ; WX 667 ; N V ; B 19 0 648 718 ; +C 87 ; WX 944 ; N W ; B 16 0 929 718 ; +C 88 ; WX 667 ; N X ; B 14 0 653 718 ; +C 89 ; WX 667 ; N Y ; B 15 0 653 718 ; +C 90 ; WX 611 ; N Z ; B 25 0 586 718 ; +C 91 ; WX 333 ; N bracketleft ; B 63 -196 309 722 ; +C 92 ; WX 278 ; N backslash ; B -33 -19 311 737 ; +C 93 ; WX 333 ; N bracketright ; B 24 -196 270 722 ; +C 94 ; WX 584 ; N asciicircum ; B 62 323 522 698 ; +C 95 ; WX 556 ; N underscore ; B 0 -125 556 -75 ; +C 96 ; WX 278 ; N quoteleft ; B 69 454 209 727 ; +C 97 ; WX 556 ; N a ; B 29 -14 527 546 ; +C 98 ; WX 611 ; N b ; B 61 -14 578 718 ; +C 99 ; WX 556 ; N c ; B 34 -14 524 546 ; +C 100 ; WX 611 ; N d ; B 34 -14 551 718 ; +C 101 ; WX 556 ; N e ; B 23 -14 528 546 ; +C 102 ; WX 333 ; N f ; B 10 0 318 727 ; L i fi ; L l fl ; +C 103 ; WX 611 ; N g ; B 40 -217 553 546 ; +C 104 ; WX 611 ; N h ; B 65 0 546 718 ; +C 105 ; WX 278 ; N i ; B 69 0 209 725 ; +C 106 ; WX 278 ; N j ; B 3 -214 209 725 ; +C 107 ; WX 556 ; N k ; B 69 0 562 718 ; +C 108 ; WX 278 ; N l ; B 69 0 209 718 ; +C 109 ; WX 889 ; N m ; B 64 0 826 546 ; +C 110 ; WX 611 ; N n ; B 65 0 546 546 ; +C 111 ; WX 611 ; N o ; B 34 -14 578 546 ; +C 112 ; WX 611 ; N p ; B 62 -207 578 546 ; +C 113 ; WX 611 ; N q ; B 34 -207 552 546 ; +C 114 ; WX 389 ; N r ; B 64 0 373 546 ; +C 115 ; WX 556 ; N s ; B 30 -14 519 546 ; +C 116 ; WX 333 ; N t ; B 10 -6 309 676 ; +C 117 ; WX 611 ; N u ; B 66 -14 545 532 ; +C 118 ; WX 556 ; N v ; B 13 0 543 532 ; +C 119 ; WX 778 ; N w ; B 10 0 769 532 ; +C 120 ; WX 556 ; N x ; B 15 0 541 532 ; +C 121 ; WX 556 ; N y ; B 10 -214 539 532 ; +C 122 ; WX 500 ; N z ; B 20 0 480 532 ; +C 123 ; WX 389 ; N braceleft ; B 48 -196 365 722 ; +C 124 ; WX 280 ; N bar ; B 84 -225 196 775 ; +C 125 ; WX 389 ; N braceright ; B 24 -196 341 722 ; +C 126 ; WX 584 ; N asciitilde ; B 61 163 523 343 ; +C 161 ; WX 333 ; N exclamdown ; B 90 -186 244 532 ; +C 162 ; WX 556 ; N cent ; B 34 -118 524 628 ; +C 163 ; WX 556 ; N sterling ; B 28 -16 541 718 ; +C 164 ; WX 167 ; N fraction ; B -170 -19 336 710 ; +C 165 ; WX 556 ; N yen ; B -9 0 565 698 ; +C 166 ; WX 556 ; N florin ; B -10 -210 516 737 ; +C 167 ; WX 556 ; N section ; B 34 -184 522 727 ; +C 168 ; WX 556 ; N currency ; B -3 76 559 636 ; +C 169 ; WX 238 ; N quotesingle ; B 70 447 168 718 ; +C 170 ; WX 500 ; N quotedblleft ; B 64 454 436 727 ; +C 171 ; WX 556 ; N guillemotleft ; B 88 76 468 484 ; +C 172 ; WX 333 ; N guilsinglleft ; B 83 76 250 484 ; +C 173 ; WX 333 ; N guilsinglright ; B 83 76 250 484 ; +C 174 ; WX 611 ; N fi ; B 10 0 542 727 ; +C 175 ; WX 611 ; N fl ; B 10 0 542 727 ; +C 177 ; WX 556 ; N endash ; B 0 227 556 333 ; +C 178 ; WX 556 ; N dagger ; B 36 -171 520 718 ; +C 179 ; WX 556 ; N daggerdbl ; B 36 -171 520 718 ; +C 180 ; WX 278 ; N periodcentered ; B 58 172 220 334 ; +C 182 ; WX 556 ; N paragraph ; B -8 -191 539 700 ; +C 183 ; WX 350 ; N bullet ; B 10 194 340 524 ; +C 184 ; WX 278 ; N quotesinglbase ; B 69 -146 209 127 ; +C 185 ; WX 500 ; N quotedblbase ; B 64 -146 436 127 ; +C 186 ; WX 500 ; N quotedblright ; B 64 445 436 718 ; +C 187 ; WX 556 ; N guillemotright ; B 88 76 468 484 ; +C 188 ; WX 1000 ; N ellipsis ; B 92 0 908 146 ; +C 189 ; WX 1000 ; N perthousand ; B -3 -19 1003 710 ; +C 191 ; WX 611 ; N questiondown ; B 55 -195 551 532 ; +C 193 ; WX 333 ; N grave ; B -23 604 225 750 ; +C 194 ; WX 333 ; N acute ; B 108 604 356 750 ; +C 195 ; WX 333 ; N circumflex ; B -10 604 343 750 ; +C 196 ; WX 333 ; N tilde ; B -17 610 350 737 ; +C 197 ; WX 333 ; N macron ; B -6 604 339 678 ; +C 198 ; WX 333 ; N breve ; B -2 604 335 750 ; +C 199 ; WX 333 ; N dotaccent ; B 104 614 230 729 ; +C 200 ; WX 333 ; N dieresis ; B 6 614 327 729 ; +C 202 ; WX 333 ; N ring ; B 59 568 275 776 ; +C 203 ; WX 333 ; N cedilla ; B 6 -228 245 0 ; +C 205 ; WX 333 ; N hungarumlaut ; B 9 604 486 750 ; +C 206 ; WX 333 ; N ogonek ; B 71 -228 304 0 ; +C 207 ; WX 333 ; N caron ; B -10 604 343 750 ; +C 208 ; WX 1000 ; N emdash ; B 0 227 1000 333 ; +C 225 ; WX 1000 ; N AE ; B 5 0 954 718 ; +C 227 ; WX 370 ; N ordfeminine ; B 22 401 347 737 ; +C 232 ; WX 611 ; N Lslash ; B -20 0 583 718 ; +C 233 ; WX 778 ; N Oslash ; B 33 -27 744 745 ; +C 234 ; WX 1000 ; N OE ; B 37 -19 961 737 ; +C 235 ; WX 365 ; N ordmasculine ; B 6 401 360 737 ; +C 241 ; WX 889 ; N ae ; B 29 -14 858 546 ; +C 245 ; WX 278 ; N dotlessi ; B 69 0 209 532 ; +C 248 ; WX 278 ; N lslash ; B -18 0 296 718 ; +C 249 ; WX 611 ; N oslash ; B 22 -29 589 560 ; +C 250 ; WX 944 ; N oe ; B 34 -14 912 546 ; +C 251 ; WX 611 ; N germandbls ; B 69 -14 579 731 ; +C -1 ; WX 278 ; N Idieresis ; B -21 0 300 915 ; +C -1 ; WX 556 ; N eacute ; B 23 -14 528 750 ; +C -1 ; WX 556 ; N abreve ; B 29 -14 527 750 ; +C -1 ; WX 611 ; N uhungarumlaut ; B 66 -14 625 750 ; +C -1 ; WX 556 ; N ecaron ; B 23 -14 528 750 ; +C -1 ; WX 667 ; N Ydieresis ; B 15 0 653 915 ; +C -1 ; WX 584 ; N divide ; B 40 -42 544 548 ; +C -1 ; WX 667 ; N Yacute ; B 15 0 653 936 ; +C -1 ; WX 722 ; N Acircumflex ; B 20 0 702 936 ; +C -1 ; WX 556 ; N aacute ; B 29 -14 527 750 ; +C -1 ; WX 722 ; N Ucircumflex ; B 72 -19 651 936 ; +C -1 ; WX 556 ; N yacute ; B 10 -214 539 750 ; +C -1 ; WX 556 ; N scommaaccent ; B 30 -228 519 546 ; +C -1 ; WX 556 ; N ecircumflex ; B 23 -14 528 750 ; +C -1 ; WX 722 ; N Uring ; B 72 -19 651 962 ; +C -1 ; WX 722 ; N Udieresis ; B 72 -19 651 915 ; +C -1 ; WX 556 ; N aogonek ; B 29 -224 545 546 ; +C -1 ; WX 722 ; N Uacute ; B 72 -19 651 936 ; +C -1 ; WX 611 ; N uogonek ; B 66 -228 545 532 ; +C -1 ; WX 667 ; N Edieresis ; B 76 0 621 915 ; +C -1 ; WX 722 ; N Dcroat ; B -5 0 685 718 ; +C -1 ; WX 250 ; N commaaccent ; B 64 -228 199 -50 ; +C -1 ; WX 737 ; N copyright ; B -11 -19 749 737 ; +C -1 ; WX 667 ; N Emacron ; B 76 0 621 864 ; +C -1 ; WX 556 ; N ccaron ; B 34 -14 524 750 ; +C -1 ; WX 556 ; N aring ; B 29 -14 527 776 ; +C -1 ; WX 722 ; N Ncommaaccent ; B 69 -228 654 718 ; +C -1 ; WX 278 ; N lacute ; B 69 0 329 936 ; +C -1 ; WX 556 ; N agrave ; B 29 -14 527 750 ; +C -1 ; WX 611 ; N Tcommaaccent ; B 14 -228 598 718 ; +C -1 ; WX 722 ; N Cacute ; B 44 -19 684 936 ; +C -1 ; WX 556 ; N atilde ; B 29 -14 527 737 ; +C -1 ; WX 667 ; N Edotaccent ; B 76 0 621 915 ; +C -1 ; WX 556 ; N scaron ; B 30 -14 519 750 ; +C -1 ; WX 556 ; N scedilla ; B 30 -228 519 546 ; +C -1 ; WX 278 ; N iacute ; B 69 0 329 750 ; +C -1 ; WX 494 ; N lozenge ; B 10 0 484 745 ; +C -1 ; WX 722 ; N Rcaron ; B 76 0 677 936 ; +C -1 ; WX 778 ; N Gcommaaccent ; B 44 -228 713 737 ; +C -1 ; WX 611 ; N ucircumflex ; B 66 -14 545 750 ; +C -1 ; WX 556 ; N acircumflex ; B 29 -14 527 750 ; +C -1 ; WX 722 ; N Amacron ; B 20 0 702 864 ; +C -1 ; WX 389 ; N rcaron ; B 18 0 373 750 ; +C -1 ; WX 556 ; N ccedilla ; B 34 -228 524 546 ; +C -1 ; WX 611 ; N Zdotaccent ; B 25 0 586 915 ; +C -1 ; WX 667 ; N Thorn ; B 76 0 627 718 ; +C -1 ; WX 778 ; N Omacron ; B 44 -19 734 864 ; +C -1 ; WX 722 ; N Racute ; B 76 0 677 936 ; +C -1 ; WX 667 ; N Sacute ; B 39 -19 629 936 ; +C -1 ; WX 743 ; N dcaron ; B 34 -14 750 718 ; +C -1 ; WX 722 ; N Umacron ; B 72 -19 651 864 ; +C -1 ; WX 611 ; N uring ; B 66 -14 545 776 ; +C -1 ; WX 333 ; N threesuperior ; B 8 271 326 710 ; +C -1 ; WX 778 ; N Ograve ; B 44 -19 734 936 ; +C -1 ; WX 722 ; N Agrave ; B 20 0 702 936 ; +C -1 ; WX 722 ; N Abreve ; B 20 0 702 936 ; +C -1 ; WX 584 ; N multiply ; B 40 1 545 505 ; +C -1 ; WX 611 ; N uacute ; B 66 -14 545 750 ; +C -1 ; WX 611 ; N Tcaron ; B 14 0 598 936 ; +C -1 ; WX 494 ; N partialdiff ; B 11 -21 494 750 ; +C -1 ; WX 556 ; N ydieresis ; B 10 -214 539 729 ; +C -1 ; WX 722 ; N Nacute ; B 69 0 654 936 ; +C -1 ; WX 278 ; N icircumflex ; B -37 0 316 750 ; +C -1 ; WX 667 ; N Ecircumflex ; B 76 0 621 936 ; +C -1 ; WX 556 ; N adieresis ; B 29 -14 527 729 ; +C -1 ; WX 556 ; N edieresis ; B 23 -14 528 729 ; +C -1 ; WX 556 ; N cacute ; B 34 -14 524 750 ; +C -1 ; WX 611 ; N nacute ; B 65 0 546 750 ; +C -1 ; WX 611 ; N umacron ; B 66 -14 545 678 ; +C -1 ; WX 722 ; N Ncaron ; B 69 0 654 936 ; +C -1 ; WX 278 ; N Iacute ; B 64 0 329 936 ; +C -1 ; WX 584 ; N plusminus ; B 40 0 544 506 ; +C -1 ; WX 280 ; N brokenbar ; B 84 -150 196 700 ; +C -1 ; WX 737 ; N registered ; B -11 -19 748 737 ; +C -1 ; WX 778 ; N Gbreve ; B 44 -19 713 936 ; +C -1 ; WX 278 ; N Idotaccent ; B 64 0 214 915 ; +C -1 ; WX 600 ; N summation ; B 14 -10 585 706 ; +C -1 ; WX 667 ; N Egrave ; B 76 0 621 936 ; +C -1 ; WX 389 ; N racute ; B 64 0 384 750 ; +C -1 ; WX 611 ; N omacron ; B 34 -14 578 678 ; +C -1 ; WX 611 ; N Zacute ; B 25 0 586 936 ; +C -1 ; WX 611 ; N Zcaron ; B 25 0 586 936 ; +C -1 ; WX 549 ; N greaterequal ; B 26 0 523 704 ; +C -1 ; WX 722 ; N Eth ; B -5 0 685 718 ; +C -1 ; WX 722 ; N Ccedilla ; B 44 -228 684 737 ; +C -1 ; WX 278 ; N lcommaaccent ; B 69 -228 213 718 ; +C -1 ; WX 389 ; N tcaron ; B 10 -6 421 878 ; +C -1 ; WX 556 ; N eogonek ; B 23 -228 528 546 ; +C -1 ; WX 722 ; N Uogonek ; B 72 -228 651 718 ; +C -1 ; WX 722 ; N Aacute ; B 20 0 702 936 ; +C -1 ; WX 722 ; N Adieresis ; B 20 0 702 915 ; +C -1 ; WX 556 ; N egrave ; B 23 -14 528 750 ; +C -1 ; WX 500 ; N zacute ; B 20 0 480 750 ; +C -1 ; WX 278 ; N iogonek ; B 16 -224 249 725 ; +C -1 ; WX 778 ; N Oacute ; B 44 -19 734 936 ; +C -1 ; WX 611 ; N oacute ; B 34 -14 578 750 ; +C -1 ; WX 556 ; N amacron ; B 29 -14 527 678 ; +C -1 ; WX 556 ; N sacute ; B 30 -14 519 750 ; +C -1 ; WX 278 ; N idieresis ; B -21 0 300 729 ; +C -1 ; WX 778 ; N Ocircumflex ; B 44 -19 734 936 ; +C -1 ; WX 722 ; N Ugrave ; B 72 -19 651 936 ; +C -1 ; WX 612 ; N Delta ; B 6 0 608 688 ; +C -1 ; WX 611 ; N thorn ; B 62 -208 578 718 ; +C -1 ; WX 333 ; N twosuperior ; B 9 283 324 710 ; +C -1 ; WX 778 ; N Odieresis ; B 44 -19 734 915 ; +C -1 ; WX 611 ; N mu ; B 66 -207 545 532 ; +C -1 ; WX 278 ; N igrave ; B -50 0 209 750 ; +C -1 ; WX 611 ; N ohungarumlaut ; B 34 -14 625 750 ; +C -1 ; WX 667 ; N Eogonek ; B 76 -224 639 718 ; +C -1 ; WX 611 ; N dcroat ; B 34 -14 650 718 ; +C -1 ; WX 834 ; N threequarters ; B 16 -19 799 710 ; +C -1 ; WX 667 ; N Scedilla ; B 39 -228 629 737 ; +C -1 ; WX 400 ; N lcaron ; B 69 0 408 718 ; +C -1 ; WX 722 ; N Kcommaaccent ; B 87 -228 722 718 ; +C -1 ; WX 611 ; N Lacute ; B 76 0 583 936 ; +C -1 ; WX 1000 ; N trademark ; B 44 306 956 718 ; +C -1 ; WX 556 ; N edotaccent ; B 23 -14 528 729 ; +C -1 ; WX 278 ; N Igrave ; B -50 0 214 936 ; +C -1 ; WX 278 ; N Imacron ; B -33 0 312 864 ; +C -1 ; WX 611 ; N Lcaron ; B 76 0 583 718 ; +C -1 ; WX 834 ; N onehalf ; B 26 -19 794 710 ; +C -1 ; WX 549 ; N lessequal ; B 29 0 526 704 ; +C -1 ; WX 611 ; N ocircumflex ; B 34 -14 578 750 ; +C -1 ; WX 611 ; N ntilde ; B 65 0 546 737 ; +C -1 ; WX 722 ; N Uhungarumlaut ; B 72 -19 681 936 ; +C -1 ; WX 667 ; N Eacute ; B 76 0 621 936 ; +C -1 ; WX 556 ; N emacron ; B 23 -14 528 678 ; +C -1 ; WX 611 ; N gbreve ; B 40 -217 553 750 ; +C -1 ; WX 834 ; N onequarter ; B 26 -19 766 710 ; +C -1 ; WX 667 ; N Scaron ; B 39 -19 629 936 ; +C -1 ; WX 667 ; N Scommaaccent ; B 39 -228 629 737 ; +C -1 ; WX 778 ; N Ohungarumlaut ; B 44 -19 734 936 ; +C -1 ; WX 400 ; N degree ; B 57 426 343 712 ; +C -1 ; WX 611 ; N ograve ; B 34 -14 578 750 ; +C -1 ; WX 722 ; N Ccaron ; B 44 -19 684 936 ; +C -1 ; WX 611 ; N ugrave ; B 66 -14 545 750 ; +C -1 ; WX 549 ; N radical ; B 10 -46 512 850 ; +C -1 ; WX 722 ; N Dcaron ; B 76 0 685 936 ; +C -1 ; WX 389 ; N rcommaaccent ; B 64 -228 373 546 ; +C -1 ; WX 722 ; N Ntilde ; B 69 0 654 923 ; +C -1 ; WX 611 ; N otilde ; B 34 -14 578 737 ; +C -1 ; WX 722 ; N Rcommaaccent ; B 76 -228 677 718 ; +C -1 ; WX 611 ; N Lcommaaccent ; B 76 -228 583 718 ; +C -1 ; WX 722 ; N Atilde ; B 20 0 702 923 ; +C -1 ; WX 722 ; N Aogonek ; B 20 -224 742 718 ; +C -1 ; WX 722 ; N Aring ; B 20 0 702 962 ; +C -1 ; WX 778 ; N Otilde ; B 44 -19 734 923 ; +C -1 ; WX 500 ; N zdotaccent ; B 20 0 480 729 ; +C -1 ; WX 667 ; N Ecaron ; B 76 0 621 936 ; +C -1 ; WX 278 ; N Iogonek ; B -11 -228 222 718 ; +C -1 ; WX 556 ; N kcommaaccent ; B 69 -228 562 718 ; +C -1 ; WX 584 ; N minus ; B 40 197 544 309 ; +C -1 ; WX 278 ; N Icircumflex ; B -37 0 316 936 ; +C -1 ; WX 611 ; N ncaron ; B 65 0 546 750 ; +C -1 ; WX 333 ; N tcommaaccent ; B 10 -228 309 676 ; +C -1 ; WX 584 ; N logicalnot ; B 40 108 544 419 ; +C -1 ; WX 611 ; N odieresis ; B 34 -14 578 729 ; +C -1 ; WX 611 ; N udieresis ; B 66 -14 545 729 ; +C -1 ; WX 549 ; N notequal ; B 15 -49 540 570 ; +C -1 ; WX 611 ; N gcommaaccent ; B 40 -217 553 850 ; +C -1 ; WX 611 ; N eth ; B 34 -14 578 737 ; +C -1 ; WX 500 ; N zcaron ; B 20 0 480 750 ; +C -1 ; WX 611 ; N ncommaaccent ; B 65 -228 546 546 ; +C -1 ; WX 333 ; N onesuperior ; B 26 283 237 710 ; +C -1 ; WX 278 ; N imacron ; B -8 0 285 678 ; +C -1 ; WX 556 ; N Euro ; B 0 0 0 0 ; +EndCharMetrics +StartKernData +StartKernPairs 2481 +KPX A C -40 +KPX A Cacute -40 +KPX A Ccaron -40 +KPX A Ccedilla -40 +KPX A G -50 +KPX A Gbreve -50 +KPX A Gcommaaccent -50 +KPX A O -40 +KPX A Oacute -40 +KPX A Ocircumflex -40 +KPX A Odieresis -40 +KPX A Ograve -40 +KPX A Ohungarumlaut -40 +KPX A Omacron -40 +KPX A Oslash -40 +KPX A Otilde -40 +KPX A Q -40 +KPX A T -90 +KPX A Tcaron -90 +KPX A Tcommaaccent -90 +KPX A U -50 +KPX A Uacute -50 +KPX A Ucircumflex -50 +KPX A Udieresis -50 +KPX A Ugrave -50 +KPX A Uhungarumlaut -50 +KPX A Umacron -50 +KPX A Uogonek -50 +KPX A Uring -50 +KPX A V -80 +KPX A W -60 +KPX A Y -110 +KPX A Yacute -110 +KPX A Ydieresis -110 +KPX A u -30 +KPX A uacute -30 +KPX A ucircumflex -30 +KPX A udieresis -30 +KPX A ugrave -30 +KPX A uhungarumlaut -30 +KPX A umacron -30 +KPX A uogonek -30 +KPX A uring -30 +KPX A v -40 +KPX A w -30 +KPX A y -30 +KPX A yacute -30 +KPX A ydieresis -30 +KPX Aacute C -40 +KPX Aacute Cacute -40 +KPX Aacute Ccaron -40 +KPX Aacute Ccedilla -40 +KPX Aacute G -50 +KPX Aacute Gbreve -50 +KPX Aacute Gcommaaccent -50 +KPX Aacute O -40 +KPX Aacute Oacute -40 +KPX Aacute Ocircumflex -40 +KPX Aacute Odieresis -40 +KPX Aacute Ograve -40 +KPX Aacute Ohungarumlaut -40 +KPX Aacute Omacron -40 +KPX Aacute Oslash -40 +KPX Aacute Otilde -40 +KPX Aacute Q -40 +KPX Aacute T -90 +KPX Aacute Tcaron -90 +KPX Aacute Tcommaaccent -90 +KPX Aacute U -50 +KPX Aacute Uacute -50 +KPX Aacute Ucircumflex -50 +KPX Aacute Udieresis -50 +KPX Aacute Ugrave -50 +KPX Aacute Uhungarumlaut -50 +KPX Aacute Umacron -50 +KPX Aacute Uogonek -50 +KPX Aacute Uring -50 +KPX Aacute V -80 +KPX Aacute W -60 +KPX Aacute Y -110 +KPX Aacute Yacute -110 +KPX Aacute Ydieresis -110 +KPX Aacute u -30 +KPX Aacute uacute -30 +KPX Aacute ucircumflex -30 +KPX Aacute udieresis -30 +KPX Aacute ugrave -30 +KPX Aacute uhungarumlaut -30 +KPX Aacute umacron -30 +KPX Aacute uogonek -30 +KPX Aacute uring -30 +KPX Aacute v -40 +KPX Aacute w -30 +KPX Aacute y -30 +KPX Aacute yacute -30 +KPX Aacute ydieresis -30 +KPX Abreve C -40 +KPX Abreve Cacute -40 +KPX Abreve Ccaron -40 +KPX Abreve Ccedilla -40 +KPX Abreve G -50 +KPX Abreve Gbreve -50 +KPX Abreve Gcommaaccent -50 +KPX Abreve O -40 +KPX Abreve Oacute -40 +KPX Abreve Ocircumflex -40 +KPX Abreve Odieresis -40 +KPX Abreve Ograve -40 +KPX Abreve Ohungarumlaut -40 +KPX Abreve Omacron -40 +KPX Abreve Oslash -40 +KPX Abreve Otilde -40 +KPX Abreve Q -40 +KPX Abreve T -90 +KPX Abreve Tcaron -90 +KPX Abreve Tcommaaccent -90 +KPX Abreve U -50 +KPX Abreve Uacute -50 +KPX Abreve Ucircumflex -50 +KPX Abreve Udieresis -50 +KPX Abreve Ugrave -50 +KPX Abreve Uhungarumlaut -50 +KPX Abreve Umacron -50 +KPX Abreve Uogonek -50 +KPX Abreve Uring -50 +KPX Abreve V -80 +KPX Abreve W -60 +KPX Abreve Y -110 +KPX Abreve Yacute -110 +KPX Abreve Ydieresis -110 +KPX Abreve u -30 +KPX Abreve uacute -30 +KPX Abreve ucircumflex -30 +KPX Abreve udieresis -30 +KPX Abreve ugrave -30 +KPX Abreve uhungarumlaut -30 +KPX Abreve umacron -30 +KPX Abreve uogonek -30 +KPX Abreve uring -30 +KPX Abreve v -40 +KPX Abreve w -30 +KPX Abreve y -30 +KPX Abreve yacute -30 +KPX Abreve ydieresis -30 +KPX Acircumflex C -40 +KPX Acircumflex Cacute -40 +KPX Acircumflex Ccaron -40 +KPX Acircumflex Ccedilla -40 +KPX Acircumflex G -50 +KPX Acircumflex Gbreve -50 +KPX Acircumflex Gcommaaccent -50 +KPX Acircumflex O -40 +KPX Acircumflex Oacute -40 +KPX Acircumflex Ocircumflex -40 +KPX Acircumflex Odieresis -40 +KPX Acircumflex Ograve -40 +KPX Acircumflex Ohungarumlaut -40 +KPX Acircumflex Omacron -40 +KPX Acircumflex Oslash -40 +KPX Acircumflex Otilde -40 +KPX Acircumflex Q -40 +KPX Acircumflex T -90 +KPX Acircumflex Tcaron -90 +KPX Acircumflex Tcommaaccent -90 +KPX Acircumflex U -50 +KPX Acircumflex Uacute -50 +KPX Acircumflex Ucircumflex -50 +KPX Acircumflex Udieresis -50 +KPX Acircumflex Ugrave -50 +KPX Acircumflex Uhungarumlaut -50 +KPX Acircumflex Umacron -50 +KPX Acircumflex Uogonek -50 +KPX Acircumflex Uring -50 +KPX Acircumflex V -80 +KPX Acircumflex W -60 +KPX Acircumflex Y -110 +KPX Acircumflex Yacute -110 +KPX Acircumflex Ydieresis -110 +KPX Acircumflex u -30 +KPX Acircumflex uacute -30 +KPX Acircumflex ucircumflex -30 +KPX Acircumflex udieresis -30 +KPX Acircumflex ugrave -30 +KPX Acircumflex uhungarumlaut -30 +KPX Acircumflex umacron -30 +KPX Acircumflex uogonek -30 +KPX Acircumflex uring -30 +KPX Acircumflex v -40 +KPX Acircumflex w -30 +KPX Acircumflex y -30 +KPX Acircumflex yacute -30 +KPX Acircumflex ydieresis -30 +KPX Adieresis C -40 +KPX Adieresis Cacute -40 +KPX Adieresis Ccaron -40 +KPX Adieresis Ccedilla -40 +KPX Adieresis G -50 +KPX Adieresis Gbreve -50 +KPX Adieresis Gcommaaccent -50 +KPX Adieresis O -40 +KPX Adieresis Oacute -40 +KPX Adieresis Ocircumflex -40 +KPX Adieresis Odieresis -40 +KPX Adieresis Ograve -40 +KPX Adieresis Ohungarumlaut -40 +KPX Adieresis Omacron -40 +KPX Adieresis Oslash -40 +KPX Adieresis Otilde -40 +KPX Adieresis Q -40 +KPX Adieresis T -90 +KPX Adieresis Tcaron -90 +KPX Adieresis Tcommaaccent -90 +KPX Adieresis U -50 +KPX Adieresis Uacute -50 +KPX Adieresis Ucircumflex -50 +KPX Adieresis Udieresis -50 +KPX Adieresis Ugrave -50 +KPX Adieresis Uhungarumlaut -50 +KPX Adieresis Umacron -50 +KPX Adieresis Uogonek -50 +KPX Adieresis Uring -50 +KPX Adieresis V -80 +KPX Adieresis W -60 +KPX Adieresis Y -110 +KPX Adieresis Yacute -110 +KPX Adieresis Ydieresis -110 +KPX Adieresis u -30 +KPX Adieresis uacute -30 +KPX Adieresis ucircumflex -30 +KPX Adieresis udieresis -30 +KPX Adieresis ugrave -30 +KPX Adieresis uhungarumlaut -30 +KPX Adieresis umacron -30 +KPX Adieresis uogonek -30 +KPX Adieresis uring -30 +KPX Adieresis v -40 +KPX Adieresis w -30 +KPX Adieresis y -30 +KPX Adieresis yacute -30 +KPX Adieresis ydieresis -30 +KPX Agrave C -40 +KPX Agrave Cacute -40 +KPX Agrave Ccaron -40 +KPX Agrave Ccedilla -40 +KPX Agrave G -50 +KPX Agrave Gbreve -50 +KPX Agrave Gcommaaccent -50 +KPX Agrave O -40 +KPX Agrave Oacute -40 +KPX Agrave Ocircumflex -40 +KPX Agrave Odieresis -40 +KPX Agrave Ograve -40 +KPX Agrave Ohungarumlaut -40 +KPX Agrave Omacron -40 +KPX Agrave Oslash -40 +KPX Agrave Otilde -40 +KPX Agrave Q -40 +KPX Agrave T -90 +KPX Agrave Tcaron -90 +KPX Agrave Tcommaaccent -90 +KPX Agrave U -50 +KPX Agrave Uacute -50 +KPX Agrave Ucircumflex -50 +KPX Agrave Udieresis -50 +KPX Agrave Ugrave -50 +KPX Agrave Uhungarumlaut -50 +KPX Agrave Umacron -50 +KPX Agrave Uogonek -50 +KPX Agrave Uring -50 +KPX Agrave V -80 +KPX Agrave W -60 +KPX Agrave Y -110 +KPX Agrave Yacute -110 +KPX Agrave Ydieresis -110 +KPX Agrave u -30 +KPX Agrave uacute -30 +KPX Agrave ucircumflex -30 +KPX Agrave udieresis -30 +KPX Agrave ugrave -30 +KPX Agrave uhungarumlaut -30 +KPX Agrave umacron -30 +KPX Agrave uogonek -30 +KPX Agrave uring -30 +KPX Agrave v -40 +KPX Agrave w -30 +KPX Agrave y -30 +KPX Agrave yacute -30 +KPX Agrave ydieresis -30 +KPX Amacron C -40 +KPX Amacron Cacute -40 +KPX Amacron Ccaron -40 +KPX Amacron Ccedilla -40 +KPX Amacron G -50 +KPX Amacron Gbreve -50 +KPX Amacron Gcommaaccent -50 +KPX Amacron O -40 +KPX Amacron Oacute -40 +KPX Amacron Ocircumflex -40 +KPX Amacron Odieresis -40 +KPX Amacron Ograve -40 +KPX Amacron Ohungarumlaut -40 +KPX Amacron Omacron -40 +KPX Amacron Oslash -40 +KPX Amacron Otilde -40 +KPX Amacron Q -40 +KPX Amacron T -90 +KPX Amacron Tcaron -90 +KPX Amacron Tcommaaccent -90 +KPX Amacron U -50 +KPX Amacron Uacute -50 +KPX Amacron Ucircumflex -50 +KPX Amacron Udieresis -50 +KPX Amacron Ugrave -50 +KPX Amacron Uhungarumlaut -50 +KPX Amacron Umacron -50 +KPX Amacron Uogonek -50 +KPX Amacron Uring -50 +KPX Amacron V -80 +KPX Amacron W -60 +KPX Amacron Y -110 +KPX Amacron Yacute -110 +KPX Amacron Ydieresis -110 +KPX Amacron u -30 +KPX Amacron uacute -30 +KPX Amacron ucircumflex -30 +KPX Amacron udieresis -30 +KPX Amacron ugrave -30 +KPX Amacron uhungarumlaut -30 +KPX Amacron umacron -30 +KPX Amacron uogonek -30 +KPX Amacron uring -30 +KPX Amacron v -40 +KPX Amacron w -30 +KPX Amacron y -30 +KPX Amacron yacute -30 +KPX Amacron ydieresis -30 +KPX Aogonek C -40 +KPX Aogonek Cacute -40 +KPX Aogonek Ccaron -40 +KPX Aogonek Ccedilla -40 +KPX Aogonek G -50 +KPX Aogonek Gbreve -50 +KPX Aogonek Gcommaaccent -50 +KPX Aogonek O -40 +KPX Aogonek Oacute -40 +KPX Aogonek Ocircumflex -40 +KPX Aogonek Odieresis -40 +KPX Aogonek Ograve -40 +KPX Aogonek Ohungarumlaut -40 +KPX Aogonek Omacron -40 +KPX Aogonek Oslash -40 +KPX Aogonek Otilde -40 +KPX Aogonek Q -40 +KPX Aogonek T -90 +KPX Aogonek Tcaron -90 +KPX Aogonek Tcommaaccent -90 +KPX Aogonek U -50 +KPX Aogonek Uacute -50 +KPX Aogonek Ucircumflex -50 +KPX Aogonek Udieresis -50 +KPX Aogonek Ugrave -50 +KPX Aogonek Uhungarumlaut -50 +KPX Aogonek Umacron -50 +KPX Aogonek Uogonek -50 +KPX Aogonek Uring -50 +KPX Aogonek V -80 +KPX Aogonek W -60 +KPX Aogonek Y -110 +KPX Aogonek Yacute -110 +KPX Aogonek Ydieresis -110 +KPX Aogonek u -30 +KPX Aogonek uacute -30 +KPX Aogonek ucircumflex -30 +KPX Aogonek udieresis -30 +KPX Aogonek ugrave -30 +KPX Aogonek uhungarumlaut -30 +KPX Aogonek umacron -30 +KPX Aogonek uogonek -30 +KPX Aogonek uring -30 +KPX Aogonek v -40 +KPX Aogonek w -30 +KPX Aogonek y -30 +KPX Aogonek yacute -30 +KPX Aogonek ydieresis -30 +KPX Aring C -40 +KPX Aring Cacute -40 +KPX Aring Ccaron -40 +KPX Aring Ccedilla -40 +KPX Aring G -50 +KPX Aring Gbreve -50 +KPX Aring Gcommaaccent -50 +KPX Aring O -40 +KPX Aring Oacute -40 +KPX Aring Ocircumflex -40 +KPX Aring Odieresis -40 +KPX Aring Ograve -40 +KPX Aring Ohungarumlaut -40 +KPX Aring Omacron -40 +KPX Aring Oslash -40 +KPX Aring Otilde -40 +KPX Aring Q -40 +KPX Aring T -90 +KPX Aring Tcaron -90 +KPX Aring Tcommaaccent -90 +KPX Aring U -50 +KPX Aring Uacute -50 +KPX Aring Ucircumflex -50 +KPX Aring Udieresis -50 +KPX Aring Ugrave -50 +KPX Aring Uhungarumlaut -50 +KPX Aring Umacron -50 +KPX Aring Uogonek -50 +KPX Aring Uring -50 +KPX Aring V -80 +KPX Aring W -60 +KPX Aring Y -110 +KPX Aring Yacute -110 +KPX Aring Ydieresis -110 +KPX Aring u -30 +KPX Aring uacute -30 +KPX Aring ucircumflex -30 +KPX Aring udieresis -30 +KPX Aring ugrave -30 +KPX Aring uhungarumlaut -30 +KPX Aring umacron -30 +KPX Aring uogonek -30 +KPX Aring uring -30 +KPX Aring v -40 +KPX Aring w -30 +KPX Aring y -30 +KPX Aring yacute -30 +KPX Aring ydieresis -30 +KPX Atilde C -40 +KPX Atilde Cacute -40 +KPX Atilde Ccaron -40 +KPX Atilde Ccedilla -40 +KPX Atilde G -50 +KPX Atilde Gbreve -50 +KPX Atilde Gcommaaccent -50 +KPX Atilde O -40 +KPX Atilde Oacute -40 +KPX Atilde Ocircumflex -40 +KPX Atilde Odieresis -40 +KPX Atilde Ograve -40 +KPX Atilde Ohungarumlaut -40 +KPX Atilde Omacron -40 +KPX Atilde Oslash -40 +KPX Atilde Otilde -40 +KPX Atilde Q -40 +KPX Atilde T -90 +KPX Atilde Tcaron -90 +KPX Atilde Tcommaaccent -90 +KPX Atilde U -50 +KPX Atilde Uacute -50 +KPX Atilde Ucircumflex -50 +KPX Atilde Udieresis -50 +KPX Atilde Ugrave -50 +KPX Atilde Uhungarumlaut -50 +KPX Atilde Umacron -50 +KPX Atilde Uogonek -50 +KPX Atilde Uring -50 +KPX Atilde V -80 +KPX Atilde W -60 +KPX Atilde Y -110 +KPX Atilde Yacute -110 +KPX Atilde Ydieresis -110 +KPX Atilde u -30 +KPX Atilde uacute -30 +KPX Atilde ucircumflex -30 +KPX Atilde udieresis -30 +KPX Atilde ugrave -30 +KPX Atilde uhungarumlaut -30 +KPX Atilde umacron -30 +KPX Atilde uogonek -30 +KPX Atilde uring -30 +KPX Atilde v -40 +KPX Atilde w -30 +KPX Atilde y -30 +KPX Atilde yacute -30 +KPX Atilde ydieresis -30 +KPX B A -30 +KPX B Aacute -30 +KPX B Abreve -30 +KPX B Acircumflex -30 +KPX B Adieresis -30 +KPX B Agrave -30 +KPX B Amacron -30 +KPX B Aogonek -30 +KPX B Aring -30 +KPX B Atilde -30 +KPX B U -10 +KPX B Uacute -10 +KPX B Ucircumflex -10 +KPX B Udieresis -10 +KPX B Ugrave -10 +KPX B Uhungarumlaut -10 +KPX B Umacron -10 +KPX B Uogonek -10 +KPX B Uring -10 +KPX D A -40 +KPX D Aacute -40 +KPX D Abreve -40 +KPX D Acircumflex -40 +KPX D Adieresis -40 +KPX D Agrave -40 +KPX D Amacron -40 +KPX D Aogonek -40 +KPX D Aring -40 +KPX D Atilde -40 +KPX D V -40 +KPX D W -40 +KPX D Y -70 +KPX D Yacute -70 +KPX D Ydieresis -70 +KPX D comma -30 +KPX D period -30 +KPX Dcaron A -40 +KPX Dcaron Aacute -40 +KPX Dcaron Abreve -40 +KPX Dcaron Acircumflex -40 +KPX Dcaron Adieresis -40 +KPX Dcaron Agrave -40 +KPX Dcaron Amacron -40 +KPX Dcaron Aogonek -40 +KPX Dcaron Aring -40 +KPX Dcaron Atilde -40 +KPX Dcaron V -40 +KPX Dcaron W -40 +KPX Dcaron Y -70 +KPX Dcaron Yacute -70 +KPX Dcaron Ydieresis -70 +KPX Dcaron comma -30 +KPX Dcaron period -30 +KPX Dcroat A -40 +KPX Dcroat Aacute -40 +KPX Dcroat Abreve -40 +KPX Dcroat Acircumflex -40 +KPX Dcroat Adieresis -40 +KPX Dcroat Agrave -40 +KPX Dcroat Amacron -40 +KPX Dcroat Aogonek -40 +KPX Dcroat Aring -40 +KPX Dcroat Atilde -40 +KPX Dcroat V -40 +KPX Dcroat W -40 +KPX Dcroat Y -70 +KPX Dcroat Yacute -70 +KPX Dcroat Ydieresis -70 +KPX Dcroat comma -30 +KPX Dcroat period -30 +KPX F A -80 +KPX F Aacute -80 +KPX F Abreve -80 +KPX F Acircumflex -80 +KPX F Adieresis -80 +KPX F Agrave -80 +KPX F Amacron -80 +KPX F Aogonek -80 +KPX F Aring -80 +KPX F Atilde -80 +KPX F a -20 +KPX F aacute -20 +KPX F abreve -20 +KPX F acircumflex -20 +KPX F adieresis -20 +KPX F agrave -20 +KPX F amacron -20 +KPX F aogonek -20 +KPX F aring -20 +KPX F atilde -20 +KPX F comma -100 +KPX F period -100 +KPX J A -20 +KPX J Aacute -20 +KPX J Abreve -20 +KPX J Acircumflex -20 +KPX J Adieresis -20 +KPX J Agrave -20 +KPX J Amacron -20 +KPX J Aogonek -20 +KPX J Aring -20 +KPX J Atilde -20 +KPX J comma -20 +KPX J period -20 +KPX J u -20 +KPX J uacute -20 +KPX J ucircumflex -20 +KPX J udieresis -20 +KPX J ugrave -20 +KPX J uhungarumlaut -20 +KPX J umacron -20 +KPX J uogonek -20 +KPX J uring -20 +KPX K O -30 +KPX K Oacute -30 +KPX K Ocircumflex -30 +KPX K Odieresis -30 +KPX K Ograve -30 +KPX K Ohungarumlaut -30 +KPX K Omacron -30 +KPX K Oslash -30 +KPX K Otilde -30 +KPX K e -15 +KPX K eacute -15 +KPX K ecaron -15 +KPX K ecircumflex -15 +KPX K edieresis -15 +KPX K edotaccent -15 +KPX K egrave -15 +KPX K emacron -15 +KPX K eogonek -15 +KPX K o -35 +KPX K oacute -35 +KPX K ocircumflex -35 +KPX K odieresis -35 +KPX K ograve -35 +KPX K ohungarumlaut -35 +KPX K omacron -35 +KPX K oslash -35 +KPX K otilde -35 +KPX K u -30 +KPX K uacute -30 +KPX K ucircumflex -30 +KPX K udieresis -30 +KPX K ugrave -30 +KPX K uhungarumlaut -30 +KPX K umacron -30 +KPX K uogonek -30 +KPX K uring -30 +KPX K y -40 +KPX K yacute -40 +KPX K ydieresis -40 +KPX Kcommaaccent O -30 +KPX Kcommaaccent Oacute -30 +KPX Kcommaaccent Ocircumflex -30 +KPX Kcommaaccent Odieresis -30 +KPX Kcommaaccent Ograve -30 +KPX Kcommaaccent Ohungarumlaut -30 +KPX Kcommaaccent Omacron -30 +KPX Kcommaaccent Oslash -30 +KPX Kcommaaccent Otilde -30 +KPX Kcommaaccent e -15 +KPX Kcommaaccent eacute -15 +KPX Kcommaaccent ecaron -15 +KPX Kcommaaccent ecircumflex -15 +KPX Kcommaaccent edieresis -15 +KPX Kcommaaccent edotaccent -15 +KPX Kcommaaccent egrave -15 +KPX Kcommaaccent emacron -15 +KPX Kcommaaccent eogonek -15 +KPX Kcommaaccent o -35 +KPX Kcommaaccent oacute -35 +KPX Kcommaaccent ocircumflex -35 +KPX Kcommaaccent odieresis -35 +KPX Kcommaaccent ograve -35 +KPX Kcommaaccent ohungarumlaut -35 +KPX Kcommaaccent omacron -35 +KPX Kcommaaccent oslash -35 +KPX Kcommaaccent otilde -35 +KPX Kcommaaccent u -30 +KPX Kcommaaccent uacute -30 +KPX Kcommaaccent ucircumflex -30 +KPX Kcommaaccent udieresis -30 +KPX Kcommaaccent ugrave -30 +KPX Kcommaaccent uhungarumlaut -30 +KPX Kcommaaccent umacron -30 +KPX Kcommaaccent uogonek -30 +KPX Kcommaaccent uring -30 +KPX Kcommaaccent y -40 +KPX Kcommaaccent yacute -40 +KPX Kcommaaccent ydieresis -40 +KPX L T -90 +KPX L Tcaron -90 +KPX L Tcommaaccent -90 +KPX L V -110 +KPX L W -80 +KPX L Y -120 +KPX L Yacute -120 +KPX L Ydieresis -120 +KPX L quotedblright -140 +KPX L quoteright -140 +KPX L y -30 +KPX L yacute -30 +KPX L ydieresis -30 +KPX Lacute T -90 +KPX Lacute Tcaron -90 +KPX Lacute Tcommaaccent -90 +KPX Lacute V -110 +KPX Lacute W -80 +KPX Lacute Y -120 +KPX Lacute Yacute -120 +KPX Lacute Ydieresis -120 +KPX Lacute quotedblright -140 +KPX Lacute quoteright -140 +KPX Lacute y -30 +KPX Lacute yacute -30 +KPX Lacute ydieresis -30 +KPX Lcommaaccent T -90 +KPX Lcommaaccent Tcaron -90 +KPX Lcommaaccent Tcommaaccent -90 +KPX Lcommaaccent V -110 +KPX Lcommaaccent W -80 +KPX Lcommaaccent Y -120 +KPX Lcommaaccent Yacute -120 +KPX Lcommaaccent Ydieresis -120 +KPX Lcommaaccent quotedblright -140 +KPX Lcommaaccent quoteright -140 +KPX Lcommaaccent y -30 +KPX Lcommaaccent yacute -30 +KPX Lcommaaccent ydieresis -30 +KPX Lslash T -90 +KPX Lslash Tcaron -90 +KPX Lslash Tcommaaccent -90 +KPX Lslash V -110 +KPX Lslash W -80 +KPX Lslash Y -120 +KPX Lslash Yacute -120 +KPX Lslash Ydieresis -120 +KPX Lslash quotedblright -140 +KPX Lslash quoteright -140 +KPX Lslash y -30 +KPX Lslash yacute -30 +KPX Lslash ydieresis -30 +KPX O A -50 +KPX O Aacute -50 +KPX O Abreve -50 +KPX O Acircumflex -50 +KPX O Adieresis -50 +KPX O Agrave -50 +KPX O Amacron -50 +KPX O Aogonek -50 +KPX O Aring -50 +KPX O Atilde -50 +KPX O T -40 +KPX O Tcaron -40 +KPX O Tcommaaccent -40 +KPX O V -50 +KPX O W -50 +KPX O X -50 +KPX O Y -70 +KPX O Yacute -70 +KPX O Ydieresis -70 +KPX O comma -40 +KPX O period -40 +KPX Oacute A -50 +KPX Oacute Aacute -50 +KPX Oacute Abreve -50 +KPX Oacute Acircumflex -50 +KPX Oacute Adieresis -50 +KPX Oacute Agrave -50 +KPX Oacute Amacron -50 +KPX Oacute Aogonek -50 +KPX Oacute Aring -50 +KPX Oacute Atilde -50 +KPX Oacute T -40 +KPX Oacute Tcaron -40 +KPX Oacute Tcommaaccent -40 +KPX Oacute V -50 +KPX Oacute W -50 +KPX Oacute X -50 +KPX Oacute Y -70 +KPX Oacute Yacute -70 +KPX Oacute Ydieresis -70 +KPX Oacute comma -40 +KPX Oacute period -40 +KPX Ocircumflex A -50 +KPX Ocircumflex Aacute -50 +KPX Ocircumflex Abreve -50 +KPX Ocircumflex Acircumflex -50 +KPX Ocircumflex Adieresis -50 +KPX Ocircumflex Agrave -50 +KPX Ocircumflex Amacron -50 +KPX Ocircumflex Aogonek -50 +KPX Ocircumflex Aring -50 +KPX Ocircumflex Atilde -50 +KPX Ocircumflex T -40 +KPX Ocircumflex Tcaron -40 +KPX Ocircumflex Tcommaaccent -40 +KPX Ocircumflex V -50 +KPX Ocircumflex W -50 +KPX Ocircumflex X -50 +KPX Ocircumflex Y -70 +KPX Ocircumflex Yacute -70 +KPX Ocircumflex Ydieresis -70 +KPX Ocircumflex comma -40 +KPX Ocircumflex period -40 +KPX Odieresis A -50 +KPX Odieresis Aacute -50 +KPX Odieresis Abreve -50 +KPX Odieresis Acircumflex -50 +KPX Odieresis Adieresis -50 +KPX Odieresis Agrave -50 +KPX Odieresis Amacron -50 +KPX Odieresis Aogonek -50 +KPX Odieresis Aring -50 +KPX Odieresis Atilde -50 +KPX Odieresis T -40 +KPX Odieresis Tcaron -40 +KPX Odieresis Tcommaaccent -40 +KPX Odieresis V -50 +KPX Odieresis W -50 +KPX Odieresis X -50 +KPX Odieresis Y -70 +KPX Odieresis Yacute -70 +KPX Odieresis Ydieresis -70 +KPX Odieresis comma -40 +KPX Odieresis period -40 +KPX Ograve A -50 +KPX Ograve Aacute -50 +KPX Ograve Abreve -50 +KPX Ograve Acircumflex -50 +KPX Ograve Adieresis -50 +KPX Ograve Agrave -50 +KPX Ograve Amacron -50 +KPX Ograve Aogonek -50 +KPX Ograve Aring -50 +KPX Ograve Atilde -50 +KPX Ograve T -40 +KPX Ograve Tcaron -40 +KPX Ograve Tcommaaccent -40 +KPX Ograve V -50 +KPX Ograve W -50 +KPX Ograve X -50 +KPX Ograve Y -70 +KPX Ograve Yacute -70 +KPX Ograve Ydieresis -70 +KPX Ograve comma -40 +KPX Ograve period -40 +KPX Ohungarumlaut A -50 +KPX Ohungarumlaut Aacute -50 +KPX Ohungarumlaut Abreve -50 +KPX Ohungarumlaut Acircumflex -50 +KPX Ohungarumlaut Adieresis -50 +KPX Ohungarumlaut Agrave -50 +KPX Ohungarumlaut Amacron -50 +KPX Ohungarumlaut Aogonek -50 +KPX Ohungarumlaut Aring -50 +KPX Ohungarumlaut Atilde -50 +KPX Ohungarumlaut T -40 +KPX Ohungarumlaut Tcaron -40 +KPX Ohungarumlaut Tcommaaccent -40 +KPX Ohungarumlaut V -50 +KPX Ohungarumlaut W -50 +KPX Ohungarumlaut X -50 +KPX Ohungarumlaut Y -70 +KPX Ohungarumlaut Yacute -70 +KPX Ohungarumlaut Ydieresis -70 +KPX Ohungarumlaut comma -40 +KPX Ohungarumlaut period -40 +KPX Omacron A -50 +KPX Omacron Aacute -50 +KPX Omacron Abreve -50 +KPX Omacron Acircumflex -50 +KPX Omacron Adieresis -50 +KPX Omacron Agrave -50 +KPX Omacron Amacron -50 +KPX Omacron Aogonek -50 +KPX Omacron Aring -50 +KPX Omacron Atilde -50 +KPX Omacron T -40 +KPX Omacron Tcaron -40 +KPX Omacron Tcommaaccent -40 +KPX Omacron V -50 +KPX Omacron W -50 +KPX Omacron X -50 +KPX Omacron Y -70 +KPX Omacron Yacute -70 +KPX Omacron Ydieresis -70 +KPX Omacron comma -40 +KPX Omacron period -40 +KPX Oslash A -50 +KPX Oslash Aacute -50 +KPX Oslash Abreve -50 +KPX Oslash Acircumflex -50 +KPX Oslash Adieresis -50 +KPX Oslash Agrave -50 +KPX Oslash Amacron -50 +KPX Oslash Aogonek -50 +KPX Oslash Aring -50 +KPX Oslash Atilde -50 +KPX Oslash T -40 +KPX Oslash Tcaron -40 +KPX Oslash Tcommaaccent -40 +KPX Oslash V -50 +KPX Oslash W -50 +KPX Oslash X -50 +KPX Oslash Y -70 +KPX Oslash Yacute -70 +KPX Oslash Ydieresis -70 +KPX Oslash comma -40 +KPX Oslash period -40 +KPX Otilde A -50 +KPX Otilde Aacute -50 +KPX Otilde Abreve -50 +KPX Otilde Acircumflex -50 +KPX Otilde Adieresis -50 +KPX Otilde Agrave -50 +KPX Otilde Amacron -50 +KPX Otilde Aogonek -50 +KPX Otilde Aring -50 +KPX Otilde Atilde -50 +KPX Otilde T -40 +KPX Otilde Tcaron -40 +KPX Otilde Tcommaaccent -40 +KPX Otilde V -50 +KPX Otilde W -50 +KPX Otilde X -50 +KPX Otilde Y -70 +KPX Otilde Yacute -70 +KPX Otilde Ydieresis -70 +KPX Otilde comma -40 +KPX Otilde period -40 +KPX P A -100 +KPX P Aacute -100 +KPX P Abreve -100 +KPX P Acircumflex -100 +KPX P Adieresis -100 +KPX P Agrave -100 +KPX P Amacron -100 +KPX P Aogonek -100 +KPX P Aring -100 +KPX P Atilde -100 +KPX P a -30 +KPX P aacute -30 +KPX P abreve -30 +KPX P acircumflex -30 +KPX P adieresis -30 +KPX P agrave -30 +KPX P amacron -30 +KPX P aogonek -30 +KPX P aring -30 +KPX P atilde -30 +KPX P comma -120 +KPX P e -30 +KPX P eacute -30 +KPX P ecaron -30 +KPX P ecircumflex -30 +KPX P edieresis -30 +KPX P edotaccent -30 +KPX P egrave -30 +KPX P emacron -30 +KPX P eogonek -30 +KPX P o -40 +KPX P oacute -40 +KPX P ocircumflex -40 +KPX P odieresis -40 +KPX P ograve -40 +KPX P ohungarumlaut -40 +KPX P omacron -40 +KPX P oslash -40 +KPX P otilde -40 +KPX P period -120 +KPX Q U -10 +KPX Q Uacute -10 +KPX Q Ucircumflex -10 +KPX Q Udieresis -10 +KPX Q Ugrave -10 +KPX Q Uhungarumlaut -10 +KPX Q Umacron -10 +KPX Q Uogonek -10 +KPX Q Uring -10 +KPX Q comma 20 +KPX Q period 20 +KPX R O -20 +KPX R Oacute -20 +KPX R Ocircumflex -20 +KPX R Odieresis -20 +KPX R Ograve -20 +KPX R Ohungarumlaut -20 +KPX R Omacron -20 +KPX R Oslash -20 +KPX R Otilde -20 +KPX R T -20 +KPX R Tcaron -20 +KPX R Tcommaaccent -20 +KPX R U -20 +KPX R Uacute -20 +KPX R Ucircumflex -20 +KPX R Udieresis -20 +KPX R Ugrave -20 +KPX R Uhungarumlaut -20 +KPX R Umacron -20 +KPX R Uogonek -20 +KPX R Uring -20 +KPX R V -50 +KPX R W -40 +KPX R Y -50 +KPX R Yacute -50 +KPX R Ydieresis -50 +KPX Racute O -20 +KPX Racute Oacute -20 +KPX Racute Ocircumflex -20 +KPX Racute Odieresis -20 +KPX Racute Ograve -20 +KPX Racute Ohungarumlaut -20 +KPX Racute Omacron -20 +KPX Racute Oslash -20 +KPX Racute Otilde -20 +KPX Racute T -20 +KPX Racute Tcaron -20 +KPX Racute Tcommaaccent -20 +KPX Racute U -20 +KPX Racute Uacute -20 +KPX Racute Ucircumflex -20 +KPX Racute Udieresis -20 +KPX Racute Ugrave -20 +KPX Racute Uhungarumlaut -20 +KPX Racute Umacron -20 +KPX Racute Uogonek -20 +KPX Racute Uring -20 +KPX Racute V -50 +KPX Racute W -40 +KPX Racute Y -50 +KPX Racute Yacute -50 +KPX Racute Ydieresis -50 +KPX Rcaron O -20 +KPX Rcaron Oacute -20 +KPX Rcaron Ocircumflex -20 +KPX Rcaron Odieresis -20 +KPX Rcaron Ograve -20 +KPX Rcaron Ohungarumlaut -20 +KPX Rcaron Omacron -20 +KPX Rcaron Oslash -20 +KPX Rcaron Otilde -20 +KPX Rcaron T -20 +KPX Rcaron Tcaron -20 +KPX Rcaron Tcommaaccent -20 +KPX Rcaron U -20 +KPX Rcaron Uacute -20 +KPX Rcaron Ucircumflex -20 +KPX Rcaron Udieresis -20 +KPX Rcaron Ugrave -20 +KPX Rcaron Uhungarumlaut -20 +KPX Rcaron Umacron -20 +KPX Rcaron Uogonek -20 +KPX Rcaron Uring -20 +KPX Rcaron V -50 +KPX Rcaron W -40 +KPX Rcaron Y -50 +KPX Rcaron Yacute -50 +KPX Rcaron Ydieresis -50 +KPX Rcommaaccent O -20 +KPX Rcommaaccent Oacute -20 +KPX Rcommaaccent Ocircumflex -20 +KPX Rcommaaccent Odieresis -20 +KPX Rcommaaccent Ograve -20 +KPX Rcommaaccent Ohungarumlaut -20 +KPX Rcommaaccent Omacron -20 +KPX Rcommaaccent Oslash -20 +KPX Rcommaaccent Otilde -20 +KPX Rcommaaccent T -20 +KPX Rcommaaccent Tcaron -20 +KPX Rcommaaccent Tcommaaccent -20 +KPX Rcommaaccent U -20 +KPX Rcommaaccent Uacute -20 +KPX Rcommaaccent Ucircumflex -20 +KPX Rcommaaccent Udieresis -20 +KPX Rcommaaccent Ugrave -20 +KPX Rcommaaccent Uhungarumlaut -20 +KPX Rcommaaccent Umacron -20 +KPX Rcommaaccent Uogonek -20 +KPX Rcommaaccent Uring -20 +KPX Rcommaaccent V -50 +KPX Rcommaaccent W -40 +KPX Rcommaaccent Y -50 +KPX Rcommaaccent Yacute -50 +KPX Rcommaaccent Ydieresis -50 +KPX T A -90 +KPX T Aacute -90 +KPX T Abreve -90 +KPX T Acircumflex -90 +KPX T Adieresis -90 +KPX T Agrave -90 +KPX T Amacron -90 +KPX T Aogonek -90 +KPX T Aring -90 +KPX T Atilde -90 +KPX T O -40 +KPX T Oacute -40 +KPX T Ocircumflex -40 +KPX T Odieresis -40 +KPX T Ograve -40 +KPX T Ohungarumlaut -40 +KPX T Omacron -40 +KPX T Oslash -40 +KPX T Otilde -40 +KPX T a -80 +KPX T aacute -80 +KPX T abreve -80 +KPX T acircumflex -80 +KPX T adieresis -80 +KPX T agrave -80 +KPX T amacron -80 +KPX T aogonek -80 +KPX T aring -80 +KPX T atilde -80 +KPX T colon -40 +KPX T comma -80 +KPX T e -60 +KPX T eacute -60 +KPX T ecaron -60 +KPX T ecircumflex -60 +KPX T edieresis -60 +KPX T edotaccent -60 +KPX T egrave -60 +KPX T emacron -60 +KPX T eogonek -60 +KPX T hyphen -120 +KPX T o -80 +KPX T oacute -80 +KPX T ocircumflex -80 +KPX T odieresis -80 +KPX T ograve -80 +KPX T ohungarumlaut -80 +KPX T omacron -80 +KPX T oslash -80 +KPX T otilde -80 +KPX T period -80 +KPX T r -80 +KPX T racute -80 +KPX T rcommaaccent -80 +KPX T semicolon -40 +KPX T u -90 +KPX T uacute -90 +KPX T ucircumflex -90 +KPX T udieresis -90 +KPX T ugrave -90 +KPX T uhungarumlaut -90 +KPX T umacron -90 +KPX T uogonek -90 +KPX T uring -90 +KPX T w -60 +KPX T y -60 +KPX T yacute -60 +KPX T ydieresis -60 +KPX Tcaron A -90 +KPX Tcaron Aacute -90 +KPX Tcaron Abreve -90 +KPX Tcaron Acircumflex -90 +KPX Tcaron Adieresis -90 +KPX Tcaron Agrave -90 +KPX Tcaron Amacron -90 +KPX Tcaron Aogonek -90 +KPX Tcaron Aring -90 +KPX Tcaron Atilde -90 +KPX Tcaron O -40 +KPX Tcaron Oacute -40 +KPX Tcaron Ocircumflex -40 +KPX Tcaron Odieresis -40 +KPX Tcaron Ograve -40 +KPX Tcaron Ohungarumlaut -40 +KPX Tcaron Omacron -40 +KPX Tcaron Oslash -40 +KPX Tcaron Otilde -40 +KPX Tcaron a -80 +KPX Tcaron aacute -80 +KPX Tcaron abreve -80 +KPX Tcaron acircumflex -80 +KPX Tcaron adieresis -80 +KPX Tcaron agrave -80 +KPX Tcaron amacron -80 +KPX Tcaron aogonek -80 +KPX Tcaron aring -80 +KPX Tcaron atilde -80 +KPX Tcaron colon -40 +KPX Tcaron comma -80 +KPX Tcaron e -60 +KPX Tcaron eacute -60 +KPX Tcaron ecaron -60 +KPX Tcaron ecircumflex -60 +KPX Tcaron edieresis -60 +KPX Tcaron edotaccent -60 +KPX Tcaron egrave -60 +KPX Tcaron emacron -60 +KPX Tcaron eogonek -60 +KPX Tcaron hyphen -120 +KPX Tcaron o -80 +KPX Tcaron oacute -80 +KPX Tcaron ocircumflex -80 +KPX Tcaron odieresis -80 +KPX Tcaron ograve -80 +KPX Tcaron ohungarumlaut -80 +KPX Tcaron omacron -80 +KPX Tcaron oslash -80 +KPX Tcaron otilde -80 +KPX Tcaron period -80 +KPX Tcaron r -80 +KPX Tcaron racute -80 +KPX Tcaron rcommaaccent -80 +KPX Tcaron semicolon -40 +KPX Tcaron u -90 +KPX Tcaron uacute -90 +KPX Tcaron ucircumflex -90 +KPX Tcaron udieresis -90 +KPX Tcaron ugrave -90 +KPX Tcaron uhungarumlaut -90 +KPX Tcaron umacron -90 +KPX Tcaron uogonek -90 +KPX Tcaron uring -90 +KPX Tcaron w -60 +KPX Tcaron y -60 +KPX Tcaron yacute -60 +KPX Tcaron ydieresis -60 +KPX Tcommaaccent A -90 +KPX Tcommaaccent Aacute -90 +KPX Tcommaaccent Abreve -90 +KPX Tcommaaccent Acircumflex -90 +KPX Tcommaaccent Adieresis -90 +KPX Tcommaaccent Agrave -90 +KPX Tcommaaccent Amacron -90 +KPX Tcommaaccent Aogonek -90 +KPX Tcommaaccent Aring -90 +KPX Tcommaaccent Atilde -90 +KPX Tcommaaccent O -40 +KPX Tcommaaccent Oacute -40 +KPX Tcommaaccent Ocircumflex -40 +KPX Tcommaaccent Odieresis -40 +KPX Tcommaaccent Ograve -40 +KPX Tcommaaccent Ohungarumlaut -40 +KPX Tcommaaccent Omacron -40 +KPX Tcommaaccent Oslash -40 +KPX Tcommaaccent Otilde -40 +KPX Tcommaaccent a -80 +KPX Tcommaaccent aacute -80 +KPX Tcommaaccent abreve -80 +KPX Tcommaaccent acircumflex -80 +KPX Tcommaaccent adieresis -80 +KPX Tcommaaccent agrave -80 +KPX Tcommaaccent amacron -80 +KPX Tcommaaccent aogonek -80 +KPX Tcommaaccent aring -80 +KPX Tcommaaccent atilde -80 +KPX Tcommaaccent colon -40 +KPX Tcommaaccent comma -80 +KPX Tcommaaccent e -60 +KPX Tcommaaccent eacute -60 +KPX Tcommaaccent ecaron -60 +KPX Tcommaaccent ecircumflex -60 +KPX Tcommaaccent edieresis -60 +KPX Tcommaaccent edotaccent -60 +KPX Tcommaaccent egrave -60 +KPX Tcommaaccent emacron -60 +KPX Tcommaaccent eogonek -60 +KPX Tcommaaccent hyphen -120 +KPX Tcommaaccent o -80 +KPX Tcommaaccent oacute -80 +KPX Tcommaaccent ocircumflex -80 +KPX Tcommaaccent odieresis -80 +KPX Tcommaaccent ograve -80 +KPX Tcommaaccent ohungarumlaut -80 +KPX Tcommaaccent omacron -80 +KPX Tcommaaccent oslash -80 +KPX Tcommaaccent otilde -80 +KPX Tcommaaccent period -80 +KPX Tcommaaccent r -80 +KPX Tcommaaccent racute -80 +KPX Tcommaaccent rcommaaccent -80 +KPX Tcommaaccent semicolon -40 +KPX Tcommaaccent u -90 +KPX Tcommaaccent uacute -90 +KPX Tcommaaccent ucircumflex -90 +KPX Tcommaaccent udieresis -90 +KPX Tcommaaccent ugrave -90 +KPX Tcommaaccent uhungarumlaut -90 +KPX Tcommaaccent umacron -90 +KPX Tcommaaccent uogonek -90 +KPX Tcommaaccent uring -90 +KPX Tcommaaccent w -60 +KPX Tcommaaccent y -60 +KPX Tcommaaccent yacute -60 +KPX Tcommaaccent ydieresis -60 +KPX U A -50 +KPX U Aacute -50 +KPX U Abreve -50 +KPX U Acircumflex -50 +KPX U Adieresis -50 +KPX U Agrave -50 +KPX U Amacron -50 +KPX U Aogonek -50 +KPX U Aring -50 +KPX U Atilde -50 +KPX U comma -30 +KPX U period -30 +KPX Uacute A -50 +KPX Uacute Aacute -50 +KPX Uacute Abreve -50 +KPX Uacute Acircumflex -50 +KPX Uacute Adieresis -50 +KPX Uacute Agrave -50 +KPX Uacute Amacron -50 +KPX Uacute Aogonek -50 +KPX Uacute Aring -50 +KPX Uacute Atilde -50 +KPX Uacute comma -30 +KPX Uacute period -30 +KPX Ucircumflex A -50 +KPX Ucircumflex Aacute -50 +KPX Ucircumflex Abreve -50 +KPX Ucircumflex Acircumflex -50 +KPX Ucircumflex Adieresis -50 +KPX Ucircumflex Agrave -50 +KPX Ucircumflex Amacron -50 +KPX Ucircumflex Aogonek -50 +KPX Ucircumflex Aring -50 +KPX Ucircumflex Atilde -50 +KPX Ucircumflex comma -30 +KPX Ucircumflex period -30 +KPX Udieresis A -50 +KPX Udieresis Aacute -50 +KPX Udieresis Abreve -50 +KPX Udieresis Acircumflex -50 +KPX Udieresis Adieresis -50 +KPX Udieresis Agrave -50 +KPX Udieresis Amacron -50 +KPX Udieresis Aogonek -50 +KPX Udieresis Aring -50 +KPX Udieresis Atilde -50 +KPX Udieresis comma -30 +KPX Udieresis period -30 +KPX Ugrave A -50 +KPX Ugrave Aacute -50 +KPX Ugrave Abreve -50 +KPX Ugrave Acircumflex -50 +KPX Ugrave Adieresis -50 +KPX Ugrave Agrave -50 +KPX Ugrave Amacron -50 +KPX Ugrave Aogonek -50 +KPX Ugrave Aring -50 +KPX Ugrave Atilde -50 +KPX Ugrave comma -30 +KPX Ugrave period -30 +KPX Uhungarumlaut A -50 +KPX Uhungarumlaut Aacute -50 +KPX Uhungarumlaut Abreve -50 +KPX Uhungarumlaut Acircumflex -50 +KPX Uhungarumlaut Adieresis -50 +KPX Uhungarumlaut Agrave -50 +KPX Uhungarumlaut Amacron -50 +KPX Uhungarumlaut Aogonek -50 +KPX Uhungarumlaut Aring -50 +KPX Uhungarumlaut Atilde -50 +KPX Uhungarumlaut comma -30 +KPX Uhungarumlaut period -30 +KPX Umacron A -50 +KPX Umacron Aacute -50 +KPX Umacron Abreve -50 +KPX Umacron Acircumflex -50 +KPX Umacron Adieresis -50 +KPX Umacron Agrave -50 +KPX Umacron Amacron -50 +KPX Umacron Aogonek -50 +KPX Umacron Aring -50 +KPX Umacron Atilde -50 +KPX Umacron comma -30 +KPX Umacron period -30 +KPX Uogonek A -50 +KPX Uogonek Aacute -50 +KPX Uogonek Abreve -50 +KPX Uogonek Acircumflex -50 +KPX Uogonek Adieresis -50 +KPX Uogonek Agrave -50 +KPX Uogonek Amacron -50 +KPX Uogonek Aogonek -50 +KPX Uogonek Aring -50 +KPX Uogonek Atilde -50 +KPX Uogonek comma -30 +KPX Uogonek period -30 +KPX Uring A -50 +KPX Uring Aacute -50 +KPX Uring Abreve -50 +KPX Uring Acircumflex -50 +KPX Uring Adieresis -50 +KPX Uring Agrave -50 +KPX Uring Amacron -50 +KPX Uring Aogonek -50 +KPX Uring Aring -50 +KPX Uring Atilde -50 +KPX Uring comma -30 +KPX Uring period -30 +KPX V A -80 +KPX V Aacute -80 +KPX V Abreve -80 +KPX V Acircumflex -80 +KPX V Adieresis -80 +KPX V Agrave -80 +KPX V Amacron -80 +KPX V Aogonek -80 +KPX V Aring -80 +KPX V Atilde -80 +KPX V G -50 +KPX V Gbreve -50 +KPX V Gcommaaccent -50 +KPX V O -50 +KPX V Oacute -50 +KPX V Ocircumflex -50 +KPX V Odieresis -50 +KPX V Ograve -50 +KPX V Ohungarumlaut -50 +KPX V Omacron -50 +KPX V Oslash -50 +KPX V Otilde -50 +KPX V a -60 +KPX V aacute -60 +KPX V abreve -60 +KPX V acircumflex -60 +KPX V adieresis -60 +KPX V agrave -60 +KPX V amacron -60 +KPX V aogonek -60 +KPX V aring -60 +KPX V atilde -60 +KPX V colon -40 +KPX V comma -120 +KPX V e -50 +KPX V eacute -50 +KPX V ecaron -50 +KPX V ecircumflex -50 +KPX V edieresis -50 +KPX V edotaccent -50 +KPX V egrave -50 +KPX V emacron -50 +KPX V eogonek -50 +KPX V hyphen -80 +KPX V o -90 +KPX V oacute -90 +KPX V ocircumflex -90 +KPX V odieresis -90 +KPX V ograve -90 +KPX V ohungarumlaut -90 +KPX V omacron -90 +KPX V oslash -90 +KPX V otilde -90 +KPX V period -120 +KPX V semicolon -40 +KPX V u -60 +KPX V uacute -60 +KPX V ucircumflex -60 +KPX V udieresis -60 +KPX V ugrave -60 +KPX V uhungarumlaut -60 +KPX V umacron -60 +KPX V uogonek -60 +KPX V uring -60 +KPX W A -60 +KPX W Aacute -60 +KPX W Abreve -60 +KPX W Acircumflex -60 +KPX W Adieresis -60 +KPX W Agrave -60 +KPX W Amacron -60 +KPX W Aogonek -60 +KPX W Aring -60 +KPX W Atilde -60 +KPX W O -20 +KPX W Oacute -20 +KPX W Ocircumflex -20 +KPX W Odieresis -20 +KPX W Ograve -20 +KPX W Ohungarumlaut -20 +KPX W Omacron -20 +KPX W Oslash -20 +KPX W Otilde -20 +KPX W a -40 +KPX W aacute -40 +KPX W abreve -40 +KPX W acircumflex -40 +KPX W adieresis -40 +KPX W agrave -40 +KPX W amacron -40 +KPX W aogonek -40 +KPX W aring -40 +KPX W atilde -40 +KPX W colon -10 +KPX W comma -80 +KPX W e -35 +KPX W eacute -35 +KPX W ecaron -35 +KPX W ecircumflex -35 +KPX W edieresis -35 +KPX W edotaccent -35 +KPX W egrave -35 +KPX W emacron -35 +KPX W eogonek -35 +KPX W hyphen -40 +KPX W o -60 +KPX W oacute -60 +KPX W ocircumflex -60 +KPX W odieresis -60 +KPX W ograve -60 +KPX W ohungarumlaut -60 +KPX W omacron -60 +KPX W oslash -60 +KPX W otilde -60 +KPX W period -80 +KPX W semicolon -10 +KPX W u -45 +KPX W uacute -45 +KPX W ucircumflex -45 +KPX W udieresis -45 +KPX W ugrave -45 +KPX W uhungarumlaut -45 +KPX W umacron -45 +KPX W uogonek -45 +KPX W uring -45 +KPX W y -20 +KPX W yacute -20 +KPX W ydieresis -20 +KPX Y A -110 +KPX Y Aacute -110 +KPX Y Abreve -110 +KPX Y Acircumflex -110 +KPX Y Adieresis -110 +KPX Y Agrave -110 +KPX Y Amacron -110 +KPX Y Aogonek -110 +KPX Y Aring -110 +KPX Y Atilde -110 +KPX Y O -70 +KPX Y Oacute -70 +KPX Y Ocircumflex -70 +KPX Y Odieresis -70 +KPX Y Ograve -70 +KPX Y Ohungarumlaut -70 +KPX Y Omacron -70 +KPX Y Oslash -70 +KPX Y Otilde -70 +KPX Y a -90 +KPX Y aacute -90 +KPX Y abreve -90 +KPX Y acircumflex -90 +KPX Y adieresis -90 +KPX Y agrave -90 +KPX Y amacron -90 +KPX Y aogonek -90 +KPX Y aring -90 +KPX Y atilde -90 +KPX Y colon -50 +KPX Y comma -100 +KPX Y e -80 +KPX Y eacute -80 +KPX Y ecaron -80 +KPX Y ecircumflex -80 +KPX Y edieresis -80 +KPX Y edotaccent -80 +KPX Y egrave -80 +KPX Y emacron -80 +KPX Y eogonek -80 +KPX Y o -100 +KPX Y oacute -100 +KPX Y ocircumflex -100 +KPX Y odieresis -100 +KPX Y ograve -100 +KPX Y ohungarumlaut -100 +KPX Y omacron -100 +KPX Y oslash -100 +KPX Y otilde -100 +KPX Y period -100 +KPX Y semicolon -50 +KPX Y u -100 +KPX Y uacute -100 +KPX Y ucircumflex -100 +KPX Y udieresis -100 +KPX Y ugrave -100 +KPX Y uhungarumlaut -100 +KPX Y umacron -100 +KPX Y uogonek -100 +KPX Y uring -100 +KPX Yacute A -110 +KPX Yacute Aacute -110 +KPX Yacute Abreve -110 +KPX Yacute Acircumflex -110 +KPX Yacute Adieresis -110 +KPX Yacute Agrave -110 +KPX Yacute Amacron -110 +KPX Yacute Aogonek -110 +KPX Yacute Aring -110 +KPX Yacute Atilde -110 +KPX Yacute O -70 +KPX Yacute Oacute -70 +KPX Yacute Ocircumflex -70 +KPX Yacute Odieresis -70 +KPX Yacute Ograve -70 +KPX Yacute Ohungarumlaut -70 +KPX Yacute Omacron -70 +KPX Yacute Oslash -70 +KPX Yacute Otilde -70 +KPX Yacute a -90 +KPX Yacute aacute -90 +KPX Yacute abreve -90 +KPX Yacute acircumflex -90 +KPX Yacute adieresis -90 +KPX Yacute agrave -90 +KPX Yacute amacron -90 +KPX Yacute aogonek -90 +KPX Yacute aring -90 +KPX Yacute atilde -90 +KPX Yacute colon -50 +KPX Yacute comma -100 +KPX Yacute e -80 +KPX Yacute eacute -80 +KPX Yacute ecaron -80 +KPX Yacute ecircumflex -80 +KPX Yacute edieresis -80 +KPX Yacute edotaccent -80 +KPX Yacute egrave -80 +KPX Yacute emacron -80 +KPX Yacute eogonek -80 +KPX Yacute o -100 +KPX Yacute oacute -100 +KPX Yacute ocircumflex -100 +KPX Yacute odieresis -100 +KPX Yacute ograve -100 +KPX Yacute ohungarumlaut -100 +KPX Yacute omacron -100 +KPX Yacute oslash -100 +KPX Yacute otilde -100 +KPX Yacute period -100 +KPX Yacute semicolon -50 +KPX Yacute u -100 +KPX Yacute uacute -100 +KPX Yacute ucircumflex -100 +KPX Yacute udieresis -100 +KPX Yacute ugrave -100 +KPX Yacute uhungarumlaut -100 +KPX Yacute umacron -100 +KPX Yacute uogonek -100 +KPX Yacute uring -100 +KPX Ydieresis A -110 +KPX Ydieresis Aacute -110 +KPX Ydieresis Abreve -110 +KPX Ydieresis Acircumflex -110 +KPX Ydieresis Adieresis -110 +KPX Ydieresis Agrave -110 +KPX Ydieresis Amacron -110 +KPX Ydieresis Aogonek -110 +KPX Ydieresis Aring -110 +KPX Ydieresis Atilde -110 +KPX Ydieresis O -70 +KPX Ydieresis Oacute -70 +KPX Ydieresis Ocircumflex -70 +KPX Ydieresis Odieresis -70 +KPX Ydieresis Ograve -70 +KPX Ydieresis Ohungarumlaut -70 +KPX Ydieresis Omacron -70 +KPX Ydieresis Oslash -70 +KPX Ydieresis Otilde -70 +KPX Ydieresis a -90 +KPX Ydieresis aacute -90 +KPX Ydieresis abreve -90 +KPX Ydieresis acircumflex -90 +KPX Ydieresis adieresis -90 +KPX Ydieresis agrave -90 +KPX Ydieresis amacron -90 +KPX Ydieresis aogonek -90 +KPX Ydieresis aring -90 +KPX Ydieresis atilde -90 +KPX Ydieresis colon -50 +KPX Ydieresis comma -100 +KPX Ydieresis e -80 +KPX Ydieresis eacute -80 +KPX Ydieresis ecaron -80 +KPX Ydieresis ecircumflex -80 +KPX Ydieresis edieresis -80 +KPX Ydieresis edotaccent -80 +KPX Ydieresis egrave -80 +KPX Ydieresis emacron -80 +KPX Ydieresis eogonek -80 +KPX Ydieresis o -100 +KPX Ydieresis oacute -100 +KPX Ydieresis ocircumflex -100 +KPX Ydieresis odieresis -100 +KPX Ydieresis ograve -100 +KPX Ydieresis ohungarumlaut -100 +KPX Ydieresis omacron -100 +KPX Ydieresis oslash -100 +KPX Ydieresis otilde -100 +KPX Ydieresis period -100 +KPX Ydieresis semicolon -50 +KPX Ydieresis u -100 +KPX Ydieresis uacute -100 +KPX Ydieresis ucircumflex -100 +KPX Ydieresis udieresis -100 +KPX Ydieresis ugrave -100 +KPX Ydieresis uhungarumlaut -100 +KPX Ydieresis umacron -100 +KPX Ydieresis uogonek -100 +KPX Ydieresis uring -100 +KPX a g -10 +KPX a gbreve -10 +KPX a gcommaaccent -10 +KPX a v -15 +KPX a w -15 +KPX a y -20 +KPX a yacute -20 +KPX a ydieresis -20 +KPX aacute g -10 +KPX aacute gbreve -10 +KPX aacute gcommaaccent -10 +KPX aacute v -15 +KPX aacute w -15 +KPX aacute y -20 +KPX aacute yacute -20 +KPX aacute ydieresis -20 +KPX abreve g -10 +KPX abreve gbreve -10 +KPX abreve gcommaaccent -10 +KPX abreve v -15 +KPX abreve w -15 +KPX abreve y -20 +KPX abreve yacute -20 +KPX abreve ydieresis -20 +KPX acircumflex g -10 +KPX acircumflex gbreve -10 +KPX acircumflex gcommaaccent -10 +KPX acircumflex v -15 +KPX acircumflex w -15 +KPX acircumflex y -20 +KPX acircumflex yacute -20 +KPX acircumflex ydieresis -20 +KPX adieresis g -10 +KPX adieresis gbreve -10 +KPX adieresis gcommaaccent -10 +KPX adieresis v -15 +KPX adieresis w -15 +KPX adieresis y -20 +KPX adieresis yacute -20 +KPX adieresis ydieresis -20 +KPX agrave g -10 +KPX agrave gbreve -10 +KPX agrave gcommaaccent -10 +KPX agrave v -15 +KPX agrave w -15 +KPX agrave y -20 +KPX agrave yacute -20 +KPX agrave ydieresis -20 +KPX amacron g -10 +KPX amacron gbreve -10 +KPX amacron gcommaaccent -10 +KPX amacron v -15 +KPX amacron w -15 +KPX amacron y -20 +KPX amacron yacute -20 +KPX amacron ydieresis -20 +KPX aogonek g -10 +KPX aogonek gbreve -10 +KPX aogonek gcommaaccent -10 +KPX aogonek v -15 +KPX aogonek w -15 +KPX aogonek y -20 +KPX aogonek yacute -20 +KPX aogonek ydieresis -20 +KPX aring g -10 +KPX aring gbreve -10 +KPX aring gcommaaccent -10 +KPX aring v -15 +KPX aring w -15 +KPX aring y -20 +KPX aring yacute -20 +KPX aring ydieresis -20 +KPX atilde g -10 +KPX atilde gbreve -10 +KPX atilde gcommaaccent -10 +KPX atilde v -15 +KPX atilde w -15 +KPX atilde y -20 +KPX atilde yacute -20 +KPX atilde ydieresis -20 +KPX b l -10 +KPX b lacute -10 +KPX b lcommaaccent -10 +KPX b lslash -10 +KPX b u -20 +KPX b uacute -20 +KPX b ucircumflex -20 +KPX b udieresis -20 +KPX b ugrave -20 +KPX b uhungarumlaut -20 +KPX b umacron -20 +KPX b uogonek -20 +KPX b uring -20 +KPX b v -20 +KPX b y -20 +KPX b yacute -20 +KPX b ydieresis -20 +KPX c h -10 +KPX c k -20 +KPX c kcommaaccent -20 +KPX c l -20 +KPX c lacute -20 +KPX c lcommaaccent -20 +KPX c lslash -20 +KPX c y -10 +KPX c yacute -10 +KPX c ydieresis -10 +KPX cacute h -10 +KPX cacute k -20 +KPX cacute kcommaaccent -20 +KPX cacute l -20 +KPX cacute lacute -20 +KPX cacute lcommaaccent -20 +KPX cacute lslash -20 +KPX cacute y -10 +KPX cacute yacute -10 +KPX cacute ydieresis -10 +KPX ccaron h -10 +KPX ccaron k -20 +KPX ccaron kcommaaccent -20 +KPX ccaron l -20 +KPX ccaron lacute -20 +KPX ccaron lcommaaccent -20 +KPX ccaron lslash -20 +KPX ccaron y -10 +KPX ccaron yacute -10 +KPX ccaron ydieresis -10 +KPX ccedilla h -10 +KPX ccedilla k -20 +KPX ccedilla kcommaaccent -20 +KPX ccedilla l -20 +KPX ccedilla lacute -20 +KPX ccedilla lcommaaccent -20 +KPX ccedilla lslash -20 +KPX ccedilla y -10 +KPX ccedilla yacute -10 +KPX ccedilla ydieresis -10 +KPX colon space -40 +KPX comma quotedblright -120 +KPX comma quoteright -120 +KPX comma space -40 +KPX d d -10 +KPX d dcroat -10 +KPX d v -15 +KPX d w -15 +KPX d y -15 +KPX d yacute -15 +KPX d ydieresis -15 +KPX dcroat d -10 +KPX dcroat dcroat -10 +KPX dcroat v -15 +KPX dcroat w -15 +KPX dcroat y -15 +KPX dcroat yacute -15 +KPX dcroat ydieresis -15 +KPX e comma 10 +KPX e period 20 +KPX e v -15 +KPX e w -15 +KPX e x -15 +KPX e y -15 +KPX e yacute -15 +KPX e ydieresis -15 +KPX eacute comma 10 +KPX eacute period 20 +KPX eacute v -15 +KPX eacute w -15 +KPX eacute x -15 +KPX eacute y -15 +KPX eacute yacute -15 +KPX eacute ydieresis -15 +KPX ecaron comma 10 +KPX ecaron period 20 +KPX ecaron v -15 +KPX ecaron w -15 +KPX ecaron x -15 +KPX ecaron y -15 +KPX ecaron yacute -15 +KPX ecaron ydieresis -15 +KPX ecircumflex comma 10 +KPX ecircumflex period 20 +KPX ecircumflex v -15 +KPX ecircumflex w -15 +KPX ecircumflex x -15 +KPX ecircumflex y -15 +KPX ecircumflex yacute -15 +KPX ecircumflex ydieresis -15 +KPX edieresis comma 10 +KPX edieresis period 20 +KPX edieresis v -15 +KPX edieresis w -15 +KPX edieresis x -15 +KPX edieresis y -15 +KPX edieresis yacute -15 +KPX edieresis ydieresis -15 +KPX edotaccent comma 10 +KPX edotaccent period 20 +KPX edotaccent v -15 +KPX edotaccent w -15 +KPX edotaccent x -15 +KPX edotaccent y -15 +KPX edotaccent yacute -15 +KPX edotaccent ydieresis -15 +KPX egrave comma 10 +KPX egrave period 20 +KPX egrave v -15 +KPX egrave w -15 +KPX egrave x -15 +KPX egrave y -15 +KPX egrave yacute -15 +KPX egrave ydieresis -15 +KPX emacron comma 10 +KPX emacron period 20 +KPX emacron v -15 +KPX emacron w -15 +KPX emacron x -15 +KPX emacron y -15 +KPX emacron yacute -15 +KPX emacron ydieresis -15 +KPX eogonek comma 10 +KPX eogonek period 20 +KPX eogonek v -15 +KPX eogonek w -15 +KPX eogonek x -15 +KPX eogonek y -15 +KPX eogonek yacute -15 +KPX eogonek ydieresis -15 +KPX f comma -10 +KPX f e -10 +KPX f eacute -10 +KPX f ecaron -10 +KPX f ecircumflex -10 +KPX f edieresis -10 +KPX f edotaccent -10 +KPX f egrave -10 +KPX f emacron -10 +KPX f eogonek -10 +KPX f o -20 +KPX f oacute -20 +KPX f ocircumflex -20 +KPX f odieresis -20 +KPX f ograve -20 +KPX f ohungarumlaut -20 +KPX f omacron -20 +KPX f oslash -20 +KPX f otilde -20 +KPX f period -10 +KPX f quotedblright 30 +KPX f quoteright 30 +KPX g e 10 +KPX g eacute 10 +KPX g ecaron 10 +KPX g ecircumflex 10 +KPX g edieresis 10 +KPX g edotaccent 10 +KPX g egrave 10 +KPX g emacron 10 +KPX g eogonek 10 +KPX g g -10 +KPX g gbreve -10 +KPX g gcommaaccent -10 +KPX gbreve e 10 +KPX gbreve eacute 10 +KPX gbreve ecaron 10 +KPX gbreve ecircumflex 10 +KPX gbreve edieresis 10 +KPX gbreve edotaccent 10 +KPX gbreve egrave 10 +KPX gbreve emacron 10 +KPX gbreve eogonek 10 +KPX gbreve g -10 +KPX gbreve gbreve -10 +KPX gbreve gcommaaccent -10 +KPX gcommaaccent e 10 +KPX gcommaaccent eacute 10 +KPX gcommaaccent ecaron 10 +KPX gcommaaccent ecircumflex 10 +KPX gcommaaccent edieresis 10 +KPX gcommaaccent edotaccent 10 +KPX gcommaaccent egrave 10 +KPX gcommaaccent emacron 10 +KPX gcommaaccent eogonek 10 +KPX gcommaaccent g -10 +KPX gcommaaccent gbreve -10 +KPX gcommaaccent gcommaaccent -10 +KPX h y -20 +KPX h yacute -20 +KPX h ydieresis -20 +KPX k o -15 +KPX k oacute -15 +KPX k ocircumflex -15 +KPX k odieresis -15 +KPX k ograve -15 +KPX k ohungarumlaut -15 +KPX k omacron -15 +KPX k oslash -15 +KPX k otilde -15 +KPX kcommaaccent o -15 +KPX kcommaaccent oacute -15 +KPX kcommaaccent ocircumflex -15 +KPX kcommaaccent odieresis -15 +KPX kcommaaccent ograve -15 +KPX kcommaaccent ohungarumlaut -15 +KPX kcommaaccent omacron -15 +KPX kcommaaccent oslash -15 +KPX kcommaaccent otilde -15 +KPX l w -15 +KPX l y -15 +KPX l yacute -15 +KPX l ydieresis -15 +KPX lacute w -15 +KPX lacute y -15 +KPX lacute yacute -15 +KPX lacute ydieresis -15 +KPX lcommaaccent w -15 +KPX lcommaaccent y -15 +KPX lcommaaccent yacute -15 +KPX lcommaaccent ydieresis -15 +KPX lslash w -15 +KPX lslash y -15 +KPX lslash yacute -15 +KPX lslash ydieresis -15 +KPX m u -20 +KPX m uacute -20 +KPX m ucircumflex -20 +KPX m udieresis -20 +KPX m ugrave -20 +KPX m uhungarumlaut -20 +KPX m umacron -20 +KPX m uogonek -20 +KPX m uring -20 +KPX m y -30 +KPX m yacute -30 +KPX m ydieresis -30 +KPX n u -10 +KPX n uacute -10 +KPX n ucircumflex -10 +KPX n udieresis -10 +KPX n ugrave -10 +KPX n uhungarumlaut -10 +KPX n umacron -10 +KPX n uogonek -10 +KPX n uring -10 +KPX n v -40 +KPX n y -20 +KPX n yacute -20 +KPX n ydieresis -20 +KPX nacute u -10 +KPX nacute uacute -10 +KPX nacute ucircumflex -10 +KPX nacute udieresis -10 +KPX nacute ugrave -10 +KPX nacute uhungarumlaut -10 +KPX nacute umacron -10 +KPX nacute uogonek -10 +KPX nacute uring -10 +KPX nacute v -40 +KPX nacute y -20 +KPX nacute yacute -20 +KPX nacute ydieresis -20 +KPX ncaron u -10 +KPX ncaron uacute -10 +KPX ncaron ucircumflex -10 +KPX ncaron udieresis -10 +KPX ncaron ugrave -10 +KPX ncaron uhungarumlaut -10 +KPX ncaron umacron -10 +KPX ncaron uogonek -10 +KPX ncaron uring -10 +KPX ncaron v -40 +KPX ncaron y -20 +KPX ncaron yacute -20 +KPX ncaron ydieresis -20 +KPX ncommaaccent u -10 +KPX ncommaaccent uacute -10 +KPX ncommaaccent ucircumflex -10 +KPX ncommaaccent udieresis -10 +KPX ncommaaccent ugrave -10 +KPX ncommaaccent uhungarumlaut -10 +KPX ncommaaccent umacron -10 +KPX ncommaaccent uogonek -10 +KPX ncommaaccent uring -10 +KPX ncommaaccent v -40 +KPX ncommaaccent y -20 +KPX ncommaaccent yacute -20 +KPX ncommaaccent ydieresis -20 +KPX ntilde u -10 +KPX ntilde uacute -10 +KPX ntilde ucircumflex -10 +KPX ntilde udieresis -10 +KPX ntilde ugrave -10 +KPX ntilde uhungarumlaut -10 +KPX ntilde umacron -10 +KPX ntilde uogonek -10 +KPX ntilde uring -10 +KPX ntilde v -40 +KPX ntilde y -20 +KPX ntilde yacute -20 +KPX ntilde ydieresis -20 +KPX o v -20 +KPX o w -15 +KPX o x -30 +KPX o y -20 +KPX o yacute -20 +KPX o ydieresis -20 +KPX oacute v -20 +KPX oacute w -15 +KPX oacute x -30 +KPX oacute y -20 +KPX oacute yacute -20 +KPX oacute ydieresis -20 +KPX ocircumflex v -20 +KPX ocircumflex w -15 +KPX ocircumflex x -30 +KPX ocircumflex y -20 +KPX ocircumflex yacute -20 +KPX ocircumflex ydieresis -20 +KPX odieresis v -20 +KPX odieresis w -15 +KPX odieresis x -30 +KPX odieresis y -20 +KPX odieresis yacute -20 +KPX odieresis ydieresis -20 +KPX ograve v -20 +KPX ograve w -15 +KPX ograve x -30 +KPX ograve y -20 +KPX ograve yacute -20 +KPX ograve ydieresis -20 +KPX ohungarumlaut v -20 +KPX ohungarumlaut w -15 +KPX ohungarumlaut x -30 +KPX ohungarumlaut y -20 +KPX ohungarumlaut yacute -20 +KPX ohungarumlaut ydieresis -20 +KPX omacron v -20 +KPX omacron w -15 +KPX omacron x -30 +KPX omacron y -20 +KPX omacron yacute -20 +KPX omacron ydieresis -20 +KPX oslash v -20 +KPX oslash w -15 +KPX oslash x -30 +KPX oslash y -20 +KPX oslash yacute -20 +KPX oslash ydieresis -20 +KPX otilde v -20 +KPX otilde w -15 +KPX otilde x -30 +KPX otilde y -20 +KPX otilde yacute -20 +KPX otilde ydieresis -20 +KPX p y -15 +KPX p yacute -15 +KPX p ydieresis -15 +KPX period quotedblright -120 +KPX period quoteright -120 +KPX period space -40 +KPX quotedblright space -80 +KPX quoteleft quoteleft -46 +KPX quoteright d -80 +KPX quoteright dcroat -80 +KPX quoteright l -20 +KPX quoteright lacute -20 +KPX quoteright lcommaaccent -20 +KPX quoteright lslash -20 +KPX quoteright quoteright -46 +KPX quoteright r -40 +KPX quoteright racute -40 +KPX quoteright rcaron -40 +KPX quoteright rcommaaccent -40 +KPX quoteright s -60 +KPX quoteright sacute -60 +KPX quoteright scaron -60 +KPX quoteright scedilla -60 +KPX quoteright scommaaccent -60 +KPX quoteright space -80 +KPX quoteright v -20 +KPX r c -20 +KPX r cacute -20 +KPX r ccaron -20 +KPX r ccedilla -20 +KPX r comma -60 +KPX r d -20 +KPX r dcroat -20 +KPX r g -15 +KPX r gbreve -15 +KPX r gcommaaccent -15 +KPX r hyphen -20 +KPX r o -20 +KPX r oacute -20 +KPX r ocircumflex -20 +KPX r odieresis -20 +KPX r ograve -20 +KPX r ohungarumlaut -20 +KPX r omacron -20 +KPX r oslash -20 +KPX r otilde -20 +KPX r period -60 +KPX r q -20 +KPX r s -15 +KPX r sacute -15 +KPX r scaron -15 +KPX r scedilla -15 +KPX r scommaaccent -15 +KPX r t 20 +KPX r tcommaaccent 20 +KPX r v 10 +KPX r y 10 +KPX r yacute 10 +KPX r ydieresis 10 +KPX racute c -20 +KPX racute cacute -20 +KPX racute ccaron -20 +KPX racute ccedilla -20 +KPX racute comma -60 +KPX racute d -20 +KPX racute dcroat -20 +KPX racute g -15 +KPX racute gbreve -15 +KPX racute gcommaaccent -15 +KPX racute hyphen -20 +KPX racute o -20 +KPX racute oacute -20 +KPX racute ocircumflex -20 +KPX racute odieresis -20 +KPX racute ograve -20 +KPX racute ohungarumlaut -20 +KPX racute omacron -20 +KPX racute oslash -20 +KPX racute otilde -20 +KPX racute period -60 +KPX racute q -20 +KPX racute s -15 +KPX racute sacute -15 +KPX racute scaron -15 +KPX racute scedilla -15 +KPX racute scommaaccent -15 +KPX racute t 20 +KPX racute tcommaaccent 20 +KPX racute v 10 +KPX racute y 10 +KPX racute yacute 10 +KPX racute ydieresis 10 +KPX rcaron c -20 +KPX rcaron cacute -20 +KPX rcaron ccaron -20 +KPX rcaron ccedilla -20 +KPX rcaron comma -60 +KPX rcaron d -20 +KPX rcaron dcroat -20 +KPX rcaron g -15 +KPX rcaron gbreve -15 +KPX rcaron gcommaaccent -15 +KPX rcaron hyphen -20 +KPX rcaron o -20 +KPX rcaron oacute -20 +KPX rcaron ocircumflex -20 +KPX rcaron odieresis -20 +KPX rcaron ograve -20 +KPX rcaron ohungarumlaut -20 +KPX rcaron omacron -20 +KPX rcaron oslash -20 +KPX rcaron otilde -20 +KPX rcaron period -60 +KPX rcaron q -20 +KPX rcaron s -15 +KPX rcaron sacute -15 +KPX rcaron scaron -15 +KPX rcaron scedilla -15 +KPX rcaron scommaaccent -15 +KPX rcaron t 20 +KPX rcaron tcommaaccent 20 +KPX rcaron v 10 +KPX rcaron y 10 +KPX rcaron yacute 10 +KPX rcaron ydieresis 10 +KPX rcommaaccent c -20 +KPX rcommaaccent cacute -20 +KPX rcommaaccent ccaron -20 +KPX rcommaaccent ccedilla -20 +KPX rcommaaccent comma -60 +KPX rcommaaccent d -20 +KPX rcommaaccent dcroat -20 +KPX rcommaaccent g -15 +KPX rcommaaccent gbreve -15 +KPX rcommaaccent gcommaaccent -15 +KPX rcommaaccent hyphen -20 +KPX rcommaaccent o -20 +KPX rcommaaccent oacute -20 +KPX rcommaaccent ocircumflex -20 +KPX rcommaaccent odieresis -20 +KPX rcommaaccent ograve -20 +KPX rcommaaccent ohungarumlaut -20 +KPX rcommaaccent omacron -20 +KPX rcommaaccent oslash -20 +KPX rcommaaccent otilde -20 +KPX rcommaaccent period -60 +KPX rcommaaccent q -20 +KPX rcommaaccent s -15 +KPX rcommaaccent sacute -15 +KPX rcommaaccent scaron -15 +KPX rcommaaccent scedilla -15 +KPX rcommaaccent scommaaccent -15 +KPX rcommaaccent t 20 +KPX rcommaaccent tcommaaccent 20 +KPX rcommaaccent v 10 +KPX rcommaaccent y 10 +KPX rcommaaccent yacute 10 +KPX rcommaaccent ydieresis 10 +KPX s w -15 +KPX sacute w -15 +KPX scaron w -15 +KPX scedilla w -15 +KPX scommaaccent w -15 +KPX semicolon space -40 +KPX space T -100 +KPX space Tcaron -100 +KPX space Tcommaaccent -100 +KPX space V -80 +KPX space W -80 +KPX space Y -120 +KPX space Yacute -120 +KPX space Ydieresis -120 +KPX space quotedblleft -80 +KPX space quoteleft -60 +KPX v a -20 +KPX v aacute -20 +KPX v abreve -20 +KPX v acircumflex -20 +KPX v adieresis -20 +KPX v agrave -20 +KPX v amacron -20 +KPX v aogonek -20 +KPX v aring -20 +KPX v atilde -20 +KPX v comma -80 +KPX v o -30 +KPX v oacute -30 +KPX v ocircumflex -30 +KPX v odieresis -30 +KPX v ograve -30 +KPX v ohungarumlaut -30 +KPX v omacron -30 +KPX v oslash -30 +KPX v otilde -30 +KPX v period -80 +KPX w comma -40 +KPX w o -20 +KPX w oacute -20 +KPX w ocircumflex -20 +KPX w odieresis -20 +KPX w ograve -20 +KPX w ohungarumlaut -20 +KPX w omacron -20 +KPX w oslash -20 +KPX w otilde -20 +KPX w period -40 +KPX x e -10 +KPX x eacute -10 +KPX x ecaron -10 +KPX x ecircumflex -10 +KPX x edieresis -10 +KPX x edotaccent -10 +KPX x egrave -10 +KPX x emacron -10 +KPX x eogonek -10 +KPX y a -30 +KPX y aacute -30 +KPX y abreve -30 +KPX y acircumflex -30 +KPX y adieresis -30 +KPX y agrave -30 +KPX y amacron -30 +KPX y aogonek -30 +KPX y aring -30 +KPX y atilde -30 +KPX y comma -80 +KPX y e -10 +KPX y eacute -10 +KPX y ecaron -10 +KPX y ecircumflex -10 +KPX y edieresis -10 +KPX y edotaccent -10 +KPX y egrave -10 +KPX y emacron -10 +KPX y eogonek -10 +KPX y o -25 +KPX y oacute -25 +KPX y ocircumflex -25 +KPX y odieresis -25 +KPX y ograve -25 +KPX y ohungarumlaut -25 +KPX y omacron -25 +KPX y oslash -25 +KPX y otilde -25 +KPX y period -80 +KPX yacute a -30 +KPX yacute aacute -30 +KPX yacute abreve -30 +KPX yacute acircumflex -30 +KPX yacute adieresis -30 +KPX yacute agrave -30 +KPX yacute amacron -30 +KPX yacute aogonek -30 +KPX yacute aring -30 +KPX yacute atilde -30 +KPX yacute comma -80 +KPX yacute e -10 +KPX yacute eacute -10 +KPX yacute ecaron -10 +KPX yacute ecircumflex -10 +KPX yacute edieresis -10 +KPX yacute edotaccent -10 +KPX yacute egrave -10 +KPX yacute emacron -10 +KPX yacute eogonek -10 +KPX yacute o -25 +KPX yacute oacute -25 +KPX yacute ocircumflex -25 +KPX yacute odieresis -25 +KPX yacute ograve -25 +KPX yacute ohungarumlaut -25 +KPX yacute omacron -25 +KPX yacute oslash -25 +KPX yacute otilde -25 +KPX yacute period -80 +KPX ydieresis a -30 +KPX ydieresis aacute -30 +KPX ydieresis abreve -30 +KPX ydieresis acircumflex -30 +KPX ydieresis adieresis -30 +KPX ydieresis agrave -30 +KPX ydieresis amacron -30 +KPX ydieresis aogonek -30 +KPX ydieresis aring -30 +KPX ydieresis atilde -30 +KPX ydieresis comma -80 +KPX ydieresis e -10 +KPX ydieresis eacute -10 +KPX ydieresis ecaron -10 +KPX ydieresis ecircumflex -10 +KPX ydieresis edieresis -10 +KPX ydieresis edotaccent -10 +KPX ydieresis egrave -10 +KPX ydieresis emacron -10 +KPX ydieresis eogonek -10 +KPX ydieresis o -25 +KPX ydieresis oacute -25 +KPX ydieresis ocircumflex -25 +KPX ydieresis odieresis -25 +KPX ydieresis ograve -25 +KPX ydieresis ohungarumlaut -25 +KPX ydieresis omacron -25 +KPX ydieresis oslash -25 +KPX ydieresis otilde -25 +KPX ydieresis period -80 +KPX z e 10 +KPX z eacute 10 +KPX z ecaron 10 +KPX z ecircumflex 10 +KPX z edieresis 10 +KPX z edotaccent 10 +KPX z egrave 10 +KPX z emacron 10 +KPX z eogonek 10 +KPX zacute e 10 +KPX zacute eacute 10 +KPX zacute ecaron 10 +KPX zacute ecircumflex 10 +KPX zacute edieresis 10 +KPX zacute edotaccent 10 +KPX zacute egrave 10 +KPX zacute emacron 10 +KPX zacute eogonek 10 +KPX zcaron e 10 +KPX zcaron eacute 10 +KPX zcaron ecaron 10 +KPX zcaron ecircumflex 10 +KPX zcaron edieresis 10 +KPX zcaron edotaccent 10 +KPX zcaron egrave 10 +KPX zcaron emacron 10 +KPX zcaron eogonek 10 +KPX zdotaccent e 10 +KPX zdotaccent eacute 10 +KPX zdotaccent ecaron 10 +KPX zdotaccent ecircumflex 10 +KPX zdotaccent edieresis 10 +KPX zdotaccent edotaccent 10 +KPX zdotaccent egrave 10 +KPX zdotaccent emacron 10 +KPX zdotaccent eogonek 10 +EndKernPairs +EndKernData +EndFontMetrics diff --git a/internal/corefont/Core14_AFMs/Helvetica-BoldOblique.afm b/internal/corefont/Core14_AFMs/Helvetica-BoldOblique.afm new file mode 100644 index 0000000000000000000000000000000000000000..1715b210467ce82625be36f6cbf20eae54cb67e0 --- /dev/null +++ b/internal/corefont/Core14_AFMs/Helvetica-BoldOblique.afm @@ -0,0 +1,2827 @@ +StartFontMetrics 4.1 +Comment Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems Incorporated. All Rights Reserved. +Comment Creation Date: Thu May 1 12:45:12 1997 +Comment UniqueID 43053 +Comment VMusage 14482 68586 +FontName Helvetica-BoldOblique +FullName Helvetica Bold Oblique +FamilyName Helvetica +Weight Bold +ItalicAngle -12 +IsFixedPitch false +CharacterSet ExtendedRoman +FontBBox -174 -228 1114 962 +UnderlinePosition -100 +UnderlineThickness 50 +Version 002.000 +Notice Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems Incorporated. All Rights Reserved.Helvetica is a trademark of Linotype-Hell AG and/or its subsidiaries. +EncodingScheme AdobeStandardEncoding +CapHeight 718 +XHeight 532 +Ascender 718 +Descender -207 +StdHW 118 +StdVW 140 +StartCharMetrics 315 +C 32 ; WX 278 ; N space ; B 0 0 0 0 ; +C 33 ; WX 333 ; N exclam ; B 94 0 397 718 ; +C 34 ; WX 474 ; N quotedbl ; B 193 447 529 718 ; +C 35 ; WX 556 ; N numbersign ; B 60 0 644 698 ; +C 36 ; WX 556 ; N dollar ; B 67 -115 622 775 ; +C 37 ; WX 889 ; N percent ; B 136 -19 901 710 ; +C 38 ; WX 722 ; N ampersand ; B 89 -19 732 718 ; +C 39 ; WX 278 ; N quoteright ; B 167 445 362 718 ; +C 40 ; WX 333 ; N parenleft ; B 76 -208 470 734 ; +C 41 ; WX 333 ; N parenright ; B -25 -208 369 734 ; +C 42 ; WX 389 ; N asterisk ; B 146 387 481 718 ; +C 43 ; WX 584 ; N plus ; B 82 0 610 506 ; +C 44 ; WX 278 ; N comma ; B 28 -168 245 146 ; +C 45 ; WX 333 ; N hyphen ; B 73 215 379 345 ; +C 46 ; WX 278 ; N period ; B 64 0 245 146 ; +C 47 ; WX 278 ; N slash ; B -37 -19 468 737 ; +C 48 ; WX 556 ; N zero ; B 86 -19 617 710 ; +C 49 ; WX 556 ; N one ; B 173 0 529 710 ; +C 50 ; WX 556 ; N two ; B 26 0 619 710 ; +C 51 ; WX 556 ; N three ; B 65 -19 608 710 ; +C 52 ; WX 556 ; N four ; B 60 0 598 710 ; +C 53 ; WX 556 ; N five ; B 64 -19 636 698 ; +C 54 ; WX 556 ; N six ; B 85 -19 619 710 ; +C 55 ; WX 556 ; N seven ; B 125 0 676 698 ; +C 56 ; WX 556 ; N eight ; B 69 -19 616 710 ; +C 57 ; WX 556 ; N nine ; B 78 -19 615 710 ; +C 58 ; WX 333 ; N colon ; B 92 0 351 512 ; +C 59 ; WX 333 ; N semicolon ; B 56 -168 351 512 ; +C 60 ; WX 584 ; N less ; B 82 -8 655 514 ; +C 61 ; WX 584 ; N equal ; B 58 87 633 419 ; +C 62 ; WX 584 ; N greater ; B 36 -8 609 514 ; +C 63 ; WX 611 ; N question ; B 165 0 671 727 ; +C 64 ; WX 975 ; N at ; B 186 -19 954 737 ; +C 65 ; WX 722 ; N A ; B 20 0 702 718 ; +C 66 ; WX 722 ; N B ; B 76 0 764 718 ; +C 67 ; WX 722 ; N C ; B 107 -19 789 737 ; +C 68 ; WX 722 ; N D ; B 76 0 777 718 ; +C 69 ; WX 667 ; N E ; B 76 0 757 718 ; +C 70 ; WX 611 ; N F ; B 76 0 740 718 ; +C 71 ; WX 778 ; N G ; B 108 -19 817 737 ; +C 72 ; WX 722 ; N H ; B 71 0 804 718 ; +C 73 ; WX 278 ; N I ; B 64 0 367 718 ; +C 74 ; WX 556 ; N J ; B 60 -18 637 718 ; +C 75 ; WX 722 ; N K ; B 87 0 858 718 ; +C 76 ; WX 611 ; N L ; B 76 0 611 718 ; +C 77 ; WX 833 ; N M ; B 69 0 918 718 ; +C 78 ; WX 722 ; N N ; B 69 0 807 718 ; +C 79 ; WX 778 ; N O ; B 107 -19 823 737 ; +C 80 ; WX 667 ; N P ; B 76 0 738 718 ; +C 81 ; WX 778 ; N Q ; B 107 -52 823 737 ; +C 82 ; WX 722 ; N R ; B 76 0 778 718 ; +C 83 ; WX 667 ; N S ; B 81 -19 718 737 ; +C 84 ; WX 611 ; N T ; B 140 0 751 718 ; +C 85 ; WX 722 ; N U ; B 116 -19 804 718 ; +C 86 ; WX 667 ; N V ; B 172 0 801 718 ; +C 87 ; WX 944 ; N W ; B 169 0 1082 718 ; +C 88 ; WX 667 ; N X ; B 14 0 791 718 ; +C 89 ; WX 667 ; N Y ; B 168 0 806 718 ; +C 90 ; WX 611 ; N Z ; B 25 0 737 718 ; +C 91 ; WX 333 ; N bracketleft ; B 21 -196 462 722 ; +C 92 ; WX 278 ; N backslash ; B 124 -19 307 737 ; +C 93 ; WX 333 ; N bracketright ; B -18 -196 423 722 ; +C 94 ; WX 584 ; N asciicircum ; B 131 323 591 698 ; +C 95 ; WX 556 ; N underscore ; B -27 -125 540 -75 ; +C 96 ; WX 278 ; N quoteleft ; B 165 454 361 727 ; +C 97 ; WX 556 ; N a ; B 55 -14 583 546 ; +C 98 ; WX 611 ; N b ; B 61 -14 645 718 ; +C 99 ; WX 556 ; N c ; B 79 -14 599 546 ; +C 100 ; WX 611 ; N d ; B 82 -14 704 718 ; +C 101 ; WX 556 ; N e ; B 70 -14 593 546 ; +C 102 ; WX 333 ; N f ; B 87 0 469 727 ; L i fi ; L l fl ; +C 103 ; WX 611 ; N g ; B 38 -217 666 546 ; +C 104 ; WX 611 ; N h ; B 65 0 629 718 ; +C 105 ; WX 278 ; N i ; B 69 0 363 725 ; +C 106 ; WX 278 ; N j ; B -42 -214 363 725 ; +C 107 ; WX 556 ; N k ; B 69 0 670 718 ; +C 108 ; WX 278 ; N l ; B 69 0 362 718 ; +C 109 ; WX 889 ; N m ; B 64 0 909 546 ; +C 110 ; WX 611 ; N n ; B 65 0 629 546 ; +C 111 ; WX 611 ; N o ; B 82 -14 643 546 ; +C 112 ; WX 611 ; N p ; B 18 -207 645 546 ; +C 113 ; WX 611 ; N q ; B 80 -207 665 546 ; +C 114 ; WX 389 ; N r ; B 64 0 489 546 ; +C 115 ; WX 556 ; N s ; B 63 -14 584 546 ; +C 116 ; WX 333 ; N t ; B 100 -6 422 676 ; +C 117 ; WX 611 ; N u ; B 98 -14 658 532 ; +C 118 ; WX 556 ; N v ; B 126 0 656 532 ; +C 119 ; WX 778 ; N w ; B 123 0 882 532 ; +C 120 ; WX 556 ; N x ; B 15 0 648 532 ; +C 121 ; WX 556 ; N y ; B 42 -214 652 532 ; +C 122 ; WX 500 ; N z ; B 20 0 583 532 ; +C 123 ; WX 389 ; N braceleft ; B 94 -196 518 722 ; +C 124 ; WX 280 ; N bar ; B 36 -225 361 775 ; +C 125 ; WX 389 ; N braceright ; B -18 -196 407 722 ; +C 126 ; WX 584 ; N asciitilde ; B 115 163 577 343 ; +C 161 ; WX 333 ; N exclamdown ; B 50 -186 353 532 ; +C 162 ; WX 556 ; N cent ; B 79 -118 599 628 ; +C 163 ; WX 556 ; N sterling ; B 50 -16 635 718 ; +C 164 ; WX 167 ; N fraction ; B -174 -19 487 710 ; +C 165 ; WX 556 ; N yen ; B 60 0 713 698 ; +C 166 ; WX 556 ; N florin ; B -50 -210 669 737 ; +C 167 ; WX 556 ; N section ; B 61 -184 598 727 ; +C 168 ; WX 556 ; N currency ; B 27 76 680 636 ; +C 169 ; WX 238 ; N quotesingle ; B 165 447 321 718 ; +C 170 ; WX 500 ; N quotedblleft ; B 160 454 588 727 ; +C 171 ; WX 556 ; N guillemotleft ; B 135 76 571 484 ; +C 172 ; WX 333 ; N guilsinglleft ; B 130 76 353 484 ; +C 173 ; WX 333 ; N guilsinglright ; B 99 76 322 484 ; +C 174 ; WX 611 ; N fi ; B 87 0 696 727 ; +C 175 ; WX 611 ; N fl ; B 87 0 695 727 ; +C 177 ; WX 556 ; N endash ; B 48 227 627 333 ; +C 178 ; WX 556 ; N dagger ; B 118 -171 626 718 ; +C 179 ; WX 556 ; N daggerdbl ; B 46 -171 628 718 ; +C 180 ; WX 278 ; N periodcentered ; B 110 172 276 334 ; +C 182 ; WX 556 ; N paragraph ; B 98 -191 688 700 ; +C 183 ; WX 350 ; N bullet ; B 83 194 420 524 ; +C 184 ; WX 278 ; N quotesinglbase ; B 41 -146 236 127 ; +C 185 ; WX 500 ; N quotedblbase ; B 36 -146 463 127 ; +C 186 ; WX 500 ; N quotedblright ; B 162 445 589 718 ; +C 187 ; WX 556 ; N guillemotright ; B 104 76 540 484 ; +C 188 ; WX 1000 ; N ellipsis ; B 92 0 939 146 ; +C 189 ; WX 1000 ; N perthousand ; B 76 -19 1038 710 ; +C 191 ; WX 611 ; N questiondown ; B 53 -195 559 532 ; +C 193 ; WX 333 ; N grave ; B 136 604 353 750 ; +C 194 ; WX 333 ; N acute ; B 236 604 515 750 ; +C 195 ; WX 333 ; N circumflex ; B 118 604 471 750 ; +C 196 ; WX 333 ; N tilde ; B 113 610 507 737 ; +C 197 ; WX 333 ; N macron ; B 122 604 483 678 ; +C 198 ; WX 333 ; N breve ; B 156 604 494 750 ; +C 199 ; WX 333 ; N dotaccent ; B 235 614 385 729 ; +C 200 ; WX 333 ; N dieresis ; B 137 614 482 729 ; +C 202 ; WX 333 ; N ring ; B 200 568 420 776 ; +C 203 ; WX 333 ; N cedilla ; B -37 -228 220 0 ; +C 205 ; WX 333 ; N hungarumlaut ; B 137 604 645 750 ; +C 206 ; WX 333 ; N ogonek ; B 41 -228 264 0 ; +C 207 ; WX 333 ; N caron ; B 149 604 502 750 ; +C 208 ; WX 1000 ; N emdash ; B 48 227 1071 333 ; +C 225 ; WX 1000 ; N AE ; B 5 0 1100 718 ; +C 227 ; WX 370 ; N ordfeminine ; B 125 401 465 737 ; +C 232 ; WX 611 ; N Lslash ; B 34 0 611 718 ; +C 233 ; WX 778 ; N Oslash ; B 35 -27 894 745 ; +C 234 ; WX 1000 ; N OE ; B 99 -19 1114 737 ; +C 235 ; WX 365 ; N ordmasculine ; B 123 401 485 737 ; +C 241 ; WX 889 ; N ae ; B 56 -14 923 546 ; +C 245 ; WX 278 ; N dotlessi ; B 69 0 322 532 ; +C 248 ; WX 278 ; N lslash ; B 40 0 407 718 ; +C 249 ; WX 611 ; N oslash ; B 22 -29 701 560 ; +C 250 ; WX 944 ; N oe ; B 82 -14 977 546 ; +C 251 ; WX 611 ; N germandbls ; B 69 -14 657 731 ; +C -1 ; WX 278 ; N Idieresis ; B 64 0 494 915 ; +C -1 ; WX 556 ; N eacute ; B 70 -14 627 750 ; +C -1 ; WX 556 ; N abreve ; B 55 -14 606 750 ; +C -1 ; WX 611 ; N uhungarumlaut ; B 98 -14 784 750 ; +C -1 ; WX 556 ; N ecaron ; B 70 -14 614 750 ; +C -1 ; WX 667 ; N Ydieresis ; B 168 0 806 915 ; +C -1 ; WX 584 ; N divide ; B 82 -42 610 548 ; +C -1 ; WX 667 ; N Yacute ; B 168 0 806 936 ; +C -1 ; WX 722 ; N Acircumflex ; B 20 0 706 936 ; +C -1 ; WX 556 ; N aacute ; B 55 -14 627 750 ; +C -1 ; WX 722 ; N Ucircumflex ; B 116 -19 804 936 ; +C -1 ; WX 556 ; N yacute ; B 42 -214 652 750 ; +C -1 ; WX 556 ; N scommaaccent ; B 63 -228 584 546 ; +C -1 ; WX 556 ; N ecircumflex ; B 70 -14 593 750 ; +C -1 ; WX 722 ; N Uring ; B 116 -19 804 962 ; +C -1 ; WX 722 ; N Udieresis ; B 116 -19 804 915 ; +C -1 ; WX 556 ; N aogonek ; B 55 -224 583 546 ; +C -1 ; WX 722 ; N Uacute ; B 116 -19 804 936 ; +C -1 ; WX 611 ; N uogonek ; B 98 -228 658 532 ; +C -1 ; WX 667 ; N Edieresis ; B 76 0 757 915 ; +C -1 ; WX 722 ; N Dcroat ; B 62 0 777 718 ; +C -1 ; WX 250 ; N commaaccent ; B 16 -228 188 -50 ; +C -1 ; WX 737 ; N copyright ; B 56 -19 835 737 ; +C -1 ; WX 667 ; N Emacron ; B 76 0 757 864 ; +C -1 ; WX 556 ; N ccaron ; B 79 -14 614 750 ; +C -1 ; WX 556 ; N aring ; B 55 -14 583 776 ; +C -1 ; WX 722 ; N Ncommaaccent ; B 69 -228 807 718 ; +C -1 ; WX 278 ; N lacute ; B 69 0 528 936 ; +C -1 ; WX 556 ; N agrave ; B 55 -14 583 750 ; +C -1 ; WX 611 ; N Tcommaaccent ; B 140 -228 751 718 ; +C -1 ; WX 722 ; N Cacute ; B 107 -19 789 936 ; +C -1 ; WX 556 ; N atilde ; B 55 -14 619 737 ; +C -1 ; WX 667 ; N Edotaccent ; B 76 0 757 915 ; +C -1 ; WX 556 ; N scaron ; B 63 -14 614 750 ; +C -1 ; WX 556 ; N scedilla ; B 63 -228 584 546 ; +C -1 ; WX 278 ; N iacute ; B 69 0 488 750 ; +C -1 ; WX 494 ; N lozenge ; B 90 0 564 745 ; +C -1 ; WX 722 ; N Rcaron ; B 76 0 778 936 ; +C -1 ; WX 778 ; N Gcommaaccent ; B 108 -228 817 737 ; +C -1 ; WX 611 ; N ucircumflex ; B 98 -14 658 750 ; +C -1 ; WX 556 ; N acircumflex ; B 55 -14 583 750 ; +C -1 ; WX 722 ; N Amacron ; B 20 0 718 864 ; +C -1 ; WX 389 ; N rcaron ; B 64 0 530 750 ; +C -1 ; WX 556 ; N ccedilla ; B 79 -228 599 546 ; +C -1 ; WX 611 ; N Zdotaccent ; B 25 0 737 915 ; +C -1 ; WX 667 ; N Thorn ; B 76 0 716 718 ; +C -1 ; WX 778 ; N Omacron ; B 107 -19 823 864 ; +C -1 ; WX 722 ; N Racute ; B 76 0 778 936 ; +C -1 ; WX 667 ; N Sacute ; B 81 -19 722 936 ; +C -1 ; WX 743 ; N dcaron ; B 82 -14 903 718 ; +C -1 ; WX 722 ; N Umacron ; B 116 -19 804 864 ; +C -1 ; WX 611 ; N uring ; B 98 -14 658 776 ; +C -1 ; WX 333 ; N threesuperior ; B 91 271 441 710 ; +C -1 ; WX 778 ; N Ograve ; B 107 -19 823 936 ; +C -1 ; WX 722 ; N Agrave ; B 20 0 702 936 ; +C -1 ; WX 722 ; N Abreve ; B 20 0 729 936 ; +C -1 ; WX 584 ; N multiply ; B 57 1 635 505 ; +C -1 ; WX 611 ; N uacute ; B 98 -14 658 750 ; +C -1 ; WX 611 ; N Tcaron ; B 140 0 751 936 ; +C -1 ; WX 494 ; N partialdiff ; B 43 -21 585 750 ; +C -1 ; WX 556 ; N ydieresis ; B 42 -214 652 729 ; +C -1 ; WX 722 ; N Nacute ; B 69 0 807 936 ; +C -1 ; WX 278 ; N icircumflex ; B 69 0 444 750 ; +C -1 ; WX 667 ; N Ecircumflex ; B 76 0 757 936 ; +C -1 ; WX 556 ; N adieresis ; B 55 -14 594 729 ; +C -1 ; WX 556 ; N edieresis ; B 70 -14 594 729 ; +C -1 ; WX 556 ; N cacute ; B 79 -14 627 750 ; +C -1 ; WX 611 ; N nacute ; B 65 0 654 750 ; +C -1 ; WX 611 ; N umacron ; B 98 -14 658 678 ; +C -1 ; WX 722 ; N Ncaron ; B 69 0 807 936 ; +C -1 ; WX 278 ; N Iacute ; B 64 0 528 936 ; +C -1 ; WX 584 ; N plusminus ; B 40 0 625 506 ; +C -1 ; WX 280 ; N brokenbar ; B 52 -150 345 700 ; +C -1 ; WX 737 ; N registered ; B 55 -19 834 737 ; +C -1 ; WX 778 ; N Gbreve ; B 108 -19 817 936 ; +C -1 ; WX 278 ; N Idotaccent ; B 64 0 397 915 ; +C -1 ; WX 600 ; N summation ; B 14 -10 670 706 ; +C -1 ; WX 667 ; N Egrave ; B 76 0 757 936 ; +C -1 ; WX 389 ; N racute ; B 64 0 543 750 ; +C -1 ; WX 611 ; N omacron ; B 82 -14 643 678 ; +C -1 ; WX 611 ; N Zacute ; B 25 0 737 936 ; +C -1 ; WX 611 ; N Zcaron ; B 25 0 737 936 ; +C -1 ; WX 549 ; N greaterequal ; B 26 0 629 704 ; +C -1 ; WX 722 ; N Eth ; B 62 0 777 718 ; +C -1 ; WX 722 ; N Ccedilla ; B 107 -228 789 737 ; +C -1 ; WX 278 ; N lcommaaccent ; B 30 -228 362 718 ; +C -1 ; WX 389 ; N tcaron ; B 100 -6 608 878 ; +C -1 ; WX 556 ; N eogonek ; B 70 -228 593 546 ; +C -1 ; WX 722 ; N Uogonek ; B 116 -228 804 718 ; +C -1 ; WX 722 ; N Aacute ; B 20 0 750 936 ; +C -1 ; WX 722 ; N Adieresis ; B 20 0 716 915 ; +C -1 ; WX 556 ; N egrave ; B 70 -14 593 750 ; +C -1 ; WX 500 ; N zacute ; B 20 0 599 750 ; +C -1 ; WX 278 ; N iogonek ; B -14 -224 363 725 ; +C -1 ; WX 778 ; N Oacute ; B 107 -19 823 936 ; +C -1 ; WX 611 ; N oacute ; B 82 -14 654 750 ; +C -1 ; WX 556 ; N amacron ; B 55 -14 595 678 ; +C -1 ; WX 556 ; N sacute ; B 63 -14 627 750 ; +C -1 ; WX 278 ; N idieresis ; B 69 0 455 729 ; +C -1 ; WX 778 ; N Ocircumflex ; B 107 -19 823 936 ; +C -1 ; WX 722 ; N Ugrave ; B 116 -19 804 936 ; +C -1 ; WX 612 ; N Delta ; B 6 0 608 688 ; +C -1 ; WX 611 ; N thorn ; B 18 -208 645 718 ; +C -1 ; WX 333 ; N twosuperior ; B 69 283 449 710 ; +C -1 ; WX 778 ; N Odieresis ; B 107 -19 823 915 ; +C -1 ; WX 611 ; N mu ; B 22 -207 658 532 ; +C -1 ; WX 278 ; N igrave ; B 69 0 326 750 ; +C -1 ; WX 611 ; N ohungarumlaut ; B 82 -14 784 750 ; +C -1 ; WX 667 ; N Eogonek ; B 76 -224 757 718 ; +C -1 ; WX 611 ; N dcroat ; B 82 -14 789 718 ; +C -1 ; WX 834 ; N threequarters ; B 99 -19 839 710 ; +C -1 ; WX 667 ; N Scedilla ; B 81 -228 718 737 ; +C -1 ; WX 400 ; N lcaron ; B 69 0 561 718 ; +C -1 ; WX 722 ; N Kcommaaccent ; B 87 -228 858 718 ; +C -1 ; WX 611 ; N Lacute ; B 76 0 611 936 ; +C -1 ; WX 1000 ; N trademark ; B 179 306 1109 718 ; +C -1 ; WX 556 ; N edotaccent ; B 70 -14 593 729 ; +C -1 ; WX 278 ; N Igrave ; B 64 0 367 936 ; +C -1 ; WX 278 ; N Imacron ; B 64 0 496 864 ; +C -1 ; WX 611 ; N Lcaron ; B 76 0 643 718 ; +C -1 ; WX 834 ; N onehalf ; B 132 -19 858 710 ; +C -1 ; WX 549 ; N lessequal ; B 29 0 676 704 ; +C -1 ; WX 611 ; N ocircumflex ; B 82 -14 643 750 ; +C -1 ; WX 611 ; N ntilde ; B 65 0 646 737 ; +C -1 ; WX 722 ; N Uhungarumlaut ; B 116 -19 880 936 ; +C -1 ; WX 667 ; N Eacute ; B 76 0 757 936 ; +C -1 ; WX 556 ; N emacron ; B 70 -14 595 678 ; +C -1 ; WX 611 ; N gbreve ; B 38 -217 666 750 ; +C -1 ; WX 834 ; N onequarter ; B 132 -19 806 710 ; +C -1 ; WX 667 ; N Scaron ; B 81 -19 718 936 ; +C -1 ; WX 667 ; N Scommaaccent ; B 81 -228 718 737 ; +C -1 ; WX 778 ; N Ohungarumlaut ; B 107 -19 908 936 ; +C -1 ; WX 400 ; N degree ; B 175 426 467 712 ; +C -1 ; WX 611 ; N ograve ; B 82 -14 643 750 ; +C -1 ; WX 722 ; N Ccaron ; B 107 -19 789 936 ; +C -1 ; WX 611 ; N ugrave ; B 98 -14 658 750 ; +C -1 ; WX 549 ; N radical ; B 112 -46 689 850 ; +C -1 ; WX 722 ; N Dcaron ; B 76 0 777 936 ; +C -1 ; WX 389 ; N rcommaaccent ; B 26 -228 489 546 ; +C -1 ; WX 722 ; N Ntilde ; B 69 0 807 923 ; +C -1 ; WX 611 ; N otilde ; B 82 -14 646 737 ; +C -1 ; WX 722 ; N Rcommaaccent ; B 76 -228 778 718 ; +C -1 ; WX 611 ; N Lcommaaccent ; B 76 -228 611 718 ; +C -1 ; WX 722 ; N Atilde ; B 20 0 741 923 ; +C -1 ; WX 722 ; N Aogonek ; B 20 -224 702 718 ; +C -1 ; WX 722 ; N Aring ; B 20 0 702 962 ; +C -1 ; WX 778 ; N Otilde ; B 107 -19 823 923 ; +C -1 ; WX 500 ; N zdotaccent ; B 20 0 583 729 ; +C -1 ; WX 667 ; N Ecaron ; B 76 0 757 936 ; +C -1 ; WX 278 ; N Iogonek ; B -41 -228 367 718 ; +C -1 ; WX 556 ; N kcommaaccent ; B 69 -228 670 718 ; +C -1 ; WX 584 ; N minus ; B 82 197 610 309 ; +C -1 ; WX 278 ; N Icircumflex ; B 64 0 484 936 ; +C -1 ; WX 611 ; N ncaron ; B 65 0 641 750 ; +C -1 ; WX 333 ; N tcommaaccent ; B 58 -228 422 676 ; +C -1 ; WX 584 ; N logicalnot ; B 105 108 633 419 ; +C -1 ; WX 611 ; N odieresis ; B 82 -14 643 729 ; +C -1 ; WX 611 ; N udieresis ; B 98 -14 658 729 ; +C -1 ; WX 549 ; N notequal ; B 32 -49 630 570 ; +C -1 ; WX 611 ; N gcommaaccent ; B 38 -217 666 850 ; +C -1 ; WX 611 ; N eth ; B 82 -14 670 737 ; +C -1 ; WX 500 ; N zcaron ; B 20 0 586 750 ; +C -1 ; WX 611 ; N ncommaaccent ; B 65 -228 629 546 ; +C -1 ; WX 333 ; N onesuperior ; B 148 283 388 710 ; +C -1 ; WX 278 ; N imacron ; B 69 0 429 678 ; +C -1 ; WX 556 ; N Euro ; B 0 0 0 0 ; +EndCharMetrics +StartKernData +StartKernPairs 2481 +KPX A C -40 +KPX A Cacute -40 +KPX A Ccaron -40 +KPX A Ccedilla -40 +KPX A G -50 +KPX A Gbreve -50 +KPX A Gcommaaccent -50 +KPX A O -40 +KPX A Oacute -40 +KPX A Ocircumflex -40 +KPX A Odieresis -40 +KPX A Ograve -40 +KPX A Ohungarumlaut -40 +KPX A Omacron -40 +KPX A Oslash -40 +KPX A Otilde -40 +KPX A Q -40 +KPX A T -90 +KPX A Tcaron -90 +KPX A Tcommaaccent -90 +KPX A U -50 +KPX A Uacute -50 +KPX A Ucircumflex -50 +KPX A Udieresis -50 +KPX A Ugrave -50 +KPX A Uhungarumlaut -50 +KPX A Umacron -50 +KPX A Uogonek -50 +KPX A Uring -50 +KPX A V -80 +KPX A W -60 +KPX A Y -110 +KPX A Yacute -110 +KPX A Ydieresis -110 +KPX A u -30 +KPX A uacute -30 +KPX A ucircumflex -30 +KPX A udieresis -30 +KPX A ugrave -30 +KPX A uhungarumlaut -30 +KPX A umacron -30 +KPX A uogonek -30 +KPX A uring -30 +KPX A v -40 +KPX A w -30 +KPX A y -30 +KPX A yacute -30 +KPX A ydieresis -30 +KPX Aacute C -40 +KPX Aacute Cacute -40 +KPX Aacute Ccaron -40 +KPX Aacute Ccedilla -40 +KPX Aacute G -50 +KPX Aacute Gbreve -50 +KPX Aacute Gcommaaccent -50 +KPX Aacute O -40 +KPX Aacute Oacute -40 +KPX Aacute Ocircumflex -40 +KPX Aacute Odieresis -40 +KPX Aacute Ograve -40 +KPX Aacute Ohungarumlaut -40 +KPX Aacute Omacron -40 +KPX Aacute Oslash -40 +KPX Aacute Otilde -40 +KPX Aacute Q -40 +KPX Aacute T -90 +KPX Aacute Tcaron -90 +KPX Aacute Tcommaaccent -90 +KPX Aacute U -50 +KPX Aacute Uacute -50 +KPX Aacute Ucircumflex -50 +KPX Aacute Udieresis -50 +KPX Aacute Ugrave -50 +KPX Aacute Uhungarumlaut -50 +KPX Aacute Umacron -50 +KPX Aacute Uogonek -50 +KPX Aacute Uring -50 +KPX Aacute V -80 +KPX Aacute W -60 +KPX Aacute Y -110 +KPX Aacute Yacute -110 +KPX Aacute Ydieresis -110 +KPX Aacute u -30 +KPX Aacute uacute -30 +KPX Aacute ucircumflex -30 +KPX Aacute udieresis -30 +KPX Aacute ugrave -30 +KPX Aacute uhungarumlaut -30 +KPX Aacute umacron -30 +KPX Aacute uogonek -30 +KPX Aacute uring -30 +KPX Aacute v -40 +KPX Aacute w -30 +KPX Aacute y -30 +KPX Aacute yacute -30 +KPX Aacute ydieresis -30 +KPX Abreve C -40 +KPX Abreve Cacute -40 +KPX Abreve Ccaron -40 +KPX Abreve Ccedilla -40 +KPX Abreve G -50 +KPX Abreve Gbreve -50 +KPX Abreve Gcommaaccent -50 +KPX Abreve O -40 +KPX Abreve Oacute -40 +KPX Abreve Ocircumflex -40 +KPX Abreve Odieresis -40 +KPX Abreve Ograve -40 +KPX Abreve Ohungarumlaut -40 +KPX Abreve Omacron -40 +KPX Abreve Oslash -40 +KPX Abreve Otilde -40 +KPX Abreve Q -40 +KPX Abreve T -90 +KPX Abreve Tcaron -90 +KPX Abreve Tcommaaccent -90 +KPX Abreve U -50 +KPX Abreve Uacute -50 +KPX Abreve Ucircumflex -50 +KPX Abreve Udieresis -50 +KPX Abreve Ugrave -50 +KPX Abreve Uhungarumlaut -50 +KPX Abreve Umacron -50 +KPX Abreve Uogonek -50 +KPX Abreve Uring -50 +KPX Abreve V -80 +KPX Abreve W -60 +KPX Abreve Y -110 +KPX Abreve Yacute -110 +KPX Abreve Ydieresis -110 +KPX Abreve u -30 +KPX Abreve uacute -30 +KPX Abreve ucircumflex -30 +KPX Abreve udieresis -30 +KPX Abreve ugrave -30 +KPX Abreve uhungarumlaut -30 +KPX Abreve umacron -30 +KPX Abreve uogonek -30 +KPX Abreve uring -30 +KPX Abreve v -40 +KPX Abreve w -30 +KPX Abreve y -30 +KPX Abreve yacute -30 +KPX Abreve ydieresis -30 +KPX Acircumflex C -40 +KPX Acircumflex Cacute -40 +KPX Acircumflex Ccaron -40 +KPX Acircumflex Ccedilla -40 +KPX Acircumflex G -50 +KPX Acircumflex Gbreve -50 +KPX Acircumflex Gcommaaccent -50 +KPX Acircumflex O -40 +KPX Acircumflex Oacute -40 +KPX Acircumflex Ocircumflex -40 +KPX Acircumflex Odieresis -40 +KPX Acircumflex Ograve -40 +KPX Acircumflex Ohungarumlaut -40 +KPX Acircumflex Omacron -40 +KPX Acircumflex Oslash -40 +KPX Acircumflex Otilde -40 +KPX Acircumflex Q -40 +KPX Acircumflex T -90 +KPX Acircumflex Tcaron -90 +KPX Acircumflex Tcommaaccent -90 +KPX Acircumflex U -50 +KPX Acircumflex Uacute -50 +KPX Acircumflex Ucircumflex -50 +KPX Acircumflex Udieresis -50 +KPX Acircumflex Ugrave -50 +KPX Acircumflex Uhungarumlaut -50 +KPX Acircumflex Umacron -50 +KPX Acircumflex Uogonek -50 +KPX Acircumflex Uring -50 +KPX Acircumflex V -80 +KPX Acircumflex W -60 +KPX Acircumflex Y -110 +KPX Acircumflex Yacute -110 +KPX Acircumflex Ydieresis -110 +KPX Acircumflex u -30 +KPX Acircumflex uacute -30 +KPX Acircumflex ucircumflex -30 +KPX Acircumflex udieresis -30 +KPX Acircumflex ugrave -30 +KPX Acircumflex uhungarumlaut -30 +KPX Acircumflex umacron -30 +KPX Acircumflex uogonek -30 +KPX Acircumflex uring -30 +KPX Acircumflex v -40 +KPX Acircumflex w -30 +KPX Acircumflex y -30 +KPX Acircumflex yacute -30 +KPX Acircumflex ydieresis -30 +KPX Adieresis C -40 +KPX Adieresis Cacute -40 +KPX Adieresis Ccaron -40 +KPX Adieresis Ccedilla -40 +KPX Adieresis G -50 +KPX Adieresis Gbreve -50 +KPX Adieresis Gcommaaccent -50 +KPX Adieresis O -40 +KPX Adieresis Oacute -40 +KPX Adieresis Ocircumflex -40 +KPX Adieresis Odieresis -40 +KPX Adieresis Ograve -40 +KPX Adieresis Ohungarumlaut -40 +KPX Adieresis Omacron -40 +KPX Adieresis Oslash -40 +KPX Adieresis Otilde -40 +KPX Adieresis Q -40 +KPX Adieresis T -90 +KPX Adieresis Tcaron -90 +KPX Adieresis Tcommaaccent -90 +KPX Adieresis U -50 +KPX Adieresis Uacute -50 +KPX Adieresis Ucircumflex -50 +KPX Adieresis Udieresis -50 +KPX Adieresis Ugrave -50 +KPX Adieresis Uhungarumlaut -50 +KPX Adieresis Umacron -50 +KPX Adieresis Uogonek -50 +KPX Adieresis Uring -50 +KPX Adieresis V -80 +KPX Adieresis W -60 +KPX Adieresis Y -110 +KPX Adieresis Yacute -110 +KPX Adieresis Ydieresis -110 +KPX Adieresis u -30 +KPX Adieresis uacute -30 +KPX Adieresis ucircumflex -30 +KPX Adieresis udieresis -30 +KPX Adieresis ugrave -30 +KPX Adieresis uhungarumlaut -30 +KPX Adieresis umacron -30 +KPX Adieresis uogonek -30 +KPX Adieresis uring -30 +KPX Adieresis v -40 +KPX Adieresis w -30 +KPX Adieresis y -30 +KPX Adieresis yacute -30 +KPX Adieresis ydieresis -30 +KPX Agrave C -40 +KPX Agrave Cacute -40 +KPX Agrave Ccaron -40 +KPX Agrave Ccedilla -40 +KPX Agrave G -50 +KPX Agrave Gbreve -50 +KPX Agrave Gcommaaccent -50 +KPX Agrave O -40 +KPX Agrave Oacute -40 +KPX Agrave Ocircumflex -40 +KPX Agrave Odieresis -40 +KPX Agrave Ograve -40 +KPX Agrave Ohungarumlaut -40 +KPX Agrave Omacron -40 +KPX Agrave Oslash -40 +KPX Agrave Otilde -40 +KPX Agrave Q -40 +KPX Agrave T -90 +KPX Agrave Tcaron -90 +KPX Agrave Tcommaaccent -90 +KPX Agrave U -50 +KPX Agrave Uacute -50 +KPX Agrave Ucircumflex -50 +KPX Agrave Udieresis -50 +KPX Agrave Ugrave -50 +KPX Agrave Uhungarumlaut -50 +KPX Agrave Umacron -50 +KPX Agrave Uogonek -50 +KPX Agrave Uring -50 +KPX Agrave V -80 +KPX Agrave W -60 +KPX Agrave Y -110 +KPX Agrave Yacute -110 +KPX Agrave Ydieresis -110 +KPX Agrave u -30 +KPX Agrave uacute -30 +KPX Agrave ucircumflex -30 +KPX Agrave udieresis -30 +KPX Agrave ugrave -30 +KPX Agrave uhungarumlaut -30 +KPX Agrave umacron -30 +KPX Agrave uogonek -30 +KPX Agrave uring -30 +KPX Agrave v -40 +KPX Agrave w -30 +KPX Agrave y -30 +KPX Agrave yacute -30 +KPX Agrave ydieresis -30 +KPX Amacron C -40 +KPX Amacron Cacute -40 +KPX Amacron Ccaron -40 +KPX Amacron Ccedilla -40 +KPX Amacron G -50 +KPX Amacron Gbreve -50 +KPX Amacron Gcommaaccent -50 +KPX Amacron O -40 +KPX Amacron Oacute -40 +KPX Amacron Ocircumflex -40 +KPX Amacron Odieresis -40 +KPX Amacron Ograve -40 +KPX Amacron Ohungarumlaut -40 +KPX Amacron Omacron -40 +KPX Amacron Oslash -40 +KPX Amacron Otilde -40 +KPX Amacron Q -40 +KPX Amacron T -90 +KPX Amacron Tcaron -90 +KPX Amacron Tcommaaccent -90 +KPX Amacron U -50 +KPX Amacron Uacute -50 +KPX Amacron Ucircumflex -50 +KPX Amacron Udieresis -50 +KPX Amacron Ugrave -50 +KPX Amacron Uhungarumlaut -50 +KPX Amacron Umacron -50 +KPX Amacron Uogonek -50 +KPX Amacron Uring -50 +KPX Amacron V -80 +KPX Amacron W -60 +KPX Amacron Y -110 +KPX Amacron Yacute -110 +KPX Amacron Ydieresis -110 +KPX Amacron u -30 +KPX Amacron uacute -30 +KPX Amacron ucircumflex -30 +KPX Amacron udieresis -30 +KPX Amacron ugrave -30 +KPX Amacron uhungarumlaut -30 +KPX Amacron umacron -30 +KPX Amacron uogonek -30 +KPX Amacron uring -30 +KPX Amacron v -40 +KPX Amacron w -30 +KPX Amacron y -30 +KPX Amacron yacute -30 +KPX Amacron ydieresis -30 +KPX Aogonek C -40 +KPX Aogonek Cacute -40 +KPX Aogonek Ccaron -40 +KPX Aogonek Ccedilla -40 +KPX Aogonek G -50 +KPX Aogonek Gbreve -50 +KPX Aogonek Gcommaaccent -50 +KPX Aogonek O -40 +KPX Aogonek Oacute -40 +KPX Aogonek Ocircumflex -40 +KPX Aogonek Odieresis -40 +KPX Aogonek Ograve -40 +KPX Aogonek Ohungarumlaut -40 +KPX Aogonek Omacron -40 +KPX Aogonek Oslash -40 +KPX Aogonek Otilde -40 +KPX Aogonek Q -40 +KPX Aogonek T -90 +KPX Aogonek Tcaron -90 +KPX Aogonek Tcommaaccent -90 +KPX Aogonek U -50 +KPX Aogonek Uacute -50 +KPX Aogonek Ucircumflex -50 +KPX Aogonek Udieresis -50 +KPX Aogonek Ugrave -50 +KPX Aogonek Uhungarumlaut -50 +KPX Aogonek Umacron -50 +KPX Aogonek Uogonek -50 +KPX Aogonek Uring -50 +KPX Aogonek V -80 +KPX Aogonek W -60 +KPX Aogonek Y -110 +KPX Aogonek Yacute -110 +KPX Aogonek Ydieresis -110 +KPX Aogonek u -30 +KPX Aogonek uacute -30 +KPX Aogonek ucircumflex -30 +KPX Aogonek udieresis -30 +KPX Aogonek ugrave -30 +KPX Aogonek uhungarumlaut -30 +KPX Aogonek umacron -30 +KPX Aogonek uogonek -30 +KPX Aogonek uring -30 +KPX Aogonek v -40 +KPX Aogonek w -30 +KPX Aogonek y -30 +KPX Aogonek yacute -30 +KPX Aogonek ydieresis -30 +KPX Aring C -40 +KPX Aring Cacute -40 +KPX Aring Ccaron -40 +KPX Aring Ccedilla -40 +KPX Aring G -50 +KPX Aring Gbreve -50 +KPX Aring Gcommaaccent -50 +KPX Aring O -40 +KPX Aring Oacute -40 +KPX Aring Ocircumflex -40 +KPX Aring Odieresis -40 +KPX Aring Ograve -40 +KPX Aring Ohungarumlaut -40 +KPX Aring Omacron -40 +KPX Aring Oslash -40 +KPX Aring Otilde -40 +KPX Aring Q -40 +KPX Aring T -90 +KPX Aring Tcaron -90 +KPX Aring Tcommaaccent -90 +KPX Aring U -50 +KPX Aring Uacute -50 +KPX Aring Ucircumflex -50 +KPX Aring Udieresis -50 +KPX Aring Ugrave -50 +KPX Aring Uhungarumlaut -50 +KPX Aring Umacron -50 +KPX Aring Uogonek -50 +KPX Aring Uring -50 +KPX Aring V -80 +KPX Aring W -60 +KPX Aring Y -110 +KPX Aring Yacute -110 +KPX Aring Ydieresis -110 +KPX Aring u -30 +KPX Aring uacute -30 +KPX Aring ucircumflex -30 +KPX Aring udieresis -30 +KPX Aring ugrave -30 +KPX Aring uhungarumlaut -30 +KPX Aring umacron -30 +KPX Aring uogonek -30 +KPX Aring uring -30 +KPX Aring v -40 +KPX Aring w -30 +KPX Aring y -30 +KPX Aring yacute -30 +KPX Aring ydieresis -30 +KPX Atilde C -40 +KPX Atilde Cacute -40 +KPX Atilde Ccaron -40 +KPX Atilde Ccedilla -40 +KPX Atilde G -50 +KPX Atilde Gbreve -50 +KPX Atilde Gcommaaccent -50 +KPX Atilde O -40 +KPX Atilde Oacute -40 +KPX Atilde Ocircumflex -40 +KPX Atilde Odieresis -40 +KPX Atilde Ograve -40 +KPX Atilde Ohungarumlaut -40 +KPX Atilde Omacron -40 +KPX Atilde Oslash -40 +KPX Atilde Otilde -40 +KPX Atilde Q -40 +KPX Atilde T -90 +KPX Atilde Tcaron -90 +KPX Atilde Tcommaaccent -90 +KPX Atilde U -50 +KPX Atilde Uacute -50 +KPX Atilde Ucircumflex -50 +KPX Atilde Udieresis -50 +KPX Atilde Ugrave -50 +KPX Atilde Uhungarumlaut -50 +KPX Atilde Umacron -50 +KPX Atilde Uogonek -50 +KPX Atilde Uring -50 +KPX Atilde V -80 +KPX Atilde W -60 +KPX Atilde Y -110 +KPX Atilde Yacute -110 +KPX Atilde Ydieresis -110 +KPX Atilde u -30 +KPX Atilde uacute -30 +KPX Atilde ucircumflex -30 +KPX Atilde udieresis -30 +KPX Atilde ugrave -30 +KPX Atilde uhungarumlaut -30 +KPX Atilde umacron -30 +KPX Atilde uogonek -30 +KPX Atilde uring -30 +KPX Atilde v -40 +KPX Atilde w -30 +KPX Atilde y -30 +KPX Atilde yacute -30 +KPX Atilde ydieresis -30 +KPX B A -30 +KPX B Aacute -30 +KPX B Abreve -30 +KPX B Acircumflex -30 +KPX B Adieresis -30 +KPX B Agrave -30 +KPX B Amacron -30 +KPX B Aogonek -30 +KPX B Aring -30 +KPX B Atilde -30 +KPX B U -10 +KPX B Uacute -10 +KPX B Ucircumflex -10 +KPX B Udieresis -10 +KPX B Ugrave -10 +KPX B Uhungarumlaut -10 +KPX B Umacron -10 +KPX B Uogonek -10 +KPX B Uring -10 +KPX D A -40 +KPX D Aacute -40 +KPX D Abreve -40 +KPX D Acircumflex -40 +KPX D Adieresis -40 +KPX D Agrave -40 +KPX D Amacron -40 +KPX D Aogonek -40 +KPX D Aring -40 +KPX D Atilde -40 +KPX D V -40 +KPX D W -40 +KPX D Y -70 +KPX D Yacute -70 +KPX D Ydieresis -70 +KPX D comma -30 +KPX D period -30 +KPX Dcaron A -40 +KPX Dcaron Aacute -40 +KPX Dcaron Abreve -40 +KPX Dcaron Acircumflex -40 +KPX Dcaron Adieresis -40 +KPX Dcaron Agrave -40 +KPX Dcaron Amacron -40 +KPX Dcaron Aogonek -40 +KPX Dcaron Aring -40 +KPX Dcaron Atilde -40 +KPX Dcaron V -40 +KPX Dcaron W -40 +KPX Dcaron Y -70 +KPX Dcaron Yacute -70 +KPX Dcaron Ydieresis -70 +KPX Dcaron comma -30 +KPX Dcaron period -30 +KPX Dcroat A -40 +KPX Dcroat Aacute -40 +KPX Dcroat Abreve -40 +KPX Dcroat Acircumflex -40 +KPX Dcroat Adieresis -40 +KPX Dcroat Agrave -40 +KPX Dcroat Amacron -40 +KPX Dcroat Aogonek -40 +KPX Dcroat Aring -40 +KPX Dcroat Atilde -40 +KPX Dcroat V -40 +KPX Dcroat W -40 +KPX Dcroat Y -70 +KPX Dcroat Yacute -70 +KPX Dcroat Ydieresis -70 +KPX Dcroat comma -30 +KPX Dcroat period -30 +KPX F A -80 +KPX F Aacute -80 +KPX F Abreve -80 +KPX F Acircumflex -80 +KPX F Adieresis -80 +KPX F Agrave -80 +KPX F Amacron -80 +KPX F Aogonek -80 +KPX F Aring -80 +KPX F Atilde -80 +KPX F a -20 +KPX F aacute -20 +KPX F abreve -20 +KPX F acircumflex -20 +KPX F adieresis -20 +KPX F agrave -20 +KPX F amacron -20 +KPX F aogonek -20 +KPX F aring -20 +KPX F atilde -20 +KPX F comma -100 +KPX F period -100 +KPX J A -20 +KPX J Aacute -20 +KPX J Abreve -20 +KPX J Acircumflex -20 +KPX J Adieresis -20 +KPX J Agrave -20 +KPX J Amacron -20 +KPX J Aogonek -20 +KPX J Aring -20 +KPX J Atilde -20 +KPX J comma -20 +KPX J period -20 +KPX J u -20 +KPX J uacute -20 +KPX J ucircumflex -20 +KPX J udieresis -20 +KPX J ugrave -20 +KPX J uhungarumlaut -20 +KPX J umacron -20 +KPX J uogonek -20 +KPX J uring -20 +KPX K O -30 +KPX K Oacute -30 +KPX K Ocircumflex -30 +KPX K Odieresis -30 +KPX K Ograve -30 +KPX K Ohungarumlaut -30 +KPX K Omacron -30 +KPX K Oslash -30 +KPX K Otilde -30 +KPX K e -15 +KPX K eacute -15 +KPX K ecaron -15 +KPX K ecircumflex -15 +KPX K edieresis -15 +KPX K edotaccent -15 +KPX K egrave -15 +KPX K emacron -15 +KPX K eogonek -15 +KPX K o -35 +KPX K oacute -35 +KPX K ocircumflex -35 +KPX K odieresis -35 +KPX K ograve -35 +KPX K ohungarumlaut -35 +KPX K omacron -35 +KPX K oslash -35 +KPX K otilde -35 +KPX K u -30 +KPX K uacute -30 +KPX K ucircumflex -30 +KPX K udieresis -30 +KPX K ugrave -30 +KPX K uhungarumlaut -30 +KPX K umacron -30 +KPX K uogonek -30 +KPX K uring -30 +KPX K y -40 +KPX K yacute -40 +KPX K ydieresis -40 +KPX Kcommaaccent O -30 +KPX Kcommaaccent Oacute -30 +KPX Kcommaaccent Ocircumflex -30 +KPX Kcommaaccent Odieresis -30 +KPX Kcommaaccent Ograve -30 +KPX Kcommaaccent Ohungarumlaut -30 +KPX Kcommaaccent Omacron -30 +KPX Kcommaaccent Oslash -30 +KPX Kcommaaccent Otilde -30 +KPX Kcommaaccent e -15 +KPX Kcommaaccent eacute -15 +KPX Kcommaaccent ecaron -15 +KPX Kcommaaccent ecircumflex -15 +KPX Kcommaaccent edieresis -15 +KPX Kcommaaccent edotaccent -15 +KPX Kcommaaccent egrave -15 +KPX Kcommaaccent emacron -15 +KPX Kcommaaccent eogonek -15 +KPX Kcommaaccent o -35 +KPX Kcommaaccent oacute -35 +KPX Kcommaaccent ocircumflex -35 +KPX Kcommaaccent odieresis -35 +KPX Kcommaaccent ograve -35 +KPX Kcommaaccent ohungarumlaut -35 +KPX Kcommaaccent omacron -35 +KPX Kcommaaccent oslash -35 +KPX Kcommaaccent otilde -35 +KPX Kcommaaccent u -30 +KPX Kcommaaccent uacute -30 +KPX Kcommaaccent ucircumflex -30 +KPX Kcommaaccent udieresis -30 +KPX Kcommaaccent ugrave -30 +KPX Kcommaaccent uhungarumlaut -30 +KPX Kcommaaccent umacron -30 +KPX Kcommaaccent uogonek -30 +KPX Kcommaaccent uring -30 +KPX Kcommaaccent y -40 +KPX Kcommaaccent yacute -40 +KPX Kcommaaccent ydieresis -40 +KPX L T -90 +KPX L Tcaron -90 +KPX L Tcommaaccent -90 +KPX L V -110 +KPX L W -80 +KPX L Y -120 +KPX L Yacute -120 +KPX L Ydieresis -120 +KPX L quotedblright -140 +KPX L quoteright -140 +KPX L y -30 +KPX L yacute -30 +KPX L ydieresis -30 +KPX Lacute T -90 +KPX Lacute Tcaron -90 +KPX Lacute Tcommaaccent -90 +KPX Lacute V -110 +KPX Lacute W -80 +KPX Lacute Y -120 +KPX Lacute Yacute -120 +KPX Lacute Ydieresis -120 +KPX Lacute quotedblright -140 +KPX Lacute quoteright -140 +KPX Lacute y -30 +KPX Lacute yacute -30 +KPX Lacute ydieresis -30 +KPX Lcommaaccent T -90 +KPX Lcommaaccent Tcaron -90 +KPX Lcommaaccent Tcommaaccent -90 +KPX Lcommaaccent V -110 +KPX Lcommaaccent W -80 +KPX Lcommaaccent Y -120 +KPX Lcommaaccent Yacute -120 +KPX Lcommaaccent Ydieresis -120 +KPX Lcommaaccent quotedblright -140 +KPX Lcommaaccent quoteright -140 +KPX Lcommaaccent y -30 +KPX Lcommaaccent yacute -30 +KPX Lcommaaccent ydieresis -30 +KPX Lslash T -90 +KPX Lslash Tcaron -90 +KPX Lslash Tcommaaccent -90 +KPX Lslash V -110 +KPX Lslash W -80 +KPX Lslash Y -120 +KPX Lslash Yacute -120 +KPX Lslash Ydieresis -120 +KPX Lslash quotedblright -140 +KPX Lslash quoteright -140 +KPX Lslash y -30 +KPX Lslash yacute -30 +KPX Lslash ydieresis -30 +KPX O A -50 +KPX O Aacute -50 +KPX O Abreve -50 +KPX O Acircumflex -50 +KPX O Adieresis -50 +KPX O Agrave -50 +KPX O Amacron -50 +KPX O Aogonek -50 +KPX O Aring -50 +KPX O Atilde -50 +KPX O T -40 +KPX O Tcaron -40 +KPX O Tcommaaccent -40 +KPX O V -50 +KPX O W -50 +KPX O X -50 +KPX O Y -70 +KPX O Yacute -70 +KPX O Ydieresis -70 +KPX O comma -40 +KPX O period -40 +KPX Oacute A -50 +KPX Oacute Aacute -50 +KPX Oacute Abreve -50 +KPX Oacute Acircumflex -50 +KPX Oacute Adieresis -50 +KPX Oacute Agrave -50 +KPX Oacute Amacron -50 +KPX Oacute Aogonek -50 +KPX Oacute Aring -50 +KPX Oacute Atilde -50 +KPX Oacute T -40 +KPX Oacute Tcaron -40 +KPX Oacute Tcommaaccent -40 +KPX Oacute V -50 +KPX Oacute W -50 +KPX Oacute X -50 +KPX Oacute Y -70 +KPX Oacute Yacute -70 +KPX Oacute Ydieresis -70 +KPX Oacute comma -40 +KPX Oacute period -40 +KPX Ocircumflex A -50 +KPX Ocircumflex Aacute -50 +KPX Ocircumflex Abreve -50 +KPX Ocircumflex Acircumflex -50 +KPX Ocircumflex Adieresis -50 +KPX Ocircumflex Agrave -50 +KPX Ocircumflex Amacron -50 +KPX Ocircumflex Aogonek -50 +KPX Ocircumflex Aring -50 +KPX Ocircumflex Atilde -50 +KPX Ocircumflex T -40 +KPX Ocircumflex Tcaron -40 +KPX Ocircumflex Tcommaaccent -40 +KPX Ocircumflex V -50 +KPX Ocircumflex W -50 +KPX Ocircumflex X -50 +KPX Ocircumflex Y -70 +KPX Ocircumflex Yacute -70 +KPX Ocircumflex Ydieresis -70 +KPX Ocircumflex comma -40 +KPX Ocircumflex period -40 +KPX Odieresis A -50 +KPX Odieresis Aacute -50 +KPX Odieresis Abreve -50 +KPX Odieresis Acircumflex -50 +KPX Odieresis Adieresis -50 +KPX Odieresis Agrave -50 +KPX Odieresis Amacron -50 +KPX Odieresis Aogonek -50 +KPX Odieresis Aring -50 +KPX Odieresis Atilde -50 +KPX Odieresis T -40 +KPX Odieresis Tcaron -40 +KPX Odieresis Tcommaaccent -40 +KPX Odieresis V -50 +KPX Odieresis W -50 +KPX Odieresis X -50 +KPX Odieresis Y -70 +KPX Odieresis Yacute -70 +KPX Odieresis Ydieresis -70 +KPX Odieresis comma -40 +KPX Odieresis period -40 +KPX Ograve A -50 +KPX Ograve Aacute -50 +KPX Ograve Abreve -50 +KPX Ograve Acircumflex -50 +KPX Ograve Adieresis -50 +KPX Ograve Agrave -50 +KPX Ograve Amacron -50 +KPX Ograve Aogonek -50 +KPX Ograve Aring -50 +KPX Ograve Atilde -50 +KPX Ograve T -40 +KPX Ograve Tcaron -40 +KPX Ograve Tcommaaccent -40 +KPX Ograve V -50 +KPX Ograve W -50 +KPX Ograve X -50 +KPX Ograve Y -70 +KPX Ograve Yacute -70 +KPX Ograve Ydieresis -70 +KPX Ograve comma -40 +KPX Ograve period -40 +KPX Ohungarumlaut A -50 +KPX Ohungarumlaut Aacute -50 +KPX Ohungarumlaut Abreve -50 +KPX Ohungarumlaut Acircumflex -50 +KPX Ohungarumlaut Adieresis -50 +KPX Ohungarumlaut Agrave -50 +KPX Ohungarumlaut Amacron -50 +KPX Ohungarumlaut Aogonek -50 +KPX Ohungarumlaut Aring -50 +KPX Ohungarumlaut Atilde -50 +KPX Ohungarumlaut T -40 +KPX Ohungarumlaut Tcaron -40 +KPX Ohungarumlaut Tcommaaccent -40 +KPX Ohungarumlaut V -50 +KPX Ohungarumlaut W -50 +KPX Ohungarumlaut X -50 +KPX Ohungarumlaut Y -70 +KPX Ohungarumlaut Yacute -70 +KPX Ohungarumlaut Ydieresis -70 +KPX Ohungarumlaut comma -40 +KPX Ohungarumlaut period -40 +KPX Omacron A -50 +KPX Omacron Aacute -50 +KPX Omacron Abreve -50 +KPX Omacron Acircumflex -50 +KPX Omacron Adieresis -50 +KPX Omacron Agrave -50 +KPX Omacron Amacron -50 +KPX Omacron Aogonek -50 +KPX Omacron Aring -50 +KPX Omacron Atilde -50 +KPX Omacron T -40 +KPX Omacron Tcaron -40 +KPX Omacron Tcommaaccent -40 +KPX Omacron V -50 +KPX Omacron W -50 +KPX Omacron X -50 +KPX Omacron Y -70 +KPX Omacron Yacute -70 +KPX Omacron Ydieresis -70 +KPX Omacron comma -40 +KPX Omacron period -40 +KPX Oslash A -50 +KPX Oslash Aacute -50 +KPX Oslash Abreve -50 +KPX Oslash Acircumflex -50 +KPX Oslash Adieresis -50 +KPX Oslash Agrave -50 +KPX Oslash Amacron -50 +KPX Oslash Aogonek -50 +KPX Oslash Aring -50 +KPX Oslash Atilde -50 +KPX Oslash T -40 +KPX Oslash Tcaron -40 +KPX Oslash Tcommaaccent -40 +KPX Oslash V -50 +KPX Oslash W -50 +KPX Oslash X -50 +KPX Oslash Y -70 +KPX Oslash Yacute -70 +KPX Oslash Ydieresis -70 +KPX Oslash comma -40 +KPX Oslash period -40 +KPX Otilde A -50 +KPX Otilde Aacute -50 +KPX Otilde Abreve -50 +KPX Otilde Acircumflex -50 +KPX Otilde Adieresis -50 +KPX Otilde Agrave -50 +KPX Otilde Amacron -50 +KPX Otilde Aogonek -50 +KPX Otilde Aring -50 +KPX Otilde Atilde -50 +KPX Otilde T -40 +KPX Otilde Tcaron -40 +KPX Otilde Tcommaaccent -40 +KPX Otilde V -50 +KPX Otilde W -50 +KPX Otilde X -50 +KPX Otilde Y -70 +KPX Otilde Yacute -70 +KPX Otilde Ydieresis -70 +KPX Otilde comma -40 +KPX Otilde period -40 +KPX P A -100 +KPX P Aacute -100 +KPX P Abreve -100 +KPX P Acircumflex -100 +KPX P Adieresis -100 +KPX P Agrave -100 +KPX P Amacron -100 +KPX P Aogonek -100 +KPX P Aring -100 +KPX P Atilde -100 +KPX P a -30 +KPX P aacute -30 +KPX P abreve -30 +KPX P acircumflex -30 +KPX P adieresis -30 +KPX P agrave -30 +KPX P amacron -30 +KPX P aogonek -30 +KPX P aring -30 +KPX P atilde -30 +KPX P comma -120 +KPX P e -30 +KPX P eacute -30 +KPX P ecaron -30 +KPX P ecircumflex -30 +KPX P edieresis -30 +KPX P edotaccent -30 +KPX P egrave -30 +KPX P emacron -30 +KPX P eogonek -30 +KPX P o -40 +KPX P oacute -40 +KPX P ocircumflex -40 +KPX P odieresis -40 +KPX P ograve -40 +KPX P ohungarumlaut -40 +KPX P omacron -40 +KPX P oslash -40 +KPX P otilde -40 +KPX P period -120 +KPX Q U -10 +KPX Q Uacute -10 +KPX Q Ucircumflex -10 +KPX Q Udieresis -10 +KPX Q Ugrave -10 +KPX Q Uhungarumlaut -10 +KPX Q Umacron -10 +KPX Q Uogonek -10 +KPX Q Uring -10 +KPX Q comma 20 +KPX Q period 20 +KPX R O -20 +KPX R Oacute -20 +KPX R Ocircumflex -20 +KPX R Odieresis -20 +KPX R Ograve -20 +KPX R Ohungarumlaut -20 +KPX R Omacron -20 +KPX R Oslash -20 +KPX R Otilde -20 +KPX R T -20 +KPX R Tcaron -20 +KPX R Tcommaaccent -20 +KPX R U -20 +KPX R Uacute -20 +KPX R Ucircumflex -20 +KPX R Udieresis -20 +KPX R Ugrave -20 +KPX R Uhungarumlaut -20 +KPX R Umacron -20 +KPX R Uogonek -20 +KPX R Uring -20 +KPX R V -50 +KPX R W -40 +KPX R Y -50 +KPX R Yacute -50 +KPX R Ydieresis -50 +KPX Racute O -20 +KPX Racute Oacute -20 +KPX Racute Ocircumflex -20 +KPX Racute Odieresis -20 +KPX Racute Ograve -20 +KPX Racute Ohungarumlaut -20 +KPX Racute Omacron -20 +KPX Racute Oslash -20 +KPX Racute Otilde -20 +KPX Racute T -20 +KPX Racute Tcaron -20 +KPX Racute Tcommaaccent -20 +KPX Racute U -20 +KPX Racute Uacute -20 +KPX Racute Ucircumflex -20 +KPX Racute Udieresis -20 +KPX Racute Ugrave -20 +KPX Racute Uhungarumlaut -20 +KPX Racute Umacron -20 +KPX Racute Uogonek -20 +KPX Racute Uring -20 +KPX Racute V -50 +KPX Racute W -40 +KPX Racute Y -50 +KPX Racute Yacute -50 +KPX Racute Ydieresis -50 +KPX Rcaron O -20 +KPX Rcaron Oacute -20 +KPX Rcaron Ocircumflex -20 +KPX Rcaron Odieresis -20 +KPX Rcaron Ograve -20 +KPX Rcaron Ohungarumlaut -20 +KPX Rcaron Omacron -20 +KPX Rcaron Oslash -20 +KPX Rcaron Otilde -20 +KPX Rcaron T -20 +KPX Rcaron Tcaron -20 +KPX Rcaron Tcommaaccent -20 +KPX Rcaron U -20 +KPX Rcaron Uacute -20 +KPX Rcaron Ucircumflex -20 +KPX Rcaron Udieresis -20 +KPX Rcaron Ugrave -20 +KPX Rcaron Uhungarumlaut -20 +KPX Rcaron Umacron -20 +KPX Rcaron Uogonek -20 +KPX Rcaron Uring -20 +KPX Rcaron V -50 +KPX Rcaron W -40 +KPX Rcaron Y -50 +KPX Rcaron Yacute -50 +KPX Rcaron Ydieresis -50 +KPX Rcommaaccent O -20 +KPX Rcommaaccent Oacute -20 +KPX Rcommaaccent Ocircumflex -20 +KPX Rcommaaccent Odieresis -20 +KPX Rcommaaccent Ograve -20 +KPX Rcommaaccent Ohungarumlaut -20 +KPX Rcommaaccent Omacron -20 +KPX Rcommaaccent Oslash -20 +KPX Rcommaaccent Otilde -20 +KPX Rcommaaccent T -20 +KPX Rcommaaccent Tcaron -20 +KPX Rcommaaccent Tcommaaccent -20 +KPX Rcommaaccent U -20 +KPX Rcommaaccent Uacute -20 +KPX Rcommaaccent Ucircumflex -20 +KPX Rcommaaccent Udieresis -20 +KPX Rcommaaccent Ugrave -20 +KPX Rcommaaccent Uhungarumlaut -20 +KPX Rcommaaccent Umacron -20 +KPX Rcommaaccent Uogonek -20 +KPX Rcommaaccent Uring -20 +KPX Rcommaaccent V -50 +KPX Rcommaaccent W -40 +KPX Rcommaaccent Y -50 +KPX Rcommaaccent Yacute -50 +KPX Rcommaaccent Ydieresis -50 +KPX T A -90 +KPX T Aacute -90 +KPX T Abreve -90 +KPX T Acircumflex -90 +KPX T Adieresis -90 +KPX T Agrave -90 +KPX T Amacron -90 +KPX T Aogonek -90 +KPX T Aring -90 +KPX T Atilde -90 +KPX T O -40 +KPX T Oacute -40 +KPX T Ocircumflex -40 +KPX T Odieresis -40 +KPX T Ograve -40 +KPX T Ohungarumlaut -40 +KPX T Omacron -40 +KPX T Oslash -40 +KPX T Otilde -40 +KPX T a -80 +KPX T aacute -80 +KPX T abreve -80 +KPX T acircumflex -80 +KPX T adieresis -80 +KPX T agrave -80 +KPX T amacron -80 +KPX T aogonek -80 +KPX T aring -80 +KPX T atilde -80 +KPX T colon -40 +KPX T comma -80 +KPX T e -60 +KPX T eacute -60 +KPX T ecaron -60 +KPX T ecircumflex -60 +KPX T edieresis -60 +KPX T edotaccent -60 +KPX T egrave -60 +KPX T emacron -60 +KPX T eogonek -60 +KPX T hyphen -120 +KPX T o -80 +KPX T oacute -80 +KPX T ocircumflex -80 +KPX T odieresis -80 +KPX T ograve -80 +KPX T ohungarumlaut -80 +KPX T omacron -80 +KPX T oslash -80 +KPX T otilde -80 +KPX T period -80 +KPX T r -80 +KPX T racute -80 +KPX T rcommaaccent -80 +KPX T semicolon -40 +KPX T u -90 +KPX T uacute -90 +KPX T ucircumflex -90 +KPX T udieresis -90 +KPX T ugrave -90 +KPX T uhungarumlaut -90 +KPX T umacron -90 +KPX T uogonek -90 +KPX T uring -90 +KPX T w -60 +KPX T y -60 +KPX T yacute -60 +KPX T ydieresis -60 +KPX Tcaron A -90 +KPX Tcaron Aacute -90 +KPX Tcaron Abreve -90 +KPX Tcaron Acircumflex -90 +KPX Tcaron Adieresis -90 +KPX Tcaron Agrave -90 +KPX Tcaron Amacron -90 +KPX Tcaron Aogonek -90 +KPX Tcaron Aring -90 +KPX Tcaron Atilde -90 +KPX Tcaron O -40 +KPX Tcaron Oacute -40 +KPX Tcaron Ocircumflex -40 +KPX Tcaron Odieresis -40 +KPX Tcaron Ograve -40 +KPX Tcaron Ohungarumlaut -40 +KPX Tcaron Omacron -40 +KPX Tcaron Oslash -40 +KPX Tcaron Otilde -40 +KPX Tcaron a -80 +KPX Tcaron aacute -80 +KPX Tcaron abreve -80 +KPX Tcaron acircumflex -80 +KPX Tcaron adieresis -80 +KPX Tcaron agrave -80 +KPX Tcaron amacron -80 +KPX Tcaron aogonek -80 +KPX Tcaron aring -80 +KPX Tcaron atilde -80 +KPX Tcaron colon -40 +KPX Tcaron comma -80 +KPX Tcaron e -60 +KPX Tcaron eacute -60 +KPX Tcaron ecaron -60 +KPX Tcaron ecircumflex -60 +KPX Tcaron edieresis -60 +KPX Tcaron edotaccent -60 +KPX Tcaron egrave -60 +KPX Tcaron emacron -60 +KPX Tcaron eogonek -60 +KPX Tcaron hyphen -120 +KPX Tcaron o -80 +KPX Tcaron oacute -80 +KPX Tcaron ocircumflex -80 +KPX Tcaron odieresis -80 +KPX Tcaron ograve -80 +KPX Tcaron ohungarumlaut -80 +KPX Tcaron omacron -80 +KPX Tcaron oslash -80 +KPX Tcaron otilde -80 +KPX Tcaron period -80 +KPX Tcaron r -80 +KPX Tcaron racute -80 +KPX Tcaron rcommaaccent -80 +KPX Tcaron semicolon -40 +KPX Tcaron u -90 +KPX Tcaron uacute -90 +KPX Tcaron ucircumflex -90 +KPX Tcaron udieresis -90 +KPX Tcaron ugrave -90 +KPX Tcaron uhungarumlaut -90 +KPX Tcaron umacron -90 +KPX Tcaron uogonek -90 +KPX Tcaron uring -90 +KPX Tcaron w -60 +KPX Tcaron y -60 +KPX Tcaron yacute -60 +KPX Tcaron ydieresis -60 +KPX Tcommaaccent A -90 +KPX Tcommaaccent Aacute -90 +KPX Tcommaaccent Abreve -90 +KPX Tcommaaccent Acircumflex -90 +KPX Tcommaaccent Adieresis -90 +KPX Tcommaaccent Agrave -90 +KPX Tcommaaccent Amacron -90 +KPX Tcommaaccent Aogonek -90 +KPX Tcommaaccent Aring -90 +KPX Tcommaaccent Atilde -90 +KPX Tcommaaccent O -40 +KPX Tcommaaccent Oacute -40 +KPX Tcommaaccent Ocircumflex -40 +KPX Tcommaaccent Odieresis -40 +KPX Tcommaaccent Ograve -40 +KPX Tcommaaccent Ohungarumlaut -40 +KPX Tcommaaccent Omacron -40 +KPX Tcommaaccent Oslash -40 +KPX Tcommaaccent Otilde -40 +KPX Tcommaaccent a -80 +KPX Tcommaaccent aacute -80 +KPX Tcommaaccent abreve -80 +KPX Tcommaaccent acircumflex -80 +KPX Tcommaaccent adieresis -80 +KPX Tcommaaccent agrave -80 +KPX Tcommaaccent amacron -80 +KPX Tcommaaccent aogonek -80 +KPX Tcommaaccent aring -80 +KPX Tcommaaccent atilde -80 +KPX Tcommaaccent colon -40 +KPX Tcommaaccent comma -80 +KPX Tcommaaccent e -60 +KPX Tcommaaccent eacute -60 +KPX Tcommaaccent ecaron -60 +KPX Tcommaaccent ecircumflex -60 +KPX Tcommaaccent edieresis -60 +KPX Tcommaaccent edotaccent -60 +KPX Tcommaaccent egrave -60 +KPX Tcommaaccent emacron -60 +KPX Tcommaaccent eogonek -60 +KPX Tcommaaccent hyphen -120 +KPX Tcommaaccent o -80 +KPX Tcommaaccent oacute -80 +KPX Tcommaaccent ocircumflex -80 +KPX Tcommaaccent odieresis -80 +KPX Tcommaaccent ograve -80 +KPX Tcommaaccent ohungarumlaut -80 +KPX Tcommaaccent omacron -80 +KPX Tcommaaccent oslash -80 +KPX Tcommaaccent otilde -80 +KPX Tcommaaccent period -80 +KPX Tcommaaccent r -80 +KPX Tcommaaccent racute -80 +KPX Tcommaaccent rcommaaccent -80 +KPX Tcommaaccent semicolon -40 +KPX Tcommaaccent u -90 +KPX Tcommaaccent uacute -90 +KPX Tcommaaccent ucircumflex -90 +KPX Tcommaaccent udieresis -90 +KPX Tcommaaccent ugrave -90 +KPX Tcommaaccent uhungarumlaut -90 +KPX Tcommaaccent umacron -90 +KPX Tcommaaccent uogonek -90 +KPX Tcommaaccent uring -90 +KPX Tcommaaccent w -60 +KPX Tcommaaccent y -60 +KPX Tcommaaccent yacute -60 +KPX Tcommaaccent ydieresis -60 +KPX U A -50 +KPX U Aacute -50 +KPX U Abreve -50 +KPX U Acircumflex -50 +KPX U Adieresis -50 +KPX U Agrave -50 +KPX U Amacron -50 +KPX U Aogonek -50 +KPX U Aring -50 +KPX U Atilde -50 +KPX U comma -30 +KPX U period -30 +KPX Uacute A -50 +KPX Uacute Aacute -50 +KPX Uacute Abreve -50 +KPX Uacute Acircumflex -50 +KPX Uacute Adieresis -50 +KPX Uacute Agrave -50 +KPX Uacute Amacron -50 +KPX Uacute Aogonek -50 +KPX Uacute Aring -50 +KPX Uacute Atilde -50 +KPX Uacute comma -30 +KPX Uacute period -30 +KPX Ucircumflex A -50 +KPX Ucircumflex Aacute -50 +KPX Ucircumflex Abreve -50 +KPX Ucircumflex Acircumflex -50 +KPX Ucircumflex Adieresis -50 +KPX Ucircumflex Agrave -50 +KPX Ucircumflex Amacron -50 +KPX Ucircumflex Aogonek -50 +KPX Ucircumflex Aring -50 +KPX Ucircumflex Atilde -50 +KPX Ucircumflex comma -30 +KPX Ucircumflex period -30 +KPX Udieresis A -50 +KPX Udieresis Aacute -50 +KPX Udieresis Abreve -50 +KPX Udieresis Acircumflex -50 +KPX Udieresis Adieresis -50 +KPX Udieresis Agrave -50 +KPX Udieresis Amacron -50 +KPX Udieresis Aogonek -50 +KPX Udieresis Aring -50 +KPX Udieresis Atilde -50 +KPX Udieresis comma -30 +KPX Udieresis period -30 +KPX Ugrave A -50 +KPX Ugrave Aacute -50 +KPX Ugrave Abreve -50 +KPX Ugrave Acircumflex -50 +KPX Ugrave Adieresis -50 +KPX Ugrave Agrave -50 +KPX Ugrave Amacron -50 +KPX Ugrave Aogonek -50 +KPX Ugrave Aring -50 +KPX Ugrave Atilde -50 +KPX Ugrave comma -30 +KPX Ugrave period -30 +KPX Uhungarumlaut A -50 +KPX Uhungarumlaut Aacute -50 +KPX Uhungarumlaut Abreve -50 +KPX Uhungarumlaut Acircumflex -50 +KPX Uhungarumlaut Adieresis -50 +KPX Uhungarumlaut Agrave -50 +KPX Uhungarumlaut Amacron -50 +KPX Uhungarumlaut Aogonek -50 +KPX Uhungarumlaut Aring -50 +KPX Uhungarumlaut Atilde -50 +KPX Uhungarumlaut comma -30 +KPX Uhungarumlaut period -30 +KPX Umacron A -50 +KPX Umacron Aacute -50 +KPX Umacron Abreve -50 +KPX Umacron Acircumflex -50 +KPX Umacron Adieresis -50 +KPX Umacron Agrave -50 +KPX Umacron Amacron -50 +KPX Umacron Aogonek -50 +KPX Umacron Aring -50 +KPX Umacron Atilde -50 +KPX Umacron comma -30 +KPX Umacron period -30 +KPX Uogonek A -50 +KPX Uogonek Aacute -50 +KPX Uogonek Abreve -50 +KPX Uogonek Acircumflex -50 +KPX Uogonek Adieresis -50 +KPX Uogonek Agrave -50 +KPX Uogonek Amacron -50 +KPX Uogonek Aogonek -50 +KPX Uogonek Aring -50 +KPX Uogonek Atilde -50 +KPX Uogonek comma -30 +KPX Uogonek period -30 +KPX Uring A -50 +KPX Uring Aacute -50 +KPX Uring Abreve -50 +KPX Uring Acircumflex -50 +KPX Uring Adieresis -50 +KPX Uring Agrave -50 +KPX Uring Amacron -50 +KPX Uring Aogonek -50 +KPX Uring Aring -50 +KPX Uring Atilde -50 +KPX Uring comma -30 +KPX Uring period -30 +KPX V A -80 +KPX V Aacute -80 +KPX V Abreve -80 +KPX V Acircumflex -80 +KPX V Adieresis -80 +KPX V Agrave -80 +KPX V Amacron -80 +KPX V Aogonek -80 +KPX V Aring -80 +KPX V Atilde -80 +KPX V G -50 +KPX V Gbreve -50 +KPX V Gcommaaccent -50 +KPX V O -50 +KPX V Oacute -50 +KPX V Ocircumflex -50 +KPX V Odieresis -50 +KPX V Ograve -50 +KPX V Ohungarumlaut -50 +KPX V Omacron -50 +KPX V Oslash -50 +KPX V Otilde -50 +KPX V a -60 +KPX V aacute -60 +KPX V abreve -60 +KPX V acircumflex -60 +KPX V adieresis -60 +KPX V agrave -60 +KPX V amacron -60 +KPX V aogonek -60 +KPX V aring -60 +KPX V atilde -60 +KPX V colon -40 +KPX V comma -120 +KPX V e -50 +KPX V eacute -50 +KPX V ecaron -50 +KPX V ecircumflex -50 +KPX V edieresis -50 +KPX V edotaccent -50 +KPX V egrave -50 +KPX V emacron -50 +KPX V eogonek -50 +KPX V hyphen -80 +KPX V o -90 +KPX V oacute -90 +KPX V ocircumflex -90 +KPX V odieresis -90 +KPX V ograve -90 +KPX V ohungarumlaut -90 +KPX V omacron -90 +KPX V oslash -90 +KPX V otilde -90 +KPX V period -120 +KPX V semicolon -40 +KPX V u -60 +KPX V uacute -60 +KPX V ucircumflex -60 +KPX V udieresis -60 +KPX V ugrave -60 +KPX V uhungarumlaut -60 +KPX V umacron -60 +KPX V uogonek -60 +KPX V uring -60 +KPX W A -60 +KPX W Aacute -60 +KPX W Abreve -60 +KPX W Acircumflex -60 +KPX W Adieresis -60 +KPX W Agrave -60 +KPX W Amacron -60 +KPX W Aogonek -60 +KPX W Aring -60 +KPX W Atilde -60 +KPX W O -20 +KPX W Oacute -20 +KPX W Ocircumflex -20 +KPX W Odieresis -20 +KPX W Ograve -20 +KPX W Ohungarumlaut -20 +KPX W Omacron -20 +KPX W Oslash -20 +KPX W Otilde -20 +KPX W a -40 +KPX W aacute -40 +KPX W abreve -40 +KPX W acircumflex -40 +KPX W adieresis -40 +KPX W agrave -40 +KPX W amacron -40 +KPX W aogonek -40 +KPX W aring -40 +KPX W atilde -40 +KPX W colon -10 +KPX W comma -80 +KPX W e -35 +KPX W eacute -35 +KPX W ecaron -35 +KPX W ecircumflex -35 +KPX W edieresis -35 +KPX W edotaccent -35 +KPX W egrave -35 +KPX W emacron -35 +KPX W eogonek -35 +KPX W hyphen -40 +KPX W o -60 +KPX W oacute -60 +KPX W ocircumflex -60 +KPX W odieresis -60 +KPX W ograve -60 +KPX W ohungarumlaut -60 +KPX W omacron -60 +KPX W oslash -60 +KPX W otilde -60 +KPX W period -80 +KPX W semicolon -10 +KPX W u -45 +KPX W uacute -45 +KPX W ucircumflex -45 +KPX W udieresis -45 +KPX W ugrave -45 +KPX W uhungarumlaut -45 +KPX W umacron -45 +KPX W uogonek -45 +KPX W uring -45 +KPX W y -20 +KPX W yacute -20 +KPX W ydieresis -20 +KPX Y A -110 +KPX Y Aacute -110 +KPX Y Abreve -110 +KPX Y Acircumflex -110 +KPX Y Adieresis -110 +KPX Y Agrave -110 +KPX Y Amacron -110 +KPX Y Aogonek -110 +KPX Y Aring -110 +KPX Y Atilde -110 +KPX Y O -70 +KPX Y Oacute -70 +KPX Y Ocircumflex -70 +KPX Y Odieresis -70 +KPX Y Ograve -70 +KPX Y Ohungarumlaut -70 +KPX Y Omacron -70 +KPX Y Oslash -70 +KPX Y Otilde -70 +KPX Y a -90 +KPX Y aacute -90 +KPX Y abreve -90 +KPX Y acircumflex -90 +KPX Y adieresis -90 +KPX Y agrave -90 +KPX Y amacron -90 +KPX Y aogonek -90 +KPX Y aring -90 +KPX Y atilde -90 +KPX Y colon -50 +KPX Y comma -100 +KPX Y e -80 +KPX Y eacute -80 +KPX Y ecaron -80 +KPX Y ecircumflex -80 +KPX Y edieresis -80 +KPX Y edotaccent -80 +KPX Y egrave -80 +KPX Y emacron -80 +KPX Y eogonek -80 +KPX Y o -100 +KPX Y oacute -100 +KPX Y ocircumflex -100 +KPX Y odieresis -100 +KPX Y ograve -100 +KPX Y ohungarumlaut -100 +KPX Y omacron -100 +KPX Y oslash -100 +KPX Y otilde -100 +KPX Y period -100 +KPX Y semicolon -50 +KPX Y u -100 +KPX Y uacute -100 +KPX Y ucircumflex -100 +KPX Y udieresis -100 +KPX Y ugrave -100 +KPX Y uhungarumlaut -100 +KPX Y umacron -100 +KPX Y uogonek -100 +KPX Y uring -100 +KPX Yacute A -110 +KPX Yacute Aacute -110 +KPX Yacute Abreve -110 +KPX Yacute Acircumflex -110 +KPX Yacute Adieresis -110 +KPX Yacute Agrave -110 +KPX Yacute Amacron -110 +KPX Yacute Aogonek -110 +KPX Yacute Aring -110 +KPX Yacute Atilde -110 +KPX Yacute O -70 +KPX Yacute Oacute -70 +KPX Yacute Ocircumflex -70 +KPX Yacute Odieresis -70 +KPX Yacute Ograve -70 +KPX Yacute Ohungarumlaut -70 +KPX Yacute Omacron -70 +KPX Yacute Oslash -70 +KPX Yacute Otilde -70 +KPX Yacute a -90 +KPX Yacute aacute -90 +KPX Yacute abreve -90 +KPX Yacute acircumflex -90 +KPX Yacute adieresis -90 +KPX Yacute agrave -90 +KPX Yacute amacron -90 +KPX Yacute aogonek -90 +KPX Yacute aring -90 +KPX Yacute atilde -90 +KPX Yacute colon -50 +KPX Yacute comma -100 +KPX Yacute e -80 +KPX Yacute eacute -80 +KPX Yacute ecaron -80 +KPX Yacute ecircumflex -80 +KPX Yacute edieresis -80 +KPX Yacute edotaccent -80 +KPX Yacute egrave -80 +KPX Yacute emacron -80 +KPX Yacute eogonek -80 +KPX Yacute o -100 +KPX Yacute oacute -100 +KPX Yacute ocircumflex -100 +KPX Yacute odieresis -100 +KPX Yacute ograve -100 +KPX Yacute ohungarumlaut -100 +KPX Yacute omacron -100 +KPX Yacute oslash -100 +KPX Yacute otilde -100 +KPX Yacute period -100 +KPX Yacute semicolon -50 +KPX Yacute u -100 +KPX Yacute uacute -100 +KPX Yacute ucircumflex -100 +KPX Yacute udieresis -100 +KPX Yacute ugrave -100 +KPX Yacute uhungarumlaut -100 +KPX Yacute umacron -100 +KPX Yacute uogonek -100 +KPX Yacute uring -100 +KPX Ydieresis A -110 +KPX Ydieresis Aacute -110 +KPX Ydieresis Abreve -110 +KPX Ydieresis Acircumflex -110 +KPX Ydieresis Adieresis -110 +KPX Ydieresis Agrave -110 +KPX Ydieresis Amacron -110 +KPX Ydieresis Aogonek -110 +KPX Ydieresis Aring -110 +KPX Ydieresis Atilde -110 +KPX Ydieresis O -70 +KPX Ydieresis Oacute -70 +KPX Ydieresis Ocircumflex -70 +KPX Ydieresis Odieresis -70 +KPX Ydieresis Ograve -70 +KPX Ydieresis Ohungarumlaut -70 +KPX Ydieresis Omacron -70 +KPX Ydieresis Oslash -70 +KPX Ydieresis Otilde -70 +KPX Ydieresis a -90 +KPX Ydieresis aacute -90 +KPX Ydieresis abreve -90 +KPX Ydieresis acircumflex -90 +KPX Ydieresis adieresis -90 +KPX Ydieresis agrave -90 +KPX Ydieresis amacron -90 +KPX Ydieresis aogonek -90 +KPX Ydieresis aring -90 +KPX Ydieresis atilde -90 +KPX Ydieresis colon -50 +KPX Ydieresis comma -100 +KPX Ydieresis e -80 +KPX Ydieresis eacute -80 +KPX Ydieresis ecaron -80 +KPX Ydieresis ecircumflex -80 +KPX Ydieresis edieresis -80 +KPX Ydieresis edotaccent -80 +KPX Ydieresis egrave -80 +KPX Ydieresis emacron -80 +KPX Ydieresis eogonek -80 +KPX Ydieresis o -100 +KPX Ydieresis oacute -100 +KPX Ydieresis ocircumflex -100 +KPX Ydieresis odieresis -100 +KPX Ydieresis ograve -100 +KPX Ydieresis ohungarumlaut -100 +KPX Ydieresis omacron -100 +KPX Ydieresis oslash -100 +KPX Ydieresis otilde -100 +KPX Ydieresis period -100 +KPX Ydieresis semicolon -50 +KPX Ydieresis u -100 +KPX Ydieresis uacute -100 +KPX Ydieresis ucircumflex -100 +KPX Ydieresis udieresis -100 +KPX Ydieresis ugrave -100 +KPX Ydieresis uhungarumlaut -100 +KPX Ydieresis umacron -100 +KPX Ydieresis uogonek -100 +KPX Ydieresis uring -100 +KPX a g -10 +KPX a gbreve -10 +KPX a gcommaaccent -10 +KPX a v -15 +KPX a w -15 +KPX a y -20 +KPX a yacute -20 +KPX a ydieresis -20 +KPX aacute g -10 +KPX aacute gbreve -10 +KPX aacute gcommaaccent -10 +KPX aacute v -15 +KPX aacute w -15 +KPX aacute y -20 +KPX aacute yacute -20 +KPX aacute ydieresis -20 +KPX abreve g -10 +KPX abreve gbreve -10 +KPX abreve gcommaaccent -10 +KPX abreve v -15 +KPX abreve w -15 +KPX abreve y -20 +KPX abreve yacute -20 +KPX abreve ydieresis -20 +KPX acircumflex g -10 +KPX acircumflex gbreve -10 +KPX acircumflex gcommaaccent -10 +KPX acircumflex v -15 +KPX acircumflex w -15 +KPX acircumflex y -20 +KPX acircumflex yacute -20 +KPX acircumflex ydieresis -20 +KPX adieresis g -10 +KPX adieresis gbreve -10 +KPX adieresis gcommaaccent -10 +KPX adieresis v -15 +KPX adieresis w -15 +KPX adieresis y -20 +KPX adieresis yacute -20 +KPX adieresis ydieresis -20 +KPX agrave g -10 +KPX agrave gbreve -10 +KPX agrave gcommaaccent -10 +KPX agrave v -15 +KPX agrave w -15 +KPX agrave y -20 +KPX agrave yacute -20 +KPX agrave ydieresis -20 +KPX amacron g -10 +KPX amacron gbreve -10 +KPX amacron gcommaaccent -10 +KPX amacron v -15 +KPX amacron w -15 +KPX amacron y -20 +KPX amacron yacute -20 +KPX amacron ydieresis -20 +KPX aogonek g -10 +KPX aogonek gbreve -10 +KPX aogonek gcommaaccent -10 +KPX aogonek v -15 +KPX aogonek w -15 +KPX aogonek y -20 +KPX aogonek yacute -20 +KPX aogonek ydieresis -20 +KPX aring g -10 +KPX aring gbreve -10 +KPX aring gcommaaccent -10 +KPX aring v -15 +KPX aring w -15 +KPX aring y -20 +KPX aring yacute -20 +KPX aring ydieresis -20 +KPX atilde g -10 +KPX atilde gbreve -10 +KPX atilde gcommaaccent -10 +KPX atilde v -15 +KPX atilde w -15 +KPX atilde y -20 +KPX atilde yacute -20 +KPX atilde ydieresis -20 +KPX b l -10 +KPX b lacute -10 +KPX b lcommaaccent -10 +KPX b lslash -10 +KPX b u -20 +KPX b uacute -20 +KPX b ucircumflex -20 +KPX b udieresis -20 +KPX b ugrave -20 +KPX b uhungarumlaut -20 +KPX b umacron -20 +KPX b uogonek -20 +KPX b uring -20 +KPX b v -20 +KPX b y -20 +KPX b yacute -20 +KPX b ydieresis -20 +KPX c h -10 +KPX c k -20 +KPX c kcommaaccent -20 +KPX c l -20 +KPX c lacute -20 +KPX c lcommaaccent -20 +KPX c lslash -20 +KPX c y -10 +KPX c yacute -10 +KPX c ydieresis -10 +KPX cacute h -10 +KPX cacute k -20 +KPX cacute kcommaaccent -20 +KPX cacute l -20 +KPX cacute lacute -20 +KPX cacute lcommaaccent -20 +KPX cacute lslash -20 +KPX cacute y -10 +KPX cacute yacute -10 +KPX cacute ydieresis -10 +KPX ccaron h -10 +KPX ccaron k -20 +KPX ccaron kcommaaccent -20 +KPX ccaron l -20 +KPX ccaron lacute -20 +KPX ccaron lcommaaccent -20 +KPX ccaron lslash -20 +KPX ccaron y -10 +KPX ccaron yacute -10 +KPX ccaron ydieresis -10 +KPX ccedilla h -10 +KPX ccedilla k -20 +KPX ccedilla kcommaaccent -20 +KPX ccedilla l -20 +KPX ccedilla lacute -20 +KPX ccedilla lcommaaccent -20 +KPX ccedilla lslash -20 +KPX ccedilla y -10 +KPX ccedilla yacute -10 +KPX ccedilla ydieresis -10 +KPX colon space -40 +KPX comma quotedblright -120 +KPX comma quoteright -120 +KPX comma space -40 +KPX d d -10 +KPX d dcroat -10 +KPX d v -15 +KPX d w -15 +KPX d y -15 +KPX d yacute -15 +KPX d ydieresis -15 +KPX dcroat d -10 +KPX dcroat dcroat -10 +KPX dcroat v -15 +KPX dcroat w -15 +KPX dcroat y -15 +KPX dcroat yacute -15 +KPX dcroat ydieresis -15 +KPX e comma 10 +KPX e period 20 +KPX e v -15 +KPX e w -15 +KPX e x -15 +KPX e y -15 +KPX e yacute -15 +KPX e ydieresis -15 +KPX eacute comma 10 +KPX eacute period 20 +KPX eacute v -15 +KPX eacute w -15 +KPX eacute x -15 +KPX eacute y -15 +KPX eacute yacute -15 +KPX eacute ydieresis -15 +KPX ecaron comma 10 +KPX ecaron period 20 +KPX ecaron v -15 +KPX ecaron w -15 +KPX ecaron x -15 +KPX ecaron y -15 +KPX ecaron yacute -15 +KPX ecaron ydieresis -15 +KPX ecircumflex comma 10 +KPX ecircumflex period 20 +KPX ecircumflex v -15 +KPX ecircumflex w -15 +KPX ecircumflex x -15 +KPX ecircumflex y -15 +KPX ecircumflex yacute -15 +KPX ecircumflex ydieresis -15 +KPX edieresis comma 10 +KPX edieresis period 20 +KPX edieresis v -15 +KPX edieresis w -15 +KPX edieresis x -15 +KPX edieresis y -15 +KPX edieresis yacute -15 +KPX edieresis ydieresis -15 +KPX edotaccent comma 10 +KPX edotaccent period 20 +KPX edotaccent v -15 +KPX edotaccent w -15 +KPX edotaccent x -15 +KPX edotaccent y -15 +KPX edotaccent yacute -15 +KPX edotaccent ydieresis -15 +KPX egrave comma 10 +KPX egrave period 20 +KPX egrave v -15 +KPX egrave w -15 +KPX egrave x -15 +KPX egrave y -15 +KPX egrave yacute -15 +KPX egrave ydieresis -15 +KPX emacron comma 10 +KPX emacron period 20 +KPX emacron v -15 +KPX emacron w -15 +KPX emacron x -15 +KPX emacron y -15 +KPX emacron yacute -15 +KPX emacron ydieresis -15 +KPX eogonek comma 10 +KPX eogonek period 20 +KPX eogonek v -15 +KPX eogonek w -15 +KPX eogonek x -15 +KPX eogonek y -15 +KPX eogonek yacute -15 +KPX eogonek ydieresis -15 +KPX f comma -10 +KPX f e -10 +KPX f eacute -10 +KPX f ecaron -10 +KPX f ecircumflex -10 +KPX f edieresis -10 +KPX f edotaccent -10 +KPX f egrave -10 +KPX f emacron -10 +KPX f eogonek -10 +KPX f o -20 +KPX f oacute -20 +KPX f ocircumflex -20 +KPX f odieresis -20 +KPX f ograve -20 +KPX f ohungarumlaut -20 +KPX f omacron -20 +KPX f oslash -20 +KPX f otilde -20 +KPX f period -10 +KPX f quotedblright 30 +KPX f quoteright 30 +KPX g e 10 +KPX g eacute 10 +KPX g ecaron 10 +KPX g ecircumflex 10 +KPX g edieresis 10 +KPX g edotaccent 10 +KPX g egrave 10 +KPX g emacron 10 +KPX g eogonek 10 +KPX g g -10 +KPX g gbreve -10 +KPX g gcommaaccent -10 +KPX gbreve e 10 +KPX gbreve eacute 10 +KPX gbreve ecaron 10 +KPX gbreve ecircumflex 10 +KPX gbreve edieresis 10 +KPX gbreve edotaccent 10 +KPX gbreve egrave 10 +KPX gbreve emacron 10 +KPX gbreve eogonek 10 +KPX gbreve g -10 +KPX gbreve gbreve -10 +KPX gbreve gcommaaccent -10 +KPX gcommaaccent e 10 +KPX gcommaaccent eacute 10 +KPX gcommaaccent ecaron 10 +KPX gcommaaccent ecircumflex 10 +KPX gcommaaccent edieresis 10 +KPX gcommaaccent edotaccent 10 +KPX gcommaaccent egrave 10 +KPX gcommaaccent emacron 10 +KPX gcommaaccent eogonek 10 +KPX gcommaaccent g -10 +KPX gcommaaccent gbreve -10 +KPX gcommaaccent gcommaaccent -10 +KPX h y -20 +KPX h yacute -20 +KPX h ydieresis -20 +KPX k o -15 +KPX k oacute -15 +KPX k ocircumflex -15 +KPX k odieresis -15 +KPX k ograve -15 +KPX k ohungarumlaut -15 +KPX k omacron -15 +KPX k oslash -15 +KPX k otilde -15 +KPX kcommaaccent o -15 +KPX kcommaaccent oacute -15 +KPX kcommaaccent ocircumflex -15 +KPX kcommaaccent odieresis -15 +KPX kcommaaccent ograve -15 +KPX kcommaaccent ohungarumlaut -15 +KPX kcommaaccent omacron -15 +KPX kcommaaccent oslash -15 +KPX kcommaaccent otilde -15 +KPX l w -15 +KPX l y -15 +KPX l yacute -15 +KPX l ydieresis -15 +KPX lacute w -15 +KPX lacute y -15 +KPX lacute yacute -15 +KPX lacute ydieresis -15 +KPX lcommaaccent w -15 +KPX lcommaaccent y -15 +KPX lcommaaccent yacute -15 +KPX lcommaaccent ydieresis -15 +KPX lslash w -15 +KPX lslash y -15 +KPX lslash yacute -15 +KPX lslash ydieresis -15 +KPX m u -20 +KPX m uacute -20 +KPX m ucircumflex -20 +KPX m udieresis -20 +KPX m ugrave -20 +KPX m uhungarumlaut -20 +KPX m umacron -20 +KPX m uogonek -20 +KPX m uring -20 +KPX m y -30 +KPX m yacute -30 +KPX m ydieresis -30 +KPX n u -10 +KPX n uacute -10 +KPX n ucircumflex -10 +KPX n udieresis -10 +KPX n ugrave -10 +KPX n uhungarumlaut -10 +KPX n umacron -10 +KPX n uogonek -10 +KPX n uring -10 +KPX n v -40 +KPX n y -20 +KPX n yacute -20 +KPX n ydieresis -20 +KPX nacute u -10 +KPX nacute uacute -10 +KPX nacute ucircumflex -10 +KPX nacute udieresis -10 +KPX nacute ugrave -10 +KPX nacute uhungarumlaut -10 +KPX nacute umacron -10 +KPX nacute uogonek -10 +KPX nacute uring -10 +KPX nacute v -40 +KPX nacute y -20 +KPX nacute yacute -20 +KPX nacute ydieresis -20 +KPX ncaron u -10 +KPX ncaron uacute -10 +KPX ncaron ucircumflex -10 +KPX ncaron udieresis -10 +KPX ncaron ugrave -10 +KPX ncaron uhungarumlaut -10 +KPX ncaron umacron -10 +KPX ncaron uogonek -10 +KPX ncaron uring -10 +KPX ncaron v -40 +KPX ncaron y -20 +KPX ncaron yacute -20 +KPX ncaron ydieresis -20 +KPX ncommaaccent u -10 +KPX ncommaaccent uacute -10 +KPX ncommaaccent ucircumflex -10 +KPX ncommaaccent udieresis -10 +KPX ncommaaccent ugrave -10 +KPX ncommaaccent uhungarumlaut -10 +KPX ncommaaccent umacron -10 +KPX ncommaaccent uogonek -10 +KPX ncommaaccent uring -10 +KPX ncommaaccent v -40 +KPX ncommaaccent y -20 +KPX ncommaaccent yacute -20 +KPX ncommaaccent ydieresis -20 +KPX ntilde u -10 +KPX ntilde uacute -10 +KPX ntilde ucircumflex -10 +KPX ntilde udieresis -10 +KPX ntilde ugrave -10 +KPX ntilde uhungarumlaut -10 +KPX ntilde umacron -10 +KPX ntilde uogonek -10 +KPX ntilde uring -10 +KPX ntilde v -40 +KPX ntilde y -20 +KPX ntilde yacute -20 +KPX ntilde ydieresis -20 +KPX o v -20 +KPX o w -15 +KPX o x -30 +KPX o y -20 +KPX o yacute -20 +KPX o ydieresis -20 +KPX oacute v -20 +KPX oacute w -15 +KPX oacute x -30 +KPX oacute y -20 +KPX oacute yacute -20 +KPX oacute ydieresis -20 +KPX ocircumflex v -20 +KPX ocircumflex w -15 +KPX ocircumflex x -30 +KPX ocircumflex y -20 +KPX ocircumflex yacute -20 +KPX ocircumflex ydieresis -20 +KPX odieresis v -20 +KPX odieresis w -15 +KPX odieresis x -30 +KPX odieresis y -20 +KPX odieresis yacute -20 +KPX odieresis ydieresis -20 +KPX ograve v -20 +KPX ograve w -15 +KPX ograve x -30 +KPX ograve y -20 +KPX ograve yacute -20 +KPX ograve ydieresis -20 +KPX ohungarumlaut v -20 +KPX ohungarumlaut w -15 +KPX ohungarumlaut x -30 +KPX ohungarumlaut y -20 +KPX ohungarumlaut yacute -20 +KPX ohungarumlaut ydieresis -20 +KPX omacron v -20 +KPX omacron w -15 +KPX omacron x -30 +KPX omacron y -20 +KPX omacron yacute -20 +KPX omacron ydieresis -20 +KPX oslash v -20 +KPX oslash w -15 +KPX oslash x -30 +KPX oslash y -20 +KPX oslash yacute -20 +KPX oslash ydieresis -20 +KPX otilde v -20 +KPX otilde w -15 +KPX otilde x -30 +KPX otilde y -20 +KPX otilde yacute -20 +KPX otilde ydieresis -20 +KPX p y -15 +KPX p yacute -15 +KPX p ydieresis -15 +KPX period quotedblright -120 +KPX period quoteright -120 +KPX period space -40 +KPX quotedblright space -80 +KPX quoteleft quoteleft -46 +KPX quoteright d -80 +KPX quoteright dcroat -80 +KPX quoteright l -20 +KPX quoteright lacute -20 +KPX quoteright lcommaaccent -20 +KPX quoteright lslash -20 +KPX quoteright quoteright -46 +KPX quoteright r -40 +KPX quoteright racute -40 +KPX quoteright rcaron -40 +KPX quoteright rcommaaccent -40 +KPX quoteright s -60 +KPX quoteright sacute -60 +KPX quoteright scaron -60 +KPX quoteright scedilla -60 +KPX quoteright scommaaccent -60 +KPX quoteright space -80 +KPX quoteright v -20 +KPX r c -20 +KPX r cacute -20 +KPX r ccaron -20 +KPX r ccedilla -20 +KPX r comma -60 +KPX r d -20 +KPX r dcroat -20 +KPX r g -15 +KPX r gbreve -15 +KPX r gcommaaccent -15 +KPX r hyphen -20 +KPX r o -20 +KPX r oacute -20 +KPX r ocircumflex -20 +KPX r odieresis -20 +KPX r ograve -20 +KPX r ohungarumlaut -20 +KPX r omacron -20 +KPX r oslash -20 +KPX r otilde -20 +KPX r period -60 +KPX r q -20 +KPX r s -15 +KPX r sacute -15 +KPX r scaron -15 +KPX r scedilla -15 +KPX r scommaaccent -15 +KPX r t 20 +KPX r tcommaaccent 20 +KPX r v 10 +KPX r y 10 +KPX r yacute 10 +KPX r ydieresis 10 +KPX racute c -20 +KPX racute cacute -20 +KPX racute ccaron -20 +KPX racute ccedilla -20 +KPX racute comma -60 +KPX racute d -20 +KPX racute dcroat -20 +KPX racute g -15 +KPX racute gbreve -15 +KPX racute gcommaaccent -15 +KPX racute hyphen -20 +KPX racute o -20 +KPX racute oacute -20 +KPX racute ocircumflex -20 +KPX racute odieresis -20 +KPX racute ograve -20 +KPX racute ohungarumlaut -20 +KPX racute omacron -20 +KPX racute oslash -20 +KPX racute otilde -20 +KPX racute period -60 +KPX racute q -20 +KPX racute s -15 +KPX racute sacute -15 +KPX racute scaron -15 +KPX racute scedilla -15 +KPX racute scommaaccent -15 +KPX racute t 20 +KPX racute tcommaaccent 20 +KPX racute v 10 +KPX racute y 10 +KPX racute yacute 10 +KPX racute ydieresis 10 +KPX rcaron c -20 +KPX rcaron cacute -20 +KPX rcaron ccaron -20 +KPX rcaron ccedilla -20 +KPX rcaron comma -60 +KPX rcaron d -20 +KPX rcaron dcroat -20 +KPX rcaron g -15 +KPX rcaron gbreve -15 +KPX rcaron gcommaaccent -15 +KPX rcaron hyphen -20 +KPX rcaron o -20 +KPX rcaron oacute -20 +KPX rcaron ocircumflex -20 +KPX rcaron odieresis -20 +KPX rcaron ograve -20 +KPX rcaron ohungarumlaut -20 +KPX rcaron omacron -20 +KPX rcaron oslash -20 +KPX rcaron otilde -20 +KPX rcaron period -60 +KPX rcaron q -20 +KPX rcaron s -15 +KPX rcaron sacute -15 +KPX rcaron scaron -15 +KPX rcaron scedilla -15 +KPX rcaron scommaaccent -15 +KPX rcaron t 20 +KPX rcaron tcommaaccent 20 +KPX rcaron v 10 +KPX rcaron y 10 +KPX rcaron yacute 10 +KPX rcaron ydieresis 10 +KPX rcommaaccent c -20 +KPX rcommaaccent cacute -20 +KPX rcommaaccent ccaron -20 +KPX rcommaaccent ccedilla -20 +KPX rcommaaccent comma -60 +KPX rcommaaccent d -20 +KPX rcommaaccent dcroat -20 +KPX rcommaaccent g -15 +KPX rcommaaccent gbreve -15 +KPX rcommaaccent gcommaaccent -15 +KPX rcommaaccent hyphen -20 +KPX rcommaaccent o -20 +KPX rcommaaccent oacute -20 +KPX rcommaaccent ocircumflex -20 +KPX rcommaaccent odieresis -20 +KPX rcommaaccent ograve -20 +KPX rcommaaccent ohungarumlaut -20 +KPX rcommaaccent omacron -20 +KPX rcommaaccent oslash -20 +KPX rcommaaccent otilde -20 +KPX rcommaaccent period -60 +KPX rcommaaccent q -20 +KPX rcommaaccent s -15 +KPX rcommaaccent sacute -15 +KPX rcommaaccent scaron -15 +KPX rcommaaccent scedilla -15 +KPX rcommaaccent scommaaccent -15 +KPX rcommaaccent t 20 +KPX rcommaaccent tcommaaccent 20 +KPX rcommaaccent v 10 +KPX rcommaaccent y 10 +KPX rcommaaccent yacute 10 +KPX rcommaaccent ydieresis 10 +KPX s w -15 +KPX sacute w -15 +KPX scaron w -15 +KPX scedilla w -15 +KPX scommaaccent w -15 +KPX semicolon space -40 +KPX space T -100 +KPX space Tcaron -100 +KPX space Tcommaaccent -100 +KPX space V -80 +KPX space W -80 +KPX space Y -120 +KPX space Yacute -120 +KPX space Ydieresis -120 +KPX space quotedblleft -80 +KPX space quoteleft -60 +KPX v a -20 +KPX v aacute -20 +KPX v abreve -20 +KPX v acircumflex -20 +KPX v adieresis -20 +KPX v agrave -20 +KPX v amacron -20 +KPX v aogonek -20 +KPX v aring -20 +KPX v atilde -20 +KPX v comma -80 +KPX v o -30 +KPX v oacute -30 +KPX v ocircumflex -30 +KPX v odieresis -30 +KPX v ograve -30 +KPX v ohungarumlaut -30 +KPX v omacron -30 +KPX v oslash -30 +KPX v otilde -30 +KPX v period -80 +KPX w comma -40 +KPX w o -20 +KPX w oacute -20 +KPX w ocircumflex -20 +KPX w odieresis -20 +KPX w ograve -20 +KPX w ohungarumlaut -20 +KPX w omacron -20 +KPX w oslash -20 +KPX w otilde -20 +KPX w period -40 +KPX x e -10 +KPX x eacute -10 +KPX x ecaron -10 +KPX x ecircumflex -10 +KPX x edieresis -10 +KPX x edotaccent -10 +KPX x egrave -10 +KPX x emacron -10 +KPX x eogonek -10 +KPX y a -30 +KPX y aacute -30 +KPX y abreve -30 +KPX y acircumflex -30 +KPX y adieresis -30 +KPX y agrave -30 +KPX y amacron -30 +KPX y aogonek -30 +KPX y aring -30 +KPX y atilde -30 +KPX y comma -80 +KPX y e -10 +KPX y eacute -10 +KPX y ecaron -10 +KPX y ecircumflex -10 +KPX y edieresis -10 +KPX y edotaccent -10 +KPX y egrave -10 +KPX y emacron -10 +KPX y eogonek -10 +KPX y o -25 +KPX y oacute -25 +KPX y ocircumflex -25 +KPX y odieresis -25 +KPX y ograve -25 +KPX y ohungarumlaut -25 +KPX y omacron -25 +KPX y oslash -25 +KPX y otilde -25 +KPX y period -80 +KPX yacute a -30 +KPX yacute aacute -30 +KPX yacute abreve -30 +KPX yacute acircumflex -30 +KPX yacute adieresis -30 +KPX yacute agrave -30 +KPX yacute amacron -30 +KPX yacute aogonek -30 +KPX yacute aring -30 +KPX yacute atilde -30 +KPX yacute comma -80 +KPX yacute e -10 +KPX yacute eacute -10 +KPX yacute ecaron -10 +KPX yacute ecircumflex -10 +KPX yacute edieresis -10 +KPX yacute edotaccent -10 +KPX yacute egrave -10 +KPX yacute emacron -10 +KPX yacute eogonek -10 +KPX yacute o -25 +KPX yacute oacute -25 +KPX yacute ocircumflex -25 +KPX yacute odieresis -25 +KPX yacute ograve -25 +KPX yacute ohungarumlaut -25 +KPX yacute omacron -25 +KPX yacute oslash -25 +KPX yacute otilde -25 +KPX yacute period -80 +KPX ydieresis a -30 +KPX ydieresis aacute -30 +KPX ydieresis abreve -30 +KPX ydieresis acircumflex -30 +KPX ydieresis adieresis -30 +KPX ydieresis agrave -30 +KPX ydieresis amacron -30 +KPX ydieresis aogonek -30 +KPX ydieresis aring -30 +KPX ydieresis atilde -30 +KPX ydieresis comma -80 +KPX ydieresis e -10 +KPX ydieresis eacute -10 +KPX ydieresis ecaron -10 +KPX ydieresis ecircumflex -10 +KPX ydieresis edieresis -10 +KPX ydieresis edotaccent -10 +KPX ydieresis egrave -10 +KPX ydieresis emacron -10 +KPX ydieresis eogonek -10 +KPX ydieresis o -25 +KPX ydieresis oacute -25 +KPX ydieresis ocircumflex -25 +KPX ydieresis odieresis -25 +KPX ydieresis ograve -25 +KPX ydieresis ohungarumlaut -25 +KPX ydieresis omacron -25 +KPX ydieresis oslash -25 +KPX ydieresis otilde -25 +KPX ydieresis period -80 +KPX z e 10 +KPX z eacute 10 +KPX z ecaron 10 +KPX z ecircumflex 10 +KPX z edieresis 10 +KPX z edotaccent 10 +KPX z egrave 10 +KPX z emacron 10 +KPX z eogonek 10 +KPX zacute e 10 +KPX zacute eacute 10 +KPX zacute ecaron 10 +KPX zacute ecircumflex 10 +KPX zacute edieresis 10 +KPX zacute edotaccent 10 +KPX zacute egrave 10 +KPX zacute emacron 10 +KPX zacute eogonek 10 +KPX zcaron e 10 +KPX zcaron eacute 10 +KPX zcaron ecaron 10 +KPX zcaron ecircumflex 10 +KPX zcaron edieresis 10 +KPX zcaron edotaccent 10 +KPX zcaron egrave 10 +KPX zcaron emacron 10 +KPX zcaron eogonek 10 +KPX zdotaccent e 10 +KPX zdotaccent eacute 10 +KPX zdotaccent ecaron 10 +KPX zdotaccent ecircumflex 10 +KPX zdotaccent edieresis 10 +KPX zdotaccent edotaccent 10 +KPX zdotaccent egrave 10 +KPX zdotaccent emacron 10 +KPX zdotaccent eogonek 10 +EndKernPairs +EndKernData +EndFontMetrics diff --git a/internal/corefont/Core14_AFMs/Helvetica-Oblique.afm b/internal/corefont/Core14_AFMs/Helvetica-Oblique.afm new file mode 100644 index 0000000000000000000000000000000000000000..7a7af0017fe740ac245c5dc6d5d1f4744e89597b --- /dev/null +++ b/internal/corefont/Core14_AFMs/Helvetica-Oblique.afm @@ -0,0 +1,3051 @@ +StartFontMetrics 4.1 +Comment Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems Incorporated. All Rights Reserved. +Comment Creation Date: Thu May 1 12:44:31 1997 +Comment UniqueID 43055 +Comment VMusage 14960 69346 +FontName Helvetica-Oblique +FullName Helvetica Oblique +FamilyName Helvetica +Weight Medium +ItalicAngle -12 +IsFixedPitch false +CharacterSet ExtendedRoman +FontBBox -170 -225 1116 931 +UnderlinePosition -100 +UnderlineThickness 50 +Version 002.000 +Notice Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems Incorporated. All Rights Reserved.Helvetica is a trademark of Linotype-Hell AG and/or its subsidiaries. +EncodingScheme AdobeStandardEncoding +CapHeight 718 +XHeight 523 +Ascender 718 +Descender -207 +StdHW 76 +StdVW 88 +StartCharMetrics 315 +C 32 ; WX 278 ; N space ; B 0 0 0 0 ; +C 33 ; WX 278 ; N exclam ; B 90 0 340 718 ; +C 34 ; WX 355 ; N quotedbl ; B 168 463 438 718 ; +C 35 ; WX 556 ; N numbersign ; B 73 0 631 688 ; +C 36 ; WX 556 ; N dollar ; B 69 -115 617 775 ; +C 37 ; WX 889 ; N percent ; B 147 -19 889 703 ; +C 38 ; WX 667 ; N ampersand ; B 77 -15 647 718 ; +C 39 ; WX 222 ; N quoteright ; B 151 463 310 718 ; +C 40 ; WX 333 ; N parenleft ; B 108 -207 454 733 ; +C 41 ; WX 333 ; N parenright ; B -9 -207 337 733 ; +C 42 ; WX 389 ; N asterisk ; B 165 431 475 718 ; +C 43 ; WX 584 ; N plus ; B 85 0 606 505 ; +C 44 ; WX 278 ; N comma ; B 56 -147 214 106 ; +C 45 ; WX 333 ; N hyphen ; B 93 232 357 322 ; +C 46 ; WX 278 ; N period ; B 87 0 214 106 ; +C 47 ; WX 278 ; N slash ; B -21 -19 452 737 ; +C 48 ; WX 556 ; N zero ; B 93 -19 608 703 ; +C 49 ; WX 556 ; N one ; B 207 0 508 703 ; +C 50 ; WX 556 ; N two ; B 26 0 617 703 ; +C 51 ; WX 556 ; N three ; B 75 -19 610 703 ; +C 52 ; WX 556 ; N four ; B 61 0 576 703 ; +C 53 ; WX 556 ; N five ; B 68 -19 621 688 ; +C 54 ; WX 556 ; N six ; B 91 -19 615 703 ; +C 55 ; WX 556 ; N seven ; B 137 0 669 688 ; +C 56 ; WX 556 ; N eight ; B 74 -19 607 703 ; +C 57 ; WX 556 ; N nine ; B 82 -19 609 703 ; +C 58 ; WX 278 ; N colon ; B 87 0 301 516 ; +C 59 ; WX 278 ; N semicolon ; B 56 -147 301 516 ; +C 60 ; WX 584 ; N less ; B 94 11 641 495 ; +C 61 ; WX 584 ; N equal ; B 63 115 628 390 ; +C 62 ; WX 584 ; N greater ; B 50 11 597 495 ; +C 63 ; WX 556 ; N question ; B 161 0 610 727 ; +C 64 ; WX 1015 ; N at ; B 215 -19 965 737 ; +C 65 ; WX 667 ; N A ; B 14 0 654 718 ; +C 66 ; WX 667 ; N B ; B 74 0 712 718 ; +C 67 ; WX 722 ; N C ; B 108 -19 782 737 ; +C 68 ; WX 722 ; N D ; B 81 0 764 718 ; +C 69 ; WX 667 ; N E ; B 86 0 762 718 ; +C 70 ; WX 611 ; N F ; B 86 0 736 718 ; +C 71 ; WX 778 ; N G ; B 111 -19 799 737 ; +C 72 ; WX 722 ; N H ; B 77 0 799 718 ; +C 73 ; WX 278 ; N I ; B 91 0 341 718 ; +C 74 ; WX 500 ; N J ; B 47 -19 581 718 ; +C 75 ; WX 667 ; N K ; B 76 0 808 718 ; +C 76 ; WX 556 ; N L ; B 76 0 555 718 ; +C 77 ; WX 833 ; N M ; B 73 0 914 718 ; +C 78 ; WX 722 ; N N ; B 76 0 799 718 ; +C 79 ; WX 778 ; N O ; B 105 -19 826 737 ; +C 80 ; WX 667 ; N P ; B 86 0 737 718 ; +C 81 ; WX 778 ; N Q ; B 105 -56 826 737 ; +C 82 ; WX 722 ; N R ; B 88 0 773 718 ; +C 83 ; WX 667 ; N S ; B 90 -19 713 737 ; +C 84 ; WX 611 ; N T ; B 148 0 750 718 ; +C 85 ; WX 722 ; N U ; B 123 -19 797 718 ; +C 86 ; WX 667 ; N V ; B 173 0 800 718 ; +C 87 ; WX 944 ; N W ; B 169 0 1081 718 ; +C 88 ; WX 667 ; N X ; B 19 0 790 718 ; +C 89 ; WX 667 ; N Y ; B 167 0 806 718 ; +C 90 ; WX 611 ; N Z ; B 23 0 741 718 ; +C 91 ; WX 278 ; N bracketleft ; B 21 -196 403 722 ; +C 92 ; WX 278 ; N backslash ; B 140 -19 291 737 ; +C 93 ; WX 278 ; N bracketright ; B -14 -196 368 722 ; +C 94 ; WX 469 ; N asciicircum ; B 42 264 539 688 ; +C 95 ; WX 556 ; N underscore ; B -27 -125 540 -75 ; +C 96 ; WX 222 ; N quoteleft ; B 165 470 323 725 ; +C 97 ; WX 556 ; N a ; B 61 -15 559 538 ; +C 98 ; WX 556 ; N b ; B 58 -15 584 718 ; +C 99 ; WX 500 ; N c ; B 74 -15 553 538 ; +C 100 ; WX 556 ; N d ; B 84 -15 652 718 ; +C 101 ; WX 556 ; N e ; B 84 -15 578 538 ; +C 102 ; WX 278 ; N f ; B 86 0 416 728 ; L i fi ; L l fl ; +C 103 ; WX 556 ; N g ; B 42 -220 610 538 ; +C 104 ; WX 556 ; N h ; B 65 0 573 718 ; +C 105 ; WX 222 ; N i ; B 67 0 308 718 ; +C 106 ; WX 222 ; N j ; B -60 -210 308 718 ; +C 107 ; WX 500 ; N k ; B 67 0 600 718 ; +C 108 ; WX 222 ; N l ; B 67 0 308 718 ; +C 109 ; WX 833 ; N m ; B 65 0 852 538 ; +C 110 ; WX 556 ; N n ; B 65 0 573 538 ; +C 111 ; WX 556 ; N o ; B 83 -14 585 538 ; +C 112 ; WX 556 ; N p ; B 14 -207 584 538 ; +C 113 ; WX 556 ; N q ; B 84 -207 605 538 ; +C 114 ; WX 333 ; N r ; B 77 0 446 538 ; +C 115 ; WX 500 ; N s ; B 63 -15 529 538 ; +C 116 ; WX 278 ; N t ; B 102 -7 368 669 ; +C 117 ; WX 556 ; N u ; B 94 -15 600 523 ; +C 118 ; WX 500 ; N v ; B 119 0 603 523 ; +C 119 ; WX 722 ; N w ; B 125 0 820 523 ; +C 120 ; WX 500 ; N x ; B 11 0 594 523 ; +C 121 ; WX 500 ; N y ; B 15 -214 600 523 ; +C 122 ; WX 500 ; N z ; B 31 0 571 523 ; +C 123 ; WX 334 ; N braceleft ; B 92 -196 445 722 ; +C 124 ; WX 260 ; N bar ; B 46 -225 332 775 ; +C 125 ; WX 334 ; N braceright ; B 0 -196 354 722 ; +C 126 ; WX 584 ; N asciitilde ; B 111 180 580 326 ; +C 161 ; WX 333 ; N exclamdown ; B 77 -195 326 523 ; +C 162 ; WX 556 ; N cent ; B 95 -115 584 623 ; +C 163 ; WX 556 ; N sterling ; B 49 -16 634 718 ; +C 164 ; WX 167 ; N fraction ; B -170 -19 482 703 ; +C 165 ; WX 556 ; N yen ; B 81 0 699 688 ; +C 166 ; WX 556 ; N florin ; B -52 -207 654 737 ; +C 167 ; WX 556 ; N section ; B 76 -191 584 737 ; +C 168 ; WX 556 ; N currency ; B 60 99 646 603 ; +C 169 ; WX 191 ; N quotesingle ; B 157 463 285 718 ; +C 170 ; WX 333 ; N quotedblleft ; B 138 470 461 725 ; +C 171 ; WX 556 ; N guillemotleft ; B 146 108 554 446 ; +C 172 ; WX 333 ; N guilsinglleft ; B 137 108 340 446 ; +C 173 ; WX 333 ; N guilsinglright ; B 111 108 314 446 ; +C 174 ; WX 500 ; N fi ; B 86 0 587 728 ; +C 175 ; WX 500 ; N fl ; B 86 0 585 728 ; +C 177 ; WX 556 ; N endash ; B 51 240 623 313 ; +C 178 ; WX 556 ; N dagger ; B 135 -159 622 718 ; +C 179 ; WX 556 ; N daggerdbl ; B 52 -159 623 718 ; +C 180 ; WX 278 ; N periodcentered ; B 129 190 257 315 ; +C 182 ; WX 537 ; N paragraph ; B 126 -173 650 718 ; +C 183 ; WX 350 ; N bullet ; B 91 202 413 517 ; +C 184 ; WX 222 ; N quotesinglbase ; B 21 -149 180 106 ; +C 185 ; WX 333 ; N quotedblbase ; B -6 -149 318 106 ; +C 186 ; WX 333 ; N quotedblright ; B 124 463 448 718 ; +C 187 ; WX 556 ; N guillemotright ; B 120 108 528 446 ; +C 188 ; WX 1000 ; N ellipsis ; B 115 0 908 106 ; +C 189 ; WX 1000 ; N perthousand ; B 88 -19 1029 703 ; +C 191 ; WX 611 ; N questiondown ; B 85 -201 534 525 ; +C 193 ; WX 333 ; N grave ; B 170 593 337 734 ; +C 194 ; WX 333 ; N acute ; B 248 593 475 734 ; +C 195 ; WX 333 ; N circumflex ; B 147 593 438 734 ; +C 196 ; WX 333 ; N tilde ; B 125 606 490 722 ; +C 197 ; WX 333 ; N macron ; B 143 627 468 684 ; +C 198 ; WX 333 ; N breve ; B 167 595 476 731 ; +C 199 ; WX 333 ; N dotaccent ; B 249 604 362 706 ; +C 200 ; WX 333 ; N dieresis ; B 168 604 443 706 ; +C 202 ; WX 333 ; N ring ; B 214 572 402 756 ; +C 203 ; WX 333 ; N cedilla ; B 2 -225 232 0 ; +C 205 ; WX 333 ; N hungarumlaut ; B 157 593 565 734 ; +C 206 ; WX 333 ; N ogonek ; B 43 -225 249 0 ; +C 207 ; WX 333 ; N caron ; B 177 593 468 734 ; +C 208 ; WX 1000 ; N emdash ; B 51 240 1067 313 ; +C 225 ; WX 1000 ; N AE ; B 8 0 1097 718 ; +C 227 ; WX 370 ; N ordfeminine ; B 127 405 449 737 ; +C 232 ; WX 556 ; N Lslash ; B 41 0 555 718 ; +C 233 ; WX 778 ; N Oslash ; B 43 -19 890 737 ; +C 234 ; WX 1000 ; N OE ; B 98 -19 1116 737 ; +C 235 ; WX 365 ; N ordmasculine ; B 141 405 468 737 ; +C 241 ; WX 889 ; N ae ; B 61 -15 909 538 ; +C 245 ; WX 278 ; N dotlessi ; B 95 0 294 523 ; +C 248 ; WX 222 ; N lslash ; B 41 0 347 718 ; +C 249 ; WX 611 ; N oslash ; B 29 -22 647 545 ; +C 250 ; WX 944 ; N oe ; B 83 -15 964 538 ; +C 251 ; WX 611 ; N germandbls ; B 67 -15 658 728 ; +C -1 ; WX 278 ; N Idieresis ; B 91 0 458 901 ; +C -1 ; WX 556 ; N eacute ; B 84 -15 587 734 ; +C -1 ; WX 556 ; N abreve ; B 61 -15 578 731 ; +C -1 ; WX 556 ; N uhungarumlaut ; B 94 -15 677 734 ; +C -1 ; WX 556 ; N ecaron ; B 84 -15 580 734 ; +C -1 ; WX 667 ; N Ydieresis ; B 167 0 806 901 ; +C -1 ; WX 584 ; N divide ; B 85 -19 606 524 ; +C -1 ; WX 667 ; N Yacute ; B 167 0 806 929 ; +C -1 ; WX 667 ; N Acircumflex ; B 14 0 654 929 ; +C -1 ; WX 556 ; N aacute ; B 61 -15 587 734 ; +C -1 ; WX 722 ; N Ucircumflex ; B 123 -19 797 929 ; +C -1 ; WX 500 ; N yacute ; B 15 -214 600 734 ; +C -1 ; WX 500 ; N scommaaccent ; B 63 -225 529 538 ; +C -1 ; WX 556 ; N ecircumflex ; B 84 -15 578 734 ; +C -1 ; WX 722 ; N Uring ; B 123 -19 797 931 ; +C -1 ; WX 722 ; N Udieresis ; B 123 -19 797 901 ; +C -1 ; WX 556 ; N aogonek ; B 61 -220 559 538 ; +C -1 ; WX 722 ; N Uacute ; B 123 -19 797 929 ; +C -1 ; WX 556 ; N uogonek ; B 94 -225 600 523 ; +C -1 ; WX 667 ; N Edieresis ; B 86 0 762 901 ; +C -1 ; WX 722 ; N Dcroat ; B 69 0 764 718 ; +C -1 ; WX 250 ; N commaaccent ; B 39 -225 172 -40 ; +C -1 ; WX 737 ; N copyright ; B 54 -19 837 737 ; +C -1 ; WX 667 ; N Emacron ; B 86 0 762 879 ; +C -1 ; WX 500 ; N ccaron ; B 74 -15 553 734 ; +C -1 ; WX 556 ; N aring ; B 61 -15 559 756 ; +C -1 ; WX 722 ; N Ncommaaccent ; B 76 -225 799 718 ; +C -1 ; WX 222 ; N lacute ; B 67 0 461 929 ; +C -1 ; WX 556 ; N agrave ; B 61 -15 559 734 ; +C -1 ; WX 611 ; N Tcommaaccent ; B 148 -225 750 718 ; +C -1 ; WX 722 ; N Cacute ; B 108 -19 782 929 ; +C -1 ; WX 556 ; N atilde ; B 61 -15 592 722 ; +C -1 ; WX 667 ; N Edotaccent ; B 86 0 762 901 ; +C -1 ; WX 500 ; N scaron ; B 63 -15 552 734 ; +C -1 ; WX 500 ; N scedilla ; B 63 -225 529 538 ; +C -1 ; WX 278 ; N iacute ; B 95 0 448 734 ; +C -1 ; WX 471 ; N lozenge ; B 88 0 540 728 ; +C -1 ; WX 722 ; N Rcaron ; B 88 0 773 929 ; +C -1 ; WX 778 ; N Gcommaaccent ; B 111 -225 799 737 ; +C -1 ; WX 556 ; N ucircumflex ; B 94 -15 600 734 ; +C -1 ; WX 556 ; N acircumflex ; B 61 -15 559 734 ; +C -1 ; WX 667 ; N Amacron ; B 14 0 677 879 ; +C -1 ; WX 333 ; N rcaron ; B 77 0 508 734 ; +C -1 ; WX 500 ; N ccedilla ; B 74 -225 553 538 ; +C -1 ; WX 611 ; N Zdotaccent ; B 23 0 741 901 ; +C -1 ; WX 667 ; N Thorn ; B 86 0 712 718 ; +C -1 ; WX 778 ; N Omacron ; B 105 -19 826 879 ; +C -1 ; WX 722 ; N Racute ; B 88 0 773 929 ; +C -1 ; WX 667 ; N Sacute ; B 90 -19 713 929 ; +C -1 ; WX 643 ; N dcaron ; B 84 -15 808 718 ; +C -1 ; WX 722 ; N Umacron ; B 123 -19 797 879 ; +C -1 ; WX 556 ; N uring ; B 94 -15 600 756 ; +C -1 ; WX 333 ; N threesuperior ; B 90 270 436 703 ; +C -1 ; WX 778 ; N Ograve ; B 105 -19 826 929 ; +C -1 ; WX 667 ; N Agrave ; B 14 0 654 929 ; +C -1 ; WX 667 ; N Abreve ; B 14 0 685 926 ; +C -1 ; WX 584 ; N multiply ; B 50 0 642 506 ; +C -1 ; WX 556 ; N uacute ; B 94 -15 600 734 ; +C -1 ; WX 611 ; N Tcaron ; B 148 0 750 929 ; +C -1 ; WX 476 ; N partialdiff ; B 41 -38 550 714 ; +C -1 ; WX 500 ; N ydieresis ; B 15 -214 600 706 ; +C -1 ; WX 722 ; N Nacute ; B 76 0 799 929 ; +C -1 ; WX 278 ; N icircumflex ; B 95 0 411 734 ; +C -1 ; WX 667 ; N Ecircumflex ; B 86 0 762 929 ; +C -1 ; WX 556 ; N adieresis ; B 61 -15 559 706 ; +C -1 ; WX 556 ; N edieresis ; B 84 -15 578 706 ; +C -1 ; WX 500 ; N cacute ; B 74 -15 559 734 ; +C -1 ; WX 556 ; N nacute ; B 65 0 587 734 ; +C -1 ; WX 556 ; N umacron ; B 94 -15 600 684 ; +C -1 ; WX 722 ; N Ncaron ; B 76 0 799 929 ; +C -1 ; WX 278 ; N Iacute ; B 91 0 489 929 ; +C -1 ; WX 584 ; N plusminus ; B 39 0 618 506 ; +C -1 ; WX 260 ; N brokenbar ; B 62 -150 316 700 ; +C -1 ; WX 737 ; N registered ; B 54 -19 837 737 ; +C -1 ; WX 778 ; N Gbreve ; B 111 -19 799 926 ; +C -1 ; WX 278 ; N Idotaccent ; B 91 0 377 901 ; +C -1 ; WX 600 ; N summation ; B 15 -10 671 706 ; +C -1 ; WX 667 ; N Egrave ; B 86 0 762 929 ; +C -1 ; WX 333 ; N racute ; B 77 0 475 734 ; +C -1 ; WX 556 ; N omacron ; B 83 -14 585 684 ; +C -1 ; WX 611 ; N Zacute ; B 23 0 741 929 ; +C -1 ; WX 611 ; N Zcaron ; B 23 0 741 929 ; +C -1 ; WX 549 ; N greaterequal ; B 26 0 620 674 ; +C -1 ; WX 722 ; N Eth ; B 69 0 764 718 ; +C -1 ; WX 722 ; N Ccedilla ; B 108 -225 782 737 ; +C -1 ; WX 222 ; N lcommaaccent ; B 25 -225 308 718 ; +C -1 ; WX 317 ; N tcaron ; B 102 -7 501 808 ; +C -1 ; WX 556 ; N eogonek ; B 84 -225 578 538 ; +C -1 ; WX 722 ; N Uogonek ; B 123 -225 797 718 ; +C -1 ; WX 667 ; N Aacute ; B 14 0 683 929 ; +C -1 ; WX 667 ; N Adieresis ; B 14 0 654 901 ; +C -1 ; WX 556 ; N egrave ; B 84 -15 578 734 ; +C -1 ; WX 500 ; N zacute ; B 31 0 571 734 ; +C -1 ; WX 222 ; N iogonek ; B -61 -225 308 718 ; +C -1 ; WX 778 ; N Oacute ; B 105 -19 826 929 ; +C -1 ; WX 556 ; N oacute ; B 83 -14 587 734 ; +C -1 ; WX 556 ; N amacron ; B 61 -15 580 684 ; +C -1 ; WX 500 ; N sacute ; B 63 -15 559 734 ; +C -1 ; WX 278 ; N idieresis ; B 95 0 416 706 ; +C -1 ; WX 778 ; N Ocircumflex ; B 105 -19 826 929 ; +C -1 ; WX 722 ; N Ugrave ; B 123 -19 797 929 ; +C -1 ; WX 612 ; N Delta ; B 6 0 608 688 ; +C -1 ; WX 556 ; N thorn ; B 14 -207 584 718 ; +C -1 ; WX 333 ; N twosuperior ; B 64 281 449 703 ; +C -1 ; WX 778 ; N Odieresis ; B 105 -19 826 901 ; +C -1 ; WX 556 ; N mu ; B 24 -207 600 523 ; +C -1 ; WX 278 ; N igrave ; B 95 0 310 734 ; +C -1 ; WX 556 ; N ohungarumlaut ; B 83 -14 677 734 ; +C -1 ; WX 667 ; N Eogonek ; B 86 -220 762 718 ; +C -1 ; WX 556 ; N dcroat ; B 84 -15 689 718 ; +C -1 ; WX 834 ; N threequarters ; B 130 -19 861 703 ; +C -1 ; WX 667 ; N Scedilla ; B 90 -225 713 737 ; +C -1 ; WX 299 ; N lcaron ; B 67 0 464 718 ; +C -1 ; WX 667 ; N Kcommaaccent ; B 76 -225 808 718 ; +C -1 ; WX 556 ; N Lacute ; B 76 0 555 929 ; +C -1 ; WX 1000 ; N trademark ; B 186 306 1056 718 ; +C -1 ; WX 556 ; N edotaccent ; B 84 -15 578 706 ; +C -1 ; WX 278 ; N Igrave ; B 91 0 351 929 ; +C -1 ; WX 278 ; N Imacron ; B 91 0 483 879 ; +C -1 ; WX 556 ; N Lcaron ; B 76 0 570 718 ; +C -1 ; WX 834 ; N onehalf ; B 114 -19 839 703 ; +C -1 ; WX 549 ; N lessequal ; B 26 0 666 674 ; +C -1 ; WX 556 ; N ocircumflex ; B 83 -14 585 734 ; +C -1 ; WX 556 ; N ntilde ; B 65 0 592 722 ; +C -1 ; WX 722 ; N Uhungarumlaut ; B 123 -19 801 929 ; +C -1 ; WX 667 ; N Eacute ; B 86 0 762 929 ; +C -1 ; WX 556 ; N emacron ; B 84 -15 580 684 ; +C -1 ; WX 556 ; N gbreve ; B 42 -220 610 731 ; +C -1 ; WX 834 ; N onequarter ; B 150 -19 802 703 ; +C -1 ; WX 667 ; N Scaron ; B 90 -19 713 929 ; +C -1 ; WX 667 ; N Scommaaccent ; B 90 -225 713 737 ; +C -1 ; WX 778 ; N Ohungarumlaut ; B 105 -19 829 929 ; +C -1 ; WX 400 ; N degree ; B 169 411 468 703 ; +C -1 ; WX 556 ; N ograve ; B 83 -14 585 734 ; +C -1 ; WX 722 ; N Ccaron ; B 108 -19 782 929 ; +C -1 ; WX 556 ; N ugrave ; B 94 -15 600 734 ; +C -1 ; WX 453 ; N radical ; B 79 -80 617 762 ; +C -1 ; WX 722 ; N Dcaron ; B 81 0 764 929 ; +C -1 ; WX 333 ; N rcommaaccent ; B 30 -225 446 538 ; +C -1 ; WX 722 ; N Ntilde ; B 76 0 799 917 ; +C -1 ; WX 556 ; N otilde ; B 83 -14 602 722 ; +C -1 ; WX 722 ; N Rcommaaccent ; B 88 -225 773 718 ; +C -1 ; WX 556 ; N Lcommaaccent ; B 76 -225 555 718 ; +C -1 ; WX 667 ; N Atilde ; B 14 0 699 917 ; +C -1 ; WX 667 ; N Aogonek ; B 14 -225 654 718 ; +C -1 ; WX 667 ; N Aring ; B 14 0 654 931 ; +C -1 ; WX 778 ; N Otilde ; B 105 -19 826 917 ; +C -1 ; WX 500 ; N zdotaccent ; B 31 0 571 706 ; +C -1 ; WX 667 ; N Ecaron ; B 86 0 762 929 ; +C -1 ; WX 278 ; N Iogonek ; B -33 -225 341 718 ; +C -1 ; WX 500 ; N kcommaaccent ; B 67 -225 600 718 ; +C -1 ; WX 584 ; N minus ; B 85 216 606 289 ; +C -1 ; WX 278 ; N Icircumflex ; B 91 0 452 929 ; +C -1 ; WX 556 ; N ncaron ; B 65 0 580 734 ; +C -1 ; WX 278 ; N tcommaaccent ; B 63 -225 368 669 ; +C -1 ; WX 584 ; N logicalnot ; B 106 108 628 390 ; +C -1 ; WX 556 ; N odieresis ; B 83 -14 585 706 ; +C -1 ; WX 556 ; N udieresis ; B 94 -15 600 706 ; +C -1 ; WX 549 ; N notequal ; B 34 -35 623 551 ; +C -1 ; WX 556 ; N gcommaaccent ; B 42 -220 610 822 ; +C -1 ; WX 556 ; N eth ; B 81 -15 617 737 ; +C -1 ; WX 500 ; N zcaron ; B 31 0 571 734 ; +C -1 ; WX 556 ; N ncommaaccent ; B 65 -225 573 538 ; +C -1 ; WX 333 ; N onesuperior ; B 166 281 371 703 ; +C -1 ; WX 278 ; N imacron ; B 95 0 417 684 ; +C -1 ; WX 556 ; N Euro ; B 0 0 0 0 ; +EndCharMetrics +StartKernData +StartKernPairs 2705 +KPX A C -30 +KPX A Cacute -30 +KPX A Ccaron -30 +KPX A Ccedilla -30 +KPX A G -30 +KPX A Gbreve -30 +KPX A Gcommaaccent -30 +KPX A O -30 +KPX A Oacute -30 +KPX A Ocircumflex -30 +KPX A Odieresis -30 +KPX A Ograve -30 +KPX A Ohungarumlaut -30 +KPX A Omacron -30 +KPX A Oslash -30 +KPX A Otilde -30 +KPX A Q -30 +KPX A T -120 +KPX A Tcaron -120 +KPX A Tcommaaccent -120 +KPX A U -50 +KPX A Uacute -50 +KPX A Ucircumflex -50 +KPX A Udieresis -50 +KPX A Ugrave -50 +KPX A Uhungarumlaut -50 +KPX A Umacron -50 +KPX A Uogonek -50 +KPX A Uring -50 +KPX A V -70 +KPX A W -50 +KPX A Y -100 +KPX A Yacute -100 +KPX A Ydieresis -100 +KPX A u -30 +KPX A uacute -30 +KPX A ucircumflex -30 +KPX A udieresis -30 +KPX A ugrave -30 +KPX A uhungarumlaut -30 +KPX A umacron -30 +KPX A uogonek -30 +KPX A uring -30 +KPX A v -40 +KPX A w -40 +KPX A y -40 +KPX A yacute -40 +KPX A ydieresis -40 +KPX Aacute C -30 +KPX Aacute Cacute -30 +KPX Aacute Ccaron -30 +KPX Aacute Ccedilla -30 +KPX Aacute G -30 +KPX Aacute Gbreve -30 +KPX Aacute Gcommaaccent -30 +KPX Aacute O -30 +KPX Aacute Oacute -30 +KPX Aacute Ocircumflex -30 +KPX Aacute Odieresis -30 +KPX Aacute Ograve -30 +KPX Aacute Ohungarumlaut -30 +KPX Aacute Omacron -30 +KPX Aacute Oslash -30 +KPX Aacute Otilde -30 +KPX Aacute Q -30 +KPX Aacute T -120 +KPX Aacute Tcaron -120 +KPX Aacute Tcommaaccent -120 +KPX Aacute U -50 +KPX Aacute Uacute -50 +KPX Aacute Ucircumflex -50 +KPX Aacute Udieresis -50 +KPX Aacute Ugrave -50 +KPX Aacute Uhungarumlaut -50 +KPX Aacute Umacron -50 +KPX Aacute Uogonek -50 +KPX Aacute Uring -50 +KPX Aacute V -70 +KPX Aacute W -50 +KPX Aacute Y -100 +KPX Aacute Yacute -100 +KPX Aacute Ydieresis -100 +KPX Aacute u -30 +KPX Aacute uacute -30 +KPX Aacute ucircumflex -30 +KPX Aacute udieresis -30 +KPX Aacute ugrave -30 +KPX Aacute uhungarumlaut -30 +KPX Aacute umacron -30 +KPX Aacute uogonek -30 +KPX Aacute uring -30 +KPX Aacute v -40 +KPX Aacute w -40 +KPX Aacute y -40 +KPX Aacute yacute -40 +KPX Aacute ydieresis -40 +KPX Abreve C -30 +KPX Abreve Cacute -30 +KPX Abreve Ccaron -30 +KPX Abreve Ccedilla -30 +KPX Abreve G -30 +KPX Abreve Gbreve -30 +KPX Abreve Gcommaaccent -30 +KPX Abreve O -30 +KPX Abreve Oacute -30 +KPX Abreve Ocircumflex -30 +KPX Abreve Odieresis -30 +KPX Abreve Ograve -30 +KPX Abreve Ohungarumlaut -30 +KPX Abreve Omacron -30 +KPX Abreve Oslash -30 +KPX Abreve Otilde -30 +KPX Abreve Q -30 +KPX Abreve T -120 +KPX Abreve Tcaron -120 +KPX Abreve Tcommaaccent -120 +KPX Abreve U -50 +KPX Abreve Uacute -50 +KPX Abreve Ucircumflex -50 +KPX Abreve Udieresis -50 +KPX Abreve Ugrave -50 +KPX Abreve Uhungarumlaut -50 +KPX Abreve Umacron -50 +KPX Abreve Uogonek -50 +KPX Abreve Uring -50 +KPX Abreve V -70 +KPX Abreve W -50 +KPX Abreve Y -100 +KPX Abreve Yacute -100 +KPX Abreve Ydieresis -100 +KPX Abreve u -30 +KPX Abreve uacute -30 +KPX Abreve ucircumflex -30 +KPX Abreve udieresis -30 +KPX Abreve ugrave -30 +KPX Abreve uhungarumlaut -30 +KPX Abreve umacron -30 +KPX Abreve uogonek -30 +KPX Abreve uring -30 +KPX Abreve v -40 +KPX Abreve w -40 +KPX Abreve y -40 +KPX Abreve yacute -40 +KPX Abreve ydieresis -40 +KPX Acircumflex C -30 +KPX Acircumflex Cacute -30 +KPX Acircumflex Ccaron -30 +KPX Acircumflex Ccedilla -30 +KPX Acircumflex G -30 +KPX Acircumflex Gbreve -30 +KPX Acircumflex Gcommaaccent -30 +KPX Acircumflex O -30 +KPX Acircumflex Oacute -30 +KPX Acircumflex Ocircumflex -30 +KPX Acircumflex Odieresis -30 +KPX Acircumflex Ograve -30 +KPX Acircumflex Ohungarumlaut -30 +KPX Acircumflex Omacron -30 +KPX Acircumflex Oslash -30 +KPX Acircumflex Otilde -30 +KPX Acircumflex Q -30 +KPX Acircumflex T -120 +KPX Acircumflex Tcaron -120 +KPX Acircumflex Tcommaaccent -120 +KPX Acircumflex U -50 +KPX Acircumflex Uacute -50 +KPX Acircumflex Ucircumflex -50 +KPX Acircumflex Udieresis -50 +KPX Acircumflex Ugrave -50 +KPX Acircumflex Uhungarumlaut -50 +KPX Acircumflex Umacron -50 +KPX Acircumflex Uogonek -50 +KPX Acircumflex Uring -50 +KPX Acircumflex V -70 +KPX Acircumflex W -50 +KPX Acircumflex Y -100 +KPX Acircumflex Yacute -100 +KPX Acircumflex Ydieresis -100 +KPX Acircumflex u -30 +KPX Acircumflex uacute -30 +KPX Acircumflex ucircumflex -30 +KPX Acircumflex udieresis -30 +KPX Acircumflex ugrave -30 +KPX Acircumflex uhungarumlaut -30 +KPX Acircumflex umacron -30 +KPX Acircumflex uogonek -30 +KPX Acircumflex uring -30 +KPX Acircumflex v -40 +KPX Acircumflex w -40 +KPX Acircumflex y -40 +KPX Acircumflex yacute -40 +KPX Acircumflex ydieresis -40 +KPX Adieresis C -30 +KPX Adieresis Cacute -30 +KPX Adieresis Ccaron -30 +KPX Adieresis Ccedilla -30 +KPX Adieresis G -30 +KPX Adieresis Gbreve -30 +KPX Adieresis Gcommaaccent -30 +KPX Adieresis O -30 +KPX Adieresis Oacute -30 +KPX Adieresis Ocircumflex -30 +KPX Adieresis Odieresis -30 +KPX Adieresis Ograve -30 +KPX Adieresis Ohungarumlaut -30 +KPX Adieresis Omacron -30 +KPX Adieresis Oslash -30 +KPX Adieresis Otilde -30 +KPX Adieresis Q -30 +KPX Adieresis T -120 +KPX Adieresis Tcaron -120 +KPX Adieresis Tcommaaccent -120 +KPX Adieresis U -50 +KPX Adieresis Uacute -50 +KPX Adieresis Ucircumflex -50 +KPX Adieresis Udieresis -50 +KPX Adieresis Ugrave -50 +KPX Adieresis Uhungarumlaut -50 +KPX Adieresis Umacron -50 +KPX Adieresis Uogonek -50 +KPX Adieresis Uring -50 +KPX Adieresis V -70 +KPX Adieresis W -50 +KPX Adieresis Y -100 +KPX Adieresis Yacute -100 +KPX Adieresis Ydieresis -100 +KPX Adieresis u -30 +KPX Adieresis uacute -30 +KPX Adieresis ucircumflex -30 +KPX Adieresis udieresis -30 +KPX Adieresis ugrave -30 +KPX Adieresis uhungarumlaut -30 +KPX Adieresis umacron -30 +KPX Adieresis uogonek -30 +KPX Adieresis uring -30 +KPX Adieresis v -40 +KPX Adieresis w -40 +KPX Adieresis y -40 +KPX Adieresis yacute -40 +KPX Adieresis ydieresis -40 +KPX Agrave C -30 +KPX Agrave Cacute -30 +KPX Agrave Ccaron -30 +KPX Agrave Ccedilla -30 +KPX Agrave G -30 +KPX Agrave Gbreve -30 +KPX Agrave Gcommaaccent -30 +KPX Agrave O -30 +KPX Agrave Oacute -30 +KPX Agrave Ocircumflex -30 +KPX Agrave Odieresis -30 +KPX Agrave Ograve -30 +KPX Agrave Ohungarumlaut -30 +KPX Agrave Omacron -30 +KPX Agrave Oslash -30 +KPX Agrave Otilde -30 +KPX Agrave Q -30 +KPX Agrave T -120 +KPX Agrave Tcaron -120 +KPX Agrave Tcommaaccent -120 +KPX Agrave U -50 +KPX Agrave Uacute -50 +KPX Agrave Ucircumflex -50 +KPX Agrave Udieresis -50 +KPX Agrave Ugrave -50 +KPX Agrave Uhungarumlaut -50 +KPX Agrave Umacron -50 +KPX Agrave Uogonek -50 +KPX Agrave Uring -50 +KPX Agrave V -70 +KPX Agrave W -50 +KPX Agrave Y -100 +KPX Agrave Yacute -100 +KPX Agrave Ydieresis -100 +KPX Agrave u -30 +KPX Agrave uacute -30 +KPX Agrave ucircumflex -30 +KPX Agrave udieresis -30 +KPX Agrave ugrave -30 +KPX Agrave uhungarumlaut -30 +KPX Agrave umacron -30 +KPX Agrave uogonek -30 +KPX Agrave uring -30 +KPX Agrave v -40 +KPX Agrave w -40 +KPX Agrave y -40 +KPX Agrave yacute -40 +KPX Agrave ydieresis -40 +KPX Amacron C -30 +KPX Amacron Cacute -30 +KPX Amacron Ccaron -30 +KPX Amacron Ccedilla -30 +KPX Amacron G -30 +KPX Amacron Gbreve -30 +KPX Amacron Gcommaaccent -30 +KPX Amacron O -30 +KPX Amacron Oacute -30 +KPX Amacron Ocircumflex -30 +KPX Amacron Odieresis -30 +KPX Amacron Ograve -30 +KPX Amacron Ohungarumlaut -30 +KPX Amacron Omacron -30 +KPX Amacron Oslash -30 +KPX Amacron Otilde -30 +KPX Amacron Q -30 +KPX Amacron T -120 +KPX Amacron Tcaron -120 +KPX Amacron Tcommaaccent -120 +KPX Amacron U -50 +KPX Amacron Uacute -50 +KPX Amacron Ucircumflex -50 +KPX Amacron Udieresis -50 +KPX Amacron Ugrave -50 +KPX Amacron Uhungarumlaut -50 +KPX Amacron Umacron -50 +KPX Amacron Uogonek -50 +KPX Amacron Uring -50 +KPX Amacron V -70 +KPX Amacron W -50 +KPX Amacron Y -100 +KPX Amacron Yacute -100 +KPX Amacron Ydieresis -100 +KPX Amacron u -30 +KPX Amacron uacute -30 +KPX Amacron ucircumflex -30 +KPX Amacron udieresis -30 +KPX Amacron ugrave -30 +KPX Amacron uhungarumlaut -30 +KPX Amacron umacron -30 +KPX Amacron uogonek -30 +KPX Amacron uring -30 +KPX Amacron v -40 +KPX Amacron w -40 +KPX Amacron y -40 +KPX Amacron yacute -40 +KPX Amacron ydieresis -40 +KPX Aogonek C -30 +KPX Aogonek Cacute -30 +KPX Aogonek Ccaron -30 +KPX Aogonek Ccedilla -30 +KPX Aogonek G -30 +KPX Aogonek Gbreve -30 +KPX Aogonek Gcommaaccent -30 +KPX Aogonek O -30 +KPX Aogonek Oacute -30 +KPX Aogonek Ocircumflex -30 +KPX Aogonek Odieresis -30 +KPX Aogonek Ograve -30 +KPX Aogonek Ohungarumlaut -30 +KPX Aogonek Omacron -30 +KPX Aogonek Oslash -30 +KPX Aogonek Otilde -30 +KPX Aogonek Q -30 +KPX Aogonek T -120 +KPX Aogonek Tcaron -120 +KPX Aogonek Tcommaaccent -120 +KPX Aogonek U -50 +KPX Aogonek Uacute -50 +KPX Aogonek Ucircumflex -50 +KPX Aogonek Udieresis -50 +KPX Aogonek Ugrave -50 +KPX Aogonek Uhungarumlaut -50 +KPX Aogonek Umacron -50 +KPX Aogonek Uogonek -50 +KPX Aogonek Uring -50 +KPX Aogonek V -70 +KPX Aogonek W -50 +KPX Aogonek Y -100 +KPX Aogonek Yacute -100 +KPX Aogonek Ydieresis -100 +KPX Aogonek u -30 +KPX Aogonek uacute -30 +KPX Aogonek ucircumflex -30 +KPX Aogonek udieresis -30 +KPX Aogonek ugrave -30 +KPX Aogonek uhungarumlaut -30 +KPX Aogonek umacron -30 +KPX Aogonek uogonek -30 +KPX Aogonek uring -30 +KPX Aogonek v -40 +KPX Aogonek w -40 +KPX Aogonek y -40 +KPX Aogonek yacute -40 +KPX Aogonek ydieresis -40 +KPX Aring C -30 +KPX Aring Cacute -30 +KPX Aring Ccaron -30 +KPX Aring Ccedilla -30 +KPX Aring G -30 +KPX Aring Gbreve -30 +KPX Aring Gcommaaccent -30 +KPX Aring O -30 +KPX Aring Oacute -30 +KPX Aring Ocircumflex -30 +KPX Aring Odieresis -30 +KPX Aring Ograve -30 +KPX Aring Ohungarumlaut -30 +KPX Aring Omacron -30 +KPX Aring Oslash -30 +KPX Aring Otilde -30 +KPX Aring Q -30 +KPX Aring T -120 +KPX Aring Tcaron -120 +KPX Aring Tcommaaccent -120 +KPX Aring U -50 +KPX Aring Uacute -50 +KPX Aring Ucircumflex -50 +KPX Aring Udieresis -50 +KPX Aring Ugrave -50 +KPX Aring Uhungarumlaut -50 +KPX Aring Umacron -50 +KPX Aring Uogonek -50 +KPX Aring Uring -50 +KPX Aring V -70 +KPX Aring W -50 +KPX Aring Y -100 +KPX Aring Yacute -100 +KPX Aring Ydieresis -100 +KPX Aring u -30 +KPX Aring uacute -30 +KPX Aring ucircumflex -30 +KPX Aring udieresis -30 +KPX Aring ugrave -30 +KPX Aring uhungarumlaut -30 +KPX Aring umacron -30 +KPX Aring uogonek -30 +KPX Aring uring -30 +KPX Aring v -40 +KPX Aring w -40 +KPX Aring y -40 +KPX Aring yacute -40 +KPX Aring ydieresis -40 +KPX Atilde C -30 +KPX Atilde Cacute -30 +KPX Atilde Ccaron -30 +KPX Atilde Ccedilla -30 +KPX Atilde G -30 +KPX Atilde Gbreve -30 +KPX Atilde Gcommaaccent -30 +KPX Atilde O -30 +KPX Atilde Oacute -30 +KPX Atilde Ocircumflex -30 +KPX Atilde Odieresis -30 +KPX Atilde Ograve -30 +KPX Atilde Ohungarumlaut -30 +KPX Atilde Omacron -30 +KPX Atilde Oslash -30 +KPX Atilde Otilde -30 +KPX Atilde Q -30 +KPX Atilde T -120 +KPX Atilde Tcaron -120 +KPX Atilde Tcommaaccent -120 +KPX Atilde U -50 +KPX Atilde Uacute -50 +KPX Atilde Ucircumflex -50 +KPX Atilde Udieresis -50 +KPX Atilde Ugrave -50 +KPX Atilde Uhungarumlaut -50 +KPX Atilde Umacron -50 +KPX Atilde Uogonek -50 +KPX Atilde Uring -50 +KPX Atilde V -70 +KPX Atilde W -50 +KPX Atilde Y -100 +KPX Atilde Yacute -100 +KPX Atilde Ydieresis -100 +KPX Atilde u -30 +KPX Atilde uacute -30 +KPX Atilde ucircumflex -30 +KPX Atilde udieresis -30 +KPX Atilde ugrave -30 +KPX Atilde uhungarumlaut -30 +KPX Atilde umacron -30 +KPX Atilde uogonek -30 +KPX Atilde uring -30 +KPX Atilde v -40 +KPX Atilde w -40 +KPX Atilde y -40 +KPX Atilde yacute -40 +KPX Atilde ydieresis -40 +KPX B U -10 +KPX B Uacute -10 +KPX B Ucircumflex -10 +KPX B Udieresis -10 +KPX B Ugrave -10 +KPX B Uhungarumlaut -10 +KPX B Umacron -10 +KPX B Uogonek -10 +KPX B Uring -10 +KPX B comma -20 +KPX B period -20 +KPX C comma -30 +KPX C period -30 +KPX Cacute comma -30 +KPX Cacute period -30 +KPX Ccaron comma -30 +KPX Ccaron period -30 +KPX Ccedilla comma -30 +KPX Ccedilla period -30 +KPX D A -40 +KPX D Aacute -40 +KPX D Abreve -40 +KPX D Acircumflex -40 +KPX D Adieresis -40 +KPX D Agrave -40 +KPX D Amacron -40 +KPX D Aogonek -40 +KPX D Aring -40 +KPX D Atilde -40 +KPX D V -70 +KPX D W -40 +KPX D Y -90 +KPX D Yacute -90 +KPX D Ydieresis -90 +KPX D comma -70 +KPX D period -70 +KPX Dcaron A -40 +KPX Dcaron Aacute -40 +KPX Dcaron Abreve -40 +KPX Dcaron Acircumflex -40 +KPX Dcaron Adieresis -40 +KPX Dcaron Agrave -40 +KPX Dcaron Amacron -40 +KPX Dcaron Aogonek -40 +KPX Dcaron Aring -40 +KPX Dcaron Atilde -40 +KPX Dcaron V -70 +KPX Dcaron W -40 +KPX Dcaron Y -90 +KPX Dcaron Yacute -90 +KPX Dcaron Ydieresis -90 +KPX Dcaron comma -70 +KPX Dcaron period -70 +KPX Dcroat A -40 +KPX Dcroat Aacute -40 +KPX Dcroat Abreve -40 +KPX Dcroat Acircumflex -40 +KPX Dcroat Adieresis -40 +KPX Dcroat Agrave -40 +KPX Dcroat Amacron -40 +KPX Dcroat Aogonek -40 +KPX Dcroat Aring -40 +KPX Dcroat Atilde -40 +KPX Dcroat V -70 +KPX Dcroat W -40 +KPX Dcroat Y -90 +KPX Dcroat Yacute -90 +KPX Dcroat Ydieresis -90 +KPX Dcroat comma -70 +KPX Dcroat period -70 +KPX F A -80 +KPX F Aacute -80 +KPX F Abreve -80 +KPX F Acircumflex -80 +KPX F Adieresis -80 +KPX F Agrave -80 +KPX F Amacron -80 +KPX F Aogonek -80 +KPX F Aring -80 +KPX F Atilde -80 +KPX F a -50 +KPX F aacute -50 +KPX F abreve -50 +KPX F acircumflex -50 +KPX F adieresis -50 +KPX F agrave -50 +KPX F amacron -50 +KPX F aogonek -50 +KPX F aring -50 +KPX F atilde -50 +KPX F comma -150 +KPX F e -30 +KPX F eacute -30 +KPX F ecaron -30 +KPX F ecircumflex -30 +KPX F edieresis -30 +KPX F edotaccent -30 +KPX F egrave -30 +KPX F emacron -30 +KPX F eogonek -30 +KPX F o -30 +KPX F oacute -30 +KPX F ocircumflex -30 +KPX F odieresis -30 +KPX F ograve -30 +KPX F ohungarumlaut -30 +KPX F omacron -30 +KPX F oslash -30 +KPX F otilde -30 +KPX F period -150 +KPX F r -45 +KPX F racute -45 +KPX F rcaron -45 +KPX F rcommaaccent -45 +KPX J A -20 +KPX J Aacute -20 +KPX J Abreve -20 +KPX J Acircumflex -20 +KPX J Adieresis -20 +KPX J Agrave -20 +KPX J Amacron -20 +KPX J Aogonek -20 +KPX J Aring -20 +KPX J Atilde -20 +KPX J a -20 +KPX J aacute -20 +KPX J abreve -20 +KPX J acircumflex -20 +KPX J adieresis -20 +KPX J agrave -20 +KPX J amacron -20 +KPX J aogonek -20 +KPX J aring -20 +KPX J atilde -20 +KPX J comma -30 +KPX J period -30 +KPX J u -20 +KPX J uacute -20 +KPX J ucircumflex -20 +KPX J udieresis -20 +KPX J ugrave -20 +KPX J uhungarumlaut -20 +KPX J umacron -20 +KPX J uogonek -20 +KPX J uring -20 +KPX K O -50 +KPX K Oacute -50 +KPX K Ocircumflex -50 +KPX K Odieresis -50 +KPX K Ograve -50 +KPX K Ohungarumlaut -50 +KPX K Omacron -50 +KPX K Oslash -50 +KPX K Otilde -50 +KPX K e -40 +KPX K eacute -40 +KPX K ecaron -40 +KPX K ecircumflex -40 +KPX K edieresis -40 +KPX K edotaccent -40 +KPX K egrave -40 +KPX K emacron -40 +KPX K eogonek -40 +KPX K o -40 +KPX K oacute -40 +KPX K ocircumflex -40 +KPX K odieresis -40 +KPX K ograve -40 +KPX K ohungarumlaut -40 +KPX K omacron -40 +KPX K oslash -40 +KPX K otilde -40 +KPX K u -30 +KPX K uacute -30 +KPX K ucircumflex -30 +KPX K udieresis -30 +KPX K ugrave -30 +KPX K uhungarumlaut -30 +KPX K umacron -30 +KPX K uogonek -30 +KPX K uring -30 +KPX K y -50 +KPX K yacute -50 +KPX K ydieresis -50 +KPX Kcommaaccent O -50 +KPX Kcommaaccent Oacute -50 +KPX Kcommaaccent Ocircumflex -50 +KPX Kcommaaccent Odieresis -50 +KPX Kcommaaccent Ograve -50 +KPX Kcommaaccent Ohungarumlaut -50 +KPX Kcommaaccent Omacron -50 +KPX Kcommaaccent Oslash -50 +KPX Kcommaaccent Otilde -50 +KPX Kcommaaccent e -40 +KPX Kcommaaccent eacute -40 +KPX Kcommaaccent ecaron -40 +KPX Kcommaaccent ecircumflex -40 +KPX Kcommaaccent edieresis -40 +KPX Kcommaaccent edotaccent -40 +KPX Kcommaaccent egrave -40 +KPX Kcommaaccent emacron -40 +KPX Kcommaaccent eogonek -40 +KPX Kcommaaccent o -40 +KPX Kcommaaccent oacute -40 +KPX Kcommaaccent ocircumflex -40 +KPX Kcommaaccent odieresis -40 +KPX Kcommaaccent ograve -40 +KPX Kcommaaccent ohungarumlaut -40 +KPX Kcommaaccent omacron -40 +KPX Kcommaaccent oslash -40 +KPX Kcommaaccent otilde -40 +KPX Kcommaaccent u -30 +KPX Kcommaaccent uacute -30 +KPX Kcommaaccent ucircumflex -30 +KPX Kcommaaccent udieresis -30 +KPX Kcommaaccent ugrave -30 +KPX Kcommaaccent uhungarumlaut -30 +KPX Kcommaaccent umacron -30 +KPX Kcommaaccent uogonek -30 +KPX Kcommaaccent uring -30 +KPX Kcommaaccent y -50 +KPX Kcommaaccent yacute -50 +KPX Kcommaaccent ydieresis -50 +KPX L T -110 +KPX L Tcaron -110 +KPX L Tcommaaccent -110 +KPX L V -110 +KPX L W -70 +KPX L Y -140 +KPX L Yacute -140 +KPX L Ydieresis -140 +KPX L quotedblright -140 +KPX L quoteright -160 +KPX L y -30 +KPX L yacute -30 +KPX L ydieresis -30 +KPX Lacute T -110 +KPX Lacute Tcaron -110 +KPX Lacute Tcommaaccent -110 +KPX Lacute V -110 +KPX Lacute W -70 +KPX Lacute Y -140 +KPX Lacute Yacute -140 +KPX Lacute Ydieresis -140 +KPX Lacute quotedblright -140 +KPX Lacute quoteright -160 +KPX Lacute y -30 +KPX Lacute yacute -30 +KPX Lacute ydieresis -30 +KPX Lcaron T -110 +KPX Lcaron Tcaron -110 +KPX Lcaron Tcommaaccent -110 +KPX Lcaron V -110 +KPX Lcaron W -70 +KPX Lcaron Y -140 +KPX Lcaron Yacute -140 +KPX Lcaron Ydieresis -140 +KPX Lcaron quotedblright -140 +KPX Lcaron quoteright -160 +KPX Lcaron y -30 +KPX Lcaron yacute -30 +KPX Lcaron ydieresis -30 +KPX Lcommaaccent T -110 +KPX Lcommaaccent Tcaron -110 +KPX Lcommaaccent Tcommaaccent -110 +KPX Lcommaaccent V -110 +KPX Lcommaaccent W -70 +KPX Lcommaaccent Y -140 +KPX Lcommaaccent Yacute -140 +KPX Lcommaaccent Ydieresis -140 +KPX Lcommaaccent quotedblright -140 +KPX Lcommaaccent quoteright -160 +KPX Lcommaaccent y -30 +KPX Lcommaaccent yacute -30 +KPX Lcommaaccent ydieresis -30 +KPX Lslash T -110 +KPX Lslash Tcaron -110 +KPX Lslash Tcommaaccent -110 +KPX Lslash V -110 +KPX Lslash W -70 +KPX Lslash Y -140 +KPX Lslash Yacute -140 +KPX Lslash Ydieresis -140 +KPX Lslash quotedblright -140 +KPX Lslash quoteright -160 +KPX Lslash y -30 +KPX Lslash yacute -30 +KPX Lslash ydieresis -30 +KPX O A -20 +KPX O Aacute -20 +KPX O Abreve -20 +KPX O Acircumflex -20 +KPX O Adieresis -20 +KPX O Agrave -20 +KPX O Amacron -20 +KPX O Aogonek -20 +KPX O Aring -20 +KPX O Atilde -20 +KPX O T -40 +KPX O Tcaron -40 +KPX O Tcommaaccent -40 +KPX O V -50 +KPX O W -30 +KPX O X -60 +KPX O Y -70 +KPX O Yacute -70 +KPX O Ydieresis -70 +KPX O comma -40 +KPX O period -40 +KPX Oacute A -20 +KPX Oacute Aacute -20 +KPX Oacute Abreve -20 +KPX Oacute Acircumflex -20 +KPX Oacute Adieresis -20 +KPX Oacute Agrave -20 +KPX Oacute Amacron -20 +KPX Oacute Aogonek -20 +KPX Oacute Aring -20 +KPX Oacute Atilde -20 +KPX Oacute T -40 +KPX Oacute Tcaron -40 +KPX Oacute Tcommaaccent -40 +KPX Oacute V -50 +KPX Oacute W -30 +KPX Oacute X -60 +KPX Oacute Y -70 +KPX Oacute Yacute -70 +KPX Oacute Ydieresis -70 +KPX Oacute comma -40 +KPX Oacute period -40 +KPX Ocircumflex A -20 +KPX Ocircumflex Aacute -20 +KPX Ocircumflex Abreve -20 +KPX Ocircumflex Acircumflex -20 +KPX Ocircumflex Adieresis -20 +KPX Ocircumflex Agrave -20 +KPX Ocircumflex Amacron -20 +KPX Ocircumflex Aogonek -20 +KPX Ocircumflex Aring -20 +KPX Ocircumflex Atilde -20 +KPX Ocircumflex T -40 +KPX Ocircumflex Tcaron -40 +KPX Ocircumflex Tcommaaccent -40 +KPX Ocircumflex V -50 +KPX Ocircumflex W -30 +KPX Ocircumflex X -60 +KPX Ocircumflex Y -70 +KPX Ocircumflex Yacute -70 +KPX Ocircumflex Ydieresis -70 +KPX Ocircumflex comma -40 +KPX Ocircumflex period -40 +KPX Odieresis A -20 +KPX Odieresis Aacute -20 +KPX Odieresis Abreve -20 +KPX Odieresis Acircumflex -20 +KPX Odieresis Adieresis -20 +KPX Odieresis Agrave -20 +KPX Odieresis Amacron -20 +KPX Odieresis Aogonek -20 +KPX Odieresis Aring -20 +KPX Odieresis Atilde -20 +KPX Odieresis T -40 +KPX Odieresis Tcaron -40 +KPX Odieresis Tcommaaccent -40 +KPX Odieresis V -50 +KPX Odieresis W -30 +KPX Odieresis X -60 +KPX Odieresis Y -70 +KPX Odieresis Yacute -70 +KPX Odieresis Ydieresis -70 +KPX Odieresis comma -40 +KPX Odieresis period -40 +KPX Ograve A -20 +KPX Ograve Aacute -20 +KPX Ograve Abreve -20 +KPX Ograve Acircumflex -20 +KPX Ograve Adieresis -20 +KPX Ograve Agrave -20 +KPX Ograve Amacron -20 +KPX Ograve Aogonek -20 +KPX Ograve Aring -20 +KPX Ograve Atilde -20 +KPX Ograve T -40 +KPX Ograve Tcaron -40 +KPX Ograve Tcommaaccent -40 +KPX Ograve V -50 +KPX Ograve W -30 +KPX Ograve X -60 +KPX Ograve Y -70 +KPX Ograve Yacute -70 +KPX Ograve Ydieresis -70 +KPX Ograve comma -40 +KPX Ograve period -40 +KPX Ohungarumlaut A -20 +KPX Ohungarumlaut Aacute -20 +KPX Ohungarumlaut Abreve -20 +KPX Ohungarumlaut Acircumflex -20 +KPX Ohungarumlaut Adieresis -20 +KPX Ohungarumlaut Agrave -20 +KPX Ohungarumlaut Amacron -20 +KPX Ohungarumlaut Aogonek -20 +KPX Ohungarumlaut Aring -20 +KPX Ohungarumlaut Atilde -20 +KPX Ohungarumlaut T -40 +KPX Ohungarumlaut Tcaron -40 +KPX Ohungarumlaut Tcommaaccent -40 +KPX Ohungarumlaut V -50 +KPX Ohungarumlaut W -30 +KPX Ohungarumlaut X -60 +KPX Ohungarumlaut Y -70 +KPX Ohungarumlaut Yacute -70 +KPX Ohungarumlaut Ydieresis -70 +KPX Ohungarumlaut comma -40 +KPX Ohungarumlaut period -40 +KPX Omacron A -20 +KPX Omacron Aacute -20 +KPX Omacron Abreve -20 +KPX Omacron Acircumflex -20 +KPX Omacron Adieresis -20 +KPX Omacron Agrave -20 +KPX Omacron Amacron -20 +KPX Omacron Aogonek -20 +KPX Omacron Aring -20 +KPX Omacron Atilde -20 +KPX Omacron T -40 +KPX Omacron Tcaron -40 +KPX Omacron Tcommaaccent -40 +KPX Omacron V -50 +KPX Omacron W -30 +KPX Omacron X -60 +KPX Omacron Y -70 +KPX Omacron Yacute -70 +KPX Omacron Ydieresis -70 +KPX Omacron comma -40 +KPX Omacron period -40 +KPX Oslash A -20 +KPX Oslash Aacute -20 +KPX Oslash Abreve -20 +KPX Oslash Acircumflex -20 +KPX Oslash Adieresis -20 +KPX Oslash Agrave -20 +KPX Oslash Amacron -20 +KPX Oslash Aogonek -20 +KPX Oslash Aring -20 +KPX Oslash Atilde -20 +KPX Oslash T -40 +KPX Oslash Tcaron -40 +KPX Oslash Tcommaaccent -40 +KPX Oslash V -50 +KPX Oslash W -30 +KPX Oslash X -60 +KPX Oslash Y -70 +KPX Oslash Yacute -70 +KPX Oslash Ydieresis -70 +KPX Oslash comma -40 +KPX Oslash period -40 +KPX Otilde A -20 +KPX Otilde Aacute -20 +KPX Otilde Abreve -20 +KPX Otilde Acircumflex -20 +KPX Otilde Adieresis -20 +KPX Otilde Agrave -20 +KPX Otilde Amacron -20 +KPX Otilde Aogonek -20 +KPX Otilde Aring -20 +KPX Otilde Atilde -20 +KPX Otilde T -40 +KPX Otilde Tcaron -40 +KPX Otilde Tcommaaccent -40 +KPX Otilde V -50 +KPX Otilde W -30 +KPX Otilde X -60 +KPX Otilde Y -70 +KPX Otilde Yacute -70 +KPX Otilde Ydieresis -70 +KPX Otilde comma -40 +KPX Otilde period -40 +KPX P A -120 +KPX P Aacute -120 +KPX P Abreve -120 +KPX P Acircumflex -120 +KPX P Adieresis -120 +KPX P Agrave -120 +KPX P Amacron -120 +KPX P Aogonek -120 +KPX P Aring -120 +KPX P Atilde -120 +KPX P a -40 +KPX P aacute -40 +KPX P abreve -40 +KPX P acircumflex -40 +KPX P adieresis -40 +KPX P agrave -40 +KPX P amacron -40 +KPX P aogonek -40 +KPX P aring -40 +KPX P atilde -40 +KPX P comma -180 +KPX P e -50 +KPX P eacute -50 +KPX P ecaron -50 +KPX P ecircumflex -50 +KPX P edieresis -50 +KPX P edotaccent -50 +KPX P egrave -50 +KPX P emacron -50 +KPX P eogonek -50 +KPX P o -50 +KPX P oacute -50 +KPX P ocircumflex -50 +KPX P odieresis -50 +KPX P ograve -50 +KPX P ohungarumlaut -50 +KPX P omacron -50 +KPX P oslash -50 +KPX P otilde -50 +KPX P period -180 +KPX Q U -10 +KPX Q Uacute -10 +KPX Q Ucircumflex -10 +KPX Q Udieresis -10 +KPX Q Ugrave -10 +KPX Q Uhungarumlaut -10 +KPX Q Umacron -10 +KPX Q Uogonek -10 +KPX Q Uring -10 +KPX R O -20 +KPX R Oacute -20 +KPX R Ocircumflex -20 +KPX R Odieresis -20 +KPX R Ograve -20 +KPX R Ohungarumlaut -20 +KPX R Omacron -20 +KPX R Oslash -20 +KPX R Otilde -20 +KPX R T -30 +KPX R Tcaron -30 +KPX R Tcommaaccent -30 +KPX R U -40 +KPX R Uacute -40 +KPX R Ucircumflex -40 +KPX R Udieresis -40 +KPX R Ugrave -40 +KPX R Uhungarumlaut -40 +KPX R Umacron -40 +KPX R Uogonek -40 +KPX R Uring -40 +KPX R V -50 +KPX R W -30 +KPX R Y -50 +KPX R Yacute -50 +KPX R Ydieresis -50 +KPX Racute O -20 +KPX Racute Oacute -20 +KPX Racute Ocircumflex -20 +KPX Racute Odieresis -20 +KPX Racute Ograve -20 +KPX Racute Ohungarumlaut -20 +KPX Racute Omacron -20 +KPX Racute Oslash -20 +KPX Racute Otilde -20 +KPX Racute T -30 +KPX Racute Tcaron -30 +KPX Racute Tcommaaccent -30 +KPX Racute U -40 +KPX Racute Uacute -40 +KPX Racute Ucircumflex -40 +KPX Racute Udieresis -40 +KPX Racute Ugrave -40 +KPX Racute Uhungarumlaut -40 +KPX Racute Umacron -40 +KPX Racute Uogonek -40 +KPX Racute Uring -40 +KPX Racute V -50 +KPX Racute W -30 +KPX Racute Y -50 +KPX Racute Yacute -50 +KPX Racute Ydieresis -50 +KPX Rcaron O -20 +KPX Rcaron Oacute -20 +KPX Rcaron Ocircumflex -20 +KPX Rcaron Odieresis -20 +KPX Rcaron Ograve -20 +KPX Rcaron Ohungarumlaut -20 +KPX Rcaron Omacron -20 +KPX Rcaron Oslash -20 +KPX Rcaron Otilde -20 +KPX Rcaron T -30 +KPX Rcaron Tcaron -30 +KPX Rcaron Tcommaaccent -30 +KPX Rcaron U -40 +KPX Rcaron Uacute -40 +KPX Rcaron Ucircumflex -40 +KPX Rcaron Udieresis -40 +KPX Rcaron Ugrave -40 +KPX Rcaron Uhungarumlaut -40 +KPX Rcaron Umacron -40 +KPX Rcaron Uogonek -40 +KPX Rcaron Uring -40 +KPX Rcaron V -50 +KPX Rcaron W -30 +KPX Rcaron Y -50 +KPX Rcaron Yacute -50 +KPX Rcaron Ydieresis -50 +KPX Rcommaaccent O -20 +KPX Rcommaaccent Oacute -20 +KPX Rcommaaccent Ocircumflex -20 +KPX Rcommaaccent Odieresis -20 +KPX Rcommaaccent Ograve -20 +KPX Rcommaaccent Ohungarumlaut -20 +KPX Rcommaaccent Omacron -20 +KPX Rcommaaccent Oslash -20 +KPX Rcommaaccent Otilde -20 +KPX Rcommaaccent T -30 +KPX Rcommaaccent Tcaron -30 +KPX Rcommaaccent Tcommaaccent -30 +KPX Rcommaaccent U -40 +KPX Rcommaaccent Uacute -40 +KPX Rcommaaccent Ucircumflex -40 +KPX Rcommaaccent Udieresis -40 +KPX Rcommaaccent Ugrave -40 +KPX Rcommaaccent Uhungarumlaut -40 +KPX Rcommaaccent Umacron -40 +KPX Rcommaaccent Uogonek -40 +KPX Rcommaaccent Uring -40 +KPX Rcommaaccent V -50 +KPX Rcommaaccent W -30 +KPX Rcommaaccent Y -50 +KPX Rcommaaccent Yacute -50 +KPX Rcommaaccent Ydieresis -50 +KPX S comma -20 +KPX S period -20 +KPX Sacute comma -20 +KPX Sacute period -20 +KPX Scaron comma -20 +KPX Scaron period -20 +KPX Scedilla comma -20 +KPX Scedilla period -20 +KPX Scommaaccent comma -20 +KPX Scommaaccent period -20 +KPX T A -120 +KPX T Aacute -120 +KPX T Abreve -120 +KPX T Acircumflex -120 +KPX T Adieresis -120 +KPX T Agrave -120 +KPX T Amacron -120 +KPX T Aogonek -120 +KPX T Aring -120 +KPX T Atilde -120 +KPX T O -40 +KPX T Oacute -40 +KPX T Ocircumflex -40 +KPX T Odieresis -40 +KPX T Ograve -40 +KPX T Ohungarumlaut -40 +KPX T Omacron -40 +KPX T Oslash -40 +KPX T Otilde -40 +KPX T a -120 +KPX T aacute -120 +KPX T abreve -60 +KPX T acircumflex -120 +KPX T adieresis -120 +KPX T agrave -120 +KPX T amacron -60 +KPX T aogonek -120 +KPX T aring -120 +KPX T atilde -60 +KPX T colon -20 +KPX T comma -120 +KPX T e -120 +KPX T eacute -120 +KPX T ecaron -120 +KPX T ecircumflex -120 +KPX T edieresis -120 +KPX T edotaccent -120 +KPX T egrave -60 +KPX T emacron -60 +KPX T eogonek -120 +KPX T hyphen -140 +KPX T o -120 +KPX T oacute -120 +KPX T ocircumflex -120 +KPX T odieresis -120 +KPX T ograve -120 +KPX T ohungarumlaut -120 +KPX T omacron -60 +KPX T oslash -120 +KPX T otilde -60 +KPX T period -120 +KPX T r -120 +KPX T racute -120 +KPX T rcaron -120 +KPX T rcommaaccent -120 +KPX T semicolon -20 +KPX T u -120 +KPX T uacute -120 +KPX T ucircumflex -120 +KPX T udieresis -120 +KPX T ugrave -120 +KPX T uhungarumlaut -120 +KPX T umacron -60 +KPX T uogonek -120 +KPX T uring -120 +KPX T w -120 +KPX T y -120 +KPX T yacute -120 +KPX T ydieresis -60 +KPX Tcaron A -120 +KPX Tcaron Aacute -120 +KPX Tcaron Abreve -120 +KPX Tcaron Acircumflex -120 +KPX Tcaron Adieresis -120 +KPX Tcaron Agrave -120 +KPX Tcaron Amacron -120 +KPX Tcaron Aogonek -120 +KPX Tcaron Aring -120 +KPX Tcaron Atilde -120 +KPX Tcaron O -40 +KPX Tcaron Oacute -40 +KPX Tcaron Ocircumflex -40 +KPX Tcaron Odieresis -40 +KPX Tcaron Ograve -40 +KPX Tcaron Ohungarumlaut -40 +KPX Tcaron Omacron -40 +KPX Tcaron Oslash -40 +KPX Tcaron Otilde -40 +KPX Tcaron a -120 +KPX Tcaron aacute -120 +KPX Tcaron abreve -60 +KPX Tcaron acircumflex -120 +KPX Tcaron adieresis -120 +KPX Tcaron agrave -120 +KPX Tcaron amacron -60 +KPX Tcaron aogonek -120 +KPX Tcaron aring -120 +KPX Tcaron atilde -60 +KPX Tcaron colon -20 +KPX Tcaron comma -120 +KPX Tcaron e -120 +KPX Tcaron eacute -120 +KPX Tcaron ecaron -120 +KPX Tcaron ecircumflex -120 +KPX Tcaron edieresis -120 +KPX Tcaron edotaccent -120 +KPX Tcaron egrave -60 +KPX Tcaron emacron -60 +KPX Tcaron eogonek -120 +KPX Tcaron hyphen -140 +KPX Tcaron o -120 +KPX Tcaron oacute -120 +KPX Tcaron ocircumflex -120 +KPX Tcaron odieresis -120 +KPX Tcaron ograve -120 +KPX Tcaron ohungarumlaut -120 +KPX Tcaron omacron -60 +KPX Tcaron oslash -120 +KPX Tcaron otilde -60 +KPX Tcaron period -120 +KPX Tcaron r -120 +KPX Tcaron racute -120 +KPX Tcaron rcaron -120 +KPX Tcaron rcommaaccent -120 +KPX Tcaron semicolon -20 +KPX Tcaron u -120 +KPX Tcaron uacute -120 +KPX Tcaron ucircumflex -120 +KPX Tcaron udieresis -120 +KPX Tcaron ugrave -120 +KPX Tcaron uhungarumlaut -120 +KPX Tcaron umacron -60 +KPX Tcaron uogonek -120 +KPX Tcaron uring -120 +KPX Tcaron w -120 +KPX Tcaron y -120 +KPX Tcaron yacute -120 +KPX Tcaron ydieresis -60 +KPX Tcommaaccent A -120 +KPX Tcommaaccent Aacute -120 +KPX Tcommaaccent Abreve -120 +KPX Tcommaaccent Acircumflex -120 +KPX Tcommaaccent Adieresis -120 +KPX Tcommaaccent Agrave -120 +KPX Tcommaaccent Amacron -120 +KPX Tcommaaccent Aogonek -120 +KPX Tcommaaccent Aring -120 +KPX Tcommaaccent Atilde -120 +KPX Tcommaaccent O -40 +KPX Tcommaaccent Oacute -40 +KPX Tcommaaccent Ocircumflex -40 +KPX Tcommaaccent Odieresis -40 +KPX Tcommaaccent Ograve -40 +KPX Tcommaaccent Ohungarumlaut -40 +KPX Tcommaaccent Omacron -40 +KPX Tcommaaccent Oslash -40 +KPX Tcommaaccent Otilde -40 +KPX Tcommaaccent a -120 +KPX Tcommaaccent aacute -120 +KPX Tcommaaccent abreve -60 +KPX Tcommaaccent acircumflex -120 +KPX Tcommaaccent adieresis -120 +KPX Tcommaaccent agrave -120 +KPX Tcommaaccent amacron -60 +KPX Tcommaaccent aogonek -120 +KPX Tcommaaccent aring -120 +KPX Tcommaaccent atilde -60 +KPX Tcommaaccent colon -20 +KPX Tcommaaccent comma -120 +KPX Tcommaaccent e -120 +KPX Tcommaaccent eacute -120 +KPX Tcommaaccent ecaron -120 +KPX Tcommaaccent ecircumflex -120 +KPX Tcommaaccent edieresis -120 +KPX Tcommaaccent edotaccent -120 +KPX Tcommaaccent egrave -60 +KPX Tcommaaccent emacron -60 +KPX Tcommaaccent eogonek -120 +KPX Tcommaaccent hyphen -140 +KPX Tcommaaccent o -120 +KPX Tcommaaccent oacute -120 +KPX Tcommaaccent ocircumflex -120 +KPX Tcommaaccent odieresis -120 +KPX Tcommaaccent ograve -120 +KPX Tcommaaccent ohungarumlaut -120 +KPX Tcommaaccent omacron -60 +KPX Tcommaaccent oslash -120 +KPX Tcommaaccent otilde -60 +KPX Tcommaaccent period -120 +KPX Tcommaaccent r -120 +KPX Tcommaaccent racute -120 +KPX Tcommaaccent rcaron -120 +KPX Tcommaaccent rcommaaccent -120 +KPX Tcommaaccent semicolon -20 +KPX Tcommaaccent u -120 +KPX Tcommaaccent uacute -120 +KPX Tcommaaccent ucircumflex -120 +KPX Tcommaaccent udieresis -120 +KPX Tcommaaccent ugrave -120 +KPX Tcommaaccent uhungarumlaut -120 +KPX Tcommaaccent umacron -60 +KPX Tcommaaccent uogonek -120 +KPX Tcommaaccent uring -120 +KPX Tcommaaccent w -120 +KPX Tcommaaccent y -120 +KPX Tcommaaccent yacute -120 +KPX Tcommaaccent ydieresis -60 +KPX U A -40 +KPX U Aacute -40 +KPX U Abreve -40 +KPX U Acircumflex -40 +KPX U Adieresis -40 +KPX U Agrave -40 +KPX U Amacron -40 +KPX U Aogonek -40 +KPX U Aring -40 +KPX U Atilde -40 +KPX U comma -40 +KPX U period -40 +KPX Uacute A -40 +KPX Uacute Aacute -40 +KPX Uacute Abreve -40 +KPX Uacute Acircumflex -40 +KPX Uacute Adieresis -40 +KPX Uacute Agrave -40 +KPX Uacute Amacron -40 +KPX Uacute Aogonek -40 +KPX Uacute Aring -40 +KPX Uacute Atilde -40 +KPX Uacute comma -40 +KPX Uacute period -40 +KPX Ucircumflex A -40 +KPX Ucircumflex Aacute -40 +KPX Ucircumflex Abreve -40 +KPX Ucircumflex Acircumflex -40 +KPX Ucircumflex Adieresis -40 +KPX Ucircumflex Agrave -40 +KPX Ucircumflex Amacron -40 +KPX Ucircumflex Aogonek -40 +KPX Ucircumflex Aring -40 +KPX Ucircumflex Atilde -40 +KPX Ucircumflex comma -40 +KPX Ucircumflex period -40 +KPX Udieresis A -40 +KPX Udieresis Aacute -40 +KPX Udieresis Abreve -40 +KPX Udieresis Acircumflex -40 +KPX Udieresis Adieresis -40 +KPX Udieresis Agrave -40 +KPX Udieresis Amacron -40 +KPX Udieresis Aogonek -40 +KPX Udieresis Aring -40 +KPX Udieresis Atilde -40 +KPX Udieresis comma -40 +KPX Udieresis period -40 +KPX Ugrave A -40 +KPX Ugrave Aacute -40 +KPX Ugrave Abreve -40 +KPX Ugrave Acircumflex -40 +KPX Ugrave Adieresis -40 +KPX Ugrave Agrave -40 +KPX Ugrave Amacron -40 +KPX Ugrave Aogonek -40 +KPX Ugrave Aring -40 +KPX Ugrave Atilde -40 +KPX Ugrave comma -40 +KPX Ugrave period -40 +KPX Uhungarumlaut A -40 +KPX Uhungarumlaut Aacute -40 +KPX Uhungarumlaut Abreve -40 +KPX Uhungarumlaut Acircumflex -40 +KPX Uhungarumlaut Adieresis -40 +KPX Uhungarumlaut Agrave -40 +KPX Uhungarumlaut Amacron -40 +KPX Uhungarumlaut Aogonek -40 +KPX Uhungarumlaut Aring -40 +KPX Uhungarumlaut Atilde -40 +KPX Uhungarumlaut comma -40 +KPX Uhungarumlaut period -40 +KPX Umacron A -40 +KPX Umacron Aacute -40 +KPX Umacron Abreve -40 +KPX Umacron Acircumflex -40 +KPX Umacron Adieresis -40 +KPX Umacron Agrave -40 +KPX Umacron Amacron -40 +KPX Umacron Aogonek -40 +KPX Umacron Aring -40 +KPX Umacron Atilde -40 +KPX Umacron comma -40 +KPX Umacron period -40 +KPX Uogonek A -40 +KPX Uogonek Aacute -40 +KPX Uogonek Abreve -40 +KPX Uogonek Acircumflex -40 +KPX Uogonek Adieresis -40 +KPX Uogonek Agrave -40 +KPX Uogonek Amacron -40 +KPX Uogonek Aogonek -40 +KPX Uogonek Aring -40 +KPX Uogonek Atilde -40 +KPX Uogonek comma -40 +KPX Uogonek period -40 +KPX Uring A -40 +KPX Uring Aacute -40 +KPX Uring Abreve -40 +KPX Uring Acircumflex -40 +KPX Uring Adieresis -40 +KPX Uring Agrave -40 +KPX Uring Amacron -40 +KPX Uring Aogonek -40 +KPX Uring Aring -40 +KPX Uring Atilde -40 +KPX Uring comma -40 +KPX Uring period -40 +KPX V A -80 +KPX V Aacute -80 +KPX V Abreve -80 +KPX V Acircumflex -80 +KPX V Adieresis -80 +KPX V Agrave -80 +KPX V Amacron -80 +KPX V Aogonek -80 +KPX V Aring -80 +KPX V Atilde -80 +KPX V G -40 +KPX V Gbreve -40 +KPX V Gcommaaccent -40 +KPX V O -40 +KPX V Oacute -40 +KPX V Ocircumflex -40 +KPX V Odieresis -40 +KPX V Ograve -40 +KPX V Ohungarumlaut -40 +KPX V Omacron -40 +KPX V Oslash -40 +KPX V Otilde -40 +KPX V a -70 +KPX V aacute -70 +KPX V abreve -70 +KPX V acircumflex -70 +KPX V adieresis -70 +KPX V agrave -70 +KPX V amacron -70 +KPX V aogonek -70 +KPX V aring -70 +KPX V atilde -70 +KPX V colon -40 +KPX V comma -125 +KPX V e -80 +KPX V eacute -80 +KPX V ecaron -80 +KPX V ecircumflex -80 +KPX V edieresis -80 +KPX V edotaccent -80 +KPX V egrave -80 +KPX V emacron -80 +KPX V eogonek -80 +KPX V hyphen -80 +KPX V o -80 +KPX V oacute -80 +KPX V ocircumflex -80 +KPX V odieresis -80 +KPX V ograve -80 +KPX V ohungarumlaut -80 +KPX V omacron -80 +KPX V oslash -80 +KPX V otilde -80 +KPX V period -125 +KPX V semicolon -40 +KPX V u -70 +KPX V uacute -70 +KPX V ucircumflex -70 +KPX V udieresis -70 +KPX V ugrave -70 +KPX V uhungarumlaut -70 +KPX V umacron -70 +KPX V uogonek -70 +KPX V uring -70 +KPX W A -50 +KPX W Aacute -50 +KPX W Abreve -50 +KPX W Acircumflex -50 +KPX W Adieresis -50 +KPX W Agrave -50 +KPX W Amacron -50 +KPX W Aogonek -50 +KPX W Aring -50 +KPX W Atilde -50 +KPX W O -20 +KPX W Oacute -20 +KPX W Ocircumflex -20 +KPX W Odieresis -20 +KPX W Ograve -20 +KPX W Ohungarumlaut -20 +KPX W Omacron -20 +KPX W Oslash -20 +KPX W Otilde -20 +KPX W a -40 +KPX W aacute -40 +KPX W abreve -40 +KPX W acircumflex -40 +KPX W adieresis -40 +KPX W agrave -40 +KPX W amacron -40 +KPX W aogonek -40 +KPX W aring -40 +KPX W atilde -40 +KPX W comma -80 +KPX W e -30 +KPX W eacute -30 +KPX W ecaron -30 +KPX W ecircumflex -30 +KPX W edieresis -30 +KPX W edotaccent -30 +KPX W egrave -30 +KPX W emacron -30 +KPX W eogonek -30 +KPX W hyphen -40 +KPX W o -30 +KPX W oacute -30 +KPX W ocircumflex -30 +KPX W odieresis -30 +KPX W ograve -30 +KPX W ohungarumlaut -30 +KPX W omacron -30 +KPX W oslash -30 +KPX W otilde -30 +KPX W period -80 +KPX W u -30 +KPX W uacute -30 +KPX W ucircumflex -30 +KPX W udieresis -30 +KPX W ugrave -30 +KPX W uhungarumlaut -30 +KPX W umacron -30 +KPX W uogonek -30 +KPX W uring -30 +KPX W y -20 +KPX W yacute -20 +KPX W ydieresis -20 +KPX Y A -110 +KPX Y Aacute -110 +KPX Y Abreve -110 +KPX Y Acircumflex -110 +KPX Y Adieresis -110 +KPX Y Agrave -110 +KPX Y Amacron -110 +KPX Y Aogonek -110 +KPX Y Aring -110 +KPX Y Atilde -110 +KPX Y O -85 +KPX Y Oacute -85 +KPX Y Ocircumflex -85 +KPX Y Odieresis -85 +KPX Y Ograve -85 +KPX Y Ohungarumlaut -85 +KPX Y Omacron -85 +KPX Y Oslash -85 +KPX Y Otilde -85 +KPX Y a -140 +KPX Y aacute -140 +KPX Y abreve -70 +KPX Y acircumflex -140 +KPX Y adieresis -140 +KPX Y agrave -140 +KPX Y amacron -70 +KPX Y aogonek -140 +KPX Y aring -140 +KPX Y atilde -140 +KPX Y colon -60 +KPX Y comma -140 +KPX Y e -140 +KPX Y eacute -140 +KPX Y ecaron -140 +KPX Y ecircumflex -140 +KPX Y edieresis -140 +KPX Y edotaccent -140 +KPX Y egrave -140 +KPX Y emacron -70 +KPX Y eogonek -140 +KPX Y hyphen -140 +KPX Y i -20 +KPX Y iacute -20 +KPX Y iogonek -20 +KPX Y o -140 +KPX Y oacute -140 +KPX Y ocircumflex -140 +KPX Y odieresis -140 +KPX Y ograve -140 +KPX Y ohungarumlaut -140 +KPX Y omacron -140 +KPX Y oslash -140 +KPX Y otilde -140 +KPX Y period -140 +KPX Y semicolon -60 +KPX Y u -110 +KPX Y uacute -110 +KPX Y ucircumflex -110 +KPX Y udieresis -110 +KPX Y ugrave -110 +KPX Y uhungarumlaut -110 +KPX Y umacron -110 +KPX Y uogonek -110 +KPX Y uring -110 +KPX Yacute A -110 +KPX Yacute Aacute -110 +KPX Yacute Abreve -110 +KPX Yacute Acircumflex -110 +KPX Yacute Adieresis -110 +KPX Yacute Agrave -110 +KPX Yacute Amacron -110 +KPX Yacute Aogonek -110 +KPX Yacute Aring -110 +KPX Yacute Atilde -110 +KPX Yacute O -85 +KPX Yacute Oacute -85 +KPX Yacute Ocircumflex -85 +KPX Yacute Odieresis -85 +KPX Yacute Ograve -85 +KPX Yacute Ohungarumlaut -85 +KPX Yacute Omacron -85 +KPX Yacute Oslash -85 +KPX Yacute Otilde -85 +KPX Yacute a -140 +KPX Yacute aacute -140 +KPX Yacute abreve -70 +KPX Yacute acircumflex -140 +KPX Yacute adieresis -140 +KPX Yacute agrave -140 +KPX Yacute amacron -70 +KPX Yacute aogonek -140 +KPX Yacute aring -140 +KPX Yacute atilde -70 +KPX Yacute colon -60 +KPX Yacute comma -140 +KPX Yacute e -140 +KPX Yacute eacute -140 +KPX Yacute ecaron -140 +KPX Yacute ecircumflex -140 +KPX Yacute edieresis -140 +KPX Yacute edotaccent -140 +KPX Yacute egrave -140 +KPX Yacute emacron -70 +KPX Yacute eogonek -140 +KPX Yacute hyphen -140 +KPX Yacute i -20 +KPX Yacute iacute -20 +KPX Yacute iogonek -20 +KPX Yacute o -140 +KPX Yacute oacute -140 +KPX Yacute ocircumflex -140 +KPX Yacute odieresis -140 +KPX Yacute ograve -140 +KPX Yacute ohungarumlaut -140 +KPX Yacute omacron -70 +KPX Yacute oslash -140 +KPX Yacute otilde -140 +KPX Yacute period -140 +KPX Yacute semicolon -60 +KPX Yacute u -110 +KPX Yacute uacute -110 +KPX Yacute ucircumflex -110 +KPX Yacute udieresis -110 +KPX Yacute ugrave -110 +KPX Yacute uhungarumlaut -110 +KPX Yacute umacron -110 +KPX Yacute uogonek -110 +KPX Yacute uring -110 +KPX Ydieresis A -110 +KPX Ydieresis Aacute -110 +KPX Ydieresis Abreve -110 +KPX Ydieresis Acircumflex -110 +KPX Ydieresis Adieresis -110 +KPX Ydieresis Agrave -110 +KPX Ydieresis Amacron -110 +KPX Ydieresis Aogonek -110 +KPX Ydieresis Aring -110 +KPX Ydieresis Atilde -110 +KPX Ydieresis O -85 +KPX Ydieresis Oacute -85 +KPX Ydieresis Ocircumflex -85 +KPX Ydieresis Odieresis -85 +KPX Ydieresis Ograve -85 +KPX Ydieresis Ohungarumlaut -85 +KPX Ydieresis Omacron -85 +KPX Ydieresis Oslash -85 +KPX Ydieresis Otilde -85 +KPX Ydieresis a -140 +KPX Ydieresis aacute -140 +KPX Ydieresis abreve -70 +KPX Ydieresis acircumflex -140 +KPX Ydieresis adieresis -140 +KPX Ydieresis agrave -140 +KPX Ydieresis amacron -70 +KPX Ydieresis aogonek -140 +KPX Ydieresis aring -140 +KPX Ydieresis atilde -70 +KPX Ydieresis colon -60 +KPX Ydieresis comma -140 +KPX Ydieresis e -140 +KPX Ydieresis eacute -140 +KPX Ydieresis ecaron -140 +KPX Ydieresis ecircumflex -140 +KPX Ydieresis edieresis -140 +KPX Ydieresis edotaccent -140 +KPX Ydieresis egrave -140 +KPX Ydieresis emacron -70 +KPX Ydieresis eogonek -140 +KPX Ydieresis hyphen -140 +KPX Ydieresis i -20 +KPX Ydieresis iacute -20 +KPX Ydieresis iogonek -20 +KPX Ydieresis o -140 +KPX Ydieresis oacute -140 +KPX Ydieresis ocircumflex -140 +KPX Ydieresis odieresis -140 +KPX Ydieresis ograve -140 +KPX Ydieresis ohungarumlaut -140 +KPX Ydieresis omacron -140 +KPX Ydieresis oslash -140 +KPX Ydieresis otilde -140 +KPX Ydieresis period -140 +KPX Ydieresis semicolon -60 +KPX Ydieresis u -110 +KPX Ydieresis uacute -110 +KPX Ydieresis ucircumflex -110 +KPX Ydieresis udieresis -110 +KPX Ydieresis ugrave -110 +KPX Ydieresis uhungarumlaut -110 +KPX Ydieresis umacron -110 +KPX Ydieresis uogonek -110 +KPX Ydieresis uring -110 +KPX a v -20 +KPX a w -20 +KPX a y -30 +KPX a yacute -30 +KPX a ydieresis -30 +KPX aacute v -20 +KPX aacute w -20 +KPX aacute y -30 +KPX aacute yacute -30 +KPX aacute ydieresis -30 +KPX abreve v -20 +KPX abreve w -20 +KPX abreve y -30 +KPX abreve yacute -30 +KPX abreve ydieresis -30 +KPX acircumflex v -20 +KPX acircumflex w -20 +KPX acircumflex y -30 +KPX acircumflex yacute -30 +KPX acircumflex ydieresis -30 +KPX adieresis v -20 +KPX adieresis w -20 +KPX adieresis y -30 +KPX adieresis yacute -30 +KPX adieresis ydieresis -30 +KPX agrave v -20 +KPX agrave w -20 +KPX agrave y -30 +KPX agrave yacute -30 +KPX agrave ydieresis -30 +KPX amacron v -20 +KPX amacron w -20 +KPX amacron y -30 +KPX amacron yacute -30 +KPX amacron ydieresis -30 +KPX aogonek v -20 +KPX aogonek w -20 +KPX aogonek y -30 +KPX aogonek yacute -30 +KPX aogonek ydieresis -30 +KPX aring v -20 +KPX aring w -20 +KPX aring y -30 +KPX aring yacute -30 +KPX aring ydieresis -30 +KPX atilde v -20 +KPX atilde w -20 +KPX atilde y -30 +KPX atilde yacute -30 +KPX atilde ydieresis -30 +KPX b b -10 +KPX b comma -40 +KPX b l -20 +KPX b lacute -20 +KPX b lcommaaccent -20 +KPX b lslash -20 +KPX b period -40 +KPX b u -20 +KPX b uacute -20 +KPX b ucircumflex -20 +KPX b udieresis -20 +KPX b ugrave -20 +KPX b uhungarumlaut -20 +KPX b umacron -20 +KPX b uogonek -20 +KPX b uring -20 +KPX b v -20 +KPX b y -20 +KPX b yacute -20 +KPX b ydieresis -20 +KPX c comma -15 +KPX c k -20 +KPX c kcommaaccent -20 +KPX cacute comma -15 +KPX cacute k -20 +KPX cacute kcommaaccent -20 +KPX ccaron comma -15 +KPX ccaron k -20 +KPX ccaron kcommaaccent -20 +KPX ccedilla comma -15 +KPX ccedilla k -20 +KPX ccedilla kcommaaccent -20 +KPX colon space -50 +KPX comma quotedblright -100 +KPX comma quoteright -100 +KPX e comma -15 +KPX e period -15 +KPX e v -30 +KPX e w -20 +KPX e x -30 +KPX e y -20 +KPX e yacute -20 +KPX e ydieresis -20 +KPX eacute comma -15 +KPX eacute period -15 +KPX eacute v -30 +KPX eacute w -20 +KPX eacute x -30 +KPX eacute y -20 +KPX eacute yacute -20 +KPX eacute ydieresis -20 +KPX ecaron comma -15 +KPX ecaron period -15 +KPX ecaron v -30 +KPX ecaron w -20 +KPX ecaron x -30 +KPX ecaron y -20 +KPX ecaron yacute -20 +KPX ecaron ydieresis -20 +KPX ecircumflex comma -15 +KPX ecircumflex period -15 +KPX ecircumflex v -30 +KPX ecircumflex w -20 +KPX ecircumflex x -30 +KPX ecircumflex y -20 +KPX ecircumflex yacute -20 +KPX ecircumflex ydieresis -20 +KPX edieresis comma -15 +KPX edieresis period -15 +KPX edieresis v -30 +KPX edieresis w -20 +KPX edieresis x -30 +KPX edieresis y -20 +KPX edieresis yacute -20 +KPX edieresis ydieresis -20 +KPX edotaccent comma -15 +KPX edotaccent period -15 +KPX edotaccent v -30 +KPX edotaccent w -20 +KPX edotaccent x -30 +KPX edotaccent y -20 +KPX edotaccent yacute -20 +KPX edotaccent ydieresis -20 +KPX egrave comma -15 +KPX egrave period -15 +KPX egrave v -30 +KPX egrave w -20 +KPX egrave x -30 +KPX egrave y -20 +KPX egrave yacute -20 +KPX egrave ydieresis -20 +KPX emacron comma -15 +KPX emacron period -15 +KPX emacron v -30 +KPX emacron w -20 +KPX emacron x -30 +KPX emacron y -20 +KPX emacron yacute -20 +KPX emacron ydieresis -20 +KPX eogonek comma -15 +KPX eogonek period -15 +KPX eogonek v -30 +KPX eogonek w -20 +KPX eogonek x -30 +KPX eogonek y -20 +KPX eogonek yacute -20 +KPX eogonek ydieresis -20 +KPX f a -30 +KPX f aacute -30 +KPX f abreve -30 +KPX f acircumflex -30 +KPX f adieresis -30 +KPX f agrave -30 +KPX f amacron -30 +KPX f aogonek -30 +KPX f aring -30 +KPX f atilde -30 +KPX f comma -30 +KPX f dotlessi -28 +KPX f e -30 +KPX f eacute -30 +KPX f ecaron -30 +KPX f ecircumflex -30 +KPX f edieresis -30 +KPX f edotaccent -30 +KPX f egrave -30 +KPX f emacron -30 +KPX f eogonek -30 +KPX f o -30 +KPX f oacute -30 +KPX f ocircumflex -30 +KPX f odieresis -30 +KPX f ograve -30 +KPX f ohungarumlaut -30 +KPX f omacron -30 +KPX f oslash -30 +KPX f otilde -30 +KPX f period -30 +KPX f quotedblright 60 +KPX f quoteright 50 +KPX g r -10 +KPX g racute -10 +KPX g rcaron -10 +KPX g rcommaaccent -10 +KPX gbreve r -10 +KPX gbreve racute -10 +KPX gbreve rcaron -10 +KPX gbreve rcommaaccent -10 +KPX gcommaaccent r -10 +KPX gcommaaccent racute -10 +KPX gcommaaccent rcaron -10 +KPX gcommaaccent rcommaaccent -10 +KPX h y -30 +KPX h yacute -30 +KPX h ydieresis -30 +KPX k e -20 +KPX k eacute -20 +KPX k ecaron -20 +KPX k ecircumflex -20 +KPX k edieresis -20 +KPX k edotaccent -20 +KPX k egrave -20 +KPX k emacron -20 +KPX k eogonek -20 +KPX k o -20 +KPX k oacute -20 +KPX k ocircumflex -20 +KPX k odieresis -20 +KPX k ograve -20 +KPX k ohungarumlaut -20 +KPX k omacron -20 +KPX k oslash -20 +KPX k otilde -20 +KPX kcommaaccent e -20 +KPX kcommaaccent eacute -20 +KPX kcommaaccent ecaron -20 +KPX kcommaaccent ecircumflex -20 +KPX kcommaaccent edieresis -20 +KPX kcommaaccent edotaccent -20 +KPX kcommaaccent egrave -20 +KPX kcommaaccent emacron -20 +KPX kcommaaccent eogonek -20 +KPX kcommaaccent o -20 +KPX kcommaaccent oacute -20 +KPX kcommaaccent ocircumflex -20 +KPX kcommaaccent odieresis -20 +KPX kcommaaccent ograve -20 +KPX kcommaaccent ohungarumlaut -20 +KPX kcommaaccent omacron -20 +KPX kcommaaccent oslash -20 +KPX kcommaaccent otilde -20 +KPX m u -10 +KPX m uacute -10 +KPX m ucircumflex -10 +KPX m udieresis -10 +KPX m ugrave -10 +KPX m uhungarumlaut -10 +KPX m umacron -10 +KPX m uogonek -10 +KPX m uring -10 +KPX m y -15 +KPX m yacute -15 +KPX m ydieresis -15 +KPX n u -10 +KPX n uacute -10 +KPX n ucircumflex -10 +KPX n udieresis -10 +KPX n ugrave -10 +KPX n uhungarumlaut -10 +KPX n umacron -10 +KPX n uogonek -10 +KPX n uring -10 +KPX n v -20 +KPX n y -15 +KPX n yacute -15 +KPX n ydieresis -15 +KPX nacute u -10 +KPX nacute uacute -10 +KPX nacute ucircumflex -10 +KPX nacute udieresis -10 +KPX nacute ugrave -10 +KPX nacute uhungarumlaut -10 +KPX nacute umacron -10 +KPX nacute uogonek -10 +KPX nacute uring -10 +KPX nacute v -20 +KPX nacute y -15 +KPX nacute yacute -15 +KPX nacute ydieresis -15 +KPX ncaron u -10 +KPX ncaron uacute -10 +KPX ncaron ucircumflex -10 +KPX ncaron udieresis -10 +KPX ncaron ugrave -10 +KPX ncaron uhungarumlaut -10 +KPX ncaron umacron -10 +KPX ncaron uogonek -10 +KPX ncaron uring -10 +KPX ncaron v -20 +KPX ncaron y -15 +KPX ncaron yacute -15 +KPX ncaron ydieresis -15 +KPX ncommaaccent u -10 +KPX ncommaaccent uacute -10 +KPX ncommaaccent ucircumflex -10 +KPX ncommaaccent udieresis -10 +KPX ncommaaccent ugrave -10 +KPX ncommaaccent uhungarumlaut -10 +KPX ncommaaccent umacron -10 +KPX ncommaaccent uogonek -10 +KPX ncommaaccent uring -10 +KPX ncommaaccent v -20 +KPX ncommaaccent y -15 +KPX ncommaaccent yacute -15 +KPX ncommaaccent ydieresis -15 +KPX ntilde u -10 +KPX ntilde uacute -10 +KPX ntilde ucircumflex -10 +KPX ntilde udieresis -10 +KPX ntilde ugrave -10 +KPX ntilde uhungarumlaut -10 +KPX ntilde umacron -10 +KPX ntilde uogonek -10 +KPX ntilde uring -10 +KPX ntilde v -20 +KPX ntilde y -15 +KPX ntilde yacute -15 +KPX ntilde ydieresis -15 +KPX o comma -40 +KPX o period -40 +KPX o v -15 +KPX o w -15 +KPX o x -30 +KPX o y -30 +KPX o yacute -30 +KPX o ydieresis -30 +KPX oacute comma -40 +KPX oacute period -40 +KPX oacute v -15 +KPX oacute w -15 +KPX oacute x -30 +KPX oacute y -30 +KPX oacute yacute -30 +KPX oacute ydieresis -30 +KPX ocircumflex comma -40 +KPX ocircumflex period -40 +KPX ocircumflex v -15 +KPX ocircumflex w -15 +KPX ocircumflex x -30 +KPX ocircumflex y -30 +KPX ocircumflex yacute -30 +KPX ocircumflex ydieresis -30 +KPX odieresis comma -40 +KPX odieresis period -40 +KPX odieresis v -15 +KPX odieresis w -15 +KPX odieresis x -30 +KPX odieresis y -30 +KPX odieresis yacute -30 +KPX odieresis ydieresis -30 +KPX ograve comma -40 +KPX ograve period -40 +KPX ograve v -15 +KPX ograve w -15 +KPX ograve x -30 +KPX ograve y -30 +KPX ograve yacute -30 +KPX ograve ydieresis -30 +KPX ohungarumlaut comma -40 +KPX ohungarumlaut period -40 +KPX ohungarumlaut v -15 +KPX ohungarumlaut w -15 +KPX ohungarumlaut x -30 +KPX ohungarumlaut y -30 +KPX ohungarumlaut yacute -30 +KPX ohungarumlaut ydieresis -30 +KPX omacron comma -40 +KPX omacron period -40 +KPX omacron v -15 +KPX omacron w -15 +KPX omacron x -30 +KPX omacron y -30 +KPX omacron yacute -30 +KPX omacron ydieresis -30 +KPX oslash a -55 +KPX oslash aacute -55 +KPX oslash abreve -55 +KPX oslash acircumflex -55 +KPX oslash adieresis -55 +KPX oslash agrave -55 +KPX oslash amacron -55 +KPX oslash aogonek -55 +KPX oslash aring -55 +KPX oslash atilde -55 +KPX oslash b -55 +KPX oslash c -55 +KPX oslash cacute -55 +KPX oslash ccaron -55 +KPX oslash ccedilla -55 +KPX oslash comma -95 +KPX oslash d -55 +KPX oslash dcroat -55 +KPX oslash e -55 +KPX oslash eacute -55 +KPX oslash ecaron -55 +KPX oslash ecircumflex -55 +KPX oslash edieresis -55 +KPX oslash edotaccent -55 +KPX oslash egrave -55 +KPX oslash emacron -55 +KPX oslash eogonek -55 +KPX oslash f -55 +KPX oslash g -55 +KPX oslash gbreve -55 +KPX oslash gcommaaccent -55 +KPX oslash h -55 +KPX oslash i -55 +KPX oslash iacute -55 +KPX oslash icircumflex -55 +KPX oslash idieresis -55 +KPX oslash igrave -55 +KPX oslash imacron -55 +KPX oslash iogonek -55 +KPX oslash j -55 +KPX oslash k -55 +KPX oslash kcommaaccent -55 +KPX oslash l -55 +KPX oslash lacute -55 +KPX oslash lcommaaccent -55 +KPX oslash lslash -55 +KPX oslash m -55 +KPX oslash n -55 +KPX oslash nacute -55 +KPX oslash ncaron -55 +KPX oslash ncommaaccent -55 +KPX oslash ntilde -55 +KPX oslash o -55 +KPX oslash oacute -55 +KPX oslash ocircumflex -55 +KPX oslash odieresis -55 +KPX oslash ograve -55 +KPX oslash ohungarumlaut -55 +KPX oslash omacron -55 +KPX oslash oslash -55 +KPX oslash otilde -55 +KPX oslash p -55 +KPX oslash period -95 +KPX oslash q -55 +KPX oslash r -55 +KPX oslash racute -55 +KPX oslash rcaron -55 +KPX oslash rcommaaccent -55 +KPX oslash s -55 +KPX oslash sacute -55 +KPX oslash scaron -55 +KPX oslash scedilla -55 +KPX oslash scommaaccent -55 +KPX oslash t -55 +KPX oslash tcommaaccent -55 +KPX oslash u -55 +KPX oslash uacute -55 +KPX oslash ucircumflex -55 +KPX oslash udieresis -55 +KPX oslash ugrave -55 +KPX oslash uhungarumlaut -55 +KPX oslash umacron -55 +KPX oslash uogonek -55 +KPX oslash uring -55 +KPX oslash v -70 +KPX oslash w -70 +KPX oslash x -85 +KPX oslash y -70 +KPX oslash yacute -70 +KPX oslash ydieresis -70 +KPX oslash z -55 +KPX oslash zacute -55 +KPX oslash zcaron -55 +KPX oslash zdotaccent -55 +KPX otilde comma -40 +KPX otilde period -40 +KPX otilde v -15 +KPX otilde w -15 +KPX otilde x -30 +KPX otilde y -30 +KPX otilde yacute -30 +KPX otilde ydieresis -30 +KPX p comma -35 +KPX p period -35 +KPX p y -30 +KPX p yacute -30 +KPX p ydieresis -30 +KPX period quotedblright -100 +KPX period quoteright -100 +KPX period space -60 +KPX quotedblright space -40 +KPX quoteleft quoteleft -57 +KPX quoteright d -50 +KPX quoteright dcroat -50 +KPX quoteright quoteright -57 +KPX quoteright r -50 +KPX quoteright racute -50 +KPX quoteright rcaron -50 +KPX quoteright rcommaaccent -50 +KPX quoteright s -50 +KPX quoteright sacute -50 +KPX quoteright scaron -50 +KPX quoteright scedilla -50 +KPX quoteright scommaaccent -50 +KPX quoteright space -70 +KPX r a -10 +KPX r aacute -10 +KPX r abreve -10 +KPX r acircumflex -10 +KPX r adieresis -10 +KPX r agrave -10 +KPX r amacron -10 +KPX r aogonek -10 +KPX r aring -10 +KPX r atilde -10 +KPX r colon 30 +KPX r comma -50 +KPX r i 15 +KPX r iacute 15 +KPX r icircumflex 15 +KPX r idieresis 15 +KPX r igrave 15 +KPX r imacron 15 +KPX r iogonek 15 +KPX r k 15 +KPX r kcommaaccent 15 +KPX r l 15 +KPX r lacute 15 +KPX r lcommaaccent 15 +KPX r lslash 15 +KPX r m 25 +KPX r n 25 +KPX r nacute 25 +KPX r ncaron 25 +KPX r ncommaaccent 25 +KPX r ntilde 25 +KPX r p 30 +KPX r period -50 +KPX r semicolon 30 +KPX r t 40 +KPX r tcommaaccent 40 +KPX r u 15 +KPX r uacute 15 +KPX r ucircumflex 15 +KPX r udieresis 15 +KPX r ugrave 15 +KPX r uhungarumlaut 15 +KPX r umacron 15 +KPX r uogonek 15 +KPX r uring 15 +KPX r v 30 +KPX r y 30 +KPX r yacute 30 +KPX r ydieresis 30 +KPX racute a -10 +KPX racute aacute -10 +KPX racute abreve -10 +KPX racute acircumflex -10 +KPX racute adieresis -10 +KPX racute agrave -10 +KPX racute amacron -10 +KPX racute aogonek -10 +KPX racute aring -10 +KPX racute atilde -10 +KPX racute colon 30 +KPX racute comma -50 +KPX racute i 15 +KPX racute iacute 15 +KPX racute icircumflex 15 +KPX racute idieresis 15 +KPX racute igrave 15 +KPX racute imacron 15 +KPX racute iogonek 15 +KPX racute k 15 +KPX racute kcommaaccent 15 +KPX racute l 15 +KPX racute lacute 15 +KPX racute lcommaaccent 15 +KPX racute lslash 15 +KPX racute m 25 +KPX racute n 25 +KPX racute nacute 25 +KPX racute ncaron 25 +KPX racute ncommaaccent 25 +KPX racute ntilde 25 +KPX racute p 30 +KPX racute period -50 +KPX racute semicolon 30 +KPX racute t 40 +KPX racute tcommaaccent 40 +KPX racute u 15 +KPX racute uacute 15 +KPX racute ucircumflex 15 +KPX racute udieresis 15 +KPX racute ugrave 15 +KPX racute uhungarumlaut 15 +KPX racute umacron 15 +KPX racute uogonek 15 +KPX racute uring 15 +KPX racute v 30 +KPX racute y 30 +KPX racute yacute 30 +KPX racute ydieresis 30 +KPX rcaron a -10 +KPX rcaron aacute -10 +KPX rcaron abreve -10 +KPX rcaron acircumflex -10 +KPX rcaron adieresis -10 +KPX rcaron agrave -10 +KPX rcaron amacron -10 +KPX rcaron aogonek -10 +KPX rcaron aring -10 +KPX rcaron atilde -10 +KPX rcaron colon 30 +KPX rcaron comma -50 +KPX rcaron i 15 +KPX rcaron iacute 15 +KPX rcaron icircumflex 15 +KPX rcaron idieresis 15 +KPX rcaron igrave 15 +KPX rcaron imacron 15 +KPX rcaron iogonek 15 +KPX rcaron k 15 +KPX rcaron kcommaaccent 15 +KPX rcaron l 15 +KPX rcaron lacute 15 +KPX rcaron lcommaaccent 15 +KPX rcaron lslash 15 +KPX rcaron m 25 +KPX rcaron n 25 +KPX rcaron nacute 25 +KPX rcaron ncaron 25 +KPX rcaron ncommaaccent 25 +KPX rcaron ntilde 25 +KPX rcaron p 30 +KPX rcaron period -50 +KPX rcaron semicolon 30 +KPX rcaron t 40 +KPX rcaron tcommaaccent 40 +KPX rcaron u 15 +KPX rcaron uacute 15 +KPX rcaron ucircumflex 15 +KPX rcaron udieresis 15 +KPX rcaron ugrave 15 +KPX rcaron uhungarumlaut 15 +KPX rcaron umacron 15 +KPX rcaron uogonek 15 +KPX rcaron uring 15 +KPX rcaron v 30 +KPX rcaron y 30 +KPX rcaron yacute 30 +KPX rcaron ydieresis 30 +KPX rcommaaccent a -10 +KPX rcommaaccent aacute -10 +KPX rcommaaccent abreve -10 +KPX rcommaaccent acircumflex -10 +KPX rcommaaccent adieresis -10 +KPX rcommaaccent agrave -10 +KPX rcommaaccent amacron -10 +KPX rcommaaccent aogonek -10 +KPX rcommaaccent aring -10 +KPX rcommaaccent atilde -10 +KPX rcommaaccent colon 30 +KPX rcommaaccent comma -50 +KPX rcommaaccent i 15 +KPX rcommaaccent iacute 15 +KPX rcommaaccent icircumflex 15 +KPX rcommaaccent idieresis 15 +KPX rcommaaccent igrave 15 +KPX rcommaaccent imacron 15 +KPX rcommaaccent iogonek 15 +KPX rcommaaccent k 15 +KPX rcommaaccent kcommaaccent 15 +KPX rcommaaccent l 15 +KPX rcommaaccent lacute 15 +KPX rcommaaccent lcommaaccent 15 +KPX rcommaaccent lslash 15 +KPX rcommaaccent m 25 +KPX rcommaaccent n 25 +KPX rcommaaccent nacute 25 +KPX rcommaaccent ncaron 25 +KPX rcommaaccent ncommaaccent 25 +KPX rcommaaccent ntilde 25 +KPX rcommaaccent p 30 +KPX rcommaaccent period -50 +KPX rcommaaccent semicolon 30 +KPX rcommaaccent t 40 +KPX rcommaaccent tcommaaccent 40 +KPX rcommaaccent u 15 +KPX rcommaaccent uacute 15 +KPX rcommaaccent ucircumflex 15 +KPX rcommaaccent udieresis 15 +KPX rcommaaccent ugrave 15 +KPX rcommaaccent uhungarumlaut 15 +KPX rcommaaccent umacron 15 +KPX rcommaaccent uogonek 15 +KPX rcommaaccent uring 15 +KPX rcommaaccent v 30 +KPX rcommaaccent y 30 +KPX rcommaaccent yacute 30 +KPX rcommaaccent ydieresis 30 +KPX s comma -15 +KPX s period -15 +KPX s w -30 +KPX sacute comma -15 +KPX sacute period -15 +KPX sacute w -30 +KPX scaron comma -15 +KPX scaron period -15 +KPX scaron w -30 +KPX scedilla comma -15 +KPX scedilla period -15 +KPX scedilla w -30 +KPX scommaaccent comma -15 +KPX scommaaccent period -15 +KPX scommaaccent w -30 +KPX semicolon space -50 +KPX space T -50 +KPX space Tcaron -50 +KPX space Tcommaaccent -50 +KPX space V -50 +KPX space W -40 +KPX space Y -90 +KPX space Yacute -90 +KPX space Ydieresis -90 +KPX space quotedblleft -30 +KPX space quoteleft -60 +KPX v a -25 +KPX v aacute -25 +KPX v abreve -25 +KPX v acircumflex -25 +KPX v adieresis -25 +KPX v agrave -25 +KPX v amacron -25 +KPX v aogonek -25 +KPX v aring -25 +KPX v atilde -25 +KPX v comma -80 +KPX v e -25 +KPX v eacute -25 +KPX v ecaron -25 +KPX v ecircumflex -25 +KPX v edieresis -25 +KPX v edotaccent -25 +KPX v egrave -25 +KPX v emacron -25 +KPX v eogonek -25 +KPX v o -25 +KPX v oacute -25 +KPX v ocircumflex -25 +KPX v odieresis -25 +KPX v ograve -25 +KPX v ohungarumlaut -25 +KPX v omacron -25 +KPX v oslash -25 +KPX v otilde -25 +KPX v period -80 +KPX w a -15 +KPX w aacute -15 +KPX w abreve -15 +KPX w acircumflex -15 +KPX w adieresis -15 +KPX w agrave -15 +KPX w amacron -15 +KPX w aogonek -15 +KPX w aring -15 +KPX w atilde -15 +KPX w comma -60 +KPX w e -10 +KPX w eacute -10 +KPX w ecaron -10 +KPX w ecircumflex -10 +KPX w edieresis -10 +KPX w edotaccent -10 +KPX w egrave -10 +KPX w emacron -10 +KPX w eogonek -10 +KPX w o -10 +KPX w oacute -10 +KPX w ocircumflex -10 +KPX w odieresis -10 +KPX w ograve -10 +KPX w ohungarumlaut -10 +KPX w omacron -10 +KPX w oslash -10 +KPX w otilde -10 +KPX w period -60 +KPX x e -30 +KPX x eacute -30 +KPX x ecaron -30 +KPX x ecircumflex -30 +KPX x edieresis -30 +KPX x edotaccent -30 +KPX x egrave -30 +KPX x emacron -30 +KPX x eogonek -30 +KPX y a -20 +KPX y aacute -20 +KPX y abreve -20 +KPX y acircumflex -20 +KPX y adieresis -20 +KPX y agrave -20 +KPX y amacron -20 +KPX y aogonek -20 +KPX y aring -20 +KPX y atilde -20 +KPX y comma -100 +KPX y e -20 +KPX y eacute -20 +KPX y ecaron -20 +KPX y ecircumflex -20 +KPX y edieresis -20 +KPX y edotaccent -20 +KPX y egrave -20 +KPX y emacron -20 +KPX y eogonek -20 +KPX y o -20 +KPX y oacute -20 +KPX y ocircumflex -20 +KPX y odieresis -20 +KPX y ograve -20 +KPX y ohungarumlaut -20 +KPX y omacron -20 +KPX y oslash -20 +KPX y otilde -20 +KPX y period -100 +KPX yacute a -20 +KPX yacute aacute -20 +KPX yacute abreve -20 +KPX yacute acircumflex -20 +KPX yacute adieresis -20 +KPX yacute agrave -20 +KPX yacute amacron -20 +KPX yacute aogonek -20 +KPX yacute aring -20 +KPX yacute atilde -20 +KPX yacute comma -100 +KPX yacute e -20 +KPX yacute eacute -20 +KPX yacute ecaron -20 +KPX yacute ecircumflex -20 +KPX yacute edieresis -20 +KPX yacute edotaccent -20 +KPX yacute egrave -20 +KPX yacute emacron -20 +KPX yacute eogonek -20 +KPX yacute o -20 +KPX yacute oacute -20 +KPX yacute ocircumflex -20 +KPX yacute odieresis -20 +KPX yacute ograve -20 +KPX yacute ohungarumlaut -20 +KPX yacute omacron -20 +KPX yacute oslash -20 +KPX yacute otilde -20 +KPX yacute period -100 +KPX ydieresis a -20 +KPX ydieresis aacute -20 +KPX ydieresis abreve -20 +KPX ydieresis acircumflex -20 +KPX ydieresis adieresis -20 +KPX ydieresis agrave -20 +KPX ydieresis amacron -20 +KPX ydieresis aogonek -20 +KPX ydieresis aring -20 +KPX ydieresis atilde -20 +KPX ydieresis comma -100 +KPX ydieresis e -20 +KPX ydieresis eacute -20 +KPX ydieresis ecaron -20 +KPX ydieresis ecircumflex -20 +KPX ydieresis edieresis -20 +KPX ydieresis edotaccent -20 +KPX ydieresis egrave -20 +KPX ydieresis emacron -20 +KPX ydieresis eogonek -20 +KPX ydieresis o -20 +KPX ydieresis oacute -20 +KPX ydieresis ocircumflex -20 +KPX ydieresis odieresis -20 +KPX ydieresis ograve -20 +KPX ydieresis ohungarumlaut -20 +KPX ydieresis omacron -20 +KPX ydieresis oslash -20 +KPX ydieresis otilde -20 +KPX ydieresis period -100 +KPX z e -15 +KPX z eacute -15 +KPX z ecaron -15 +KPX z ecircumflex -15 +KPX z edieresis -15 +KPX z edotaccent -15 +KPX z egrave -15 +KPX z emacron -15 +KPX z eogonek -15 +KPX z o -15 +KPX z oacute -15 +KPX z ocircumflex -15 +KPX z odieresis -15 +KPX z ograve -15 +KPX z ohungarumlaut -15 +KPX z omacron -15 +KPX z oslash -15 +KPX z otilde -15 +KPX zacute e -15 +KPX zacute eacute -15 +KPX zacute ecaron -15 +KPX zacute ecircumflex -15 +KPX zacute edieresis -15 +KPX zacute edotaccent -15 +KPX zacute egrave -15 +KPX zacute emacron -15 +KPX zacute eogonek -15 +KPX zacute o -15 +KPX zacute oacute -15 +KPX zacute ocircumflex -15 +KPX zacute odieresis -15 +KPX zacute ograve -15 +KPX zacute ohungarumlaut -15 +KPX zacute omacron -15 +KPX zacute oslash -15 +KPX zacute otilde -15 +KPX zcaron e -15 +KPX zcaron eacute -15 +KPX zcaron ecaron -15 +KPX zcaron ecircumflex -15 +KPX zcaron edieresis -15 +KPX zcaron edotaccent -15 +KPX zcaron egrave -15 +KPX zcaron emacron -15 +KPX zcaron eogonek -15 +KPX zcaron o -15 +KPX zcaron oacute -15 +KPX zcaron ocircumflex -15 +KPX zcaron odieresis -15 +KPX zcaron ograve -15 +KPX zcaron ohungarumlaut -15 +KPX zcaron omacron -15 +KPX zcaron oslash -15 +KPX zcaron otilde -15 +KPX zdotaccent e -15 +KPX zdotaccent eacute -15 +KPX zdotaccent ecaron -15 +KPX zdotaccent ecircumflex -15 +KPX zdotaccent edieresis -15 +KPX zdotaccent edotaccent -15 +KPX zdotaccent egrave -15 +KPX zdotaccent emacron -15 +KPX zdotaccent eogonek -15 +KPX zdotaccent o -15 +KPX zdotaccent oacute -15 +KPX zdotaccent ocircumflex -15 +KPX zdotaccent odieresis -15 +KPX zdotaccent ograve -15 +KPX zdotaccent ohungarumlaut -15 +KPX zdotaccent omacron -15 +KPX zdotaccent oslash -15 +KPX zdotaccent otilde -15 +EndKernPairs +EndKernData +EndFontMetrics diff --git a/internal/corefont/Core14_AFMs/Helvetica.afm b/internal/corefont/Core14_AFMs/Helvetica.afm new file mode 100644 index 0000000000000000000000000000000000000000..bd32af54dec5563db4033ed2c8bb4a26f1eaeecf --- /dev/null +++ b/internal/corefont/Core14_AFMs/Helvetica.afm @@ -0,0 +1,3051 @@ +StartFontMetrics 4.1 +Comment Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems Incorporated. All Rights Reserved. +Comment Creation Date: Thu May 1 12:38:23 1997 +Comment UniqueID 43054 +Comment VMusage 37069 48094 +FontName Helvetica +FullName Helvetica +FamilyName Helvetica +Weight Medium +ItalicAngle 0 +IsFixedPitch false +CharacterSet ExtendedRoman +FontBBox -166 -225 1000 931 +UnderlinePosition -100 +UnderlineThickness 50 +Version 002.000 +Notice Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems Incorporated. All Rights Reserved.Helvetica is a trademark of Linotype-Hell AG and/or its subsidiaries. +EncodingScheme AdobeStandardEncoding +CapHeight 718 +XHeight 523 +Ascender 718 +Descender -207 +StdHW 76 +StdVW 88 +StartCharMetrics 315 +C 32 ; WX 278 ; N space ; B 0 0 0 0 ; +C 33 ; WX 278 ; N exclam ; B 90 0 187 718 ; +C 34 ; WX 355 ; N quotedbl ; B 70 463 285 718 ; +C 35 ; WX 556 ; N numbersign ; B 28 0 529 688 ; +C 36 ; WX 556 ; N dollar ; B 32 -115 520 775 ; +C 37 ; WX 889 ; N percent ; B 39 -19 850 703 ; +C 38 ; WX 667 ; N ampersand ; B 44 -15 645 718 ; +C 39 ; WX 222 ; N quoteright ; B 53 463 157 718 ; +C 40 ; WX 333 ; N parenleft ; B 68 -207 299 733 ; +C 41 ; WX 333 ; N parenright ; B 34 -207 265 733 ; +C 42 ; WX 389 ; N asterisk ; B 39 431 349 718 ; +C 43 ; WX 584 ; N plus ; B 39 0 545 505 ; +C 44 ; WX 278 ; N comma ; B 87 -147 191 106 ; +C 45 ; WX 333 ; N hyphen ; B 44 232 289 322 ; +C 46 ; WX 278 ; N period ; B 87 0 191 106 ; +C 47 ; WX 278 ; N slash ; B -17 -19 295 737 ; +C 48 ; WX 556 ; N zero ; B 37 -19 519 703 ; +C 49 ; WX 556 ; N one ; B 101 0 359 703 ; +C 50 ; WX 556 ; N two ; B 26 0 507 703 ; +C 51 ; WX 556 ; N three ; B 34 -19 522 703 ; +C 52 ; WX 556 ; N four ; B 25 0 523 703 ; +C 53 ; WX 556 ; N five ; B 32 -19 514 688 ; +C 54 ; WX 556 ; N six ; B 38 -19 518 703 ; +C 55 ; WX 556 ; N seven ; B 37 0 523 688 ; +C 56 ; WX 556 ; N eight ; B 38 -19 517 703 ; +C 57 ; WX 556 ; N nine ; B 42 -19 514 703 ; +C 58 ; WX 278 ; N colon ; B 87 0 191 516 ; +C 59 ; WX 278 ; N semicolon ; B 87 -147 191 516 ; +C 60 ; WX 584 ; N less ; B 48 11 536 495 ; +C 61 ; WX 584 ; N equal ; B 39 115 545 390 ; +C 62 ; WX 584 ; N greater ; B 48 11 536 495 ; +C 63 ; WX 556 ; N question ; B 56 0 492 727 ; +C 64 ; WX 1015 ; N at ; B 147 -19 868 737 ; +C 65 ; WX 667 ; N A ; B 14 0 654 718 ; +C 66 ; WX 667 ; N B ; B 74 0 627 718 ; +C 67 ; WX 722 ; N C ; B 44 -19 681 737 ; +C 68 ; WX 722 ; N D ; B 81 0 674 718 ; +C 69 ; WX 667 ; N E ; B 86 0 616 718 ; +C 70 ; WX 611 ; N F ; B 86 0 583 718 ; +C 71 ; WX 778 ; N G ; B 48 -19 704 737 ; +C 72 ; WX 722 ; N H ; B 77 0 646 718 ; +C 73 ; WX 278 ; N I ; B 91 0 188 718 ; +C 74 ; WX 500 ; N J ; B 17 -19 428 718 ; +C 75 ; WX 667 ; N K ; B 76 0 663 718 ; +C 76 ; WX 556 ; N L ; B 76 0 537 718 ; +C 77 ; WX 833 ; N M ; B 73 0 761 718 ; +C 78 ; WX 722 ; N N ; B 76 0 646 718 ; +C 79 ; WX 778 ; N O ; B 39 -19 739 737 ; +C 80 ; WX 667 ; N P ; B 86 0 622 718 ; +C 81 ; WX 778 ; N Q ; B 39 -56 739 737 ; +C 82 ; WX 722 ; N R ; B 88 0 684 718 ; +C 83 ; WX 667 ; N S ; B 49 -19 620 737 ; +C 84 ; WX 611 ; N T ; B 14 0 597 718 ; +C 85 ; WX 722 ; N U ; B 79 -19 644 718 ; +C 86 ; WX 667 ; N V ; B 20 0 647 718 ; +C 87 ; WX 944 ; N W ; B 16 0 928 718 ; +C 88 ; WX 667 ; N X ; B 19 0 648 718 ; +C 89 ; WX 667 ; N Y ; B 14 0 653 718 ; +C 90 ; WX 611 ; N Z ; B 23 0 588 718 ; +C 91 ; WX 278 ; N bracketleft ; B 63 -196 250 722 ; +C 92 ; WX 278 ; N backslash ; B -17 -19 295 737 ; +C 93 ; WX 278 ; N bracketright ; B 28 -196 215 722 ; +C 94 ; WX 469 ; N asciicircum ; B -14 264 483 688 ; +C 95 ; WX 556 ; N underscore ; B 0 -125 556 -75 ; +C 96 ; WX 222 ; N quoteleft ; B 65 470 169 725 ; +C 97 ; WX 556 ; N a ; B 36 -15 530 538 ; +C 98 ; WX 556 ; N b ; B 58 -15 517 718 ; +C 99 ; WX 500 ; N c ; B 30 -15 477 538 ; +C 100 ; WX 556 ; N d ; B 35 -15 499 718 ; +C 101 ; WX 556 ; N e ; B 40 -15 516 538 ; +C 102 ; WX 278 ; N f ; B 14 0 262 728 ; L i fi ; L l fl ; +C 103 ; WX 556 ; N g ; B 40 -220 499 538 ; +C 104 ; WX 556 ; N h ; B 65 0 491 718 ; +C 105 ; WX 222 ; N i ; B 67 0 155 718 ; +C 106 ; WX 222 ; N j ; B -16 -210 155 718 ; +C 107 ; WX 500 ; N k ; B 67 0 501 718 ; +C 108 ; WX 222 ; N l ; B 67 0 155 718 ; +C 109 ; WX 833 ; N m ; B 65 0 769 538 ; +C 110 ; WX 556 ; N n ; B 65 0 491 538 ; +C 111 ; WX 556 ; N o ; B 35 -14 521 538 ; +C 112 ; WX 556 ; N p ; B 58 -207 517 538 ; +C 113 ; WX 556 ; N q ; B 35 -207 494 538 ; +C 114 ; WX 333 ; N r ; B 77 0 332 538 ; +C 115 ; WX 500 ; N s ; B 32 -15 464 538 ; +C 116 ; WX 278 ; N t ; B 14 -7 257 669 ; +C 117 ; WX 556 ; N u ; B 68 -15 489 523 ; +C 118 ; WX 500 ; N v ; B 8 0 492 523 ; +C 119 ; WX 722 ; N w ; B 14 0 709 523 ; +C 120 ; WX 500 ; N x ; B 11 0 490 523 ; +C 121 ; WX 500 ; N y ; B 11 -214 489 523 ; +C 122 ; WX 500 ; N z ; B 31 0 469 523 ; +C 123 ; WX 334 ; N braceleft ; B 42 -196 292 722 ; +C 124 ; WX 260 ; N bar ; B 94 -225 167 775 ; +C 125 ; WX 334 ; N braceright ; B 42 -196 292 722 ; +C 126 ; WX 584 ; N asciitilde ; B 61 180 523 326 ; +C 161 ; WX 333 ; N exclamdown ; B 118 -195 215 523 ; +C 162 ; WX 556 ; N cent ; B 51 -115 513 623 ; +C 163 ; WX 556 ; N sterling ; B 33 -16 539 718 ; +C 164 ; WX 167 ; N fraction ; B -166 -19 333 703 ; +C 165 ; WX 556 ; N yen ; B 3 0 553 688 ; +C 166 ; WX 556 ; N florin ; B -11 -207 501 737 ; +C 167 ; WX 556 ; N section ; B 43 -191 512 737 ; +C 168 ; WX 556 ; N currency ; B 28 99 528 603 ; +C 169 ; WX 191 ; N quotesingle ; B 59 463 132 718 ; +C 170 ; WX 333 ; N quotedblleft ; B 38 470 307 725 ; +C 171 ; WX 556 ; N guillemotleft ; B 97 108 459 446 ; +C 172 ; WX 333 ; N guilsinglleft ; B 88 108 245 446 ; +C 173 ; WX 333 ; N guilsinglright ; B 88 108 245 446 ; +C 174 ; WX 500 ; N fi ; B 14 0 434 728 ; +C 175 ; WX 500 ; N fl ; B 14 0 432 728 ; +C 177 ; WX 556 ; N endash ; B 0 240 556 313 ; +C 178 ; WX 556 ; N dagger ; B 43 -159 514 718 ; +C 179 ; WX 556 ; N daggerdbl ; B 43 -159 514 718 ; +C 180 ; WX 278 ; N periodcentered ; B 77 190 202 315 ; +C 182 ; WX 537 ; N paragraph ; B 18 -173 497 718 ; +C 183 ; WX 350 ; N bullet ; B 18 202 333 517 ; +C 184 ; WX 222 ; N quotesinglbase ; B 53 -149 157 106 ; +C 185 ; WX 333 ; N quotedblbase ; B 26 -149 295 106 ; +C 186 ; WX 333 ; N quotedblright ; B 26 463 295 718 ; +C 187 ; WX 556 ; N guillemotright ; B 97 108 459 446 ; +C 188 ; WX 1000 ; N ellipsis ; B 115 0 885 106 ; +C 189 ; WX 1000 ; N perthousand ; B 7 -19 994 703 ; +C 191 ; WX 611 ; N questiondown ; B 91 -201 527 525 ; +C 193 ; WX 333 ; N grave ; B 14 593 211 734 ; +C 194 ; WX 333 ; N acute ; B 122 593 319 734 ; +C 195 ; WX 333 ; N circumflex ; B 21 593 312 734 ; +C 196 ; WX 333 ; N tilde ; B -4 606 337 722 ; +C 197 ; WX 333 ; N macron ; B 10 627 323 684 ; +C 198 ; WX 333 ; N breve ; B 13 595 321 731 ; +C 199 ; WX 333 ; N dotaccent ; B 121 604 212 706 ; +C 200 ; WX 333 ; N dieresis ; B 40 604 293 706 ; +C 202 ; WX 333 ; N ring ; B 75 572 259 756 ; +C 203 ; WX 333 ; N cedilla ; B 45 -225 259 0 ; +C 205 ; WX 333 ; N hungarumlaut ; B 31 593 409 734 ; +C 206 ; WX 333 ; N ogonek ; B 73 -225 287 0 ; +C 207 ; WX 333 ; N caron ; B 21 593 312 734 ; +C 208 ; WX 1000 ; N emdash ; B 0 240 1000 313 ; +C 225 ; WX 1000 ; N AE ; B 8 0 951 718 ; +C 227 ; WX 370 ; N ordfeminine ; B 24 405 346 737 ; +C 232 ; WX 556 ; N Lslash ; B -20 0 537 718 ; +C 233 ; WX 778 ; N Oslash ; B 39 -19 740 737 ; +C 234 ; WX 1000 ; N OE ; B 36 -19 965 737 ; +C 235 ; WX 365 ; N ordmasculine ; B 25 405 341 737 ; +C 241 ; WX 889 ; N ae ; B 36 -15 847 538 ; +C 245 ; WX 278 ; N dotlessi ; B 95 0 183 523 ; +C 248 ; WX 222 ; N lslash ; B -20 0 242 718 ; +C 249 ; WX 611 ; N oslash ; B 28 -22 537 545 ; +C 250 ; WX 944 ; N oe ; B 35 -15 902 538 ; +C 251 ; WX 611 ; N germandbls ; B 67 -15 571 728 ; +C -1 ; WX 278 ; N Idieresis ; B 13 0 266 901 ; +C -1 ; WX 556 ; N eacute ; B 40 -15 516 734 ; +C -1 ; WX 556 ; N abreve ; B 36 -15 530 731 ; +C -1 ; WX 556 ; N uhungarumlaut ; B 68 -15 521 734 ; +C -1 ; WX 556 ; N ecaron ; B 40 -15 516 734 ; +C -1 ; WX 667 ; N Ydieresis ; B 14 0 653 901 ; +C -1 ; WX 584 ; N divide ; B 39 -19 545 524 ; +C -1 ; WX 667 ; N Yacute ; B 14 0 653 929 ; +C -1 ; WX 667 ; N Acircumflex ; B 14 0 654 929 ; +C -1 ; WX 556 ; N aacute ; B 36 -15 530 734 ; +C -1 ; WX 722 ; N Ucircumflex ; B 79 -19 644 929 ; +C -1 ; WX 500 ; N yacute ; B 11 -214 489 734 ; +C -1 ; WX 500 ; N scommaaccent ; B 32 -225 464 538 ; +C -1 ; WX 556 ; N ecircumflex ; B 40 -15 516 734 ; +C -1 ; WX 722 ; N Uring ; B 79 -19 644 931 ; +C -1 ; WX 722 ; N Udieresis ; B 79 -19 644 901 ; +C -1 ; WX 556 ; N aogonek ; B 36 -220 547 538 ; +C -1 ; WX 722 ; N Uacute ; B 79 -19 644 929 ; +C -1 ; WX 556 ; N uogonek ; B 68 -225 519 523 ; +C -1 ; WX 667 ; N Edieresis ; B 86 0 616 901 ; +C -1 ; WX 722 ; N Dcroat ; B 0 0 674 718 ; +C -1 ; WX 250 ; N commaaccent ; B 87 -225 181 -40 ; +C -1 ; WX 737 ; N copyright ; B -14 -19 752 737 ; +C -1 ; WX 667 ; N Emacron ; B 86 0 616 879 ; +C -1 ; WX 500 ; N ccaron ; B 30 -15 477 734 ; +C -1 ; WX 556 ; N aring ; B 36 -15 530 756 ; +C -1 ; WX 722 ; N Ncommaaccent ; B 76 -225 646 718 ; +C -1 ; WX 222 ; N lacute ; B 67 0 264 929 ; +C -1 ; WX 556 ; N agrave ; B 36 -15 530 734 ; +C -1 ; WX 611 ; N Tcommaaccent ; B 14 -225 597 718 ; +C -1 ; WX 722 ; N Cacute ; B 44 -19 681 929 ; +C -1 ; WX 556 ; N atilde ; B 36 -15 530 722 ; +C -1 ; WX 667 ; N Edotaccent ; B 86 0 616 901 ; +C -1 ; WX 500 ; N scaron ; B 32 -15 464 734 ; +C -1 ; WX 500 ; N scedilla ; B 32 -225 464 538 ; +C -1 ; WX 278 ; N iacute ; B 95 0 292 734 ; +C -1 ; WX 471 ; N lozenge ; B 10 0 462 728 ; +C -1 ; WX 722 ; N Rcaron ; B 88 0 684 929 ; +C -1 ; WX 778 ; N Gcommaaccent ; B 48 -225 704 737 ; +C -1 ; WX 556 ; N ucircumflex ; B 68 -15 489 734 ; +C -1 ; WX 556 ; N acircumflex ; B 36 -15 530 734 ; +C -1 ; WX 667 ; N Amacron ; B 14 0 654 879 ; +C -1 ; WX 333 ; N rcaron ; B 61 0 352 734 ; +C -1 ; WX 500 ; N ccedilla ; B 30 -225 477 538 ; +C -1 ; WX 611 ; N Zdotaccent ; B 23 0 588 901 ; +C -1 ; WX 667 ; N Thorn ; B 86 0 622 718 ; +C -1 ; WX 778 ; N Omacron ; B 39 -19 739 879 ; +C -1 ; WX 722 ; N Racute ; B 88 0 684 929 ; +C -1 ; WX 667 ; N Sacute ; B 49 -19 620 929 ; +C -1 ; WX 643 ; N dcaron ; B 35 -15 655 718 ; +C -1 ; WX 722 ; N Umacron ; B 79 -19 644 879 ; +C -1 ; WX 556 ; N uring ; B 68 -15 489 756 ; +C -1 ; WX 333 ; N threesuperior ; B 5 270 325 703 ; +C -1 ; WX 778 ; N Ograve ; B 39 -19 739 929 ; +C -1 ; WX 667 ; N Agrave ; B 14 0 654 929 ; +C -1 ; WX 667 ; N Abreve ; B 14 0 654 926 ; +C -1 ; WX 584 ; N multiply ; B 39 0 545 506 ; +C -1 ; WX 556 ; N uacute ; B 68 -15 489 734 ; +C -1 ; WX 611 ; N Tcaron ; B 14 0 597 929 ; +C -1 ; WX 476 ; N partialdiff ; B 13 -38 463 714 ; +C -1 ; WX 500 ; N ydieresis ; B 11 -214 489 706 ; +C -1 ; WX 722 ; N Nacute ; B 76 0 646 929 ; +C -1 ; WX 278 ; N icircumflex ; B -6 0 285 734 ; +C -1 ; WX 667 ; N Ecircumflex ; B 86 0 616 929 ; +C -1 ; WX 556 ; N adieresis ; B 36 -15 530 706 ; +C -1 ; WX 556 ; N edieresis ; B 40 -15 516 706 ; +C -1 ; WX 500 ; N cacute ; B 30 -15 477 734 ; +C -1 ; WX 556 ; N nacute ; B 65 0 491 734 ; +C -1 ; WX 556 ; N umacron ; B 68 -15 489 684 ; +C -1 ; WX 722 ; N Ncaron ; B 76 0 646 929 ; +C -1 ; WX 278 ; N Iacute ; B 91 0 292 929 ; +C -1 ; WX 584 ; N plusminus ; B 39 0 545 506 ; +C -1 ; WX 260 ; N brokenbar ; B 94 -150 167 700 ; +C -1 ; WX 737 ; N registered ; B -14 -19 752 737 ; +C -1 ; WX 778 ; N Gbreve ; B 48 -19 704 926 ; +C -1 ; WX 278 ; N Idotaccent ; B 91 0 188 901 ; +C -1 ; WX 600 ; N summation ; B 15 -10 586 706 ; +C -1 ; WX 667 ; N Egrave ; B 86 0 616 929 ; +C -1 ; WX 333 ; N racute ; B 77 0 332 734 ; +C -1 ; WX 556 ; N omacron ; B 35 -14 521 684 ; +C -1 ; WX 611 ; N Zacute ; B 23 0 588 929 ; +C -1 ; WX 611 ; N Zcaron ; B 23 0 588 929 ; +C -1 ; WX 549 ; N greaterequal ; B 26 0 523 674 ; +C -1 ; WX 722 ; N Eth ; B 0 0 674 718 ; +C -1 ; WX 722 ; N Ccedilla ; B 44 -225 681 737 ; +C -1 ; WX 222 ; N lcommaaccent ; B 67 -225 167 718 ; +C -1 ; WX 317 ; N tcaron ; B 14 -7 329 808 ; +C -1 ; WX 556 ; N eogonek ; B 40 -225 516 538 ; +C -1 ; WX 722 ; N Uogonek ; B 79 -225 644 718 ; +C -1 ; WX 667 ; N Aacute ; B 14 0 654 929 ; +C -1 ; WX 667 ; N Adieresis ; B 14 0 654 901 ; +C -1 ; WX 556 ; N egrave ; B 40 -15 516 734 ; +C -1 ; WX 500 ; N zacute ; B 31 0 469 734 ; +C -1 ; WX 222 ; N iogonek ; B -31 -225 183 718 ; +C -1 ; WX 778 ; N Oacute ; B 39 -19 739 929 ; +C -1 ; WX 556 ; N oacute ; B 35 -14 521 734 ; +C -1 ; WX 556 ; N amacron ; B 36 -15 530 684 ; +C -1 ; WX 500 ; N sacute ; B 32 -15 464 734 ; +C -1 ; WX 278 ; N idieresis ; B 13 0 266 706 ; +C -1 ; WX 778 ; N Ocircumflex ; B 39 -19 739 929 ; +C -1 ; WX 722 ; N Ugrave ; B 79 -19 644 929 ; +C -1 ; WX 612 ; N Delta ; B 6 0 608 688 ; +C -1 ; WX 556 ; N thorn ; B 58 -207 517 718 ; +C -1 ; WX 333 ; N twosuperior ; B 4 281 323 703 ; +C -1 ; WX 778 ; N Odieresis ; B 39 -19 739 901 ; +C -1 ; WX 556 ; N mu ; B 68 -207 489 523 ; +C -1 ; WX 278 ; N igrave ; B -13 0 184 734 ; +C -1 ; WX 556 ; N ohungarumlaut ; B 35 -14 521 734 ; +C -1 ; WX 667 ; N Eogonek ; B 86 -220 633 718 ; +C -1 ; WX 556 ; N dcroat ; B 35 -15 550 718 ; +C -1 ; WX 834 ; N threequarters ; B 45 -19 810 703 ; +C -1 ; WX 667 ; N Scedilla ; B 49 -225 620 737 ; +C -1 ; WX 299 ; N lcaron ; B 67 0 311 718 ; +C -1 ; WX 667 ; N Kcommaaccent ; B 76 -225 663 718 ; +C -1 ; WX 556 ; N Lacute ; B 76 0 537 929 ; +C -1 ; WX 1000 ; N trademark ; B 46 306 903 718 ; +C -1 ; WX 556 ; N edotaccent ; B 40 -15 516 706 ; +C -1 ; WX 278 ; N Igrave ; B -13 0 188 929 ; +C -1 ; WX 278 ; N Imacron ; B -17 0 296 879 ; +C -1 ; WX 556 ; N Lcaron ; B 76 0 537 718 ; +C -1 ; WX 834 ; N onehalf ; B 43 -19 773 703 ; +C -1 ; WX 549 ; N lessequal ; B 26 0 523 674 ; +C -1 ; WX 556 ; N ocircumflex ; B 35 -14 521 734 ; +C -1 ; WX 556 ; N ntilde ; B 65 0 491 722 ; +C -1 ; WX 722 ; N Uhungarumlaut ; B 79 -19 644 929 ; +C -1 ; WX 667 ; N Eacute ; B 86 0 616 929 ; +C -1 ; WX 556 ; N emacron ; B 40 -15 516 684 ; +C -1 ; WX 556 ; N gbreve ; B 40 -220 499 731 ; +C -1 ; WX 834 ; N onequarter ; B 73 -19 756 703 ; +C -1 ; WX 667 ; N Scaron ; B 49 -19 620 929 ; +C -1 ; WX 667 ; N Scommaaccent ; B 49 -225 620 737 ; +C -1 ; WX 778 ; N Ohungarumlaut ; B 39 -19 739 929 ; +C -1 ; WX 400 ; N degree ; B 54 411 346 703 ; +C -1 ; WX 556 ; N ograve ; B 35 -14 521 734 ; +C -1 ; WX 722 ; N Ccaron ; B 44 -19 681 929 ; +C -1 ; WX 556 ; N ugrave ; B 68 -15 489 734 ; +C -1 ; WX 453 ; N radical ; B -4 -80 458 762 ; +C -1 ; WX 722 ; N Dcaron ; B 81 0 674 929 ; +C -1 ; WX 333 ; N rcommaaccent ; B 77 -225 332 538 ; +C -1 ; WX 722 ; N Ntilde ; B 76 0 646 917 ; +C -1 ; WX 556 ; N otilde ; B 35 -14 521 722 ; +C -1 ; WX 722 ; N Rcommaaccent ; B 88 -225 684 718 ; +C -1 ; WX 556 ; N Lcommaaccent ; B 76 -225 537 718 ; +C -1 ; WX 667 ; N Atilde ; B 14 0 654 917 ; +C -1 ; WX 667 ; N Aogonek ; B 14 -225 654 718 ; +C -1 ; WX 667 ; N Aring ; B 14 0 654 931 ; +C -1 ; WX 778 ; N Otilde ; B 39 -19 739 917 ; +C -1 ; WX 500 ; N zdotaccent ; B 31 0 469 706 ; +C -1 ; WX 667 ; N Ecaron ; B 86 0 616 929 ; +C -1 ; WX 278 ; N Iogonek ; B -3 -225 211 718 ; +C -1 ; WX 500 ; N kcommaaccent ; B 67 -225 501 718 ; +C -1 ; WX 584 ; N minus ; B 39 216 545 289 ; +C -1 ; WX 278 ; N Icircumflex ; B -6 0 285 929 ; +C -1 ; WX 556 ; N ncaron ; B 65 0 491 734 ; +C -1 ; WX 278 ; N tcommaaccent ; B 14 -225 257 669 ; +C -1 ; WX 584 ; N logicalnot ; B 39 108 545 390 ; +C -1 ; WX 556 ; N odieresis ; B 35 -14 521 706 ; +C -1 ; WX 556 ; N udieresis ; B 68 -15 489 706 ; +C -1 ; WX 549 ; N notequal ; B 12 -35 537 551 ; +C -1 ; WX 556 ; N gcommaaccent ; B 40 -220 499 822 ; +C -1 ; WX 556 ; N eth ; B 35 -15 522 737 ; +C -1 ; WX 500 ; N zcaron ; B 31 0 469 734 ; +C -1 ; WX 556 ; N ncommaaccent ; B 65 -225 491 538 ; +C -1 ; WX 333 ; N onesuperior ; B 43 281 222 703 ; +C -1 ; WX 278 ; N imacron ; B 5 0 272 684 ; +C -1 ; WX 556 ; N Euro ; B 0 0 0 0 ; +EndCharMetrics +StartKernData +StartKernPairs 2705 +KPX A C -30 +KPX A Cacute -30 +KPX A Ccaron -30 +KPX A Ccedilla -30 +KPX A G -30 +KPX A Gbreve -30 +KPX A Gcommaaccent -30 +KPX A O -30 +KPX A Oacute -30 +KPX A Ocircumflex -30 +KPX A Odieresis -30 +KPX A Ograve -30 +KPX A Ohungarumlaut -30 +KPX A Omacron -30 +KPX A Oslash -30 +KPX A Otilde -30 +KPX A Q -30 +KPX A T -120 +KPX A Tcaron -120 +KPX A Tcommaaccent -120 +KPX A U -50 +KPX A Uacute -50 +KPX A Ucircumflex -50 +KPX A Udieresis -50 +KPX A Ugrave -50 +KPX A Uhungarumlaut -50 +KPX A Umacron -50 +KPX A Uogonek -50 +KPX A Uring -50 +KPX A V -70 +KPX A W -50 +KPX A Y -100 +KPX A Yacute -100 +KPX A Ydieresis -100 +KPX A u -30 +KPX A uacute -30 +KPX A ucircumflex -30 +KPX A udieresis -30 +KPX A ugrave -30 +KPX A uhungarumlaut -30 +KPX A umacron -30 +KPX A uogonek -30 +KPX A uring -30 +KPX A v -40 +KPX A w -40 +KPX A y -40 +KPX A yacute -40 +KPX A ydieresis -40 +KPX Aacute C -30 +KPX Aacute Cacute -30 +KPX Aacute Ccaron -30 +KPX Aacute Ccedilla -30 +KPX Aacute G -30 +KPX Aacute Gbreve -30 +KPX Aacute Gcommaaccent -30 +KPX Aacute O -30 +KPX Aacute Oacute -30 +KPX Aacute Ocircumflex -30 +KPX Aacute Odieresis -30 +KPX Aacute Ograve -30 +KPX Aacute Ohungarumlaut -30 +KPX Aacute Omacron -30 +KPX Aacute Oslash -30 +KPX Aacute Otilde -30 +KPX Aacute Q -30 +KPX Aacute T -120 +KPX Aacute Tcaron -120 +KPX Aacute Tcommaaccent -120 +KPX Aacute U -50 +KPX Aacute Uacute -50 +KPX Aacute Ucircumflex -50 +KPX Aacute Udieresis -50 +KPX Aacute Ugrave -50 +KPX Aacute Uhungarumlaut -50 +KPX Aacute Umacron -50 +KPX Aacute Uogonek -50 +KPX Aacute Uring -50 +KPX Aacute V -70 +KPX Aacute W -50 +KPX Aacute Y -100 +KPX Aacute Yacute -100 +KPX Aacute Ydieresis -100 +KPX Aacute u -30 +KPX Aacute uacute -30 +KPX Aacute ucircumflex -30 +KPX Aacute udieresis -30 +KPX Aacute ugrave -30 +KPX Aacute uhungarumlaut -30 +KPX Aacute umacron -30 +KPX Aacute uogonek -30 +KPX Aacute uring -30 +KPX Aacute v -40 +KPX Aacute w -40 +KPX Aacute y -40 +KPX Aacute yacute -40 +KPX Aacute ydieresis -40 +KPX Abreve C -30 +KPX Abreve Cacute -30 +KPX Abreve Ccaron -30 +KPX Abreve Ccedilla -30 +KPX Abreve G -30 +KPX Abreve Gbreve -30 +KPX Abreve Gcommaaccent -30 +KPX Abreve O -30 +KPX Abreve Oacute -30 +KPX Abreve Ocircumflex -30 +KPX Abreve Odieresis -30 +KPX Abreve Ograve -30 +KPX Abreve Ohungarumlaut -30 +KPX Abreve Omacron -30 +KPX Abreve Oslash -30 +KPX Abreve Otilde -30 +KPX Abreve Q -30 +KPX Abreve T -120 +KPX Abreve Tcaron -120 +KPX Abreve Tcommaaccent -120 +KPX Abreve U -50 +KPX Abreve Uacute -50 +KPX Abreve Ucircumflex -50 +KPX Abreve Udieresis -50 +KPX Abreve Ugrave -50 +KPX Abreve Uhungarumlaut -50 +KPX Abreve Umacron -50 +KPX Abreve Uogonek -50 +KPX Abreve Uring -50 +KPX Abreve V -70 +KPX Abreve W -50 +KPX Abreve Y -100 +KPX Abreve Yacute -100 +KPX Abreve Ydieresis -100 +KPX Abreve u -30 +KPX Abreve uacute -30 +KPX Abreve ucircumflex -30 +KPX Abreve udieresis -30 +KPX Abreve ugrave -30 +KPX Abreve uhungarumlaut -30 +KPX Abreve umacron -30 +KPX Abreve uogonek -30 +KPX Abreve uring -30 +KPX Abreve v -40 +KPX Abreve w -40 +KPX Abreve y -40 +KPX Abreve yacute -40 +KPX Abreve ydieresis -40 +KPX Acircumflex C -30 +KPX Acircumflex Cacute -30 +KPX Acircumflex Ccaron -30 +KPX Acircumflex Ccedilla -30 +KPX Acircumflex G -30 +KPX Acircumflex Gbreve -30 +KPX Acircumflex Gcommaaccent -30 +KPX Acircumflex O -30 +KPX Acircumflex Oacute -30 +KPX Acircumflex Ocircumflex -30 +KPX Acircumflex Odieresis -30 +KPX Acircumflex Ograve -30 +KPX Acircumflex Ohungarumlaut -30 +KPX Acircumflex Omacron -30 +KPX Acircumflex Oslash -30 +KPX Acircumflex Otilde -30 +KPX Acircumflex Q -30 +KPX Acircumflex T -120 +KPX Acircumflex Tcaron -120 +KPX Acircumflex Tcommaaccent -120 +KPX Acircumflex U -50 +KPX Acircumflex Uacute -50 +KPX Acircumflex Ucircumflex -50 +KPX Acircumflex Udieresis -50 +KPX Acircumflex Ugrave -50 +KPX Acircumflex Uhungarumlaut -50 +KPX Acircumflex Umacron -50 +KPX Acircumflex Uogonek -50 +KPX Acircumflex Uring -50 +KPX Acircumflex V -70 +KPX Acircumflex W -50 +KPX Acircumflex Y -100 +KPX Acircumflex Yacute -100 +KPX Acircumflex Ydieresis -100 +KPX Acircumflex u -30 +KPX Acircumflex uacute -30 +KPX Acircumflex ucircumflex -30 +KPX Acircumflex udieresis -30 +KPX Acircumflex ugrave -30 +KPX Acircumflex uhungarumlaut -30 +KPX Acircumflex umacron -30 +KPX Acircumflex uogonek -30 +KPX Acircumflex uring -30 +KPX Acircumflex v -40 +KPX Acircumflex w -40 +KPX Acircumflex y -40 +KPX Acircumflex yacute -40 +KPX Acircumflex ydieresis -40 +KPX Adieresis C -30 +KPX Adieresis Cacute -30 +KPX Adieresis Ccaron -30 +KPX Adieresis Ccedilla -30 +KPX Adieresis G -30 +KPX Adieresis Gbreve -30 +KPX Adieresis Gcommaaccent -30 +KPX Adieresis O -30 +KPX Adieresis Oacute -30 +KPX Adieresis Ocircumflex -30 +KPX Adieresis Odieresis -30 +KPX Adieresis Ograve -30 +KPX Adieresis Ohungarumlaut -30 +KPX Adieresis Omacron -30 +KPX Adieresis Oslash -30 +KPX Adieresis Otilde -30 +KPX Adieresis Q -30 +KPX Adieresis T -120 +KPX Adieresis Tcaron -120 +KPX Adieresis Tcommaaccent -120 +KPX Adieresis U -50 +KPX Adieresis Uacute -50 +KPX Adieresis Ucircumflex -50 +KPX Adieresis Udieresis -50 +KPX Adieresis Ugrave -50 +KPX Adieresis Uhungarumlaut -50 +KPX Adieresis Umacron -50 +KPX Adieresis Uogonek -50 +KPX Adieresis Uring -50 +KPX Adieresis V -70 +KPX Adieresis W -50 +KPX Adieresis Y -100 +KPX Adieresis Yacute -100 +KPX Adieresis Ydieresis -100 +KPX Adieresis u -30 +KPX Adieresis uacute -30 +KPX Adieresis ucircumflex -30 +KPX Adieresis udieresis -30 +KPX Adieresis ugrave -30 +KPX Adieresis uhungarumlaut -30 +KPX Adieresis umacron -30 +KPX Adieresis uogonek -30 +KPX Adieresis uring -30 +KPX Adieresis v -40 +KPX Adieresis w -40 +KPX Adieresis y -40 +KPX Adieresis yacute -40 +KPX Adieresis ydieresis -40 +KPX Agrave C -30 +KPX Agrave Cacute -30 +KPX Agrave Ccaron -30 +KPX Agrave Ccedilla -30 +KPX Agrave G -30 +KPX Agrave Gbreve -30 +KPX Agrave Gcommaaccent -30 +KPX Agrave O -30 +KPX Agrave Oacute -30 +KPX Agrave Ocircumflex -30 +KPX Agrave Odieresis -30 +KPX Agrave Ograve -30 +KPX Agrave Ohungarumlaut -30 +KPX Agrave Omacron -30 +KPX Agrave Oslash -30 +KPX Agrave Otilde -30 +KPX Agrave Q -30 +KPX Agrave T -120 +KPX Agrave Tcaron -120 +KPX Agrave Tcommaaccent -120 +KPX Agrave U -50 +KPX Agrave Uacute -50 +KPX Agrave Ucircumflex -50 +KPX Agrave Udieresis -50 +KPX Agrave Ugrave -50 +KPX Agrave Uhungarumlaut -50 +KPX Agrave Umacron -50 +KPX Agrave Uogonek -50 +KPX Agrave Uring -50 +KPX Agrave V -70 +KPX Agrave W -50 +KPX Agrave Y -100 +KPX Agrave Yacute -100 +KPX Agrave Ydieresis -100 +KPX Agrave u -30 +KPX Agrave uacute -30 +KPX Agrave ucircumflex -30 +KPX Agrave udieresis -30 +KPX Agrave ugrave -30 +KPX Agrave uhungarumlaut -30 +KPX Agrave umacron -30 +KPX Agrave uogonek -30 +KPX Agrave uring -30 +KPX Agrave v -40 +KPX Agrave w -40 +KPX Agrave y -40 +KPX Agrave yacute -40 +KPX Agrave ydieresis -40 +KPX Amacron C -30 +KPX Amacron Cacute -30 +KPX Amacron Ccaron -30 +KPX Amacron Ccedilla -30 +KPX Amacron G -30 +KPX Amacron Gbreve -30 +KPX Amacron Gcommaaccent -30 +KPX Amacron O -30 +KPX Amacron Oacute -30 +KPX Amacron Ocircumflex -30 +KPX Amacron Odieresis -30 +KPX Amacron Ograve -30 +KPX Amacron Ohungarumlaut -30 +KPX Amacron Omacron -30 +KPX Amacron Oslash -30 +KPX Amacron Otilde -30 +KPX Amacron Q -30 +KPX Amacron T -120 +KPX Amacron Tcaron -120 +KPX Amacron Tcommaaccent -120 +KPX Amacron U -50 +KPX Amacron Uacute -50 +KPX Amacron Ucircumflex -50 +KPX Amacron Udieresis -50 +KPX Amacron Ugrave -50 +KPX Amacron Uhungarumlaut -50 +KPX Amacron Umacron -50 +KPX Amacron Uogonek -50 +KPX Amacron Uring -50 +KPX Amacron V -70 +KPX Amacron W -50 +KPX Amacron Y -100 +KPX Amacron Yacute -100 +KPX Amacron Ydieresis -100 +KPX Amacron u -30 +KPX Amacron uacute -30 +KPX Amacron ucircumflex -30 +KPX Amacron udieresis -30 +KPX Amacron ugrave -30 +KPX Amacron uhungarumlaut -30 +KPX Amacron umacron -30 +KPX Amacron uogonek -30 +KPX Amacron uring -30 +KPX Amacron v -40 +KPX Amacron w -40 +KPX Amacron y -40 +KPX Amacron yacute -40 +KPX Amacron ydieresis -40 +KPX Aogonek C -30 +KPX Aogonek Cacute -30 +KPX Aogonek Ccaron -30 +KPX Aogonek Ccedilla -30 +KPX Aogonek G -30 +KPX Aogonek Gbreve -30 +KPX Aogonek Gcommaaccent -30 +KPX Aogonek O -30 +KPX Aogonek Oacute -30 +KPX Aogonek Ocircumflex -30 +KPX Aogonek Odieresis -30 +KPX Aogonek Ograve -30 +KPX Aogonek Ohungarumlaut -30 +KPX Aogonek Omacron -30 +KPX Aogonek Oslash -30 +KPX Aogonek Otilde -30 +KPX Aogonek Q -30 +KPX Aogonek T -120 +KPX Aogonek Tcaron -120 +KPX Aogonek Tcommaaccent -120 +KPX Aogonek U -50 +KPX Aogonek Uacute -50 +KPX Aogonek Ucircumflex -50 +KPX Aogonek Udieresis -50 +KPX Aogonek Ugrave -50 +KPX Aogonek Uhungarumlaut -50 +KPX Aogonek Umacron -50 +KPX Aogonek Uogonek -50 +KPX Aogonek Uring -50 +KPX Aogonek V -70 +KPX Aogonek W -50 +KPX Aogonek Y -100 +KPX Aogonek Yacute -100 +KPX Aogonek Ydieresis -100 +KPX Aogonek u -30 +KPX Aogonek uacute -30 +KPX Aogonek ucircumflex -30 +KPX Aogonek udieresis -30 +KPX Aogonek ugrave -30 +KPX Aogonek uhungarumlaut -30 +KPX Aogonek umacron -30 +KPX Aogonek uogonek -30 +KPX Aogonek uring -30 +KPX Aogonek v -40 +KPX Aogonek w -40 +KPX Aogonek y -40 +KPX Aogonek yacute -40 +KPX Aogonek ydieresis -40 +KPX Aring C -30 +KPX Aring Cacute -30 +KPX Aring Ccaron -30 +KPX Aring Ccedilla -30 +KPX Aring G -30 +KPX Aring Gbreve -30 +KPX Aring Gcommaaccent -30 +KPX Aring O -30 +KPX Aring Oacute -30 +KPX Aring Ocircumflex -30 +KPX Aring Odieresis -30 +KPX Aring Ograve -30 +KPX Aring Ohungarumlaut -30 +KPX Aring Omacron -30 +KPX Aring Oslash -30 +KPX Aring Otilde -30 +KPX Aring Q -30 +KPX Aring T -120 +KPX Aring Tcaron -120 +KPX Aring Tcommaaccent -120 +KPX Aring U -50 +KPX Aring Uacute -50 +KPX Aring Ucircumflex -50 +KPX Aring Udieresis -50 +KPX Aring Ugrave -50 +KPX Aring Uhungarumlaut -50 +KPX Aring Umacron -50 +KPX Aring Uogonek -50 +KPX Aring Uring -50 +KPX Aring V -70 +KPX Aring W -50 +KPX Aring Y -100 +KPX Aring Yacute -100 +KPX Aring Ydieresis -100 +KPX Aring u -30 +KPX Aring uacute -30 +KPX Aring ucircumflex -30 +KPX Aring udieresis -30 +KPX Aring ugrave -30 +KPX Aring uhungarumlaut -30 +KPX Aring umacron -30 +KPX Aring uogonek -30 +KPX Aring uring -30 +KPX Aring v -40 +KPX Aring w -40 +KPX Aring y -40 +KPX Aring yacute -40 +KPX Aring ydieresis -40 +KPX Atilde C -30 +KPX Atilde Cacute -30 +KPX Atilde Ccaron -30 +KPX Atilde Ccedilla -30 +KPX Atilde G -30 +KPX Atilde Gbreve -30 +KPX Atilde Gcommaaccent -30 +KPX Atilde O -30 +KPX Atilde Oacute -30 +KPX Atilde Ocircumflex -30 +KPX Atilde Odieresis -30 +KPX Atilde Ograve -30 +KPX Atilde Ohungarumlaut -30 +KPX Atilde Omacron -30 +KPX Atilde Oslash -30 +KPX Atilde Otilde -30 +KPX Atilde Q -30 +KPX Atilde T -120 +KPX Atilde Tcaron -120 +KPX Atilde Tcommaaccent -120 +KPX Atilde U -50 +KPX Atilde Uacute -50 +KPX Atilde Ucircumflex -50 +KPX Atilde Udieresis -50 +KPX Atilde Ugrave -50 +KPX Atilde Uhungarumlaut -50 +KPX Atilde Umacron -50 +KPX Atilde Uogonek -50 +KPX Atilde Uring -50 +KPX Atilde V -70 +KPX Atilde W -50 +KPX Atilde Y -100 +KPX Atilde Yacute -100 +KPX Atilde Ydieresis -100 +KPX Atilde u -30 +KPX Atilde uacute -30 +KPX Atilde ucircumflex -30 +KPX Atilde udieresis -30 +KPX Atilde ugrave -30 +KPX Atilde uhungarumlaut -30 +KPX Atilde umacron -30 +KPX Atilde uogonek -30 +KPX Atilde uring -30 +KPX Atilde v -40 +KPX Atilde w -40 +KPX Atilde y -40 +KPX Atilde yacute -40 +KPX Atilde ydieresis -40 +KPX B U -10 +KPX B Uacute -10 +KPX B Ucircumflex -10 +KPX B Udieresis -10 +KPX B Ugrave -10 +KPX B Uhungarumlaut -10 +KPX B Umacron -10 +KPX B Uogonek -10 +KPX B Uring -10 +KPX B comma -20 +KPX B period -20 +KPX C comma -30 +KPX C period -30 +KPX Cacute comma -30 +KPX Cacute period -30 +KPX Ccaron comma -30 +KPX Ccaron period -30 +KPX Ccedilla comma -30 +KPX Ccedilla period -30 +KPX D A -40 +KPX D Aacute -40 +KPX D Abreve -40 +KPX D Acircumflex -40 +KPX D Adieresis -40 +KPX D Agrave -40 +KPX D Amacron -40 +KPX D Aogonek -40 +KPX D Aring -40 +KPX D Atilde -40 +KPX D V -70 +KPX D W -40 +KPX D Y -90 +KPX D Yacute -90 +KPX D Ydieresis -90 +KPX D comma -70 +KPX D period -70 +KPX Dcaron A -40 +KPX Dcaron Aacute -40 +KPX Dcaron Abreve -40 +KPX Dcaron Acircumflex -40 +KPX Dcaron Adieresis -40 +KPX Dcaron Agrave -40 +KPX Dcaron Amacron -40 +KPX Dcaron Aogonek -40 +KPX Dcaron Aring -40 +KPX Dcaron Atilde -40 +KPX Dcaron V -70 +KPX Dcaron W -40 +KPX Dcaron Y -90 +KPX Dcaron Yacute -90 +KPX Dcaron Ydieresis -90 +KPX Dcaron comma -70 +KPX Dcaron period -70 +KPX Dcroat A -40 +KPX Dcroat Aacute -40 +KPX Dcroat Abreve -40 +KPX Dcroat Acircumflex -40 +KPX Dcroat Adieresis -40 +KPX Dcroat Agrave -40 +KPX Dcroat Amacron -40 +KPX Dcroat Aogonek -40 +KPX Dcroat Aring -40 +KPX Dcroat Atilde -40 +KPX Dcroat V -70 +KPX Dcroat W -40 +KPX Dcroat Y -90 +KPX Dcroat Yacute -90 +KPX Dcroat Ydieresis -90 +KPX Dcroat comma -70 +KPX Dcroat period -70 +KPX F A -80 +KPX F Aacute -80 +KPX F Abreve -80 +KPX F Acircumflex -80 +KPX F Adieresis -80 +KPX F Agrave -80 +KPX F Amacron -80 +KPX F Aogonek -80 +KPX F Aring -80 +KPX F Atilde -80 +KPX F a -50 +KPX F aacute -50 +KPX F abreve -50 +KPX F acircumflex -50 +KPX F adieresis -50 +KPX F agrave -50 +KPX F amacron -50 +KPX F aogonek -50 +KPX F aring -50 +KPX F atilde -50 +KPX F comma -150 +KPX F e -30 +KPX F eacute -30 +KPX F ecaron -30 +KPX F ecircumflex -30 +KPX F edieresis -30 +KPX F edotaccent -30 +KPX F egrave -30 +KPX F emacron -30 +KPX F eogonek -30 +KPX F o -30 +KPX F oacute -30 +KPX F ocircumflex -30 +KPX F odieresis -30 +KPX F ograve -30 +KPX F ohungarumlaut -30 +KPX F omacron -30 +KPX F oslash -30 +KPX F otilde -30 +KPX F period -150 +KPX F r -45 +KPX F racute -45 +KPX F rcaron -45 +KPX F rcommaaccent -45 +KPX J A -20 +KPX J Aacute -20 +KPX J Abreve -20 +KPX J Acircumflex -20 +KPX J Adieresis -20 +KPX J Agrave -20 +KPX J Amacron -20 +KPX J Aogonek -20 +KPX J Aring -20 +KPX J Atilde -20 +KPX J a -20 +KPX J aacute -20 +KPX J abreve -20 +KPX J acircumflex -20 +KPX J adieresis -20 +KPX J agrave -20 +KPX J amacron -20 +KPX J aogonek -20 +KPX J aring -20 +KPX J atilde -20 +KPX J comma -30 +KPX J period -30 +KPX J u -20 +KPX J uacute -20 +KPX J ucircumflex -20 +KPX J udieresis -20 +KPX J ugrave -20 +KPX J uhungarumlaut -20 +KPX J umacron -20 +KPX J uogonek -20 +KPX J uring -20 +KPX K O -50 +KPX K Oacute -50 +KPX K Ocircumflex -50 +KPX K Odieresis -50 +KPX K Ograve -50 +KPX K Ohungarumlaut -50 +KPX K Omacron -50 +KPX K Oslash -50 +KPX K Otilde -50 +KPX K e -40 +KPX K eacute -40 +KPX K ecaron -40 +KPX K ecircumflex -40 +KPX K edieresis -40 +KPX K edotaccent -40 +KPX K egrave -40 +KPX K emacron -40 +KPX K eogonek -40 +KPX K o -40 +KPX K oacute -40 +KPX K ocircumflex -40 +KPX K odieresis -40 +KPX K ograve -40 +KPX K ohungarumlaut -40 +KPX K omacron -40 +KPX K oslash -40 +KPX K otilde -40 +KPX K u -30 +KPX K uacute -30 +KPX K ucircumflex -30 +KPX K udieresis -30 +KPX K ugrave -30 +KPX K uhungarumlaut -30 +KPX K umacron -30 +KPX K uogonek -30 +KPX K uring -30 +KPX K y -50 +KPX K yacute -50 +KPX K ydieresis -50 +KPX Kcommaaccent O -50 +KPX Kcommaaccent Oacute -50 +KPX Kcommaaccent Ocircumflex -50 +KPX Kcommaaccent Odieresis -50 +KPX Kcommaaccent Ograve -50 +KPX Kcommaaccent Ohungarumlaut -50 +KPX Kcommaaccent Omacron -50 +KPX Kcommaaccent Oslash -50 +KPX Kcommaaccent Otilde -50 +KPX Kcommaaccent e -40 +KPX Kcommaaccent eacute -40 +KPX Kcommaaccent ecaron -40 +KPX Kcommaaccent ecircumflex -40 +KPX Kcommaaccent edieresis -40 +KPX Kcommaaccent edotaccent -40 +KPX Kcommaaccent egrave -40 +KPX Kcommaaccent emacron -40 +KPX Kcommaaccent eogonek -40 +KPX Kcommaaccent o -40 +KPX Kcommaaccent oacute -40 +KPX Kcommaaccent ocircumflex -40 +KPX Kcommaaccent odieresis -40 +KPX Kcommaaccent ograve -40 +KPX Kcommaaccent ohungarumlaut -40 +KPX Kcommaaccent omacron -40 +KPX Kcommaaccent oslash -40 +KPX Kcommaaccent otilde -40 +KPX Kcommaaccent u -30 +KPX Kcommaaccent uacute -30 +KPX Kcommaaccent ucircumflex -30 +KPX Kcommaaccent udieresis -30 +KPX Kcommaaccent ugrave -30 +KPX Kcommaaccent uhungarumlaut -30 +KPX Kcommaaccent umacron -30 +KPX Kcommaaccent uogonek -30 +KPX Kcommaaccent uring -30 +KPX Kcommaaccent y -50 +KPX Kcommaaccent yacute -50 +KPX Kcommaaccent ydieresis -50 +KPX L T -110 +KPX L Tcaron -110 +KPX L Tcommaaccent -110 +KPX L V -110 +KPX L W -70 +KPX L Y -140 +KPX L Yacute -140 +KPX L Ydieresis -140 +KPX L quotedblright -140 +KPX L quoteright -160 +KPX L y -30 +KPX L yacute -30 +KPX L ydieresis -30 +KPX Lacute T -110 +KPX Lacute Tcaron -110 +KPX Lacute Tcommaaccent -110 +KPX Lacute V -110 +KPX Lacute W -70 +KPX Lacute Y -140 +KPX Lacute Yacute -140 +KPX Lacute Ydieresis -140 +KPX Lacute quotedblright -140 +KPX Lacute quoteright -160 +KPX Lacute y -30 +KPX Lacute yacute -30 +KPX Lacute ydieresis -30 +KPX Lcaron T -110 +KPX Lcaron Tcaron -110 +KPX Lcaron Tcommaaccent -110 +KPX Lcaron V -110 +KPX Lcaron W -70 +KPX Lcaron Y -140 +KPX Lcaron Yacute -140 +KPX Lcaron Ydieresis -140 +KPX Lcaron quotedblright -140 +KPX Lcaron quoteright -160 +KPX Lcaron y -30 +KPX Lcaron yacute -30 +KPX Lcaron ydieresis -30 +KPX Lcommaaccent T -110 +KPX Lcommaaccent Tcaron -110 +KPX Lcommaaccent Tcommaaccent -110 +KPX Lcommaaccent V -110 +KPX Lcommaaccent W -70 +KPX Lcommaaccent Y -140 +KPX Lcommaaccent Yacute -140 +KPX Lcommaaccent Ydieresis -140 +KPX Lcommaaccent quotedblright -140 +KPX Lcommaaccent quoteright -160 +KPX Lcommaaccent y -30 +KPX Lcommaaccent yacute -30 +KPX Lcommaaccent ydieresis -30 +KPX Lslash T -110 +KPX Lslash Tcaron -110 +KPX Lslash Tcommaaccent -110 +KPX Lslash V -110 +KPX Lslash W -70 +KPX Lslash Y -140 +KPX Lslash Yacute -140 +KPX Lslash Ydieresis -140 +KPX Lslash quotedblright -140 +KPX Lslash quoteright -160 +KPX Lslash y -30 +KPX Lslash yacute -30 +KPX Lslash ydieresis -30 +KPX O A -20 +KPX O Aacute -20 +KPX O Abreve -20 +KPX O Acircumflex -20 +KPX O Adieresis -20 +KPX O Agrave -20 +KPX O Amacron -20 +KPX O Aogonek -20 +KPX O Aring -20 +KPX O Atilde -20 +KPX O T -40 +KPX O Tcaron -40 +KPX O Tcommaaccent -40 +KPX O V -50 +KPX O W -30 +KPX O X -60 +KPX O Y -70 +KPX O Yacute -70 +KPX O Ydieresis -70 +KPX O comma -40 +KPX O period -40 +KPX Oacute A -20 +KPX Oacute Aacute -20 +KPX Oacute Abreve -20 +KPX Oacute Acircumflex -20 +KPX Oacute Adieresis -20 +KPX Oacute Agrave -20 +KPX Oacute Amacron -20 +KPX Oacute Aogonek -20 +KPX Oacute Aring -20 +KPX Oacute Atilde -20 +KPX Oacute T -40 +KPX Oacute Tcaron -40 +KPX Oacute Tcommaaccent -40 +KPX Oacute V -50 +KPX Oacute W -30 +KPX Oacute X -60 +KPX Oacute Y -70 +KPX Oacute Yacute -70 +KPX Oacute Ydieresis -70 +KPX Oacute comma -40 +KPX Oacute period -40 +KPX Ocircumflex A -20 +KPX Ocircumflex Aacute -20 +KPX Ocircumflex Abreve -20 +KPX Ocircumflex Acircumflex -20 +KPX Ocircumflex Adieresis -20 +KPX Ocircumflex Agrave -20 +KPX Ocircumflex Amacron -20 +KPX Ocircumflex Aogonek -20 +KPX Ocircumflex Aring -20 +KPX Ocircumflex Atilde -20 +KPX Ocircumflex T -40 +KPX Ocircumflex Tcaron -40 +KPX Ocircumflex Tcommaaccent -40 +KPX Ocircumflex V -50 +KPX Ocircumflex W -30 +KPX Ocircumflex X -60 +KPX Ocircumflex Y -70 +KPX Ocircumflex Yacute -70 +KPX Ocircumflex Ydieresis -70 +KPX Ocircumflex comma -40 +KPX Ocircumflex period -40 +KPX Odieresis A -20 +KPX Odieresis Aacute -20 +KPX Odieresis Abreve -20 +KPX Odieresis Acircumflex -20 +KPX Odieresis Adieresis -20 +KPX Odieresis Agrave -20 +KPX Odieresis Amacron -20 +KPX Odieresis Aogonek -20 +KPX Odieresis Aring -20 +KPX Odieresis Atilde -20 +KPX Odieresis T -40 +KPX Odieresis Tcaron -40 +KPX Odieresis Tcommaaccent -40 +KPX Odieresis V -50 +KPX Odieresis W -30 +KPX Odieresis X -60 +KPX Odieresis Y -70 +KPX Odieresis Yacute -70 +KPX Odieresis Ydieresis -70 +KPX Odieresis comma -40 +KPX Odieresis period -40 +KPX Ograve A -20 +KPX Ograve Aacute -20 +KPX Ograve Abreve -20 +KPX Ograve Acircumflex -20 +KPX Ograve Adieresis -20 +KPX Ograve Agrave -20 +KPX Ograve Amacron -20 +KPX Ograve Aogonek -20 +KPX Ograve Aring -20 +KPX Ograve Atilde -20 +KPX Ograve T -40 +KPX Ograve Tcaron -40 +KPX Ograve Tcommaaccent -40 +KPX Ograve V -50 +KPX Ograve W -30 +KPX Ograve X -60 +KPX Ograve Y -70 +KPX Ograve Yacute -70 +KPX Ograve Ydieresis -70 +KPX Ograve comma -40 +KPX Ograve period -40 +KPX Ohungarumlaut A -20 +KPX Ohungarumlaut Aacute -20 +KPX Ohungarumlaut Abreve -20 +KPX Ohungarumlaut Acircumflex -20 +KPX Ohungarumlaut Adieresis -20 +KPX Ohungarumlaut Agrave -20 +KPX Ohungarumlaut Amacron -20 +KPX Ohungarumlaut Aogonek -20 +KPX Ohungarumlaut Aring -20 +KPX Ohungarumlaut Atilde -20 +KPX Ohungarumlaut T -40 +KPX Ohungarumlaut Tcaron -40 +KPX Ohungarumlaut Tcommaaccent -40 +KPX Ohungarumlaut V -50 +KPX Ohungarumlaut W -30 +KPX Ohungarumlaut X -60 +KPX Ohungarumlaut Y -70 +KPX Ohungarumlaut Yacute -70 +KPX Ohungarumlaut Ydieresis -70 +KPX Ohungarumlaut comma -40 +KPX Ohungarumlaut period -40 +KPX Omacron A -20 +KPX Omacron Aacute -20 +KPX Omacron Abreve -20 +KPX Omacron Acircumflex -20 +KPX Omacron Adieresis -20 +KPX Omacron Agrave -20 +KPX Omacron Amacron -20 +KPX Omacron Aogonek -20 +KPX Omacron Aring -20 +KPX Omacron Atilde -20 +KPX Omacron T -40 +KPX Omacron Tcaron -40 +KPX Omacron Tcommaaccent -40 +KPX Omacron V -50 +KPX Omacron W -30 +KPX Omacron X -60 +KPX Omacron Y -70 +KPX Omacron Yacute -70 +KPX Omacron Ydieresis -70 +KPX Omacron comma -40 +KPX Omacron period -40 +KPX Oslash A -20 +KPX Oslash Aacute -20 +KPX Oslash Abreve -20 +KPX Oslash Acircumflex -20 +KPX Oslash Adieresis -20 +KPX Oslash Agrave -20 +KPX Oslash Amacron -20 +KPX Oslash Aogonek -20 +KPX Oslash Aring -20 +KPX Oslash Atilde -20 +KPX Oslash T -40 +KPX Oslash Tcaron -40 +KPX Oslash Tcommaaccent -40 +KPX Oslash V -50 +KPX Oslash W -30 +KPX Oslash X -60 +KPX Oslash Y -70 +KPX Oslash Yacute -70 +KPX Oslash Ydieresis -70 +KPX Oslash comma -40 +KPX Oslash period -40 +KPX Otilde A -20 +KPX Otilde Aacute -20 +KPX Otilde Abreve -20 +KPX Otilde Acircumflex -20 +KPX Otilde Adieresis -20 +KPX Otilde Agrave -20 +KPX Otilde Amacron -20 +KPX Otilde Aogonek -20 +KPX Otilde Aring -20 +KPX Otilde Atilde -20 +KPX Otilde T -40 +KPX Otilde Tcaron -40 +KPX Otilde Tcommaaccent -40 +KPX Otilde V -50 +KPX Otilde W -30 +KPX Otilde X -60 +KPX Otilde Y -70 +KPX Otilde Yacute -70 +KPX Otilde Ydieresis -70 +KPX Otilde comma -40 +KPX Otilde period -40 +KPX P A -120 +KPX P Aacute -120 +KPX P Abreve -120 +KPX P Acircumflex -120 +KPX P Adieresis -120 +KPX P Agrave -120 +KPX P Amacron -120 +KPX P Aogonek -120 +KPX P Aring -120 +KPX P Atilde -120 +KPX P a -40 +KPX P aacute -40 +KPX P abreve -40 +KPX P acircumflex -40 +KPX P adieresis -40 +KPX P agrave -40 +KPX P amacron -40 +KPX P aogonek -40 +KPX P aring -40 +KPX P atilde -40 +KPX P comma -180 +KPX P e -50 +KPX P eacute -50 +KPX P ecaron -50 +KPX P ecircumflex -50 +KPX P edieresis -50 +KPX P edotaccent -50 +KPX P egrave -50 +KPX P emacron -50 +KPX P eogonek -50 +KPX P o -50 +KPX P oacute -50 +KPX P ocircumflex -50 +KPX P odieresis -50 +KPX P ograve -50 +KPX P ohungarumlaut -50 +KPX P omacron -50 +KPX P oslash -50 +KPX P otilde -50 +KPX P period -180 +KPX Q U -10 +KPX Q Uacute -10 +KPX Q Ucircumflex -10 +KPX Q Udieresis -10 +KPX Q Ugrave -10 +KPX Q Uhungarumlaut -10 +KPX Q Umacron -10 +KPX Q Uogonek -10 +KPX Q Uring -10 +KPX R O -20 +KPX R Oacute -20 +KPX R Ocircumflex -20 +KPX R Odieresis -20 +KPX R Ograve -20 +KPX R Ohungarumlaut -20 +KPX R Omacron -20 +KPX R Oslash -20 +KPX R Otilde -20 +KPX R T -30 +KPX R Tcaron -30 +KPX R Tcommaaccent -30 +KPX R U -40 +KPX R Uacute -40 +KPX R Ucircumflex -40 +KPX R Udieresis -40 +KPX R Ugrave -40 +KPX R Uhungarumlaut -40 +KPX R Umacron -40 +KPX R Uogonek -40 +KPX R Uring -40 +KPX R V -50 +KPX R W -30 +KPX R Y -50 +KPX R Yacute -50 +KPX R Ydieresis -50 +KPX Racute O -20 +KPX Racute Oacute -20 +KPX Racute Ocircumflex -20 +KPX Racute Odieresis -20 +KPX Racute Ograve -20 +KPX Racute Ohungarumlaut -20 +KPX Racute Omacron -20 +KPX Racute Oslash -20 +KPX Racute Otilde -20 +KPX Racute T -30 +KPX Racute Tcaron -30 +KPX Racute Tcommaaccent -30 +KPX Racute U -40 +KPX Racute Uacute -40 +KPX Racute Ucircumflex -40 +KPX Racute Udieresis -40 +KPX Racute Ugrave -40 +KPX Racute Uhungarumlaut -40 +KPX Racute Umacron -40 +KPX Racute Uogonek -40 +KPX Racute Uring -40 +KPX Racute V -50 +KPX Racute W -30 +KPX Racute Y -50 +KPX Racute Yacute -50 +KPX Racute Ydieresis -50 +KPX Rcaron O -20 +KPX Rcaron Oacute -20 +KPX Rcaron Ocircumflex -20 +KPX Rcaron Odieresis -20 +KPX Rcaron Ograve -20 +KPX Rcaron Ohungarumlaut -20 +KPX Rcaron Omacron -20 +KPX Rcaron Oslash -20 +KPX Rcaron Otilde -20 +KPX Rcaron T -30 +KPX Rcaron Tcaron -30 +KPX Rcaron Tcommaaccent -30 +KPX Rcaron U -40 +KPX Rcaron Uacute -40 +KPX Rcaron Ucircumflex -40 +KPX Rcaron Udieresis -40 +KPX Rcaron Ugrave -40 +KPX Rcaron Uhungarumlaut -40 +KPX Rcaron Umacron -40 +KPX Rcaron Uogonek -40 +KPX Rcaron Uring -40 +KPX Rcaron V -50 +KPX Rcaron W -30 +KPX Rcaron Y -50 +KPX Rcaron Yacute -50 +KPX Rcaron Ydieresis -50 +KPX Rcommaaccent O -20 +KPX Rcommaaccent Oacute -20 +KPX Rcommaaccent Ocircumflex -20 +KPX Rcommaaccent Odieresis -20 +KPX Rcommaaccent Ograve -20 +KPX Rcommaaccent Ohungarumlaut -20 +KPX Rcommaaccent Omacron -20 +KPX Rcommaaccent Oslash -20 +KPX Rcommaaccent Otilde -20 +KPX Rcommaaccent T -30 +KPX Rcommaaccent Tcaron -30 +KPX Rcommaaccent Tcommaaccent -30 +KPX Rcommaaccent U -40 +KPX Rcommaaccent Uacute -40 +KPX Rcommaaccent Ucircumflex -40 +KPX Rcommaaccent Udieresis -40 +KPX Rcommaaccent Ugrave -40 +KPX Rcommaaccent Uhungarumlaut -40 +KPX Rcommaaccent Umacron -40 +KPX Rcommaaccent Uogonek -40 +KPX Rcommaaccent Uring -40 +KPX Rcommaaccent V -50 +KPX Rcommaaccent W -30 +KPX Rcommaaccent Y -50 +KPX Rcommaaccent Yacute -50 +KPX Rcommaaccent Ydieresis -50 +KPX S comma -20 +KPX S period -20 +KPX Sacute comma -20 +KPX Sacute period -20 +KPX Scaron comma -20 +KPX Scaron period -20 +KPX Scedilla comma -20 +KPX Scedilla period -20 +KPX Scommaaccent comma -20 +KPX Scommaaccent period -20 +KPX T A -120 +KPX T Aacute -120 +KPX T Abreve -120 +KPX T Acircumflex -120 +KPX T Adieresis -120 +KPX T Agrave -120 +KPX T Amacron -120 +KPX T Aogonek -120 +KPX T Aring -120 +KPX T Atilde -120 +KPX T O -40 +KPX T Oacute -40 +KPX T Ocircumflex -40 +KPX T Odieresis -40 +KPX T Ograve -40 +KPX T Ohungarumlaut -40 +KPX T Omacron -40 +KPX T Oslash -40 +KPX T Otilde -40 +KPX T a -120 +KPX T aacute -120 +KPX T abreve -60 +KPX T acircumflex -120 +KPX T adieresis -120 +KPX T agrave -120 +KPX T amacron -60 +KPX T aogonek -120 +KPX T aring -120 +KPX T atilde -60 +KPX T colon -20 +KPX T comma -120 +KPX T e -120 +KPX T eacute -120 +KPX T ecaron -120 +KPX T ecircumflex -120 +KPX T edieresis -120 +KPX T edotaccent -120 +KPX T egrave -60 +KPX T emacron -60 +KPX T eogonek -120 +KPX T hyphen -140 +KPX T o -120 +KPX T oacute -120 +KPX T ocircumflex -120 +KPX T odieresis -120 +KPX T ograve -120 +KPX T ohungarumlaut -120 +KPX T omacron -60 +KPX T oslash -120 +KPX T otilde -60 +KPX T period -120 +KPX T r -120 +KPX T racute -120 +KPX T rcaron -120 +KPX T rcommaaccent -120 +KPX T semicolon -20 +KPX T u -120 +KPX T uacute -120 +KPX T ucircumflex -120 +KPX T udieresis -120 +KPX T ugrave -120 +KPX T uhungarumlaut -120 +KPX T umacron -60 +KPX T uogonek -120 +KPX T uring -120 +KPX T w -120 +KPX T y -120 +KPX T yacute -120 +KPX T ydieresis -60 +KPX Tcaron A -120 +KPX Tcaron Aacute -120 +KPX Tcaron Abreve -120 +KPX Tcaron Acircumflex -120 +KPX Tcaron Adieresis -120 +KPX Tcaron Agrave -120 +KPX Tcaron Amacron -120 +KPX Tcaron Aogonek -120 +KPX Tcaron Aring -120 +KPX Tcaron Atilde -120 +KPX Tcaron O -40 +KPX Tcaron Oacute -40 +KPX Tcaron Ocircumflex -40 +KPX Tcaron Odieresis -40 +KPX Tcaron Ograve -40 +KPX Tcaron Ohungarumlaut -40 +KPX Tcaron Omacron -40 +KPX Tcaron Oslash -40 +KPX Tcaron Otilde -40 +KPX Tcaron a -120 +KPX Tcaron aacute -120 +KPX Tcaron abreve -60 +KPX Tcaron acircumflex -120 +KPX Tcaron adieresis -120 +KPX Tcaron agrave -120 +KPX Tcaron amacron -60 +KPX Tcaron aogonek -120 +KPX Tcaron aring -120 +KPX Tcaron atilde -60 +KPX Tcaron colon -20 +KPX Tcaron comma -120 +KPX Tcaron e -120 +KPX Tcaron eacute -120 +KPX Tcaron ecaron -120 +KPX Tcaron ecircumflex -120 +KPX Tcaron edieresis -120 +KPX Tcaron edotaccent -120 +KPX Tcaron egrave -60 +KPX Tcaron emacron -60 +KPX Tcaron eogonek -120 +KPX Tcaron hyphen -140 +KPX Tcaron o -120 +KPX Tcaron oacute -120 +KPX Tcaron ocircumflex -120 +KPX Tcaron odieresis -120 +KPX Tcaron ograve -120 +KPX Tcaron ohungarumlaut -120 +KPX Tcaron omacron -60 +KPX Tcaron oslash -120 +KPX Tcaron otilde -60 +KPX Tcaron period -120 +KPX Tcaron r -120 +KPX Tcaron racute -120 +KPX Tcaron rcaron -120 +KPX Tcaron rcommaaccent -120 +KPX Tcaron semicolon -20 +KPX Tcaron u -120 +KPX Tcaron uacute -120 +KPX Tcaron ucircumflex -120 +KPX Tcaron udieresis -120 +KPX Tcaron ugrave -120 +KPX Tcaron uhungarumlaut -120 +KPX Tcaron umacron -60 +KPX Tcaron uogonek -120 +KPX Tcaron uring -120 +KPX Tcaron w -120 +KPX Tcaron y -120 +KPX Tcaron yacute -120 +KPX Tcaron ydieresis -60 +KPX Tcommaaccent A -120 +KPX Tcommaaccent Aacute -120 +KPX Tcommaaccent Abreve -120 +KPX Tcommaaccent Acircumflex -120 +KPX Tcommaaccent Adieresis -120 +KPX Tcommaaccent Agrave -120 +KPX Tcommaaccent Amacron -120 +KPX Tcommaaccent Aogonek -120 +KPX Tcommaaccent Aring -120 +KPX Tcommaaccent Atilde -120 +KPX Tcommaaccent O -40 +KPX Tcommaaccent Oacute -40 +KPX Tcommaaccent Ocircumflex -40 +KPX Tcommaaccent Odieresis -40 +KPX Tcommaaccent Ograve -40 +KPX Tcommaaccent Ohungarumlaut -40 +KPX Tcommaaccent Omacron -40 +KPX Tcommaaccent Oslash -40 +KPX Tcommaaccent Otilde -40 +KPX Tcommaaccent a -120 +KPX Tcommaaccent aacute -120 +KPX Tcommaaccent abreve -60 +KPX Tcommaaccent acircumflex -120 +KPX Tcommaaccent adieresis -120 +KPX Tcommaaccent agrave -120 +KPX Tcommaaccent amacron -60 +KPX Tcommaaccent aogonek -120 +KPX Tcommaaccent aring -120 +KPX Tcommaaccent atilde -60 +KPX Tcommaaccent colon -20 +KPX Tcommaaccent comma -120 +KPX Tcommaaccent e -120 +KPX Tcommaaccent eacute -120 +KPX Tcommaaccent ecaron -120 +KPX Tcommaaccent ecircumflex -120 +KPX Tcommaaccent edieresis -120 +KPX Tcommaaccent edotaccent -120 +KPX Tcommaaccent egrave -60 +KPX Tcommaaccent emacron -60 +KPX Tcommaaccent eogonek -120 +KPX Tcommaaccent hyphen -140 +KPX Tcommaaccent o -120 +KPX Tcommaaccent oacute -120 +KPX Tcommaaccent ocircumflex -120 +KPX Tcommaaccent odieresis -120 +KPX Tcommaaccent ograve -120 +KPX Tcommaaccent ohungarumlaut -120 +KPX Tcommaaccent omacron -60 +KPX Tcommaaccent oslash -120 +KPX Tcommaaccent otilde -60 +KPX Tcommaaccent period -120 +KPX Tcommaaccent r -120 +KPX Tcommaaccent racute -120 +KPX Tcommaaccent rcaron -120 +KPX Tcommaaccent rcommaaccent -120 +KPX Tcommaaccent semicolon -20 +KPX Tcommaaccent u -120 +KPX Tcommaaccent uacute -120 +KPX Tcommaaccent ucircumflex -120 +KPX Tcommaaccent udieresis -120 +KPX Tcommaaccent ugrave -120 +KPX Tcommaaccent uhungarumlaut -120 +KPX Tcommaaccent umacron -60 +KPX Tcommaaccent uogonek -120 +KPX Tcommaaccent uring -120 +KPX Tcommaaccent w -120 +KPX Tcommaaccent y -120 +KPX Tcommaaccent yacute -120 +KPX Tcommaaccent ydieresis -60 +KPX U A -40 +KPX U Aacute -40 +KPX U Abreve -40 +KPX U Acircumflex -40 +KPX U Adieresis -40 +KPX U Agrave -40 +KPX U Amacron -40 +KPX U Aogonek -40 +KPX U Aring -40 +KPX U Atilde -40 +KPX U comma -40 +KPX U period -40 +KPX Uacute A -40 +KPX Uacute Aacute -40 +KPX Uacute Abreve -40 +KPX Uacute Acircumflex -40 +KPX Uacute Adieresis -40 +KPX Uacute Agrave -40 +KPX Uacute Amacron -40 +KPX Uacute Aogonek -40 +KPX Uacute Aring -40 +KPX Uacute Atilde -40 +KPX Uacute comma -40 +KPX Uacute period -40 +KPX Ucircumflex A -40 +KPX Ucircumflex Aacute -40 +KPX Ucircumflex Abreve -40 +KPX Ucircumflex Acircumflex -40 +KPX Ucircumflex Adieresis -40 +KPX Ucircumflex Agrave -40 +KPX Ucircumflex Amacron -40 +KPX Ucircumflex Aogonek -40 +KPX Ucircumflex Aring -40 +KPX Ucircumflex Atilde -40 +KPX Ucircumflex comma -40 +KPX Ucircumflex period -40 +KPX Udieresis A -40 +KPX Udieresis Aacute -40 +KPX Udieresis Abreve -40 +KPX Udieresis Acircumflex -40 +KPX Udieresis Adieresis -40 +KPX Udieresis Agrave -40 +KPX Udieresis Amacron -40 +KPX Udieresis Aogonek -40 +KPX Udieresis Aring -40 +KPX Udieresis Atilde -40 +KPX Udieresis comma -40 +KPX Udieresis period -40 +KPX Ugrave A -40 +KPX Ugrave Aacute -40 +KPX Ugrave Abreve -40 +KPX Ugrave Acircumflex -40 +KPX Ugrave Adieresis -40 +KPX Ugrave Agrave -40 +KPX Ugrave Amacron -40 +KPX Ugrave Aogonek -40 +KPX Ugrave Aring -40 +KPX Ugrave Atilde -40 +KPX Ugrave comma -40 +KPX Ugrave period -40 +KPX Uhungarumlaut A -40 +KPX Uhungarumlaut Aacute -40 +KPX Uhungarumlaut Abreve -40 +KPX Uhungarumlaut Acircumflex -40 +KPX Uhungarumlaut Adieresis -40 +KPX Uhungarumlaut Agrave -40 +KPX Uhungarumlaut Amacron -40 +KPX Uhungarumlaut Aogonek -40 +KPX Uhungarumlaut Aring -40 +KPX Uhungarumlaut Atilde -40 +KPX Uhungarumlaut comma -40 +KPX Uhungarumlaut period -40 +KPX Umacron A -40 +KPX Umacron Aacute -40 +KPX Umacron Abreve -40 +KPX Umacron Acircumflex -40 +KPX Umacron Adieresis -40 +KPX Umacron Agrave -40 +KPX Umacron Amacron -40 +KPX Umacron Aogonek -40 +KPX Umacron Aring -40 +KPX Umacron Atilde -40 +KPX Umacron comma -40 +KPX Umacron period -40 +KPX Uogonek A -40 +KPX Uogonek Aacute -40 +KPX Uogonek Abreve -40 +KPX Uogonek Acircumflex -40 +KPX Uogonek Adieresis -40 +KPX Uogonek Agrave -40 +KPX Uogonek Amacron -40 +KPX Uogonek Aogonek -40 +KPX Uogonek Aring -40 +KPX Uogonek Atilde -40 +KPX Uogonek comma -40 +KPX Uogonek period -40 +KPX Uring A -40 +KPX Uring Aacute -40 +KPX Uring Abreve -40 +KPX Uring Acircumflex -40 +KPX Uring Adieresis -40 +KPX Uring Agrave -40 +KPX Uring Amacron -40 +KPX Uring Aogonek -40 +KPX Uring Aring -40 +KPX Uring Atilde -40 +KPX Uring comma -40 +KPX Uring period -40 +KPX V A -80 +KPX V Aacute -80 +KPX V Abreve -80 +KPX V Acircumflex -80 +KPX V Adieresis -80 +KPX V Agrave -80 +KPX V Amacron -80 +KPX V Aogonek -80 +KPX V Aring -80 +KPX V Atilde -80 +KPX V G -40 +KPX V Gbreve -40 +KPX V Gcommaaccent -40 +KPX V O -40 +KPX V Oacute -40 +KPX V Ocircumflex -40 +KPX V Odieresis -40 +KPX V Ograve -40 +KPX V Ohungarumlaut -40 +KPX V Omacron -40 +KPX V Oslash -40 +KPX V Otilde -40 +KPX V a -70 +KPX V aacute -70 +KPX V abreve -70 +KPX V acircumflex -70 +KPX V adieresis -70 +KPX V agrave -70 +KPX V amacron -70 +KPX V aogonek -70 +KPX V aring -70 +KPX V atilde -70 +KPX V colon -40 +KPX V comma -125 +KPX V e -80 +KPX V eacute -80 +KPX V ecaron -80 +KPX V ecircumflex -80 +KPX V edieresis -80 +KPX V edotaccent -80 +KPX V egrave -80 +KPX V emacron -80 +KPX V eogonek -80 +KPX V hyphen -80 +KPX V o -80 +KPX V oacute -80 +KPX V ocircumflex -80 +KPX V odieresis -80 +KPX V ograve -80 +KPX V ohungarumlaut -80 +KPX V omacron -80 +KPX V oslash -80 +KPX V otilde -80 +KPX V period -125 +KPX V semicolon -40 +KPX V u -70 +KPX V uacute -70 +KPX V ucircumflex -70 +KPX V udieresis -70 +KPX V ugrave -70 +KPX V uhungarumlaut -70 +KPX V umacron -70 +KPX V uogonek -70 +KPX V uring -70 +KPX W A -50 +KPX W Aacute -50 +KPX W Abreve -50 +KPX W Acircumflex -50 +KPX W Adieresis -50 +KPX W Agrave -50 +KPX W Amacron -50 +KPX W Aogonek -50 +KPX W Aring -50 +KPX W Atilde -50 +KPX W O -20 +KPX W Oacute -20 +KPX W Ocircumflex -20 +KPX W Odieresis -20 +KPX W Ograve -20 +KPX W Ohungarumlaut -20 +KPX W Omacron -20 +KPX W Oslash -20 +KPX W Otilde -20 +KPX W a -40 +KPX W aacute -40 +KPX W abreve -40 +KPX W acircumflex -40 +KPX W adieresis -40 +KPX W agrave -40 +KPX W amacron -40 +KPX W aogonek -40 +KPX W aring -40 +KPX W atilde -40 +KPX W comma -80 +KPX W e -30 +KPX W eacute -30 +KPX W ecaron -30 +KPX W ecircumflex -30 +KPX W edieresis -30 +KPX W edotaccent -30 +KPX W egrave -30 +KPX W emacron -30 +KPX W eogonek -30 +KPX W hyphen -40 +KPX W o -30 +KPX W oacute -30 +KPX W ocircumflex -30 +KPX W odieresis -30 +KPX W ograve -30 +KPX W ohungarumlaut -30 +KPX W omacron -30 +KPX W oslash -30 +KPX W otilde -30 +KPX W period -80 +KPX W u -30 +KPX W uacute -30 +KPX W ucircumflex -30 +KPX W udieresis -30 +KPX W ugrave -30 +KPX W uhungarumlaut -30 +KPX W umacron -30 +KPX W uogonek -30 +KPX W uring -30 +KPX W y -20 +KPX W yacute -20 +KPX W ydieresis -20 +KPX Y A -110 +KPX Y Aacute -110 +KPX Y Abreve -110 +KPX Y Acircumflex -110 +KPX Y Adieresis -110 +KPX Y Agrave -110 +KPX Y Amacron -110 +KPX Y Aogonek -110 +KPX Y Aring -110 +KPX Y Atilde -110 +KPX Y O -85 +KPX Y Oacute -85 +KPX Y Ocircumflex -85 +KPX Y Odieresis -85 +KPX Y Ograve -85 +KPX Y Ohungarumlaut -85 +KPX Y Omacron -85 +KPX Y Oslash -85 +KPX Y Otilde -85 +KPX Y a -140 +KPX Y aacute -140 +KPX Y abreve -70 +KPX Y acircumflex -140 +KPX Y adieresis -140 +KPX Y agrave -140 +KPX Y amacron -70 +KPX Y aogonek -140 +KPX Y aring -140 +KPX Y atilde -140 +KPX Y colon -60 +KPX Y comma -140 +KPX Y e -140 +KPX Y eacute -140 +KPX Y ecaron -140 +KPX Y ecircumflex -140 +KPX Y edieresis -140 +KPX Y edotaccent -140 +KPX Y egrave -140 +KPX Y emacron -70 +KPX Y eogonek -140 +KPX Y hyphen -140 +KPX Y i -20 +KPX Y iacute -20 +KPX Y iogonek -20 +KPX Y o -140 +KPX Y oacute -140 +KPX Y ocircumflex -140 +KPX Y odieresis -140 +KPX Y ograve -140 +KPX Y ohungarumlaut -140 +KPX Y omacron -140 +KPX Y oslash -140 +KPX Y otilde -140 +KPX Y period -140 +KPX Y semicolon -60 +KPX Y u -110 +KPX Y uacute -110 +KPX Y ucircumflex -110 +KPX Y udieresis -110 +KPX Y ugrave -110 +KPX Y uhungarumlaut -110 +KPX Y umacron -110 +KPX Y uogonek -110 +KPX Y uring -110 +KPX Yacute A -110 +KPX Yacute Aacute -110 +KPX Yacute Abreve -110 +KPX Yacute Acircumflex -110 +KPX Yacute Adieresis -110 +KPX Yacute Agrave -110 +KPX Yacute Amacron -110 +KPX Yacute Aogonek -110 +KPX Yacute Aring -110 +KPX Yacute Atilde -110 +KPX Yacute O -85 +KPX Yacute Oacute -85 +KPX Yacute Ocircumflex -85 +KPX Yacute Odieresis -85 +KPX Yacute Ograve -85 +KPX Yacute Ohungarumlaut -85 +KPX Yacute Omacron -85 +KPX Yacute Oslash -85 +KPX Yacute Otilde -85 +KPX Yacute a -140 +KPX Yacute aacute -140 +KPX Yacute abreve -70 +KPX Yacute acircumflex -140 +KPX Yacute adieresis -140 +KPX Yacute agrave -140 +KPX Yacute amacron -70 +KPX Yacute aogonek -140 +KPX Yacute aring -140 +KPX Yacute atilde -70 +KPX Yacute colon -60 +KPX Yacute comma -140 +KPX Yacute e -140 +KPX Yacute eacute -140 +KPX Yacute ecaron -140 +KPX Yacute ecircumflex -140 +KPX Yacute edieresis -140 +KPX Yacute edotaccent -140 +KPX Yacute egrave -140 +KPX Yacute emacron -70 +KPX Yacute eogonek -140 +KPX Yacute hyphen -140 +KPX Yacute i -20 +KPX Yacute iacute -20 +KPX Yacute iogonek -20 +KPX Yacute o -140 +KPX Yacute oacute -140 +KPX Yacute ocircumflex -140 +KPX Yacute odieresis -140 +KPX Yacute ograve -140 +KPX Yacute ohungarumlaut -140 +KPX Yacute omacron -70 +KPX Yacute oslash -140 +KPX Yacute otilde -140 +KPX Yacute period -140 +KPX Yacute semicolon -60 +KPX Yacute u -110 +KPX Yacute uacute -110 +KPX Yacute ucircumflex -110 +KPX Yacute udieresis -110 +KPX Yacute ugrave -110 +KPX Yacute uhungarumlaut -110 +KPX Yacute umacron -110 +KPX Yacute uogonek -110 +KPX Yacute uring -110 +KPX Ydieresis A -110 +KPX Ydieresis Aacute -110 +KPX Ydieresis Abreve -110 +KPX Ydieresis Acircumflex -110 +KPX Ydieresis Adieresis -110 +KPX Ydieresis Agrave -110 +KPX Ydieresis Amacron -110 +KPX Ydieresis Aogonek -110 +KPX Ydieresis Aring -110 +KPX Ydieresis Atilde -110 +KPX Ydieresis O -85 +KPX Ydieresis Oacute -85 +KPX Ydieresis Ocircumflex -85 +KPX Ydieresis Odieresis -85 +KPX Ydieresis Ograve -85 +KPX Ydieresis Ohungarumlaut -85 +KPX Ydieresis Omacron -85 +KPX Ydieresis Oslash -85 +KPX Ydieresis Otilde -85 +KPX Ydieresis a -140 +KPX Ydieresis aacute -140 +KPX Ydieresis abreve -70 +KPX Ydieresis acircumflex -140 +KPX Ydieresis adieresis -140 +KPX Ydieresis agrave -140 +KPX Ydieresis amacron -70 +KPX Ydieresis aogonek -140 +KPX Ydieresis aring -140 +KPX Ydieresis atilde -70 +KPX Ydieresis colon -60 +KPX Ydieresis comma -140 +KPX Ydieresis e -140 +KPX Ydieresis eacute -140 +KPX Ydieresis ecaron -140 +KPX Ydieresis ecircumflex -140 +KPX Ydieresis edieresis -140 +KPX Ydieresis edotaccent -140 +KPX Ydieresis egrave -140 +KPX Ydieresis emacron -70 +KPX Ydieresis eogonek -140 +KPX Ydieresis hyphen -140 +KPX Ydieresis i -20 +KPX Ydieresis iacute -20 +KPX Ydieresis iogonek -20 +KPX Ydieresis o -140 +KPX Ydieresis oacute -140 +KPX Ydieresis ocircumflex -140 +KPX Ydieresis odieresis -140 +KPX Ydieresis ograve -140 +KPX Ydieresis ohungarumlaut -140 +KPX Ydieresis omacron -140 +KPX Ydieresis oslash -140 +KPX Ydieresis otilde -140 +KPX Ydieresis period -140 +KPX Ydieresis semicolon -60 +KPX Ydieresis u -110 +KPX Ydieresis uacute -110 +KPX Ydieresis ucircumflex -110 +KPX Ydieresis udieresis -110 +KPX Ydieresis ugrave -110 +KPX Ydieresis uhungarumlaut -110 +KPX Ydieresis umacron -110 +KPX Ydieresis uogonek -110 +KPX Ydieresis uring -110 +KPX a v -20 +KPX a w -20 +KPX a y -30 +KPX a yacute -30 +KPX a ydieresis -30 +KPX aacute v -20 +KPX aacute w -20 +KPX aacute y -30 +KPX aacute yacute -30 +KPX aacute ydieresis -30 +KPX abreve v -20 +KPX abreve w -20 +KPX abreve y -30 +KPX abreve yacute -30 +KPX abreve ydieresis -30 +KPX acircumflex v -20 +KPX acircumflex w -20 +KPX acircumflex y -30 +KPX acircumflex yacute -30 +KPX acircumflex ydieresis -30 +KPX adieresis v -20 +KPX adieresis w -20 +KPX adieresis y -30 +KPX adieresis yacute -30 +KPX adieresis ydieresis -30 +KPX agrave v -20 +KPX agrave w -20 +KPX agrave y -30 +KPX agrave yacute -30 +KPX agrave ydieresis -30 +KPX amacron v -20 +KPX amacron w -20 +KPX amacron y -30 +KPX amacron yacute -30 +KPX amacron ydieresis -30 +KPX aogonek v -20 +KPX aogonek w -20 +KPX aogonek y -30 +KPX aogonek yacute -30 +KPX aogonek ydieresis -30 +KPX aring v -20 +KPX aring w -20 +KPX aring y -30 +KPX aring yacute -30 +KPX aring ydieresis -30 +KPX atilde v -20 +KPX atilde w -20 +KPX atilde y -30 +KPX atilde yacute -30 +KPX atilde ydieresis -30 +KPX b b -10 +KPX b comma -40 +KPX b l -20 +KPX b lacute -20 +KPX b lcommaaccent -20 +KPX b lslash -20 +KPX b period -40 +KPX b u -20 +KPX b uacute -20 +KPX b ucircumflex -20 +KPX b udieresis -20 +KPX b ugrave -20 +KPX b uhungarumlaut -20 +KPX b umacron -20 +KPX b uogonek -20 +KPX b uring -20 +KPX b v -20 +KPX b y -20 +KPX b yacute -20 +KPX b ydieresis -20 +KPX c comma -15 +KPX c k -20 +KPX c kcommaaccent -20 +KPX cacute comma -15 +KPX cacute k -20 +KPX cacute kcommaaccent -20 +KPX ccaron comma -15 +KPX ccaron k -20 +KPX ccaron kcommaaccent -20 +KPX ccedilla comma -15 +KPX ccedilla k -20 +KPX ccedilla kcommaaccent -20 +KPX colon space -50 +KPX comma quotedblright -100 +KPX comma quoteright -100 +KPX e comma -15 +KPX e period -15 +KPX e v -30 +KPX e w -20 +KPX e x -30 +KPX e y -20 +KPX e yacute -20 +KPX e ydieresis -20 +KPX eacute comma -15 +KPX eacute period -15 +KPX eacute v -30 +KPX eacute w -20 +KPX eacute x -30 +KPX eacute y -20 +KPX eacute yacute -20 +KPX eacute ydieresis -20 +KPX ecaron comma -15 +KPX ecaron period -15 +KPX ecaron v -30 +KPX ecaron w -20 +KPX ecaron x -30 +KPX ecaron y -20 +KPX ecaron yacute -20 +KPX ecaron ydieresis -20 +KPX ecircumflex comma -15 +KPX ecircumflex period -15 +KPX ecircumflex v -30 +KPX ecircumflex w -20 +KPX ecircumflex x -30 +KPX ecircumflex y -20 +KPX ecircumflex yacute -20 +KPX ecircumflex ydieresis -20 +KPX edieresis comma -15 +KPX edieresis period -15 +KPX edieresis v -30 +KPX edieresis w -20 +KPX edieresis x -30 +KPX edieresis y -20 +KPX edieresis yacute -20 +KPX edieresis ydieresis -20 +KPX edotaccent comma -15 +KPX edotaccent period -15 +KPX edotaccent v -30 +KPX edotaccent w -20 +KPX edotaccent x -30 +KPX edotaccent y -20 +KPX edotaccent yacute -20 +KPX edotaccent ydieresis -20 +KPX egrave comma -15 +KPX egrave period -15 +KPX egrave v -30 +KPX egrave w -20 +KPX egrave x -30 +KPX egrave y -20 +KPX egrave yacute -20 +KPX egrave ydieresis -20 +KPX emacron comma -15 +KPX emacron period -15 +KPX emacron v -30 +KPX emacron w -20 +KPX emacron x -30 +KPX emacron y -20 +KPX emacron yacute -20 +KPX emacron ydieresis -20 +KPX eogonek comma -15 +KPX eogonek period -15 +KPX eogonek v -30 +KPX eogonek w -20 +KPX eogonek x -30 +KPX eogonek y -20 +KPX eogonek yacute -20 +KPX eogonek ydieresis -20 +KPX f a -30 +KPX f aacute -30 +KPX f abreve -30 +KPX f acircumflex -30 +KPX f adieresis -30 +KPX f agrave -30 +KPX f amacron -30 +KPX f aogonek -30 +KPX f aring -30 +KPX f atilde -30 +KPX f comma -30 +KPX f dotlessi -28 +KPX f e -30 +KPX f eacute -30 +KPX f ecaron -30 +KPX f ecircumflex -30 +KPX f edieresis -30 +KPX f edotaccent -30 +KPX f egrave -30 +KPX f emacron -30 +KPX f eogonek -30 +KPX f o -30 +KPX f oacute -30 +KPX f ocircumflex -30 +KPX f odieresis -30 +KPX f ograve -30 +KPX f ohungarumlaut -30 +KPX f omacron -30 +KPX f oslash -30 +KPX f otilde -30 +KPX f period -30 +KPX f quotedblright 60 +KPX f quoteright 50 +KPX g r -10 +KPX g racute -10 +KPX g rcaron -10 +KPX g rcommaaccent -10 +KPX gbreve r -10 +KPX gbreve racute -10 +KPX gbreve rcaron -10 +KPX gbreve rcommaaccent -10 +KPX gcommaaccent r -10 +KPX gcommaaccent racute -10 +KPX gcommaaccent rcaron -10 +KPX gcommaaccent rcommaaccent -10 +KPX h y -30 +KPX h yacute -30 +KPX h ydieresis -30 +KPX k e -20 +KPX k eacute -20 +KPX k ecaron -20 +KPX k ecircumflex -20 +KPX k edieresis -20 +KPX k edotaccent -20 +KPX k egrave -20 +KPX k emacron -20 +KPX k eogonek -20 +KPX k o -20 +KPX k oacute -20 +KPX k ocircumflex -20 +KPX k odieresis -20 +KPX k ograve -20 +KPX k ohungarumlaut -20 +KPX k omacron -20 +KPX k oslash -20 +KPX k otilde -20 +KPX kcommaaccent e -20 +KPX kcommaaccent eacute -20 +KPX kcommaaccent ecaron -20 +KPX kcommaaccent ecircumflex -20 +KPX kcommaaccent edieresis -20 +KPX kcommaaccent edotaccent -20 +KPX kcommaaccent egrave -20 +KPX kcommaaccent emacron -20 +KPX kcommaaccent eogonek -20 +KPX kcommaaccent o -20 +KPX kcommaaccent oacute -20 +KPX kcommaaccent ocircumflex -20 +KPX kcommaaccent odieresis -20 +KPX kcommaaccent ograve -20 +KPX kcommaaccent ohungarumlaut -20 +KPX kcommaaccent omacron -20 +KPX kcommaaccent oslash -20 +KPX kcommaaccent otilde -20 +KPX m u -10 +KPX m uacute -10 +KPX m ucircumflex -10 +KPX m udieresis -10 +KPX m ugrave -10 +KPX m uhungarumlaut -10 +KPX m umacron -10 +KPX m uogonek -10 +KPX m uring -10 +KPX m y -15 +KPX m yacute -15 +KPX m ydieresis -15 +KPX n u -10 +KPX n uacute -10 +KPX n ucircumflex -10 +KPX n udieresis -10 +KPX n ugrave -10 +KPX n uhungarumlaut -10 +KPX n umacron -10 +KPX n uogonek -10 +KPX n uring -10 +KPX n v -20 +KPX n y -15 +KPX n yacute -15 +KPX n ydieresis -15 +KPX nacute u -10 +KPX nacute uacute -10 +KPX nacute ucircumflex -10 +KPX nacute udieresis -10 +KPX nacute ugrave -10 +KPX nacute uhungarumlaut -10 +KPX nacute umacron -10 +KPX nacute uogonek -10 +KPX nacute uring -10 +KPX nacute v -20 +KPX nacute y -15 +KPX nacute yacute -15 +KPX nacute ydieresis -15 +KPX ncaron u -10 +KPX ncaron uacute -10 +KPX ncaron ucircumflex -10 +KPX ncaron udieresis -10 +KPX ncaron ugrave -10 +KPX ncaron uhungarumlaut -10 +KPX ncaron umacron -10 +KPX ncaron uogonek -10 +KPX ncaron uring -10 +KPX ncaron v -20 +KPX ncaron y -15 +KPX ncaron yacute -15 +KPX ncaron ydieresis -15 +KPX ncommaaccent u -10 +KPX ncommaaccent uacute -10 +KPX ncommaaccent ucircumflex -10 +KPX ncommaaccent udieresis -10 +KPX ncommaaccent ugrave -10 +KPX ncommaaccent uhungarumlaut -10 +KPX ncommaaccent umacron -10 +KPX ncommaaccent uogonek -10 +KPX ncommaaccent uring -10 +KPX ncommaaccent v -20 +KPX ncommaaccent y -15 +KPX ncommaaccent yacute -15 +KPX ncommaaccent ydieresis -15 +KPX ntilde u -10 +KPX ntilde uacute -10 +KPX ntilde ucircumflex -10 +KPX ntilde udieresis -10 +KPX ntilde ugrave -10 +KPX ntilde uhungarumlaut -10 +KPX ntilde umacron -10 +KPX ntilde uogonek -10 +KPX ntilde uring -10 +KPX ntilde v -20 +KPX ntilde y -15 +KPX ntilde yacute -15 +KPX ntilde ydieresis -15 +KPX o comma -40 +KPX o period -40 +KPX o v -15 +KPX o w -15 +KPX o x -30 +KPX o y -30 +KPX o yacute -30 +KPX o ydieresis -30 +KPX oacute comma -40 +KPX oacute period -40 +KPX oacute v -15 +KPX oacute w -15 +KPX oacute x -30 +KPX oacute y -30 +KPX oacute yacute -30 +KPX oacute ydieresis -30 +KPX ocircumflex comma -40 +KPX ocircumflex period -40 +KPX ocircumflex v -15 +KPX ocircumflex w -15 +KPX ocircumflex x -30 +KPX ocircumflex y -30 +KPX ocircumflex yacute -30 +KPX ocircumflex ydieresis -30 +KPX odieresis comma -40 +KPX odieresis period -40 +KPX odieresis v -15 +KPX odieresis w -15 +KPX odieresis x -30 +KPX odieresis y -30 +KPX odieresis yacute -30 +KPX odieresis ydieresis -30 +KPX ograve comma -40 +KPX ograve period -40 +KPX ograve v -15 +KPX ograve w -15 +KPX ograve x -30 +KPX ograve y -30 +KPX ograve yacute -30 +KPX ograve ydieresis -30 +KPX ohungarumlaut comma -40 +KPX ohungarumlaut period -40 +KPX ohungarumlaut v -15 +KPX ohungarumlaut w -15 +KPX ohungarumlaut x -30 +KPX ohungarumlaut y -30 +KPX ohungarumlaut yacute -30 +KPX ohungarumlaut ydieresis -30 +KPX omacron comma -40 +KPX omacron period -40 +KPX omacron v -15 +KPX omacron w -15 +KPX omacron x -30 +KPX omacron y -30 +KPX omacron yacute -30 +KPX omacron ydieresis -30 +KPX oslash a -55 +KPX oslash aacute -55 +KPX oslash abreve -55 +KPX oslash acircumflex -55 +KPX oslash adieresis -55 +KPX oslash agrave -55 +KPX oslash amacron -55 +KPX oslash aogonek -55 +KPX oslash aring -55 +KPX oslash atilde -55 +KPX oslash b -55 +KPX oslash c -55 +KPX oslash cacute -55 +KPX oslash ccaron -55 +KPX oslash ccedilla -55 +KPX oslash comma -95 +KPX oslash d -55 +KPX oslash dcroat -55 +KPX oslash e -55 +KPX oslash eacute -55 +KPX oslash ecaron -55 +KPX oslash ecircumflex -55 +KPX oslash edieresis -55 +KPX oslash edotaccent -55 +KPX oslash egrave -55 +KPX oslash emacron -55 +KPX oslash eogonek -55 +KPX oslash f -55 +KPX oslash g -55 +KPX oslash gbreve -55 +KPX oslash gcommaaccent -55 +KPX oslash h -55 +KPX oslash i -55 +KPX oslash iacute -55 +KPX oslash icircumflex -55 +KPX oslash idieresis -55 +KPX oslash igrave -55 +KPX oslash imacron -55 +KPX oslash iogonek -55 +KPX oslash j -55 +KPX oslash k -55 +KPX oslash kcommaaccent -55 +KPX oslash l -55 +KPX oslash lacute -55 +KPX oslash lcommaaccent -55 +KPX oslash lslash -55 +KPX oslash m -55 +KPX oslash n -55 +KPX oslash nacute -55 +KPX oslash ncaron -55 +KPX oslash ncommaaccent -55 +KPX oslash ntilde -55 +KPX oslash o -55 +KPX oslash oacute -55 +KPX oslash ocircumflex -55 +KPX oslash odieresis -55 +KPX oslash ograve -55 +KPX oslash ohungarumlaut -55 +KPX oslash omacron -55 +KPX oslash oslash -55 +KPX oslash otilde -55 +KPX oslash p -55 +KPX oslash period -95 +KPX oslash q -55 +KPX oslash r -55 +KPX oslash racute -55 +KPX oslash rcaron -55 +KPX oslash rcommaaccent -55 +KPX oslash s -55 +KPX oslash sacute -55 +KPX oslash scaron -55 +KPX oslash scedilla -55 +KPX oslash scommaaccent -55 +KPX oslash t -55 +KPX oslash tcommaaccent -55 +KPX oslash u -55 +KPX oslash uacute -55 +KPX oslash ucircumflex -55 +KPX oslash udieresis -55 +KPX oslash ugrave -55 +KPX oslash uhungarumlaut -55 +KPX oslash umacron -55 +KPX oslash uogonek -55 +KPX oslash uring -55 +KPX oslash v -70 +KPX oslash w -70 +KPX oslash x -85 +KPX oslash y -70 +KPX oslash yacute -70 +KPX oslash ydieresis -70 +KPX oslash z -55 +KPX oslash zacute -55 +KPX oslash zcaron -55 +KPX oslash zdotaccent -55 +KPX otilde comma -40 +KPX otilde period -40 +KPX otilde v -15 +KPX otilde w -15 +KPX otilde x -30 +KPX otilde y -30 +KPX otilde yacute -30 +KPX otilde ydieresis -30 +KPX p comma -35 +KPX p period -35 +KPX p y -30 +KPX p yacute -30 +KPX p ydieresis -30 +KPX period quotedblright -100 +KPX period quoteright -100 +KPX period space -60 +KPX quotedblright space -40 +KPX quoteleft quoteleft -57 +KPX quoteright d -50 +KPX quoteright dcroat -50 +KPX quoteright quoteright -57 +KPX quoteright r -50 +KPX quoteright racute -50 +KPX quoteright rcaron -50 +KPX quoteright rcommaaccent -50 +KPX quoteright s -50 +KPX quoteright sacute -50 +KPX quoteright scaron -50 +KPX quoteright scedilla -50 +KPX quoteright scommaaccent -50 +KPX quoteright space -70 +KPX r a -10 +KPX r aacute -10 +KPX r abreve -10 +KPX r acircumflex -10 +KPX r adieresis -10 +KPX r agrave -10 +KPX r amacron -10 +KPX r aogonek -10 +KPX r aring -10 +KPX r atilde -10 +KPX r colon 30 +KPX r comma -50 +KPX r i 15 +KPX r iacute 15 +KPX r icircumflex 15 +KPX r idieresis 15 +KPX r igrave 15 +KPX r imacron 15 +KPX r iogonek 15 +KPX r k 15 +KPX r kcommaaccent 15 +KPX r l 15 +KPX r lacute 15 +KPX r lcommaaccent 15 +KPX r lslash 15 +KPX r m 25 +KPX r n 25 +KPX r nacute 25 +KPX r ncaron 25 +KPX r ncommaaccent 25 +KPX r ntilde 25 +KPX r p 30 +KPX r period -50 +KPX r semicolon 30 +KPX r t 40 +KPX r tcommaaccent 40 +KPX r u 15 +KPX r uacute 15 +KPX r ucircumflex 15 +KPX r udieresis 15 +KPX r ugrave 15 +KPX r uhungarumlaut 15 +KPX r umacron 15 +KPX r uogonek 15 +KPX r uring 15 +KPX r v 30 +KPX r y 30 +KPX r yacute 30 +KPX r ydieresis 30 +KPX racute a -10 +KPX racute aacute -10 +KPX racute abreve -10 +KPX racute acircumflex -10 +KPX racute adieresis -10 +KPX racute agrave -10 +KPX racute amacron -10 +KPX racute aogonek -10 +KPX racute aring -10 +KPX racute atilde -10 +KPX racute colon 30 +KPX racute comma -50 +KPX racute i 15 +KPX racute iacute 15 +KPX racute icircumflex 15 +KPX racute idieresis 15 +KPX racute igrave 15 +KPX racute imacron 15 +KPX racute iogonek 15 +KPX racute k 15 +KPX racute kcommaaccent 15 +KPX racute l 15 +KPX racute lacute 15 +KPX racute lcommaaccent 15 +KPX racute lslash 15 +KPX racute m 25 +KPX racute n 25 +KPX racute nacute 25 +KPX racute ncaron 25 +KPX racute ncommaaccent 25 +KPX racute ntilde 25 +KPX racute p 30 +KPX racute period -50 +KPX racute semicolon 30 +KPX racute t 40 +KPX racute tcommaaccent 40 +KPX racute u 15 +KPX racute uacute 15 +KPX racute ucircumflex 15 +KPX racute udieresis 15 +KPX racute ugrave 15 +KPX racute uhungarumlaut 15 +KPX racute umacron 15 +KPX racute uogonek 15 +KPX racute uring 15 +KPX racute v 30 +KPX racute y 30 +KPX racute yacute 30 +KPX racute ydieresis 30 +KPX rcaron a -10 +KPX rcaron aacute -10 +KPX rcaron abreve -10 +KPX rcaron acircumflex -10 +KPX rcaron adieresis -10 +KPX rcaron agrave -10 +KPX rcaron amacron -10 +KPX rcaron aogonek -10 +KPX rcaron aring -10 +KPX rcaron atilde -10 +KPX rcaron colon 30 +KPX rcaron comma -50 +KPX rcaron i 15 +KPX rcaron iacute 15 +KPX rcaron icircumflex 15 +KPX rcaron idieresis 15 +KPX rcaron igrave 15 +KPX rcaron imacron 15 +KPX rcaron iogonek 15 +KPX rcaron k 15 +KPX rcaron kcommaaccent 15 +KPX rcaron l 15 +KPX rcaron lacute 15 +KPX rcaron lcommaaccent 15 +KPX rcaron lslash 15 +KPX rcaron m 25 +KPX rcaron n 25 +KPX rcaron nacute 25 +KPX rcaron ncaron 25 +KPX rcaron ncommaaccent 25 +KPX rcaron ntilde 25 +KPX rcaron p 30 +KPX rcaron period -50 +KPX rcaron semicolon 30 +KPX rcaron t 40 +KPX rcaron tcommaaccent 40 +KPX rcaron u 15 +KPX rcaron uacute 15 +KPX rcaron ucircumflex 15 +KPX rcaron udieresis 15 +KPX rcaron ugrave 15 +KPX rcaron uhungarumlaut 15 +KPX rcaron umacron 15 +KPX rcaron uogonek 15 +KPX rcaron uring 15 +KPX rcaron v 30 +KPX rcaron y 30 +KPX rcaron yacute 30 +KPX rcaron ydieresis 30 +KPX rcommaaccent a -10 +KPX rcommaaccent aacute -10 +KPX rcommaaccent abreve -10 +KPX rcommaaccent acircumflex -10 +KPX rcommaaccent adieresis -10 +KPX rcommaaccent agrave -10 +KPX rcommaaccent amacron -10 +KPX rcommaaccent aogonek -10 +KPX rcommaaccent aring -10 +KPX rcommaaccent atilde -10 +KPX rcommaaccent colon 30 +KPX rcommaaccent comma -50 +KPX rcommaaccent i 15 +KPX rcommaaccent iacute 15 +KPX rcommaaccent icircumflex 15 +KPX rcommaaccent idieresis 15 +KPX rcommaaccent igrave 15 +KPX rcommaaccent imacron 15 +KPX rcommaaccent iogonek 15 +KPX rcommaaccent k 15 +KPX rcommaaccent kcommaaccent 15 +KPX rcommaaccent l 15 +KPX rcommaaccent lacute 15 +KPX rcommaaccent lcommaaccent 15 +KPX rcommaaccent lslash 15 +KPX rcommaaccent m 25 +KPX rcommaaccent n 25 +KPX rcommaaccent nacute 25 +KPX rcommaaccent ncaron 25 +KPX rcommaaccent ncommaaccent 25 +KPX rcommaaccent ntilde 25 +KPX rcommaaccent p 30 +KPX rcommaaccent period -50 +KPX rcommaaccent semicolon 30 +KPX rcommaaccent t 40 +KPX rcommaaccent tcommaaccent 40 +KPX rcommaaccent u 15 +KPX rcommaaccent uacute 15 +KPX rcommaaccent ucircumflex 15 +KPX rcommaaccent udieresis 15 +KPX rcommaaccent ugrave 15 +KPX rcommaaccent uhungarumlaut 15 +KPX rcommaaccent umacron 15 +KPX rcommaaccent uogonek 15 +KPX rcommaaccent uring 15 +KPX rcommaaccent v 30 +KPX rcommaaccent y 30 +KPX rcommaaccent yacute 30 +KPX rcommaaccent ydieresis 30 +KPX s comma -15 +KPX s period -15 +KPX s w -30 +KPX sacute comma -15 +KPX sacute period -15 +KPX sacute w -30 +KPX scaron comma -15 +KPX scaron period -15 +KPX scaron w -30 +KPX scedilla comma -15 +KPX scedilla period -15 +KPX scedilla w -30 +KPX scommaaccent comma -15 +KPX scommaaccent period -15 +KPX scommaaccent w -30 +KPX semicolon space -50 +KPX space T -50 +KPX space Tcaron -50 +KPX space Tcommaaccent -50 +KPX space V -50 +KPX space W -40 +KPX space Y -90 +KPX space Yacute -90 +KPX space Ydieresis -90 +KPX space quotedblleft -30 +KPX space quoteleft -60 +KPX v a -25 +KPX v aacute -25 +KPX v abreve -25 +KPX v acircumflex -25 +KPX v adieresis -25 +KPX v agrave -25 +KPX v amacron -25 +KPX v aogonek -25 +KPX v aring -25 +KPX v atilde -25 +KPX v comma -80 +KPX v e -25 +KPX v eacute -25 +KPX v ecaron -25 +KPX v ecircumflex -25 +KPX v edieresis -25 +KPX v edotaccent -25 +KPX v egrave -25 +KPX v emacron -25 +KPX v eogonek -25 +KPX v o -25 +KPX v oacute -25 +KPX v ocircumflex -25 +KPX v odieresis -25 +KPX v ograve -25 +KPX v ohungarumlaut -25 +KPX v omacron -25 +KPX v oslash -25 +KPX v otilde -25 +KPX v period -80 +KPX w a -15 +KPX w aacute -15 +KPX w abreve -15 +KPX w acircumflex -15 +KPX w adieresis -15 +KPX w agrave -15 +KPX w amacron -15 +KPX w aogonek -15 +KPX w aring -15 +KPX w atilde -15 +KPX w comma -60 +KPX w e -10 +KPX w eacute -10 +KPX w ecaron -10 +KPX w ecircumflex -10 +KPX w edieresis -10 +KPX w edotaccent -10 +KPX w egrave -10 +KPX w emacron -10 +KPX w eogonek -10 +KPX w o -10 +KPX w oacute -10 +KPX w ocircumflex -10 +KPX w odieresis -10 +KPX w ograve -10 +KPX w ohungarumlaut -10 +KPX w omacron -10 +KPX w oslash -10 +KPX w otilde -10 +KPX w period -60 +KPX x e -30 +KPX x eacute -30 +KPX x ecaron -30 +KPX x ecircumflex -30 +KPX x edieresis -30 +KPX x edotaccent -30 +KPX x egrave -30 +KPX x emacron -30 +KPX x eogonek -30 +KPX y a -20 +KPX y aacute -20 +KPX y abreve -20 +KPX y acircumflex -20 +KPX y adieresis -20 +KPX y agrave -20 +KPX y amacron -20 +KPX y aogonek -20 +KPX y aring -20 +KPX y atilde -20 +KPX y comma -100 +KPX y e -20 +KPX y eacute -20 +KPX y ecaron -20 +KPX y ecircumflex -20 +KPX y edieresis -20 +KPX y edotaccent -20 +KPX y egrave -20 +KPX y emacron -20 +KPX y eogonek -20 +KPX y o -20 +KPX y oacute -20 +KPX y ocircumflex -20 +KPX y odieresis -20 +KPX y ograve -20 +KPX y ohungarumlaut -20 +KPX y omacron -20 +KPX y oslash -20 +KPX y otilde -20 +KPX y period -100 +KPX yacute a -20 +KPX yacute aacute -20 +KPX yacute abreve -20 +KPX yacute acircumflex -20 +KPX yacute adieresis -20 +KPX yacute agrave -20 +KPX yacute amacron -20 +KPX yacute aogonek -20 +KPX yacute aring -20 +KPX yacute atilde -20 +KPX yacute comma -100 +KPX yacute e -20 +KPX yacute eacute -20 +KPX yacute ecaron -20 +KPX yacute ecircumflex -20 +KPX yacute edieresis -20 +KPX yacute edotaccent -20 +KPX yacute egrave -20 +KPX yacute emacron -20 +KPX yacute eogonek -20 +KPX yacute o -20 +KPX yacute oacute -20 +KPX yacute ocircumflex -20 +KPX yacute odieresis -20 +KPX yacute ograve -20 +KPX yacute ohungarumlaut -20 +KPX yacute omacron -20 +KPX yacute oslash -20 +KPX yacute otilde -20 +KPX yacute period -100 +KPX ydieresis a -20 +KPX ydieresis aacute -20 +KPX ydieresis abreve -20 +KPX ydieresis acircumflex -20 +KPX ydieresis adieresis -20 +KPX ydieresis agrave -20 +KPX ydieresis amacron -20 +KPX ydieresis aogonek -20 +KPX ydieresis aring -20 +KPX ydieresis atilde -20 +KPX ydieresis comma -100 +KPX ydieresis e -20 +KPX ydieresis eacute -20 +KPX ydieresis ecaron -20 +KPX ydieresis ecircumflex -20 +KPX ydieresis edieresis -20 +KPX ydieresis edotaccent -20 +KPX ydieresis egrave -20 +KPX ydieresis emacron -20 +KPX ydieresis eogonek -20 +KPX ydieresis o -20 +KPX ydieresis oacute -20 +KPX ydieresis ocircumflex -20 +KPX ydieresis odieresis -20 +KPX ydieresis ograve -20 +KPX ydieresis ohungarumlaut -20 +KPX ydieresis omacron -20 +KPX ydieresis oslash -20 +KPX ydieresis otilde -20 +KPX ydieresis period -100 +KPX z e -15 +KPX z eacute -15 +KPX z ecaron -15 +KPX z ecircumflex -15 +KPX z edieresis -15 +KPX z edotaccent -15 +KPX z egrave -15 +KPX z emacron -15 +KPX z eogonek -15 +KPX z o -15 +KPX z oacute -15 +KPX z ocircumflex -15 +KPX z odieresis -15 +KPX z ograve -15 +KPX z ohungarumlaut -15 +KPX z omacron -15 +KPX z oslash -15 +KPX z otilde -15 +KPX zacute e -15 +KPX zacute eacute -15 +KPX zacute ecaron -15 +KPX zacute ecircumflex -15 +KPX zacute edieresis -15 +KPX zacute edotaccent -15 +KPX zacute egrave -15 +KPX zacute emacron -15 +KPX zacute eogonek -15 +KPX zacute o -15 +KPX zacute oacute -15 +KPX zacute ocircumflex -15 +KPX zacute odieresis -15 +KPX zacute ograve -15 +KPX zacute ohungarumlaut -15 +KPX zacute omacron -15 +KPX zacute oslash -15 +KPX zacute otilde -15 +KPX zcaron e -15 +KPX zcaron eacute -15 +KPX zcaron ecaron -15 +KPX zcaron ecircumflex -15 +KPX zcaron edieresis -15 +KPX zcaron edotaccent -15 +KPX zcaron egrave -15 +KPX zcaron emacron -15 +KPX zcaron eogonek -15 +KPX zcaron o -15 +KPX zcaron oacute -15 +KPX zcaron ocircumflex -15 +KPX zcaron odieresis -15 +KPX zcaron ograve -15 +KPX zcaron ohungarumlaut -15 +KPX zcaron omacron -15 +KPX zcaron oslash -15 +KPX zcaron otilde -15 +KPX zdotaccent e -15 +KPX zdotaccent eacute -15 +KPX zdotaccent ecaron -15 +KPX zdotaccent ecircumflex -15 +KPX zdotaccent edieresis -15 +KPX zdotaccent edotaccent -15 +KPX zdotaccent egrave -15 +KPX zdotaccent emacron -15 +KPX zdotaccent eogonek -15 +KPX zdotaccent o -15 +KPX zdotaccent oacute -15 +KPX zdotaccent ocircumflex -15 +KPX zdotaccent odieresis -15 +KPX zdotaccent ograve -15 +KPX zdotaccent ohungarumlaut -15 +KPX zdotaccent omacron -15 +KPX zdotaccent oslash -15 +KPX zdotaccent otilde -15 +EndKernPairs +EndKernData +EndFontMetrics diff --git a/internal/corefont/Core14_AFMs/MustRead.html b/internal/corefont/Core14_AFMs/MustRead.html new file mode 100644 index 0000000000000000000000000000000000000000..d4d7e8aa4c6134f6359c0d074721b97342a161a9 --- /dev/null +++ b/internal/corefont/Core14_AFMs/MustRead.html @@ -0,0 +1 @@ + Core 14 AFM Files - ReadMe or
This file and the 14 PostScript(R) AFM files it accompanies may be used, copied, and distributed for any purpose and without charge, with or without modification, provided that all copyright notices are retained; that the AFM files are not distributed without this file; that all modifications to this file or any of the AFM files are prominently noted in the modified file(s); and that this paragraph is not modified. Adobe Systems has no responsibility or obligation to support the use of the AFM files. Col
\ No newline at end of file diff --git a/internal/corefont/Core14_AFMs/Symbol.afm b/internal/corefont/Core14_AFMs/Symbol.afm new file mode 100644 index 0000000000000000000000000000000000000000..6a5386a9190eda3d7e9d4b49cfa7a6dd971c9e70 --- /dev/null +++ b/internal/corefont/Core14_AFMs/Symbol.afm @@ -0,0 +1,213 @@ +StartFontMetrics 4.1 +Comment Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems Incorporated. All rights reserved. +Comment Creation Date: Thu May 1 15:12:25 1997 +Comment UniqueID 43064 +Comment VMusage 30820 39997 +FontName Symbol +FullName Symbol +FamilyName Symbol +Weight Medium +ItalicAngle 0 +IsFixedPitch false +CharacterSet Special +FontBBox -180 -293 1090 1010 +UnderlinePosition -100 +UnderlineThickness 50 +Version 001.008 +Notice Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems Incorporated. All rights reserved. +EncodingScheme FontSpecific +StdHW 92 +StdVW 85 +StartCharMetrics 190 +C 32 ; WX 250 ; N space ; B 0 0 0 0 ; +C 33 ; WX 333 ; N exclam ; B 128 -17 240 672 ; +C 34 ; WX 713 ; N universal ; B 31 0 681 705 ; +C 35 ; WX 500 ; N numbersign ; B 20 -16 481 673 ; +C 36 ; WX 549 ; N existential ; B 25 0 478 707 ; +C 37 ; WX 833 ; N percent ; B 63 -36 771 655 ; +C 38 ; WX 778 ; N ampersand ; B 41 -18 750 661 ; +C 39 ; WX 439 ; N suchthat ; B 48 -17 414 500 ; +C 40 ; WX 333 ; N parenleft ; B 53 -191 300 673 ; +C 41 ; WX 333 ; N parenright ; B 30 -191 277 673 ; +C 42 ; WX 500 ; N asteriskmath ; B 65 134 427 551 ; +C 43 ; WX 549 ; N plus ; B 10 0 539 533 ; +C 44 ; WX 250 ; N comma ; B 56 -152 194 104 ; +C 45 ; WX 549 ; N minus ; B 11 233 535 288 ; +C 46 ; WX 250 ; N period ; B 69 -17 181 95 ; +C 47 ; WX 278 ; N slash ; B 0 -18 254 646 ; +C 48 ; WX 500 ; N zero ; B 24 -14 476 685 ; +C 49 ; WX 500 ; N one ; B 117 0 390 673 ; +C 50 ; WX 500 ; N two ; B 25 0 475 685 ; +C 51 ; WX 500 ; N three ; B 43 -14 435 685 ; +C 52 ; WX 500 ; N four ; B 15 0 469 685 ; +C 53 ; WX 500 ; N five ; B 32 -14 445 690 ; +C 54 ; WX 500 ; N six ; B 34 -14 468 685 ; +C 55 ; WX 500 ; N seven ; B 24 -16 448 673 ; +C 56 ; WX 500 ; N eight ; B 56 -14 445 685 ; +C 57 ; WX 500 ; N nine ; B 30 -18 459 685 ; +C 58 ; WX 278 ; N colon ; B 81 -17 193 460 ; +C 59 ; WX 278 ; N semicolon ; B 83 -152 221 460 ; +C 60 ; WX 549 ; N less ; B 26 0 523 522 ; +C 61 ; WX 549 ; N equal ; B 11 141 537 390 ; +C 62 ; WX 549 ; N greater ; B 26 0 523 522 ; +C 63 ; WX 444 ; N question ; B 70 -17 412 686 ; +C 64 ; WX 549 ; N congruent ; B 11 0 537 475 ; +C 65 ; WX 722 ; N Alpha ; B 4 0 684 673 ; +C 66 ; WX 667 ; N Beta ; B 29 0 592 673 ; +C 67 ; WX 722 ; N Chi ; B -9 0 704 673 ; +C 68 ; WX 612 ; N Delta ; B 6 0 608 688 ; +C 69 ; WX 611 ; N Epsilon ; B 32 0 617 673 ; +C 70 ; WX 763 ; N Phi ; B 26 0 741 673 ; +C 71 ; WX 603 ; N Gamma ; B 24 0 609 673 ; +C 72 ; WX 722 ; N Eta ; B 39 0 729 673 ; +C 73 ; WX 333 ; N Iota ; B 32 0 316 673 ; +C 74 ; WX 631 ; N theta1 ; B 18 -18 623 689 ; +C 75 ; WX 722 ; N Kappa ; B 35 0 722 673 ; +C 76 ; WX 686 ; N Lambda ; B 6 0 680 688 ; +C 77 ; WX 889 ; N Mu ; B 28 0 887 673 ; +C 78 ; WX 722 ; N Nu ; B 29 -8 720 673 ; +C 79 ; WX 722 ; N Omicron ; B 41 -17 715 685 ; +C 80 ; WX 768 ; N Pi ; B 25 0 745 673 ; +C 81 ; WX 741 ; N Theta ; B 41 -17 715 685 ; +C 82 ; WX 556 ; N Rho ; B 28 0 563 673 ; +C 83 ; WX 592 ; N Sigma ; B 5 0 589 673 ; +C 84 ; WX 611 ; N Tau ; B 33 0 607 673 ; +C 85 ; WX 690 ; N Upsilon ; B -8 0 694 673 ; +C 86 ; WX 439 ; N sigma1 ; B 40 -233 436 500 ; +C 87 ; WX 768 ; N Omega ; B 34 0 736 688 ; +C 88 ; WX 645 ; N Xi ; B 40 0 599 673 ; +C 89 ; WX 795 ; N Psi ; B 15 0 781 684 ; +C 90 ; WX 611 ; N Zeta ; B 44 0 636 673 ; +C 91 ; WX 333 ; N bracketleft ; B 86 -155 299 674 ; +C 92 ; WX 863 ; N therefore ; B 163 0 701 487 ; +C 93 ; WX 333 ; N bracketright ; B 33 -155 246 674 ; +C 94 ; WX 658 ; N perpendicular ; B 15 0 652 674 ; +C 95 ; WX 500 ; N underscore ; B -2 -125 502 -75 ; +C 96 ; WX 500 ; N radicalex ; B 480 881 1090 917 ; +C 97 ; WX 631 ; N alpha ; B 41 -18 622 500 ; +C 98 ; WX 549 ; N beta ; B 61 -223 515 741 ; +C 99 ; WX 549 ; N chi ; B 12 -231 522 499 ; +C 100 ; WX 494 ; N delta ; B 40 -19 481 740 ; +C 101 ; WX 439 ; N epsilon ; B 22 -19 427 502 ; +C 102 ; WX 521 ; N phi ; B 28 -224 492 673 ; +C 103 ; WX 411 ; N gamma ; B 5 -225 484 499 ; +C 104 ; WX 603 ; N eta ; B 0 -202 527 514 ; +C 105 ; WX 329 ; N iota ; B 0 -17 301 503 ; +C 106 ; WX 603 ; N phi1 ; B 36 -224 587 499 ; +C 107 ; WX 549 ; N kappa ; B 33 0 558 501 ; +C 108 ; WX 549 ; N lambda ; B 24 -17 548 739 ; +C 109 ; WX 576 ; N mu ; B 33 -223 567 500 ; +C 110 ; WX 521 ; N nu ; B -9 -16 475 507 ; +C 111 ; WX 549 ; N omicron ; B 35 -19 501 499 ; +C 112 ; WX 549 ; N pi ; B 10 -19 530 487 ; +C 113 ; WX 521 ; N theta ; B 43 -17 485 690 ; +C 114 ; WX 549 ; N rho ; B 50 -230 490 499 ; +C 115 ; WX 603 ; N sigma ; B 30 -21 588 500 ; +C 116 ; WX 439 ; N tau ; B 10 -19 418 500 ; +C 117 ; WX 576 ; N upsilon ; B 7 -18 535 507 ; +C 118 ; WX 713 ; N omega1 ; B 12 -18 671 583 ; +C 119 ; WX 686 ; N omega ; B 42 -17 684 500 ; +C 120 ; WX 493 ; N xi ; B 27 -224 469 766 ; +C 121 ; WX 686 ; N psi ; B 12 -228 701 500 ; +C 122 ; WX 494 ; N zeta ; B 60 -225 467 756 ; +C 123 ; WX 480 ; N braceleft ; B 58 -183 397 673 ; +C 124 ; WX 200 ; N bar ; B 65 -293 135 707 ; +C 125 ; WX 480 ; N braceright ; B 79 -183 418 673 ; +C 126 ; WX 549 ; N similar ; B 17 203 529 307 ; +C 160 ; WX 750 ; N Euro ; B 20 -12 714 685 ; +C 161 ; WX 620 ; N Upsilon1 ; B -2 0 610 685 ; +C 162 ; WX 247 ; N minute ; B 27 459 228 735 ; +C 163 ; WX 549 ; N lessequal ; B 29 0 526 639 ; +C 164 ; WX 167 ; N fraction ; B -180 -12 340 677 ; +C 165 ; WX 713 ; N infinity ; B 26 124 688 404 ; +C 166 ; WX 500 ; N florin ; B 2 -193 494 686 ; +C 167 ; WX 753 ; N club ; B 86 -26 660 533 ; +C 168 ; WX 753 ; N diamond ; B 142 -36 600 550 ; +C 169 ; WX 753 ; N heart ; B 117 -33 631 532 ; +C 170 ; WX 753 ; N spade ; B 113 -36 629 548 ; +C 171 ; WX 1042 ; N arrowboth ; B 24 -15 1024 511 ; +C 172 ; WX 987 ; N arrowleft ; B 32 -15 942 511 ; +C 173 ; WX 603 ; N arrowup ; B 45 0 571 910 ; +C 174 ; WX 987 ; N arrowright ; B 49 -15 959 511 ; +C 175 ; WX 603 ; N arrowdown ; B 45 -22 571 888 ; +C 176 ; WX 400 ; N degree ; B 50 385 350 685 ; +C 177 ; WX 549 ; N plusminus ; B 10 0 539 645 ; +C 178 ; WX 411 ; N second ; B 20 459 413 737 ; +C 179 ; WX 549 ; N greaterequal ; B 29 0 526 639 ; +C 180 ; WX 549 ; N multiply ; B 17 8 533 524 ; +C 181 ; WX 713 ; N proportional ; B 27 123 639 404 ; +C 182 ; WX 494 ; N partialdiff ; B 26 -20 462 746 ; +C 183 ; WX 460 ; N bullet ; B 50 113 410 473 ; +C 184 ; WX 549 ; N divide ; B 10 71 536 456 ; +C 185 ; WX 549 ; N notequal ; B 15 -25 540 549 ; +C 186 ; WX 549 ; N equivalence ; B 14 82 538 443 ; +C 187 ; WX 549 ; N approxequal ; B 14 135 527 394 ; +C 188 ; WX 1000 ; N ellipsis ; B 111 -17 889 95 ; +C 189 ; WX 603 ; N arrowvertex ; B 280 -120 336 1010 ; +C 190 ; WX 1000 ; N arrowhorizex ; B -60 220 1050 276 ; +C 191 ; WX 658 ; N carriagereturn ; B 15 -16 602 629 ; +C 192 ; WX 823 ; N aleph ; B 175 -18 661 658 ; +C 193 ; WX 686 ; N Ifraktur ; B 10 -53 578 740 ; +C 194 ; WX 795 ; N Rfraktur ; B 26 -15 759 734 ; +C 195 ; WX 987 ; N weierstrass ; B 159 -211 870 573 ; +C 196 ; WX 768 ; N circlemultiply ; B 43 -17 733 673 ; +C 197 ; WX 768 ; N circleplus ; B 43 -15 733 675 ; +C 198 ; WX 823 ; N emptyset ; B 39 -24 781 719 ; +C 199 ; WX 768 ; N intersection ; B 40 0 732 509 ; +C 200 ; WX 768 ; N union ; B 40 -17 732 492 ; +C 201 ; WX 713 ; N propersuperset ; B 20 0 673 470 ; +C 202 ; WX 713 ; N reflexsuperset ; B 20 -125 673 470 ; +C 203 ; WX 713 ; N notsubset ; B 36 -70 690 540 ; +C 204 ; WX 713 ; N propersubset ; B 37 0 690 470 ; +C 205 ; WX 713 ; N reflexsubset ; B 37 -125 690 470 ; +C 206 ; WX 713 ; N element ; B 45 0 505 468 ; +C 207 ; WX 713 ; N notelement ; B 45 -58 505 555 ; +C 208 ; WX 768 ; N angle ; B 26 0 738 673 ; +C 209 ; WX 713 ; N gradient ; B 36 -19 681 718 ; +C 210 ; WX 790 ; N registerserif ; B 50 -17 740 673 ; +C 211 ; WX 790 ; N copyrightserif ; B 51 -15 741 675 ; +C 212 ; WX 890 ; N trademarkserif ; B 18 293 855 673 ; +C 213 ; WX 823 ; N product ; B 25 -101 803 751 ; +C 214 ; WX 549 ; N radical ; B 10 -38 515 917 ; +C 215 ; WX 250 ; N dotmath ; B 69 210 169 310 ; +C 216 ; WX 713 ; N logicalnot ; B 15 0 680 288 ; +C 217 ; WX 603 ; N logicaland ; B 23 0 583 454 ; +C 218 ; WX 603 ; N logicalor ; B 30 0 578 477 ; +C 219 ; WX 1042 ; N arrowdblboth ; B 27 -20 1023 510 ; +C 220 ; WX 987 ; N arrowdblleft ; B 30 -15 939 513 ; +C 221 ; WX 603 ; N arrowdblup ; B 39 2 567 911 ; +C 222 ; WX 987 ; N arrowdblright ; B 45 -20 954 508 ; +C 223 ; WX 603 ; N arrowdbldown ; B 44 -19 572 890 ; +C 224 ; WX 494 ; N lozenge ; B 18 0 466 745 ; +C 225 ; WX 329 ; N angleleft ; B 25 -198 306 746 ; +C 226 ; WX 790 ; N registersans ; B 50 -20 740 670 ; +C 227 ; WX 790 ; N copyrightsans ; B 49 -15 739 675 ; +C 228 ; WX 786 ; N trademarksans ; B 5 293 725 673 ; +C 229 ; WX 713 ; N summation ; B 14 -108 695 752 ; +C 230 ; WX 384 ; N parenlefttp ; B 24 -293 436 926 ; +C 231 ; WX 384 ; N parenleftex ; B 24 -85 108 925 ; +C 232 ; WX 384 ; N parenleftbt ; B 24 -293 436 926 ; +C 233 ; WX 384 ; N bracketlefttp ; B 0 -80 349 926 ; +C 234 ; WX 384 ; N bracketleftex ; B 0 -79 77 925 ; +C 235 ; WX 384 ; N bracketleftbt ; B 0 -80 349 926 ; +C 236 ; WX 494 ; N bracelefttp ; B 209 -85 445 925 ; +C 237 ; WX 494 ; N braceleftmid ; B 20 -85 284 935 ; +C 238 ; WX 494 ; N braceleftbt ; B 209 -75 445 935 ; +C 239 ; WX 494 ; N braceex ; B 209 -85 284 935 ; +C 241 ; WX 329 ; N angleright ; B 21 -198 302 746 ; +C 242 ; WX 274 ; N integral ; B 2 -107 291 916 ; +C 243 ; WX 686 ; N integraltp ; B 308 -88 675 920 ; +C 244 ; WX 686 ; N integralex ; B 308 -88 378 975 ; +C 245 ; WX 686 ; N integralbt ; B 11 -87 378 921 ; +C 246 ; WX 384 ; N parenrighttp ; B 54 -293 466 926 ; +C 247 ; WX 384 ; N parenrightex ; B 382 -85 466 925 ; +C 248 ; WX 384 ; N parenrightbt ; B 54 -293 466 926 ; +C 249 ; WX 384 ; N bracketrighttp ; B 22 -80 371 926 ; +C 250 ; WX 384 ; N bracketrightex ; B 294 -79 371 925 ; +C 251 ; WX 384 ; N bracketrightbt ; B 22 -80 371 926 ; +C 252 ; WX 494 ; N bracerighttp ; B 48 -85 284 925 ; +C 253 ; WX 494 ; N bracerightmid ; B 209 -85 473 935 ; +C 254 ; WX 494 ; N bracerightbt ; B 48 -75 284 935 ; +C -1 ; WX 790 ; N apple ; B 56 -3 733 808 ; +EndCharMetrics +EndFontMetrics diff --git a/internal/corefont/Core14_AFMs/Times-Bold.afm b/internal/corefont/Core14_AFMs/Times-Bold.afm new file mode 100644 index 0000000000000000000000000000000000000000..559ebaeb6f099c547502c0e5186df745e36774a7 --- /dev/null +++ b/internal/corefont/Core14_AFMs/Times-Bold.afm @@ -0,0 +1,2588 @@ +StartFontMetrics 4.1 +Comment Copyright (c) 1985, 1987, 1989, 1990, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved. +Comment Creation Date: Thu May 1 12:52:56 1997 +Comment UniqueID 43065 +Comment VMusage 41636 52661 +FontName Times-Bold +FullName Times Bold +FamilyName Times +Weight Bold +ItalicAngle 0 +IsFixedPitch false +CharacterSet ExtendedRoman +FontBBox -168 -218 1000 935 +UnderlinePosition -100 +UnderlineThickness 50 +Version 002.000 +Notice Copyright (c) 1985, 1987, 1989, 1990, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved.Times is a trademark of Linotype-Hell AG and/or its subsidiaries. +EncodingScheme AdobeStandardEncoding +CapHeight 676 +XHeight 461 +Ascender 683 +Descender -217 +StdHW 44 +StdVW 139 +StartCharMetrics 315 +C 32 ; WX 250 ; N space ; B 0 0 0 0 ; +C 33 ; WX 333 ; N exclam ; B 81 -13 251 691 ; +C 34 ; WX 555 ; N quotedbl ; B 83 404 472 691 ; +C 35 ; WX 500 ; N numbersign ; B 4 0 496 700 ; +C 36 ; WX 500 ; N dollar ; B 29 -99 472 750 ; +C 37 ; WX 1000 ; N percent ; B 124 -14 877 692 ; +C 38 ; WX 833 ; N ampersand ; B 62 -16 787 691 ; +C 39 ; WX 333 ; N quoteright ; B 79 356 263 691 ; +C 40 ; WX 333 ; N parenleft ; B 46 -168 306 694 ; +C 41 ; WX 333 ; N parenright ; B 27 -168 287 694 ; +C 42 ; WX 500 ; N asterisk ; B 56 255 447 691 ; +C 43 ; WX 570 ; N plus ; B 33 0 537 506 ; +C 44 ; WX 250 ; N comma ; B 39 -180 223 155 ; +C 45 ; WX 333 ; N hyphen ; B 44 171 287 287 ; +C 46 ; WX 250 ; N period ; B 41 -13 210 156 ; +C 47 ; WX 278 ; N slash ; B -24 -19 302 691 ; +C 48 ; WX 500 ; N zero ; B 24 -13 476 688 ; +C 49 ; WX 500 ; N one ; B 65 0 442 688 ; +C 50 ; WX 500 ; N two ; B 17 0 478 688 ; +C 51 ; WX 500 ; N three ; B 16 -14 468 688 ; +C 52 ; WX 500 ; N four ; B 19 0 475 688 ; +C 53 ; WX 500 ; N five ; B 22 -8 470 676 ; +C 54 ; WX 500 ; N six ; B 28 -13 475 688 ; +C 55 ; WX 500 ; N seven ; B 17 0 477 676 ; +C 56 ; WX 500 ; N eight ; B 28 -13 472 688 ; +C 57 ; WX 500 ; N nine ; B 26 -13 473 688 ; +C 58 ; WX 333 ; N colon ; B 82 -13 251 472 ; +C 59 ; WX 333 ; N semicolon ; B 82 -180 266 472 ; +C 60 ; WX 570 ; N less ; B 31 -8 539 514 ; +C 61 ; WX 570 ; N equal ; B 33 107 537 399 ; +C 62 ; WX 570 ; N greater ; B 31 -8 539 514 ; +C 63 ; WX 500 ; N question ; B 57 -13 445 689 ; +C 64 ; WX 930 ; N at ; B 108 -19 822 691 ; +C 65 ; WX 722 ; N A ; B 9 0 689 690 ; +C 66 ; WX 667 ; N B ; B 16 0 619 676 ; +C 67 ; WX 722 ; N C ; B 49 -19 687 691 ; +C 68 ; WX 722 ; N D ; B 14 0 690 676 ; +C 69 ; WX 667 ; N E ; B 16 0 641 676 ; +C 70 ; WX 611 ; N F ; B 16 0 583 676 ; +C 71 ; WX 778 ; N G ; B 37 -19 755 691 ; +C 72 ; WX 778 ; N H ; B 21 0 759 676 ; +C 73 ; WX 389 ; N I ; B 20 0 370 676 ; +C 74 ; WX 500 ; N J ; B 3 -96 479 676 ; +C 75 ; WX 778 ; N K ; B 30 0 769 676 ; +C 76 ; WX 667 ; N L ; B 19 0 638 676 ; +C 77 ; WX 944 ; N M ; B 14 0 921 676 ; +C 78 ; WX 722 ; N N ; B 16 -18 701 676 ; +C 79 ; WX 778 ; N O ; B 35 -19 743 691 ; +C 80 ; WX 611 ; N P ; B 16 0 600 676 ; +C 81 ; WX 778 ; N Q ; B 35 -176 743 691 ; +C 82 ; WX 722 ; N R ; B 26 0 715 676 ; +C 83 ; WX 556 ; N S ; B 35 -19 513 692 ; +C 84 ; WX 667 ; N T ; B 31 0 636 676 ; +C 85 ; WX 722 ; N U ; B 16 -19 701 676 ; +C 86 ; WX 722 ; N V ; B 16 -18 701 676 ; +C 87 ; WX 1000 ; N W ; B 19 -15 981 676 ; +C 88 ; WX 722 ; N X ; B 16 0 699 676 ; +C 89 ; WX 722 ; N Y ; B 15 0 699 676 ; +C 90 ; WX 667 ; N Z ; B 28 0 634 676 ; +C 91 ; WX 333 ; N bracketleft ; B 67 -149 301 678 ; +C 92 ; WX 278 ; N backslash ; B -25 -19 303 691 ; +C 93 ; WX 333 ; N bracketright ; B 32 -149 266 678 ; +C 94 ; WX 581 ; N asciicircum ; B 73 311 509 676 ; +C 95 ; WX 500 ; N underscore ; B 0 -125 500 -75 ; +C 96 ; WX 333 ; N quoteleft ; B 70 356 254 691 ; +C 97 ; WX 500 ; N a ; B 25 -14 488 473 ; +C 98 ; WX 556 ; N b ; B 17 -14 521 676 ; +C 99 ; WX 444 ; N c ; B 25 -14 430 473 ; +C 100 ; WX 556 ; N d ; B 25 -14 534 676 ; +C 101 ; WX 444 ; N e ; B 25 -14 426 473 ; +C 102 ; WX 333 ; N f ; B 14 0 389 691 ; L i fi ; L l fl ; +C 103 ; WX 500 ; N g ; B 28 -206 483 473 ; +C 104 ; WX 556 ; N h ; B 16 0 534 676 ; +C 105 ; WX 278 ; N i ; B 16 0 255 691 ; +C 106 ; WX 333 ; N j ; B -57 -203 263 691 ; +C 107 ; WX 556 ; N k ; B 22 0 543 676 ; +C 108 ; WX 278 ; N l ; B 16 0 255 676 ; +C 109 ; WX 833 ; N m ; B 16 0 814 473 ; +C 110 ; WX 556 ; N n ; B 21 0 539 473 ; +C 111 ; WX 500 ; N o ; B 25 -14 476 473 ; +C 112 ; WX 556 ; N p ; B 19 -205 524 473 ; +C 113 ; WX 556 ; N q ; B 34 -205 536 473 ; +C 114 ; WX 444 ; N r ; B 29 0 434 473 ; +C 115 ; WX 389 ; N s ; B 25 -14 361 473 ; +C 116 ; WX 333 ; N t ; B 20 -12 332 630 ; +C 117 ; WX 556 ; N u ; B 16 -14 537 461 ; +C 118 ; WX 500 ; N v ; B 21 -14 485 461 ; +C 119 ; WX 722 ; N w ; B 23 -14 707 461 ; +C 120 ; WX 500 ; N x ; B 12 0 484 461 ; +C 121 ; WX 500 ; N y ; B 16 -205 480 461 ; +C 122 ; WX 444 ; N z ; B 21 0 420 461 ; +C 123 ; WX 394 ; N braceleft ; B 22 -175 340 698 ; +C 124 ; WX 220 ; N bar ; B 66 -218 154 782 ; +C 125 ; WX 394 ; N braceright ; B 54 -175 372 698 ; +C 126 ; WX 520 ; N asciitilde ; B 29 173 491 333 ; +C 161 ; WX 333 ; N exclamdown ; B 82 -203 252 501 ; +C 162 ; WX 500 ; N cent ; B 53 -140 458 588 ; +C 163 ; WX 500 ; N sterling ; B 21 -14 477 684 ; +C 164 ; WX 167 ; N fraction ; B -168 -12 329 688 ; +C 165 ; WX 500 ; N yen ; B -64 0 547 676 ; +C 166 ; WX 500 ; N florin ; B 0 -155 498 706 ; +C 167 ; WX 500 ; N section ; B 57 -132 443 691 ; +C 168 ; WX 500 ; N currency ; B -26 61 526 613 ; +C 169 ; WX 278 ; N quotesingle ; B 75 404 204 691 ; +C 170 ; WX 500 ; N quotedblleft ; B 32 356 486 691 ; +C 171 ; WX 500 ; N guillemotleft ; B 23 36 473 415 ; +C 172 ; WX 333 ; N guilsinglleft ; B 51 36 305 415 ; +C 173 ; WX 333 ; N guilsinglright ; B 28 36 282 415 ; +C 174 ; WX 556 ; N fi ; B 14 0 536 691 ; +C 175 ; WX 556 ; N fl ; B 14 0 536 691 ; +C 177 ; WX 500 ; N endash ; B 0 181 500 271 ; +C 178 ; WX 500 ; N dagger ; B 47 -134 453 691 ; +C 179 ; WX 500 ; N daggerdbl ; B 45 -132 456 691 ; +C 180 ; WX 250 ; N periodcentered ; B 41 248 210 417 ; +C 182 ; WX 540 ; N paragraph ; B 0 -186 519 676 ; +C 183 ; WX 350 ; N bullet ; B 35 198 315 478 ; +C 184 ; WX 333 ; N quotesinglbase ; B 79 -180 263 155 ; +C 185 ; WX 500 ; N quotedblbase ; B 14 -180 468 155 ; +C 186 ; WX 500 ; N quotedblright ; B 14 356 468 691 ; +C 187 ; WX 500 ; N guillemotright ; B 27 36 477 415 ; +C 188 ; WX 1000 ; N ellipsis ; B 82 -13 917 156 ; +C 189 ; WX 1000 ; N perthousand ; B 7 -29 995 706 ; +C 191 ; WX 500 ; N questiondown ; B 55 -201 443 501 ; +C 193 ; WX 333 ; N grave ; B 8 528 246 713 ; +C 194 ; WX 333 ; N acute ; B 86 528 324 713 ; +C 195 ; WX 333 ; N circumflex ; B -2 528 335 704 ; +C 196 ; WX 333 ; N tilde ; B -16 547 349 674 ; +C 197 ; WX 333 ; N macron ; B 1 565 331 637 ; +C 198 ; WX 333 ; N breve ; B 15 528 318 691 ; +C 199 ; WX 333 ; N dotaccent ; B 103 536 258 691 ; +C 200 ; WX 333 ; N dieresis ; B -2 537 335 667 ; +C 202 ; WX 333 ; N ring ; B 60 527 273 740 ; +C 203 ; WX 333 ; N cedilla ; B 68 -218 294 0 ; +C 205 ; WX 333 ; N hungarumlaut ; B -13 528 425 713 ; +C 206 ; WX 333 ; N ogonek ; B 90 -193 319 24 ; +C 207 ; WX 333 ; N caron ; B -2 528 335 704 ; +C 208 ; WX 1000 ; N emdash ; B 0 181 1000 271 ; +C 225 ; WX 1000 ; N AE ; B 4 0 951 676 ; +C 227 ; WX 300 ; N ordfeminine ; B -1 397 301 688 ; +C 232 ; WX 667 ; N Lslash ; B 19 0 638 676 ; +C 233 ; WX 778 ; N Oslash ; B 35 -74 743 737 ; +C 234 ; WX 1000 ; N OE ; B 22 -5 981 684 ; +C 235 ; WX 330 ; N ordmasculine ; B 18 397 312 688 ; +C 241 ; WX 722 ; N ae ; B 33 -14 693 473 ; +C 245 ; WX 278 ; N dotlessi ; B 16 0 255 461 ; +C 248 ; WX 278 ; N lslash ; B -22 0 303 676 ; +C 249 ; WX 500 ; N oslash ; B 25 -92 476 549 ; +C 250 ; WX 722 ; N oe ; B 22 -14 696 473 ; +C 251 ; WX 556 ; N germandbls ; B 19 -12 517 691 ; +C -1 ; WX 389 ; N Idieresis ; B 20 0 370 877 ; +C -1 ; WX 444 ; N eacute ; B 25 -14 426 713 ; +C -1 ; WX 500 ; N abreve ; B 25 -14 488 691 ; +C -1 ; WX 556 ; N uhungarumlaut ; B 16 -14 557 713 ; +C -1 ; WX 444 ; N ecaron ; B 25 -14 426 704 ; +C -1 ; WX 722 ; N Ydieresis ; B 15 0 699 877 ; +C -1 ; WX 570 ; N divide ; B 33 -31 537 537 ; +C -1 ; WX 722 ; N Yacute ; B 15 0 699 923 ; +C -1 ; WX 722 ; N Acircumflex ; B 9 0 689 914 ; +C -1 ; WX 500 ; N aacute ; B 25 -14 488 713 ; +C -1 ; WX 722 ; N Ucircumflex ; B 16 -19 701 914 ; +C -1 ; WX 500 ; N yacute ; B 16 -205 480 713 ; +C -1 ; WX 389 ; N scommaaccent ; B 25 -218 361 473 ; +C -1 ; WX 444 ; N ecircumflex ; B 25 -14 426 704 ; +C -1 ; WX 722 ; N Uring ; B 16 -19 701 935 ; +C -1 ; WX 722 ; N Udieresis ; B 16 -19 701 877 ; +C -1 ; WX 500 ; N aogonek ; B 25 -193 504 473 ; +C -1 ; WX 722 ; N Uacute ; B 16 -19 701 923 ; +C -1 ; WX 556 ; N uogonek ; B 16 -193 539 461 ; +C -1 ; WX 667 ; N Edieresis ; B 16 0 641 877 ; +C -1 ; WX 722 ; N Dcroat ; B 6 0 690 676 ; +C -1 ; WX 250 ; N commaaccent ; B 47 -218 203 -50 ; +C -1 ; WX 747 ; N copyright ; B 26 -19 721 691 ; +C -1 ; WX 667 ; N Emacron ; B 16 0 641 847 ; +C -1 ; WX 444 ; N ccaron ; B 25 -14 430 704 ; +C -1 ; WX 500 ; N aring ; B 25 -14 488 740 ; +C -1 ; WX 722 ; N Ncommaaccent ; B 16 -188 701 676 ; +C -1 ; WX 278 ; N lacute ; B 16 0 297 923 ; +C -1 ; WX 500 ; N agrave ; B 25 -14 488 713 ; +C -1 ; WX 667 ; N Tcommaaccent ; B 31 -218 636 676 ; +C -1 ; WX 722 ; N Cacute ; B 49 -19 687 923 ; +C -1 ; WX 500 ; N atilde ; B 25 -14 488 674 ; +C -1 ; WX 667 ; N Edotaccent ; B 16 0 641 901 ; +C -1 ; WX 389 ; N scaron ; B 25 -14 363 704 ; +C -1 ; WX 389 ; N scedilla ; B 25 -218 361 473 ; +C -1 ; WX 278 ; N iacute ; B 16 0 289 713 ; +C -1 ; WX 494 ; N lozenge ; B 10 0 484 745 ; +C -1 ; WX 722 ; N Rcaron ; B 26 0 715 914 ; +C -1 ; WX 778 ; N Gcommaaccent ; B 37 -218 755 691 ; +C -1 ; WX 556 ; N ucircumflex ; B 16 -14 537 704 ; +C -1 ; WX 500 ; N acircumflex ; B 25 -14 488 704 ; +C -1 ; WX 722 ; N Amacron ; B 9 0 689 847 ; +C -1 ; WX 444 ; N rcaron ; B 29 0 434 704 ; +C -1 ; WX 444 ; N ccedilla ; B 25 -218 430 473 ; +C -1 ; WX 667 ; N Zdotaccent ; B 28 0 634 901 ; +C -1 ; WX 611 ; N Thorn ; B 16 0 600 676 ; +C -1 ; WX 778 ; N Omacron ; B 35 -19 743 847 ; +C -1 ; WX 722 ; N Racute ; B 26 0 715 923 ; +C -1 ; WX 556 ; N Sacute ; B 35 -19 513 923 ; +C -1 ; WX 672 ; N dcaron ; B 25 -14 681 682 ; +C -1 ; WX 722 ; N Umacron ; B 16 -19 701 847 ; +C -1 ; WX 556 ; N uring ; B 16 -14 537 740 ; +C -1 ; WX 300 ; N threesuperior ; B 3 268 297 688 ; +C -1 ; WX 778 ; N Ograve ; B 35 -19 743 923 ; +C -1 ; WX 722 ; N Agrave ; B 9 0 689 923 ; +C -1 ; WX 722 ; N Abreve ; B 9 0 689 901 ; +C -1 ; WX 570 ; N multiply ; B 48 16 522 490 ; +C -1 ; WX 556 ; N uacute ; B 16 -14 537 713 ; +C -1 ; WX 667 ; N Tcaron ; B 31 0 636 914 ; +C -1 ; WX 494 ; N partialdiff ; B 11 -21 494 750 ; +C -1 ; WX 500 ; N ydieresis ; B 16 -205 480 667 ; +C -1 ; WX 722 ; N Nacute ; B 16 -18 701 923 ; +C -1 ; WX 278 ; N icircumflex ; B -37 0 300 704 ; +C -1 ; WX 667 ; N Ecircumflex ; B 16 0 641 914 ; +C -1 ; WX 500 ; N adieresis ; B 25 -14 488 667 ; +C -1 ; WX 444 ; N edieresis ; B 25 -14 426 667 ; +C -1 ; WX 444 ; N cacute ; B 25 -14 430 713 ; +C -1 ; WX 556 ; N nacute ; B 21 0 539 713 ; +C -1 ; WX 556 ; N umacron ; B 16 -14 537 637 ; +C -1 ; WX 722 ; N Ncaron ; B 16 -18 701 914 ; +C -1 ; WX 389 ; N Iacute ; B 20 0 370 923 ; +C -1 ; WX 570 ; N plusminus ; B 33 0 537 506 ; +C -1 ; WX 220 ; N brokenbar ; B 66 -143 154 707 ; +C -1 ; WX 747 ; N registered ; B 26 -19 721 691 ; +C -1 ; WX 778 ; N Gbreve ; B 37 -19 755 901 ; +C -1 ; WX 389 ; N Idotaccent ; B 20 0 370 901 ; +C -1 ; WX 600 ; N summation ; B 14 -10 585 706 ; +C -1 ; WX 667 ; N Egrave ; B 16 0 641 923 ; +C -1 ; WX 444 ; N racute ; B 29 0 434 713 ; +C -1 ; WX 500 ; N omacron ; B 25 -14 476 637 ; +C -1 ; WX 667 ; N Zacute ; B 28 0 634 923 ; +C -1 ; WX 667 ; N Zcaron ; B 28 0 634 914 ; +C -1 ; WX 549 ; N greaterequal ; B 26 0 523 704 ; +C -1 ; WX 722 ; N Eth ; B 6 0 690 676 ; +C -1 ; WX 722 ; N Ccedilla ; B 49 -218 687 691 ; +C -1 ; WX 278 ; N lcommaaccent ; B 16 -218 255 676 ; +C -1 ; WX 416 ; N tcaron ; B 20 -12 425 815 ; +C -1 ; WX 444 ; N eogonek ; B 25 -193 426 473 ; +C -1 ; WX 722 ; N Uogonek ; B 16 -193 701 676 ; +C -1 ; WX 722 ; N Aacute ; B 9 0 689 923 ; +C -1 ; WX 722 ; N Adieresis ; B 9 0 689 877 ; +C -1 ; WX 444 ; N egrave ; B 25 -14 426 713 ; +C -1 ; WX 444 ; N zacute ; B 21 0 420 713 ; +C -1 ; WX 278 ; N iogonek ; B 16 -193 274 691 ; +C -1 ; WX 778 ; N Oacute ; B 35 -19 743 923 ; +C -1 ; WX 500 ; N oacute ; B 25 -14 476 713 ; +C -1 ; WX 500 ; N amacron ; B 25 -14 488 637 ; +C -1 ; WX 389 ; N sacute ; B 25 -14 361 713 ; +C -1 ; WX 278 ; N idieresis ; B -37 0 300 667 ; +C -1 ; WX 778 ; N Ocircumflex ; B 35 -19 743 914 ; +C -1 ; WX 722 ; N Ugrave ; B 16 -19 701 923 ; +C -1 ; WX 612 ; N Delta ; B 6 0 608 688 ; +C -1 ; WX 556 ; N thorn ; B 19 -205 524 676 ; +C -1 ; WX 300 ; N twosuperior ; B 0 275 300 688 ; +C -1 ; WX 778 ; N Odieresis ; B 35 -19 743 877 ; +C -1 ; WX 556 ; N mu ; B 33 -206 536 461 ; +C -1 ; WX 278 ; N igrave ; B -27 0 255 713 ; +C -1 ; WX 500 ; N ohungarumlaut ; B 25 -14 529 713 ; +C -1 ; WX 667 ; N Eogonek ; B 16 -193 644 676 ; +C -1 ; WX 556 ; N dcroat ; B 25 -14 534 676 ; +C -1 ; WX 750 ; N threequarters ; B 23 -12 733 688 ; +C -1 ; WX 556 ; N Scedilla ; B 35 -218 513 692 ; +C -1 ; WX 394 ; N lcaron ; B 16 0 412 682 ; +C -1 ; WX 778 ; N Kcommaaccent ; B 30 -218 769 676 ; +C -1 ; WX 667 ; N Lacute ; B 19 0 638 923 ; +C -1 ; WX 1000 ; N trademark ; B 24 271 977 676 ; +C -1 ; WX 444 ; N edotaccent ; B 25 -14 426 691 ; +C -1 ; WX 389 ; N Igrave ; B 20 0 370 923 ; +C -1 ; WX 389 ; N Imacron ; B 20 0 370 847 ; +C -1 ; WX 667 ; N Lcaron ; B 19 0 652 682 ; +C -1 ; WX 750 ; N onehalf ; B -7 -12 775 688 ; +C -1 ; WX 549 ; N lessequal ; B 29 0 526 704 ; +C -1 ; WX 500 ; N ocircumflex ; B 25 -14 476 704 ; +C -1 ; WX 556 ; N ntilde ; B 21 0 539 674 ; +C -1 ; WX 722 ; N Uhungarumlaut ; B 16 -19 701 923 ; +C -1 ; WX 667 ; N Eacute ; B 16 0 641 923 ; +C -1 ; WX 444 ; N emacron ; B 25 -14 426 637 ; +C -1 ; WX 500 ; N gbreve ; B 28 -206 483 691 ; +C -1 ; WX 750 ; N onequarter ; B 28 -12 743 688 ; +C -1 ; WX 556 ; N Scaron ; B 35 -19 513 914 ; +C -1 ; WX 556 ; N Scommaaccent ; B 35 -218 513 692 ; +C -1 ; WX 778 ; N Ohungarumlaut ; B 35 -19 743 923 ; +C -1 ; WX 400 ; N degree ; B 57 402 343 688 ; +C -1 ; WX 500 ; N ograve ; B 25 -14 476 713 ; +C -1 ; WX 722 ; N Ccaron ; B 49 -19 687 914 ; +C -1 ; WX 556 ; N ugrave ; B 16 -14 537 713 ; +C -1 ; WX 549 ; N radical ; B 10 -46 512 850 ; +C -1 ; WX 722 ; N Dcaron ; B 14 0 690 914 ; +C -1 ; WX 444 ; N rcommaaccent ; B 29 -218 434 473 ; +C -1 ; WX 722 ; N Ntilde ; B 16 -18 701 884 ; +C -1 ; WX 500 ; N otilde ; B 25 -14 476 674 ; +C -1 ; WX 722 ; N Rcommaaccent ; B 26 -218 715 676 ; +C -1 ; WX 667 ; N Lcommaaccent ; B 19 -218 638 676 ; +C -1 ; WX 722 ; N Atilde ; B 9 0 689 884 ; +C -1 ; WX 722 ; N Aogonek ; B 9 -193 699 690 ; +C -1 ; WX 722 ; N Aring ; B 9 0 689 935 ; +C -1 ; WX 778 ; N Otilde ; B 35 -19 743 884 ; +C -1 ; WX 444 ; N zdotaccent ; B 21 0 420 691 ; +C -1 ; WX 667 ; N Ecaron ; B 16 0 641 914 ; +C -1 ; WX 389 ; N Iogonek ; B 20 -193 370 676 ; +C -1 ; WX 556 ; N kcommaaccent ; B 22 -218 543 676 ; +C -1 ; WX 570 ; N minus ; B 33 209 537 297 ; +C -1 ; WX 389 ; N Icircumflex ; B 20 0 370 914 ; +C -1 ; WX 556 ; N ncaron ; B 21 0 539 704 ; +C -1 ; WX 333 ; N tcommaaccent ; B 20 -218 332 630 ; +C -1 ; WX 570 ; N logicalnot ; B 33 108 537 399 ; +C -1 ; WX 500 ; N odieresis ; B 25 -14 476 667 ; +C -1 ; WX 556 ; N udieresis ; B 16 -14 537 667 ; +C -1 ; WX 549 ; N notequal ; B 15 -49 540 570 ; +C -1 ; WX 500 ; N gcommaaccent ; B 28 -206 483 829 ; +C -1 ; WX 500 ; N eth ; B 25 -14 476 691 ; +C -1 ; WX 444 ; N zcaron ; B 21 0 420 704 ; +C -1 ; WX 556 ; N ncommaaccent ; B 21 -218 539 473 ; +C -1 ; WX 300 ; N onesuperior ; B 28 275 273 688 ; +C -1 ; WX 278 ; N imacron ; B -8 0 272 637 ; +C -1 ; WX 500 ; N Euro ; B 0 0 0 0 ; +EndCharMetrics +StartKernData +StartKernPairs 2242 +KPX A C -55 +KPX A Cacute -55 +KPX A Ccaron -55 +KPX A Ccedilla -55 +KPX A G -55 +KPX A Gbreve -55 +KPX A Gcommaaccent -55 +KPX A O -45 +KPX A Oacute -45 +KPX A Ocircumflex -45 +KPX A Odieresis -45 +KPX A Ograve -45 +KPX A Ohungarumlaut -45 +KPX A Omacron -45 +KPX A Oslash -45 +KPX A Otilde -45 +KPX A Q -45 +KPX A T -95 +KPX A Tcaron -95 +KPX A Tcommaaccent -95 +KPX A U -50 +KPX A Uacute -50 +KPX A Ucircumflex -50 +KPX A Udieresis -50 +KPX A Ugrave -50 +KPX A Uhungarumlaut -50 +KPX A Umacron -50 +KPX A Uogonek -50 +KPX A Uring -50 +KPX A V -145 +KPX A W -130 +KPX A Y -100 +KPX A Yacute -100 +KPX A Ydieresis -100 +KPX A p -25 +KPX A quoteright -74 +KPX A u -50 +KPX A uacute -50 +KPX A ucircumflex -50 +KPX A udieresis -50 +KPX A ugrave -50 +KPX A uhungarumlaut -50 +KPX A umacron -50 +KPX A uogonek -50 +KPX A uring -50 +KPX A v -100 +KPX A w -90 +KPX A y -74 +KPX A yacute -74 +KPX A ydieresis -74 +KPX Aacute C -55 +KPX Aacute Cacute -55 +KPX Aacute Ccaron -55 +KPX Aacute Ccedilla -55 +KPX Aacute G -55 +KPX Aacute Gbreve -55 +KPX Aacute Gcommaaccent -55 +KPX Aacute O -45 +KPX Aacute Oacute -45 +KPX Aacute Ocircumflex -45 +KPX Aacute Odieresis -45 +KPX Aacute Ograve -45 +KPX Aacute Ohungarumlaut -45 +KPX Aacute Omacron -45 +KPX Aacute Oslash -45 +KPX Aacute Otilde -45 +KPX Aacute Q -45 +KPX Aacute T -95 +KPX Aacute Tcaron -95 +KPX Aacute Tcommaaccent -95 +KPX Aacute U -50 +KPX Aacute Uacute -50 +KPX Aacute Ucircumflex -50 +KPX Aacute Udieresis -50 +KPX Aacute Ugrave -50 +KPX Aacute Uhungarumlaut -50 +KPX Aacute Umacron -50 +KPX Aacute Uogonek -50 +KPX Aacute Uring -50 +KPX Aacute V -145 +KPX Aacute W -130 +KPX Aacute Y -100 +KPX Aacute Yacute -100 +KPX Aacute Ydieresis -100 +KPX Aacute p -25 +KPX Aacute quoteright -74 +KPX Aacute u -50 +KPX Aacute uacute -50 +KPX Aacute ucircumflex -50 +KPX Aacute udieresis -50 +KPX Aacute ugrave -50 +KPX Aacute uhungarumlaut -50 +KPX Aacute umacron -50 +KPX Aacute uogonek -50 +KPX Aacute uring -50 +KPX Aacute v -100 +KPX Aacute w -90 +KPX Aacute y -74 +KPX Aacute yacute -74 +KPX Aacute ydieresis -74 +KPX Abreve C -55 +KPX Abreve Cacute -55 +KPX Abreve Ccaron -55 +KPX Abreve Ccedilla -55 +KPX Abreve G -55 +KPX Abreve Gbreve -55 +KPX Abreve Gcommaaccent -55 +KPX Abreve O -45 +KPX Abreve Oacute -45 +KPX Abreve Ocircumflex -45 +KPX Abreve Odieresis -45 +KPX Abreve Ograve -45 +KPX Abreve Ohungarumlaut -45 +KPX Abreve Omacron -45 +KPX Abreve Oslash -45 +KPX Abreve Otilde -45 +KPX Abreve Q -45 +KPX Abreve T -95 +KPX Abreve Tcaron -95 +KPX Abreve Tcommaaccent -95 +KPX Abreve U -50 +KPX Abreve Uacute -50 +KPX Abreve Ucircumflex -50 +KPX Abreve Udieresis -50 +KPX Abreve Ugrave -50 +KPX Abreve Uhungarumlaut -50 +KPX Abreve Umacron -50 +KPX Abreve Uogonek -50 +KPX Abreve Uring -50 +KPX Abreve V -145 +KPX Abreve W -130 +KPX Abreve Y -100 +KPX Abreve Yacute -100 +KPX Abreve Ydieresis -100 +KPX Abreve p -25 +KPX Abreve quoteright -74 +KPX Abreve u -50 +KPX Abreve uacute -50 +KPX Abreve ucircumflex -50 +KPX Abreve udieresis -50 +KPX Abreve ugrave -50 +KPX Abreve uhungarumlaut -50 +KPX Abreve umacron -50 +KPX Abreve uogonek -50 +KPX Abreve uring -50 +KPX Abreve v -100 +KPX Abreve w -90 +KPX Abreve y -74 +KPX Abreve yacute -74 +KPX Abreve ydieresis -74 +KPX Acircumflex C -55 +KPX Acircumflex Cacute -55 +KPX Acircumflex Ccaron -55 +KPX Acircumflex Ccedilla -55 +KPX Acircumflex G -55 +KPX Acircumflex Gbreve -55 +KPX Acircumflex Gcommaaccent -55 +KPX Acircumflex O -45 +KPX Acircumflex Oacute -45 +KPX Acircumflex Ocircumflex -45 +KPX Acircumflex Odieresis -45 +KPX Acircumflex Ograve -45 +KPX Acircumflex Ohungarumlaut -45 +KPX Acircumflex Omacron -45 +KPX Acircumflex Oslash -45 +KPX Acircumflex Otilde -45 +KPX Acircumflex Q -45 +KPX Acircumflex T -95 +KPX Acircumflex Tcaron -95 +KPX Acircumflex Tcommaaccent -95 +KPX Acircumflex U -50 +KPX Acircumflex Uacute -50 +KPX Acircumflex Ucircumflex -50 +KPX Acircumflex Udieresis -50 +KPX Acircumflex Ugrave -50 +KPX Acircumflex Uhungarumlaut -50 +KPX Acircumflex Umacron -50 +KPX Acircumflex Uogonek -50 +KPX Acircumflex Uring -50 +KPX Acircumflex V -145 +KPX Acircumflex W -130 +KPX Acircumflex Y -100 +KPX Acircumflex Yacute -100 +KPX Acircumflex Ydieresis -100 +KPX Acircumflex p -25 +KPX Acircumflex quoteright -74 +KPX Acircumflex u -50 +KPX Acircumflex uacute -50 +KPX Acircumflex ucircumflex -50 +KPX Acircumflex udieresis -50 +KPX Acircumflex ugrave -50 +KPX Acircumflex uhungarumlaut -50 +KPX Acircumflex umacron -50 +KPX Acircumflex uogonek -50 +KPX Acircumflex uring -50 +KPX Acircumflex v -100 +KPX Acircumflex w -90 +KPX Acircumflex y -74 +KPX Acircumflex yacute -74 +KPX Acircumflex ydieresis -74 +KPX Adieresis C -55 +KPX Adieresis Cacute -55 +KPX Adieresis Ccaron -55 +KPX Adieresis Ccedilla -55 +KPX Adieresis G -55 +KPX Adieresis Gbreve -55 +KPX Adieresis Gcommaaccent -55 +KPX Adieresis O -45 +KPX Adieresis Oacute -45 +KPX Adieresis Ocircumflex -45 +KPX Adieresis Odieresis -45 +KPX Adieresis Ograve -45 +KPX Adieresis Ohungarumlaut -45 +KPX Adieresis Omacron -45 +KPX Adieresis Oslash -45 +KPX Adieresis Otilde -45 +KPX Adieresis Q -45 +KPX Adieresis T -95 +KPX Adieresis Tcaron -95 +KPX Adieresis Tcommaaccent -95 +KPX Adieresis U -50 +KPX Adieresis Uacute -50 +KPX Adieresis Ucircumflex -50 +KPX Adieresis Udieresis -50 +KPX Adieresis Ugrave -50 +KPX Adieresis Uhungarumlaut -50 +KPX Adieresis Umacron -50 +KPX Adieresis Uogonek -50 +KPX Adieresis Uring -50 +KPX Adieresis V -145 +KPX Adieresis W -130 +KPX Adieresis Y -100 +KPX Adieresis Yacute -100 +KPX Adieresis Ydieresis -100 +KPX Adieresis p -25 +KPX Adieresis quoteright -74 +KPX Adieresis u -50 +KPX Adieresis uacute -50 +KPX Adieresis ucircumflex -50 +KPX Adieresis udieresis -50 +KPX Adieresis ugrave -50 +KPX Adieresis uhungarumlaut -50 +KPX Adieresis umacron -50 +KPX Adieresis uogonek -50 +KPX Adieresis uring -50 +KPX Adieresis v -100 +KPX Adieresis w -90 +KPX Adieresis y -74 +KPX Adieresis yacute -74 +KPX Adieresis ydieresis -74 +KPX Agrave C -55 +KPX Agrave Cacute -55 +KPX Agrave Ccaron -55 +KPX Agrave Ccedilla -55 +KPX Agrave G -55 +KPX Agrave Gbreve -55 +KPX Agrave Gcommaaccent -55 +KPX Agrave O -45 +KPX Agrave Oacute -45 +KPX Agrave Ocircumflex -45 +KPX Agrave Odieresis -45 +KPX Agrave Ograve -45 +KPX Agrave Ohungarumlaut -45 +KPX Agrave Omacron -45 +KPX Agrave Oslash -45 +KPX Agrave Otilde -45 +KPX Agrave Q -45 +KPX Agrave T -95 +KPX Agrave Tcaron -95 +KPX Agrave Tcommaaccent -95 +KPX Agrave U -50 +KPX Agrave Uacute -50 +KPX Agrave Ucircumflex -50 +KPX Agrave Udieresis -50 +KPX Agrave Ugrave -50 +KPX Agrave Uhungarumlaut -50 +KPX Agrave Umacron -50 +KPX Agrave Uogonek -50 +KPX Agrave Uring -50 +KPX Agrave V -145 +KPX Agrave W -130 +KPX Agrave Y -100 +KPX Agrave Yacute -100 +KPX Agrave Ydieresis -100 +KPX Agrave p -25 +KPX Agrave quoteright -74 +KPX Agrave u -50 +KPX Agrave uacute -50 +KPX Agrave ucircumflex -50 +KPX Agrave udieresis -50 +KPX Agrave ugrave -50 +KPX Agrave uhungarumlaut -50 +KPX Agrave umacron -50 +KPX Agrave uogonek -50 +KPX Agrave uring -50 +KPX Agrave v -100 +KPX Agrave w -90 +KPX Agrave y -74 +KPX Agrave yacute -74 +KPX Agrave ydieresis -74 +KPX Amacron C -55 +KPX Amacron Cacute -55 +KPX Amacron Ccaron -55 +KPX Amacron Ccedilla -55 +KPX Amacron G -55 +KPX Amacron Gbreve -55 +KPX Amacron Gcommaaccent -55 +KPX Amacron O -45 +KPX Amacron Oacute -45 +KPX Amacron Ocircumflex -45 +KPX Amacron Odieresis -45 +KPX Amacron Ograve -45 +KPX Amacron Ohungarumlaut -45 +KPX Amacron Omacron -45 +KPX Amacron Oslash -45 +KPX Amacron Otilde -45 +KPX Amacron Q -45 +KPX Amacron T -95 +KPX Amacron Tcaron -95 +KPX Amacron Tcommaaccent -95 +KPX Amacron U -50 +KPX Amacron Uacute -50 +KPX Amacron Ucircumflex -50 +KPX Amacron Udieresis -50 +KPX Amacron Ugrave -50 +KPX Amacron Uhungarumlaut -50 +KPX Amacron Umacron -50 +KPX Amacron Uogonek -50 +KPX Amacron Uring -50 +KPX Amacron V -145 +KPX Amacron W -130 +KPX Amacron Y -100 +KPX Amacron Yacute -100 +KPX Amacron Ydieresis -100 +KPX Amacron p -25 +KPX Amacron quoteright -74 +KPX Amacron u -50 +KPX Amacron uacute -50 +KPX Amacron ucircumflex -50 +KPX Amacron udieresis -50 +KPX Amacron ugrave -50 +KPX Amacron uhungarumlaut -50 +KPX Amacron umacron -50 +KPX Amacron uogonek -50 +KPX Amacron uring -50 +KPX Amacron v -100 +KPX Amacron w -90 +KPX Amacron y -74 +KPX Amacron yacute -74 +KPX Amacron ydieresis -74 +KPX Aogonek C -55 +KPX Aogonek Cacute -55 +KPX Aogonek Ccaron -55 +KPX Aogonek Ccedilla -55 +KPX Aogonek G -55 +KPX Aogonek Gbreve -55 +KPX Aogonek Gcommaaccent -55 +KPX Aogonek O -45 +KPX Aogonek Oacute -45 +KPX Aogonek Ocircumflex -45 +KPX Aogonek Odieresis -45 +KPX Aogonek Ograve -45 +KPX Aogonek Ohungarumlaut -45 +KPX Aogonek Omacron -45 +KPX Aogonek Oslash -45 +KPX Aogonek Otilde -45 +KPX Aogonek Q -45 +KPX Aogonek T -95 +KPX Aogonek Tcaron -95 +KPX Aogonek Tcommaaccent -95 +KPX Aogonek U -50 +KPX Aogonek Uacute -50 +KPX Aogonek Ucircumflex -50 +KPX Aogonek Udieresis -50 +KPX Aogonek Ugrave -50 +KPX Aogonek Uhungarumlaut -50 +KPX Aogonek Umacron -50 +KPX Aogonek Uogonek -50 +KPX Aogonek Uring -50 +KPX Aogonek V -145 +KPX Aogonek W -130 +KPX Aogonek Y -100 +KPX Aogonek Yacute -100 +KPX Aogonek Ydieresis -100 +KPX Aogonek p -25 +KPX Aogonek quoteright -74 +KPX Aogonek u -50 +KPX Aogonek uacute -50 +KPX Aogonek ucircumflex -50 +KPX Aogonek udieresis -50 +KPX Aogonek ugrave -50 +KPX Aogonek uhungarumlaut -50 +KPX Aogonek umacron -50 +KPX Aogonek uogonek -50 +KPX Aogonek uring -50 +KPX Aogonek v -100 +KPX Aogonek w -90 +KPX Aogonek y -34 +KPX Aogonek yacute -34 +KPX Aogonek ydieresis -34 +KPX Aring C -55 +KPX Aring Cacute -55 +KPX Aring Ccaron -55 +KPX Aring Ccedilla -55 +KPX Aring G -55 +KPX Aring Gbreve -55 +KPX Aring Gcommaaccent -55 +KPX Aring O -45 +KPX Aring Oacute -45 +KPX Aring Ocircumflex -45 +KPX Aring Odieresis -45 +KPX Aring Ograve -45 +KPX Aring Ohungarumlaut -45 +KPX Aring Omacron -45 +KPX Aring Oslash -45 +KPX Aring Otilde -45 +KPX Aring Q -45 +KPX Aring T -95 +KPX Aring Tcaron -95 +KPX Aring Tcommaaccent -95 +KPX Aring U -50 +KPX Aring Uacute -50 +KPX Aring Ucircumflex -50 +KPX Aring Udieresis -50 +KPX Aring Ugrave -50 +KPX Aring Uhungarumlaut -50 +KPX Aring Umacron -50 +KPX Aring Uogonek -50 +KPX Aring Uring -50 +KPX Aring V -145 +KPX Aring W -130 +KPX Aring Y -100 +KPX Aring Yacute -100 +KPX Aring Ydieresis -100 +KPX Aring p -25 +KPX Aring quoteright -74 +KPX Aring u -50 +KPX Aring uacute -50 +KPX Aring ucircumflex -50 +KPX Aring udieresis -50 +KPX Aring ugrave -50 +KPX Aring uhungarumlaut -50 +KPX Aring umacron -50 +KPX Aring uogonek -50 +KPX Aring uring -50 +KPX Aring v -100 +KPX Aring w -90 +KPX Aring y -74 +KPX Aring yacute -74 +KPX Aring ydieresis -74 +KPX Atilde C -55 +KPX Atilde Cacute -55 +KPX Atilde Ccaron -55 +KPX Atilde Ccedilla -55 +KPX Atilde G -55 +KPX Atilde Gbreve -55 +KPX Atilde Gcommaaccent -55 +KPX Atilde O -45 +KPX Atilde Oacute -45 +KPX Atilde Ocircumflex -45 +KPX Atilde Odieresis -45 +KPX Atilde Ograve -45 +KPX Atilde Ohungarumlaut -45 +KPX Atilde Omacron -45 +KPX Atilde Oslash -45 +KPX Atilde Otilde -45 +KPX Atilde Q -45 +KPX Atilde T -95 +KPX Atilde Tcaron -95 +KPX Atilde Tcommaaccent -95 +KPX Atilde U -50 +KPX Atilde Uacute -50 +KPX Atilde Ucircumflex -50 +KPX Atilde Udieresis -50 +KPX Atilde Ugrave -50 +KPX Atilde Uhungarumlaut -50 +KPX Atilde Umacron -50 +KPX Atilde Uogonek -50 +KPX Atilde Uring -50 +KPX Atilde V -145 +KPX Atilde W -130 +KPX Atilde Y -100 +KPX Atilde Yacute -100 +KPX Atilde Ydieresis -100 +KPX Atilde p -25 +KPX Atilde quoteright -74 +KPX Atilde u -50 +KPX Atilde uacute -50 +KPX Atilde ucircumflex -50 +KPX Atilde udieresis -50 +KPX Atilde ugrave -50 +KPX Atilde uhungarumlaut -50 +KPX Atilde umacron -50 +KPX Atilde uogonek -50 +KPX Atilde uring -50 +KPX Atilde v -100 +KPX Atilde w -90 +KPX Atilde y -74 +KPX Atilde yacute -74 +KPX Atilde ydieresis -74 +KPX B A -30 +KPX B Aacute -30 +KPX B Abreve -30 +KPX B Acircumflex -30 +KPX B Adieresis -30 +KPX B Agrave -30 +KPX B Amacron -30 +KPX B Aogonek -30 +KPX B Aring -30 +KPX B Atilde -30 +KPX B U -10 +KPX B Uacute -10 +KPX B Ucircumflex -10 +KPX B Udieresis -10 +KPX B Ugrave -10 +KPX B Uhungarumlaut -10 +KPX B Umacron -10 +KPX B Uogonek -10 +KPX B Uring -10 +KPX D A -35 +KPX D Aacute -35 +KPX D Abreve -35 +KPX D Acircumflex -35 +KPX D Adieresis -35 +KPX D Agrave -35 +KPX D Amacron -35 +KPX D Aogonek -35 +KPX D Aring -35 +KPX D Atilde -35 +KPX D V -40 +KPX D W -40 +KPX D Y -40 +KPX D Yacute -40 +KPX D Ydieresis -40 +KPX D period -20 +KPX Dcaron A -35 +KPX Dcaron Aacute -35 +KPX Dcaron Abreve -35 +KPX Dcaron Acircumflex -35 +KPX Dcaron Adieresis -35 +KPX Dcaron Agrave -35 +KPX Dcaron Amacron -35 +KPX Dcaron Aogonek -35 +KPX Dcaron Aring -35 +KPX Dcaron Atilde -35 +KPX Dcaron V -40 +KPX Dcaron W -40 +KPX Dcaron Y -40 +KPX Dcaron Yacute -40 +KPX Dcaron Ydieresis -40 +KPX Dcaron period -20 +KPX Dcroat A -35 +KPX Dcroat Aacute -35 +KPX Dcroat Abreve -35 +KPX Dcroat Acircumflex -35 +KPX Dcroat Adieresis -35 +KPX Dcroat Agrave -35 +KPX Dcroat Amacron -35 +KPX Dcroat Aogonek -35 +KPX Dcroat Aring -35 +KPX Dcroat Atilde -35 +KPX Dcroat V -40 +KPX Dcroat W -40 +KPX Dcroat Y -40 +KPX Dcroat Yacute -40 +KPX Dcroat Ydieresis -40 +KPX Dcroat period -20 +KPX F A -90 +KPX F Aacute -90 +KPX F Abreve -90 +KPX F Acircumflex -90 +KPX F Adieresis -90 +KPX F Agrave -90 +KPX F Amacron -90 +KPX F Aogonek -90 +KPX F Aring -90 +KPX F Atilde -90 +KPX F a -25 +KPX F aacute -25 +KPX F abreve -25 +KPX F acircumflex -25 +KPX F adieresis -25 +KPX F agrave -25 +KPX F amacron -25 +KPX F aogonek -25 +KPX F aring -25 +KPX F atilde -25 +KPX F comma -92 +KPX F e -25 +KPX F eacute -25 +KPX F ecaron -25 +KPX F ecircumflex -25 +KPX F edieresis -25 +KPX F edotaccent -25 +KPX F egrave -25 +KPX F emacron -25 +KPX F eogonek -25 +KPX F o -25 +KPX F oacute -25 +KPX F ocircumflex -25 +KPX F odieresis -25 +KPX F ograve -25 +KPX F ohungarumlaut -25 +KPX F omacron -25 +KPX F oslash -25 +KPX F otilde -25 +KPX F period -110 +KPX J A -30 +KPX J Aacute -30 +KPX J Abreve -30 +KPX J Acircumflex -30 +KPX J Adieresis -30 +KPX J Agrave -30 +KPX J Amacron -30 +KPX J Aogonek -30 +KPX J Aring -30 +KPX J Atilde -30 +KPX J a -15 +KPX J aacute -15 +KPX J abreve -15 +KPX J acircumflex -15 +KPX J adieresis -15 +KPX J agrave -15 +KPX J amacron -15 +KPX J aogonek -15 +KPX J aring -15 +KPX J atilde -15 +KPX J e -15 +KPX J eacute -15 +KPX J ecaron -15 +KPX J ecircumflex -15 +KPX J edieresis -15 +KPX J edotaccent -15 +KPX J egrave -15 +KPX J emacron -15 +KPX J eogonek -15 +KPX J o -15 +KPX J oacute -15 +KPX J ocircumflex -15 +KPX J odieresis -15 +KPX J ograve -15 +KPX J ohungarumlaut -15 +KPX J omacron -15 +KPX J oslash -15 +KPX J otilde -15 +KPX J period -20 +KPX J u -15 +KPX J uacute -15 +KPX J ucircumflex -15 +KPX J udieresis -15 +KPX J ugrave -15 +KPX J uhungarumlaut -15 +KPX J umacron -15 +KPX J uogonek -15 +KPX J uring -15 +KPX K O -30 +KPX K Oacute -30 +KPX K Ocircumflex -30 +KPX K Odieresis -30 +KPX K Ograve -30 +KPX K Ohungarumlaut -30 +KPX K Omacron -30 +KPX K Oslash -30 +KPX K Otilde -30 +KPX K e -25 +KPX K eacute -25 +KPX K ecaron -25 +KPX K ecircumflex -25 +KPX K edieresis -25 +KPX K edotaccent -25 +KPX K egrave -25 +KPX K emacron -25 +KPX K eogonek -25 +KPX K o -25 +KPX K oacute -25 +KPX K ocircumflex -25 +KPX K odieresis -25 +KPX K ograve -25 +KPX K ohungarumlaut -25 +KPX K omacron -25 +KPX K oslash -25 +KPX K otilde -25 +KPX K u -15 +KPX K uacute -15 +KPX K ucircumflex -15 +KPX K udieresis -15 +KPX K ugrave -15 +KPX K uhungarumlaut -15 +KPX K umacron -15 +KPX K uogonek -15 +KPX K uring -15 +KPX K y -45 +KPX K yacute -45 +KPX K ydieresis -45 +KPX Kcommaaccent O -30 +KPX Kcommaaccent Oacute -30 +KPX Kcommaaccent Ocircumflex -30 +KPX Kcommaaccent Odieresis -30 +KPX Kcommaaccent Ograve -30 +KPX Kcommaaccent Ohungarumlaut -30 +KPX Kcommaaccent Omacron -30 +KPX Kcommaaccent Oslash -30 +KPX Kcommaaccent Otilde -30 +KPX Kcommaaccent e -25 +KPX Kcommaaccent eacute -25 +KPX Kcommaaccent ecaron -25 +KPX Kcommaaccent ecircumflex -25 +KPX Kcommaaccent edieresis -25 +KPX Kcommaaccent edotaccent -25 +KPX Kcommaaccent egrave -25 +KPX Kcommaaccent emacron -25 +KPX Kcommaaccent eogonek -25 +KPX Kcommaaccent o -25 +KPX Kcommaaccent oacute -25 +KPX Kcommaaccent ocircumflex -25 +KPX Kcommaaccent odieresis -25 +KPX Kcommaaccent ograve -25 +KPX Kcommaaccent ohungarumlaut -25 +KPX Kcommaaccent omacron -25 +KPX Kcommaaccent oslash -25 +KPX Kcommaaccent otilde -25 +KPX Kcommaaccent u -15 +KPX Kcommaaccent uacute -15 +KPX Kcommaaccent ucircumflex -15 +KPX Kcommaaccent udieresis -15 +KPX Kcommaaccent ugrave -15 +KPX Kcommaaccent uhungarumlaut -15 +KPX Kcommaaccent umacron -15 +KPX Kcommaaccent uogonek -15 +KPX Kcommaaccent uring -15 +KPX Kcommaaccent y -45 +KPX Kcommaaccent yacute -45 +KPX Kcommaaccent ydieresis -45 +KPX L T -92 +KPX L Tcaron -92 +KPX L Tcommaaccent -92 +KPX L V -92 +KPX L W -92 +KPX L Y -92 +KPX L Yacute -92 +KPX L Ydieresis -92 +KPX L quotedblright -20 +KPX L quoteright -110 +KPX L y -55 +KPX L yacute -55 +KPX L ydieresis -55 +KPX Lacute T -92 +KPX Lacute Tcaron -92 +KPX Lacute Tcommaaccent -92 +KPX Lacute V -92 +KPX Lacute W -92 +KPX Lacute Y -92 +KPX Lacute Yacute -92 +KPX Lacute Ydieresis -92 +KPX Lacute quotedblright -20 +KPX Lacute quoteright -110 +KPX Lacute y -55 +KPX Lacute yacute -55 +KPX Lacute ydieresis -55 +KPX Lcommaaccent T -92 +KPX Lcommaaccent Tcaron -92 +KPX Lcommaaccent Tcommaaccent -92 +KPX Lcommaaccent V -92 +KPX Lcommaaccent W -92 +KPX Lcommaaccent Y -92 +KPX Lcommaaccent Yacute -92 +KPX Lcommaaccent Ydieresis -92 +KPX Lcommaaccent quotedblright -20 +KPX Lcommaaccent quoteright -110 +KPX Lcommaaccent y -55 +KPX Lcommaaccent yacute -55 +KPX Lcommaaccent ydieresis -55 +KPX Lslash T -92 +KPX Lslash Tcaron -92 +KPX Lslash Tcommaaccent -92 +KPX Lslash V -92 +KPX Lslash W -92 +KPX Lslash Y -92 +KPX Lslash Yacute -92 +KPX Lslash Ydieresis -92 +KPX Lslash quotedblright -20 +KPX Lslash quoteright -110 +KPX Lslash y -55 +KPX Lslash yacute -55 +KPX Lslash ydieresis -55 +KPX N A -20 +KPX N Aacute -20 +KPX N Abreve -20 +KPX N Acircumflex -20 +KPX N Adieresis -20 +KPX N Agrave -20 +KPX N Amacron -20 +KPX N Aogonek -20 +KPX N Aring -20 +KPX N Atilde -20 +KPX Nacute A -20 +KPX Nacute Aacute -20 +KPX Nacute Abreve -20 +KPX Nacute Acircumflex -20 +KPX Nacute Adieresis -20 +KPX Nacute Agrave -20 +KPX Nacute Amacron -20 +KPX Nacute Aogonek -20 +KPX Nacute Aring -20 +KPX Nacute Atilde -20 +KPX Ncaron A -20 +KPX Ncaron Aacute -20 +KPX Ncaron Abreve -20 +KPX Ncaron Acircumflex -20 +KPX Ncaron Adieresis -20 +KPX Ncaron Agrave -20 +KPX Ncaron Amacron -20 +KPX Ncaron Aogonek -20 +KPX Ncaron Aring -20 +KPX Ncaron Atilde -20 +KPX Ncommaaccent A -20 +KPX Ncommaaccent Aacute -20 +KPX Ncommaaccent Abreve -20 +KPX Ncommaaccent Acircumflex -20 +KPX Ncommaaccent Adieresis -20 +KPX Ncommaaccent Agrave -20 +KPX Ncommaaccent Amacron -20 +KPX Ncommaaccent Aogonek -20 +KPX Ncommaaccent Aring -20 +KPX Ncommaaccent Atilde -20 +KPX Ntilde A -20 +KPX Ntilde Aacute -20 +KPX Ntilde Abreve -20 +KPX Ntilde Acircumflex -20 +KPX Ntilde Adieresis -20 +KPX Ntilde Agrave -20 +KPX Ntilde Amacron -20 +KPX Ntilde Aogonek -20 +KPX Ntilde Aring -20 +KPX Ntilde Atilde -20 +KPX O A -40 +KPX O Aacute -40 +KPX O Abreve -40 +KPX O Acircumflex -40 +KPX O Adieresis -40 +KPX O Agrave -40 +KPX O Amacron -40 +KPX O Aogonek -40 +KPX O Aring -40 +KPX O Atilde -40 +KPX O T -40 +KPX O Tcaron -40 +KPX O Tcommaaccent -40 +KPX O V -50 +KPX O W -50 +KPX O X -40 +KPX O Y -50 +KPX O Yacute -50 +KPX O Ydieresis -50 +KPX Oacute A -40 +KPX Oacute Aacute -40 +KPX Oacute Abreve -40 +KPX Oacute Acircumflex -40 +KPX Oacute Adieresis -40 +KPX Oacute Agrave -40 +KPX Oacute Amacron -40 +KPX Oacute Aogonek -40 +KPX Oacute Aring -40 +KPX Oacute Atilde -40 +KPX Oacute T -40 +KPX Oacute Tcaron -40 +KPX Oacute Tcommaaccent -40 +KPX Oacute V -50 +KPX Oacute W -50 +KPX Oacute X -40 +KPX Oacute Y -50 +KPX Oacute Yacute -50 +KPX Oacute Ydieresis -50 +KPX Ocircumflex A -40 +KPX Ocircumflex Aacute -40 +KPX Ocircumflex Abreve -40 +KPX Ocircumflex Acircumflex -40 +KPX Ocircumflex Adieresis -40 +KPX Ocircumflex Agrave -40 +KPX Ocircumflex Amacron -40 +KPX Ocircumflex Aogonek -40 +KPX Ocircumflex Aring -40 +KPX Ocircumflex Atilde -40 +KPX Ocircumflex T -40 +KPX Ocircumflex Tcaron -40 +KPX Ocircumflex Tcommaaccent -40 +KPX Ocircumflex V -50 +KPX Ocircumflex W -50 +KPX Ocircumflex X -40 +KPX Ocircumflex Y -50 +KPX Ocircumflex Yacute -50 +KPX Ocircumflex Ydieresis -50 +KPX Odieresis A -40 +KPX Odieresis Aacute -40 +KPX Odieresis Abreve -40 +KPX Odieresis Acircumflex -40 +KPX Odieresis Adieresis -40 +KPX Odieresis Agrave -40 +KPX Odieresis Amacron -40 +KPX Odieresis Aogonek -40 +KPX Odieresis Aring -40 +KPX Odieresis Atilde -40 +KPX Odieresis T -40 +KPX Odieresis Tcaron -40 +KPX Odieresis Tcommaaccent -40 +KPX Odieresis V -50 +KPX Odieresis W -50 +KPX Odieresis X -40 +KPX Odieresis Y -50 +KPX Odieresis Yacute -50 +KPX Odieresis Ydieresis -50 +KPX Ograve A -40 +KPX Ograve Aacute -40 +KPX Ograve Abreve -40 +KPX Ograve Acircumflex -40 +KPX Ograve Adieresis -40 +KPX Ograve Agrave -40 +KPX Ograve Amacron -40 +KPX Ograve Aogonek -40 +KPX Ograve Aring -40 +KPX Ograve Atilde -40 +KPX Ograve T -40 +KPX Ograve Tcaron -40 +KPX Ograve Tcommaaccent -40 +KPX Ograve V -50 +KPX Ograve W -50 +KPX Ograve X -40 +KPX Ograve Y -50 +KPX Ograve Yacute -50 +KPX Ograve Ydieresis -50 +KPX Ohungarumlaut A -40 +KPX Ohungarumlaut Aacute -40 +KPX Ohungarumlaut Abreve -40 +KPX Ohungarumlaut Acircumflex -40 +KPX Ohungarumlaut Adieresis -40 +KPX Ohungarumlaut Agrave -40 +KPX Ohungarumlaut Amacron -40 +KPX Ohungarumlaut Aogonek -40 +KPX Ohungarumlaut Aring -40 +KPX Ohungarumlaut Atilde -40 +KPX Ohungarumlaut T -40 +KPX Ohungarumlaut Tcaron -40 +KPX Ohungarumlaut Tcommaaccent -40 +KPX Ohungarumlaut V -50 +KPX Ohungarumlaut W -50 +KPX Ohungarumlaut X -40 +KPX Ohungarumlaut Y -50 +KPX Ohungarumlaut Yacute -50 +KPX Ohungarumlaut Ydieresis -50 +KPX Omacron A -40 +KPX Omacron Aacute -40 +KPX Omacron Abreve -40 +KPX Omacron Acircumflex -40 +KPX Omacron Adieresis -40 +KPX Omacron Agrave -40 +KPX Omacron Amacron -40 +KPX Omacron Aogonek -40 +KPX Omacron Aring -40 +KPX Omacron Atilde -40 +KPX Omacron T -40 +KPX Omacron Tcaron -40 +KPX Omacron Tcommaaccent -40 +KPX Omacron V -50 +KPX Omacron W -50 +KPX Omacron X -40 +KPX Omacron Y -50 +KPX Omacron Yacute -50 +KPX Omacron Ydieresis -50 +KPX Oslash A -40 +KPX Oslash Aacute -40 +KPX Oslash Abreve -40 +KPX Oslash Acircumflex -40 +KPX Oslash Adieresis -40 +KPX Oslash Agrave -40 +KPX Oslash Amacron -40 +KPX Oslash Aogonek -40 +KPX Oslash Aring -40 +KPX Oslash Atilde -40 +KPX Oslash T -40 +KPX Oslash Tcaron -40 +KPX Oslash Tcommaaccent -40 +KPX Oslash V -50 +KPX Oslash W -50 +KPX Oslash X -40 +KPX Oslash Y -50 +KPX Oslash Yacute -50 +KPX Oslash Ydieresis -50 +KPX Otilde A -40 +KPX Otilde Aacute -40 +KPX Otilde Abreve -40 +KPX Otilde Acircumflex -40 +KPX Otilde Adieresis -40 +KPX Otilde Agrave -40 +KPX Otilde Amacron -40 +KPX Otilde Aogonek -40 +KPX Otilde Aring -40 +KPX Otilde Atilde -40 +KPX Otilde T -40 +KPX Otilde Tcaron -40 +KPX Otilde Tcommaaccent -40 +KPX Otilde V -50 +KPX Otilde W -50 +KPX Otilde X -40 +KPX Otilde Y -50 +KPX Otilde Yacute -50 +KPX Otilde Ydieresis -50 +KPX P A -74 +KPX P Aacute -74 +KPX P Abreve -74 +KPX P Acircumflex -74 +KPX P Adieresis -74 +KPX P Agrave -74 +KPX P Amacron -74 +KPX P Aogonek -74 +KPX P Aring -74 +KPX P Atilde -74 +KPX P a -10 +KPX P aacute -10 +KPX P abreve -10 +KPX P acircumflex -10 +KPX P adieresis -10 +KPX P agrave -10 +KPX P amacron -10 +KPX P aogonek -10 +KPX P aring -10 +KPX P atilde -10 +KPX P comma -92 +KPX P e -20 +KPX P eacute -20 +KPX P ecaron -20 +KPX P ecircumflex -20 +KPX P edieresis -20 +KPX P edotaccent -20 +KPX P egrave -20 +KPX P emacron -20 +KPX P eogonek -20 +KPX P o -20 +KPX P oacute -20 +KPX P ocircumflex -20 +KPX P odieresis -20 +KPX P ograve -20 +KPX P ohungarumlaut -20 +KPX P omacron -20 +KPX P oslash -20 +KPX P otilde -20 +KPX P period -110 +KPX Q U -10 +KPX Q Uacute -10 +KPX Q Ucircumflex -10 +KPX Q Udieresis -10 +KPX Q Ugrave -10 +KPX Q Uhungarumlaut -10 +KPX Q Umacron -10 +KPX Q Uogonek -10 +KPX Q Uring -10 +KPX Q period -20 +KPX R O -30 +KPX R Oacute -30 +KPX R Ocircumflex -30 +KPX R Odieresis -30 +KPX R Ograve -30 +KPX R Ohungarumlaut -30 +KPX R Omacron -30 +KPX R Oslash -30 +KPX R Otilde -30 +KPX R T -40 +KPX R Tcaron -40 +KPX R Tcommaaccent -40 +KPX R U -30 +KPX R Uacute -30 +KPX R Ucircumflex -30 +KPX R Udieresis -30 +KPX R Ugrave -30 +KPX R Uhungarumlaut -30 +KPX R Umacron -30 +KPX R Uogonek -30 +KPX R Uring -30 +KPX R V -55 +KPX R W -35 +KPX R Y -35 +KPX R Yacute -35 +KPX R Ydieresis -35 +KPX Racute O -30 +KPX Racute Oacute -30 +KPX Racute Ocircumflex -30 +KPX Racute Odieresis -30 +KPX Racute Ograve -30 +KPX Racute Ohungarumlaut -30 +KPX Racute Omacron -30 +KPX Racute Oslash -30 +KPX Racute Otilde -30 +KPX Racute T -40 +KPX Racute Tcaron -40 +KPX Racute Tcommaaccent -40 +KPX Racute U -30 +KPX Racute Uacute -30 +KPX Racute Ucircumflex -30 +KPX Racute Udieresis -30 +KPX Racute Ugrave -30 +KPX Racute Uhungarumlaut -30 +KPX Racute Umacron -30 +KPX Racute Uogonek -30 +KPX Racute Uring -30 +KPX Racute V -55 +KPX Racute W -35 +KPX Racute Y -35 +KPX Racute Yacute -35 +KPX Racute Ydieresis -35 +KPX Rcaron O -30 +KPX Rcaron Oacute -30 +KPX Rcaron Ocircumflex -30 +KPX Rcaron Odieresis -30 +KPX Rcaron Ograve -30 +KPX Rcaron Ohungarumlaut -30 +KPX Rcaron Omacron -30 +KPX Rcaron Oslash -30 +KPX Rcaron Otilde -30 +KPX Rcaron T -40 +KPX Rcaron Tcaron -40 +KPX Rcaron Tcommaaccent -40 +KPX Rcaron U -30 +KPX Rcaron Uacute -30 +KPX Rcaron Ucircumflex -30 +KPX Rcaron Udieresis -30 +KPX Rcaron Ugrave -30 +KPX Rcaron Uhungarumlaut -30 +KPX Rcaron Umacron -30 +KPX Rcaron Uogonek -30 +KPX Rcaron Uring -30 +KPX Rcaron V -55 +KPX Rcaron W -35 +KPX Rcaron Y -35 +KPX Rcaron Yacute -35 +KPX Rcaron Ydieresis -35 +KPX Rcommaaccent O -30 +KPX Rcommaaccent Oacute -30 +KPX Rcommaaccent Ocircumflex -30 +KPX Rcommaaccent Odieresis -30 +KPX Rcommaaccent Ograve -30 +KPX Rcommaaccent Ohungarumlaut -30 +KPX Rcommaaccent Omacron -30 +KPX Rcommaaccent Oslash -30 +KPX Rcommaaccent Otilde -30 +KPX Rcommaaccent T -40 +KPX Rcommaaccent Tcaron -40 +KPX Rcommaaccent Tcommaaccent -40 +KPX Rcommaaccent U -30 +KPX Rcommaaccent Uacute -30 +KPX Rcommaaccent Ucircumflex -30 +KPX Rcommaaccent Udieresis -30 +KPX Rcommaaccent Ugrave -30 +KPX Rcommaaccent Uhungarumlaut -30 +KPX Rcommaaccent Umacron -30 +KPX Rcommaaccent Uogonek -30 +KPX Rcommaaccent Uring -30 +KPX Rcommaaccent V -55 +KPX Rcommaaccent W -35 +KPX Rcommaaccent Y -35 +KPX Rcommaaccent Yacute -35 +KPX Rcommaaccent Ydieresis -35 +KPX T A -90 +KPX T Aacute -90 +KPX T Abreve -90 +KPX T Acircumflex -90 +KPX T Adieresis -90 +KPX T Agrave -90 +KPX T Amacron -90 +KPX T Aogonek -90 +KPX T Aring -90 +KPX T Atilde -90 +KPX T O -18 +KPX T Oacute -18 +KPX T Ocircumflex -18 +KPX T Odieresis -18 +KPX T Ograve -18 +KPX T Ohungarumlaut -18 +KPX T Omacron -18 +KPX T Oslash -18 +KPX T Otilde -18 +KPX T a -92 +KPX T aacute -92 +KPX T abreve -52 +KPX T acircumflex -52 +KPX T adieresis -52 +KPX T agrave -52 +KPX T amacron -52 +KPX T aogonek -92 +KPX T aring -92 +KPX T atilde -52 +KPX T colon -74 +KPX T comma -74 +KPX T e -92 +KPX T eacute -92 +KPX T ecaron -92 +KPX T ecircumflex -92 +KPX T edieresis -52 +KPX T edotaccent -92 +KPX T egrave -52 +KPX T emacron -52 +KPX T eogonek -92 +KPX T hyphen -92 +KPX T i -18 +KPX T iacute -18 +KPX T iogonek -18 +KPX T o -92 +KPX T oacute -92 +KPX T ocircumflex -92 +KPX T odieresis -92 +KPX T ograve -92 +KPX T ohungarumlaut -92 +KPX T omacron -92 +KPX T oslash -92 +KPX T otilde -92 +KPX T period -90 +KPX T r -74 +KPX T racute -74 +KPX T rcaron -74 +KPX T rcommaaccent -74 +KPX T semicolon -74 +KPX T u -92 +KPX T uacute -92 +KPX T ucircumflex -92 +KPX T udieresis -92 +KPX T ugrave -92 +KPX T uhungarumlaut -92 +KPX T umacron -92 +KPX T uogonek -92 +KPX T uring -92 +KPX T w -74 +KPX T y -34 +KPX T yacute -34 +KPX T ydieresis -34 +KPX Tcaron A -90 +KPX Tcaron Aacute -90 +KPX Tcaron Abreve -90 +KPX Tcaron Acircumflex -90 +KPX Tcaron Adieresis -90 +KPX Tcaron Agrave -90 +KPX Tcaron Amacron -90 +KPX Tcaron Aogonek -90 +KPX Tcaron Aring -90 +KPX Tcaron Atilde -90 +KPX Tcaron O -18 +KPX Tcaron Oacute -18 +KPX Tcaron Ocircumflex -18 +KPX Tcaron Odieresis -18 +KPX Tcaron Ograve -18 +KPX Tcaron Ohungarumlaut -18 +KPX Tcaron Omacron -18 +KPX Tcaron Oslash -18 +KPX Tcaron Otilde -18 +KPX Tcaron a -92 +KPX Tcaron aacute -92 +KPX Tcaron abreve -52 +KPX Tcaron acircumflex -52 +KPX Tcaron adieresis -52 +KPX Tcaron agrave -52 +KPX Tcaron amacron -52 +KPX Tcaron aogonek -92 +KPX Tcaron aring -92 +KPX Tcaron atilde -52 +KPX Tcaron colon -74 +KPX Tcaron comma -74 +KPX Tcaron e -92 +KPX Tcaron eacute -92 +KPX Tcaron ecaron -92 +KPX Tcaron ecircumflex -92 +KPX Tcaron edieresis -52 +KPX Tcaron edotaccent -92 +KPX Tcaron egrave -52 +KPX Tcaron emacron -52 +KPX Tcaron eogonek -92 +KPX Tcaron hyphen -92 +KPX Tcaron i -18 +KPX Tcaron iacute -18 +KPX Tcaron iogonek -18 +KPX Tcaron o -92 +KPX Tcaron oacute -92 +KPX Tcaron ocircumflex -92 +KPX Tcaron odieresis -92 +KPX Tcaron ograve -92 +KPX Tcaron ohungarumlaut -92 +KPX Tcaron omacron -92 +KPX Tcaron oslash -92 +KPX Tcaron otilde -92 +KPX Tcaron period -90 +KPX Tcaron r -74 +KPX Tcaron racute -74 +KPX Tcaron rcaron -74 +KPX Tcaron rcommaaccent -74 +KPX Tcaron semicolon -74 +KPX Tcaron u -92 +KPX Tcaron uacute -92 +KPX Tcaron ucircumflex -92 +KPX Tcaron udieresis -92 +KPX Tcaron ugrave -92 +KPX Tcaron uhungarumlaut -92 +KPX Tcaron umacron -92 +KPX Tcaron uogonek -92 +KPX Tcaron uring -92 +KPX Tcaron w -74 +KPX Tcaron y -34 +KPX Tcaron yacute -34 +KPX Tcaron ydieresis -34 +KPX Tcommaaccent A -90 +KPX Tcommaaccent Aacute -90 +KPX Tcommaaccent Abreve -90 +KPX Tcommaaccent Acircumflex -90 +KPX Tcommaaccent Adieresis -90 +KPX Tcommaaccent Agrave -90 +KPX Tcommaaccent Amacron -90 +KPX Tcommaaccent Aogonek -90 +KPX Tcommaaccent Aring -90 +KPX Tcommaaccent Atilde -90 +KPX Tcommaaccent O -18 +KPX Tcommaaccent Oacute -18 +KPX Tcommaaccent Ocircumflex -18 +KPX Tcommaaccent Odieresis -18 +KPX Tcommaaccent Ograve -18 +KPX Tcommaaccent Ohungarumlaut -18 +KPX Tcommaaccent Omacron -18 +KPX Tcommaaccent Oslash -18 +KPX Tcommaaccent Otilde -18 +KPX Tcommaaccent a -92 +KPX Tcommaaccent aacute -92 +KPX Tcommaaccent abreve -52 +KPX Tcommaaccent acircumflex -52 +KPX Tcommaaccent adieresis -52 +KPX Tcommaaccent agrave -52 +KPX Tcommaaccent amacron -52 +KPX Tcommaaccent aogonek -92 +KPX Tcommaaccent aring -92 +KPX Tcommaaccent atilde -52 +KPX Tcommaaccent colon -74 +KPX Tcommaaccent comma -74 +KPX Tcommaaccent e -92 +KPX Tcommaaccent eacute -92 +KPX Tcommaaccent ecaron -92 +KPX Tcommaaccent ecircumflex -92 +KPX Tcommaaccent edieresis -52 +KPX Tcommaaccent edotaccent -92 +KPX Tcommaaccent egrave -52 +KPX Tcommaaccent emacron -52 +KPX Tcommaaccent eogonek -92 +KPX Tcommaaccent hyphen -92 +KPX Tcommaaccent i -18 +KPX Tcommaaccent iacute -18 +KPX Tcommaaccent iogonek -18 +KPX Tcommaaccent o -92 +KPX Tcommaaccent oacute -92 +KPX Tcommaaccent ocircumflex -92 +KPX Tcommaaccent odieresis -92 +KPX Tcommaaccent ograve -92 +KPX Tcommaaccent ohungarumlaut -92 +KPX Tcommaaccent omacron -92 +KPX Tcommaaccent oslash -92 +KPX Tcommaaccent otilde -92 +KPX Tcommaaccent period -90 +KPX Tcommaaccent r -74 +KPX Tcommaaccent racute -74 +KPX Tcommaaccent rcaron -74 +KPX Tcommaaccent rcommaaccent -74 +KPX Tcommaaccent semicolon -74 +KPX Tcommaaccent u -92 +KPX Tcommaaccent uacute -92 +KPX Tcommaaccent ucircumflex -92 +KPX Tcommaaccent udieresis -92 +KPX Tcommaaccent ugrave -92 +KPX Tcommaaccent uhungarumlaut -92 +KPX Tcommaaccent umacron -92 +KPX Tcommaaccent uogonek -92 +KPX Tcommaaccent uring -92 +KPX Tcommaaccent w -74 +KPX Tcommaaccent y -34 +KPX Tcommaaccent yacute -34 +KPX Tcommaaccent ydieresis -34 +KPX U A -60 +KPX U Aacute -60 +KPX U Abreve -60 +KPX U Acircumflex -60 +KPX U Adieresis -60 +KPX U Agrave -60 +KPX U Amacron -60 +KPX U Aogonek -60 +KPX U Aring -60 +KPX U Atilde -60 +KPX U comma -50 +KPX U period -50 +KPX Uacute A -60 +KPX Uacute Aacute -60 +KPX Uacute Abreve -60 +KPX Uacute Acircumflex -60 +KPX Uacute Adieresis -60 +KPX Uacute Agrave -60 +KPX Uacute Amacron -60 +KPX Uacute Aogonek -60 +KPX Uacute Aring -60 +KPX Uacute Atilde -60 +KPX Uacute comma -50 +KPX Uacute period -50 +KPX Ucircumflex A -60 +KPX Ucircumflex Aacute -60 +KPX Ucircumflex Abreve -60 +KPX Ucircumflex Acircumflex -60 +KPX Ucircumflex Adieresis -60 +KPX Ucircumflex Agrave -60 +KPX Ucircumflex Amacron -60 +KPX Ucircumflex Aogonek -60 +KPX Ucircumflex Aring -60 +KPX Ucircumflex Atilde -60 +KPX Ucircumflex comma -50 +KPX Ucircumflex period -50 +KPX Udieresis A -60 +KPX Udieresis Aacute -60 +KPX Udieresis Abreve -60 +KPX Udieresis Acircumflex -60 +KPX Udieresis Adieresis -60 +KPX Udieresis Agrave -60 +KPX Udieresis Amacron -60 +KPX Udieresis Aogonek -60 +KPX Udieresis Aring -60 +KPX Udieresis Atilde -60 +KPX Udieresis comma -50 +KPX Udieresis period -50 +KPX Ugrave A -60 +KPX Ugrave Aacute -60 +KPX Ugrave Abreve -60 +KPX Ugrave Acircumflex -60 +KPX Ugrave Adieresis -60 +KPX Ugrave Agrave -60 +KPX Ugrave Amacron -60 +KPX Ugrave Aogonek -60 +KPX Ugrave Aring -60 +KPX Ugrave Atilde -60 +KPX Ugrave comma -50 +KPX Ugrave period -50 +KPX Uhungarumlaut A -60 +KPX Uhungarumlaut Aacute -60 +KPX Uhungarumlaut Abreve -60 +KPX Uhungarumlaut Acircumflex -60 +KPX Uhungarumlaut Adieresis -60 +KPX Uhungarumlaut Agrave -60 +KPX Uhungarumlaut Amacron -60 +KPX Uhungarumlaut Aogonek -60 +KPX Uhungarumlaut Aring -60 +KPX Uhungarumlaut Atilde -60 +KPX Uhungarumlaut comma -50 +KPX Uhungarumlaut period -50 +KPX Umacron A -60 +KPX Umacron Aacute -60 +KPX Umacron Abreve -60 +KPX Umacron Acircumflex -60 +KPX Umacron Adieresis -60 +KPX Umacron Agrave -60 +KPX Umacron Amacron -60 +KPX Umacron Aogonek -60 +KPX Umacron Aring -60 +KPX Umacron Atilde -60 +KPX Umacron comma -50 +KPX Umacron period -50 +KPX Uogonek A -60 +KPX Uogonek Aacute -60 +KPX Uogonek Abreve -60 +KPX Uogonek Acircumflex -60 +KPX Uogonek Adieresis -60 +KPX Uogonek Agrave -60 +KPX Uogonek Amacron -60 +KPX Uogonek Aogonek -60 +KPX Uogonek Aring -60 +KPX Uogonek Atilde -60 +KPX Uogonek comma -50 +KPX Uogonek period -50 +KPX Uring A -60 +KPX Uring Aacute -60 +KPX Uring Abreve -60 +KPX Uring Acircumflex -60 +KPX Uring Adieresis -60 +KPX Uring Agrave -60 +KPX Uring Amacron -60 +KPX Uring Aogonek -60 +KPX Uring Aring -60 +KPX Uring Atilde -60 +KPX Uring comma -50 +KPX Uring period -50 +KPX V A -135 +KPX V Aacute -135 +KPX V Abreve -135 +KPX V Acircumflex -135 +KPX V Adieresis -135 +KPX V Agrave -135 +KPX V Amacron -135 +KPX V Aogonek -135 +KPX V Aring -135 +KPX V Atilde -135 +KPX V G -30 +KPX V Gbreve -30 +KPX V Gcommaaccent -30 +KPX V O -45 +KPX V Oacute -45 +KPX V Ocircumflex -45 +KPX V Odieresis -45 +KPX V Ograve -45 +KPX V Ohungarumlaut -45 +KPX V Omacron -45 +KPX V Oslash -45 +KPX V Otilde -45 +KPX V a -92 +KPX V aacute -92 +KPX V abreve -92 +KPX V acircumflex -92 +KPX V adieresis -92 +KPX V agrave -92 +KPX V amacron -92 +KPX V aogonek -92 +KPX V aring -92 +KPX V atilde -92 +KPX V colon -92 +KPX V comma -129 +KPX V e -100 +KPX V eacute -100 +KPX V ecaron -100 +KPX V ecircumflex -100 +KPX V edieresis -100 +KPX V edotaccent -100 +KPX V egrave -100 +KPX V emacron -100 +KPX V eogonek -100 +KPX V hyphen -74 +KPX V i -37 +KPX V iacute -37 +KPX V icircumflex -37 +KPX V idieresis -37 +KPX V igrave -37 +KPX V imacron -37 +KPX V iogonek -37 +KPX V o -100 +KPX V oacute -100 +KPX V ocircumflex -100 +KPX V odieresis -100 +KPX V ograve -100 +KPX V ohungarumlaut -100 +KPX V omacron -100 +KPX V oslash -100 +KPX V otilde -100 +KPX V period -145 +KPX V semicolon -92 +KPX V u -92 +KPX V uacute -92 +KPX V ucircumflex -92 +KPX V udieresis -92 +KPX V ugrave -92 +KPX V uhungarumlaut -92 +KPX V umacron -92 +KPX V uogonek -92 +KPX V uring -92 +KPX W A -120 +KPX W Aacute -120 +KPX W Abreve -120 +KPX W Acircumflex -120 +KPX W Adieresis -120 +KPX W Agrave -120 +KPX W Amacron -120 +KPX W Aogonek -120 +KPX W Aring -120 +KPX W Atilde -120 +KPX W O -10 +KPX W Oacute -10 +KPX W Ocircumflex -10 +KPX W Odieresis -10 +KPX W Ograve -10 +KPX W Ohungarumlaut -10 +KPX W Omacron -10 +KPX W Oslash -10 +KPX W Otilde -10 +KPX W a -65 +KPX W aacute -65 +KPX W abreve -65 +KPX W acircumflex -65 +KPX W adieresis -65 +KPX W agrave -65 +KPX W amacron -65 +KPX W aogonek -65 +KPX W aring -65 +KPX W atilde -65 +KPX W colon -55 +KPX W comma -92 +KPX W e -65 +KPX W eacute -65 +KPX W ecaron -65 +KPX W ecircumflex -65 +KPX W edieresis -65 +KPX W edotaccent -65 +KPX W egrave -65 +KPX W emacron -65 +KPX W eogonek -65 +KPX W hyphen -37 +KPX W i -18 +KPX W iacute -18 +KPX W iogonek -18 +KPX W o -75 +KPX W oacute -75 +KPX W ocircumflex -75 +KPX W odieresis -75 +KPX W ograve -75 +KPX W ohungarumlaut -75 +KPX W omacron -75 +KPX W oslash -75 +KPX W otilde -75 +KPX W period -92 +KPX W semicolon -55 +KPX W u -50 +KPX W uacute -50 +KPX W ucircumflex -50 +KPX W udieresis -50 +KPX W ugrave -50 +KPX W uhungarumlaut -50 +KPX W umacron -50 +KPX W uogonek -50 +KPX W uring -50 +KPX W y -60 +KPX W yacute -60 +KPX W ydieresis -60 +KPX Y A -110 +KPX Y Aacute -110 +KPX Y Abreve -110 +KPX Y Acircumflex -110 +KPX Y Adieresis -110 +KPX Y Agrave -110 +KPX Y Amacron -110 +KPX Y Aogonek -110 +KPX Y Aring -110 +KPX Y Atilde -110 +KPX Y O -35 +KPX Y Oacute -35 +KPX Y Ocircumflex -35 +KPX Y Odieresis -35 +KPX Y Ograve -35 +KPX Y Ohungarumlaut -35 +KPX Y Omacron -35 +KPX Y Oslash -35 +KPX Y Otilde -35 +KPX Y a -85 +KPX Y aacute -85 +KPX Y abreve -85 +KPX Y acircumflex -85 +KPX Y adieresis -85 +KPX Y agrave -85 +KPX Y amacron -85 +KPX Y aogonek -85 +KPX Y aring -85 +KPX Y atilde -85 +KPX Y colon -92 +KPX Y comma -92 +KPX Y e -111 +KPX Y eacute -111 +KPX Y ecaron -111 +KPX Y ecircumflex -111 +KPX Y edieresis -71 +KPX Y edotaccent -111 +KPX Y egrave -71 +KPX Y emacron -71 +KPX Y eogonek -111 +KPX Y hyphen -92 +KPX Y i -37 +KPX Y iacute -37 +KPX Y iogonek -37 +KPX Y o -111 +KPX Y oacute -111 +KPX Y ocircumflex -111 +KPX Y odieresis -111 +KPX Y ograve -111 +KPX Y ohungarumlaut -111 +KPX Y omacron -111 +KPX Y oslash -111 +KPX Y otilde -111 +KPX Y period -92 +KPX Y semicolon -92 +KPX Y u -92 +KPX Y uacute -92 +KPX Y ucircumflex -92 +KPX Y udieresis -92 +KPX Y ugrave -92 +KPX Y uhungarumlaut -92 +KPX Y umacron -92 +KPX Y uogonek -92 +KPX Y uring -92 +KPX Yacute A -110 +KPX Yacute Aacute -110 +KPX Yacute Abreve -110 +KPX Yacute Acircumflex -110 +KPX Yacute Adieresis -110 +KPX Yacute Agrave -110 +KPX Yacute Amacron -110 +KPX Yacute Aogonek -110 +KPX Yacute Aring -110 +KPX Yacute Atilde -110 +KPX Yacute O -35 +KPX Yacute Oacute -35 +KPX Yacute Ocircumflex -35 +KPX Yacute Odieresis -35 +KPX Yacute Ograve -35 +KPX Yacute Ohungarumlaut -35 +KPX Yacute Omacron -35 +KPX Yacute Oslash -35 +KPX Yacute Otilde -35 +KPX Yacute a -85 +KPX Yacute aacute -85 +KPX Yacute abreve -85 +KPX Yacute acircumflex -85 +KPX Yacute adieresis -85 +KPX Yacute agrave -85 +KPX Yacute amacron -85 +KPX Yacute aogonek -85 +KPX Yacute aring -85 +KPX Yacute atilde -85 +KPX Yacute colon -92 +KPX Yacute comma -92 +KPX Yacute e -111 +KPX Yacute eacute -111 +KPX Yacute ecaron -111 +KPX Yacute ecircumflex -111 +KPX Yacute edieresis -71 +KPX Yacute edotaccent -111 +KPX Yacute egrave -71 +KPX Yacute emacron -71 +KPX Yacute eogonek -111 +KPX Yacute hyphen -92 +KPX Yacute i -37 +KPX Yacute iacute -37 +KPX Yacute iogonek -37 +KPX Yacute o -111 +KPX Yacute oacute -111 +KPX Yacute ocircumflex -111 +KPX Yacute odieresis -111 +KPX Yacute ograve -111 +KPX Yacute ohungarumlaut -111 +KPX Yacute omacron -111 +KPX Yacute oslash -111 +KPX Yacute otilde -111 +KPX Yacute period -92 +KPX Yacute semicolon -92 +KPX Yacute u -92 +KPX Yacute uacute -92 +KPX Yacute ucircumflex -92 +KPX Yacute udieresis -92 +KPX Yacute ugrave -92 +KPX Yacute uhungarumlaut -92 +KPX Yacute umacron -92 +KPX Yacute uogonek -92 +KPX Yacute uring -92 +KPX Ydieresis A -110 +KPX Ydieresis Aacute -110 +KPX Ydieresis Abreve -110 +KPX Ydieresis Acircumflex -110 +KPX Ydieresis Adieresis -110 +KPX Ydieresis Agrave -110 +KPX Ydieresis Amacron -110 +KPX Ydieresis Aogonek -110 +KPX Ydieresis Aring -110 +KPX Ydieresis Atilde -110 +KPX Ydieresis O -35 +KPX Ydieresis Oacute -35 +KPX Ydieresis Ocircumflex -35 +KPX Ydieresis Odieresis -35 +KPX Ydieresis Ograve -35 +KPX Ydieresis Ohungarumlaut -35 +KPX Ydieresis Omacron -35 +KPX Ydieresis Oslash -35 +KPX Ydieresis Otilde -35 +KPX Ydieresis a -85 +KPX Ydieresis aacute -85 +KPX Ydieresis abreve -85 +KPX Ydieresis acircumflex -85 +KPX Ydieresis adieresis -85 +KPX Ydieresis agrave -85 +KPX Ydieresis amacron -85 +KPX Ydieresis aogonek -85 +KPX Ydieresis aring -85 +KPX Ydieresis atilde -85 +KPX Ydieresis colon -92 +KPX Ydieresis comma -92 +KPX Ydieresis e -111 +KPX Ydieresis eacute -111 +KPX Ydieresis ecaron -111 +KPX Ydieresis ecircumflex -111 +KPX Ydieresis edieresis -71 +KPX Ydieresis edotaccent -111 +KPX Ydieresis egrave -71 +KPX Ydieresis emacron -71 +KPX Ydieresis eogonek -111 +KPX Ydieresis hyphen -92 +KPX Ydieresis i -37 +KPX Ydieresis iacute -37 +KPX Ydieresis iogonek -37 +KPX Ydieresis o -111 +KPX Ydieresis oacute -111 +KPX Ydieresis ocircumflex -111 +KPX Ydieresis odieresis -111 +KPX Ydieresis ograve -111 +KPX Ydieresis ohungarumlaut -111 +KPX Ydieresis omacron -111 +KPX Ydieresis oslash -111 +KPX Ydieresis otilde -111 +KPX Ydieresis period -92 +KPX Ydieresis semicolon -92 +KPX Ydieresis u -92 +KPX Ydieresis uacute -92 +KPX Ydieresis ucircumflex -92 +KPX Ydieresis udieresis -92 +KPX Ydieresis ugrave -92 +KPX Ydieresis uhungarumlaut -92 +KPX Ydieresis umacron -92 +KPX Ydieresis uogonek -92 +KPX Ydieresis uring -92 +KPX a v -25 +KPX aacute v -25 +KPX abreve v -25 +KPX acircumflex v -25 +KPX adieresis v -25 +KPX agrave v -25 +KPX amacron v -25 +KPX aogonek v -25 +KPX aring v -25 +KPX atilde v -25 +KPX b b -10 +KPX b period -40 +KPX b u -20 +KPX b uacute -20 +KPX b ucircumflex -20 +KPX b udieresis -20 +KPX b ugrave -20 +KPX b uhungarumlaut -20 +KPX b umacron -20 +KPX b uogonek -20 +KPX b uring -20 +KPX b v -15 +KPX comma quotedblright -45 +KPX comma quoteright -55 +KPX d w -15 +KPX dcroat w -15 +KPX e v -15 +KPX eacute v -15 +KPX ecaron v -15 +KPX ecircumflex v -15 +KPX edieresis v -15 +KPX edotaccent v -15 +KPX egrave v -15 +KPX emacron v -15 +KPX eogonek v -15 +KPX f comma -15 +KPX f dotlessi -35 +KPX f i -25 +KPX f o -25 +KPX f oacute -25 +KPX f ocircumflex -25 +KPX f odieresis -25 +KPX f ograve -25 +KPX f ohungarumlaut -25 +KPX f omacron -25 +KPX f oslash -25 +KPX f otilde -25 +KPX f period -15 +KPX f quotedblright 50 +KPX f quoteright 55 +KPX g period -15 +KPX gbreve period -15 +KPX gcommaaccent period -15 +KPX h y -15 +KPX h yacute -15 +KPX h ydieresis -15 +KPX i v -10 +KPX iacute v -10 +KPX icircumflex v -10 +KPX idieresis v -10 +KPX igrave v -10 +KPX imacron v -10 +KPX iogonek v -10 +KPX k e -10 +KPX k eacute -10 +KPX k ecaron -10 +KPX k ecircumflex -10 +KPX k edieresis -10 +KPX k edotaccent -10 +KPX k egrave -10 +KPX k emacron -10 +KPX k eogonek -10 +KPX k o -15 +KPX k oacute -15 +KPX k ocircumflex -15 +KPX k odieresis -15 +KPX k ograve -15 +KPX k ohungarumlaut -15 +KPX k omacron -15 +KPX k oslash -15 +KPX k otilde -15 +KPX k y -15 +KPX k yacute -15 +KPX k ydieresis -15 +KPX kcommaaccent e -10 +KPX kcommaaccent eacute -10 +KPX kcommaaccent ecaron -10 +KPX kcommaaccent ecircumflex -10 +KPX kcommaaccent edieresis -10 +KPX kcommaaccent edotaccent -10 +KPX kcommaaccent egrave -10 +KPX kcommaaccent emacron -10 +KPX kcommaaccent eogonek -10 +KPX kcommaaccent o -15 +KPX kcommaaccent oacute -15 +KPX kcommaaccent ocircumflex -15 +KPX kcommaaccent odieresis -15 +KPX kcommaaccent ograve -15 +KPX kcommaaccent ohungarumlaut -15 +KPX kcommaaccent omacron -15 +KPX kcommaaccent oslash -15 +KPX kcommaaccent otilde -15 +KPX kcommaaccent y -15 +KPX kcommaaccent yacute -15 +KPX kcommaaccent ydieresis -15 +KPX n v -40 +KPX nacute v -40 +KPX ncaron v -40 +KPX ncommaaccent v -40 +KPX ntilde v -40 +KPX o v -10 +KPX o w -10 +KPX oacute v -10 +KPX oacute w -10 +KPX ocircumflex v -10 +KPX ocircumflex w -10 +KPX odieresis v -10 +KPX odieresis w -10 +KPX ograve v -10 +KPX ograve w -10 +KPX ohungarumlaut v -10 +KPX ohungarumlaut w -10 +KPX omacron v -10 +KPX omacron w -10 +KPX oslash v -10 +KPX oslash w -10 +KPX otilde v -10 +KPX otilde w -10 +KPX period quotedblright -55 +KPX period quoteright -55 +KPX quotedblleft A -10 +KPX quotedblleft Aacute -10 +KPX quotedblleft Abreve -10 +KPX quotedblleft Acircumflex -10 +KPX quotedblleft Adieresis -10 +KPX quotedblleft Agrave -10 +KPX quotedblleft Amacron -10 +KPX quotedblleft Aogonek -10 +KPX quotedblleft Aring -10 +KPX quotedblleft Atilde -10 +KPX quoteleft A -10 +KPX quoteleft Aacute -10 +KPX quoteleft Abreve -10 +KPX quoteleft Acircumflex -10 +KPX quoteleft Adieresis -10 +KPX quoteleft Agrave -10 +KPX quoteleft Amacron -10 +KPX quoteleft Aogonek -10 +KPX quoteleft Aring -10 +KPX quoteleft Atilde -10 +KPX quoteleft quoteleft -63 +KPX quoteright d -20 +KPX quoteright dcroat -20 +KPX quoteright quoteright -63 +KPX quoteright r -20 +KPX quoteright racute -20 +KPX quoteright rcaron -20 +KPX quoteright rcommaaccent -20 +KPX quoteright s -37 +KPX quoteright sacute -37 +KPX quoteright scaron -37 +KPX quoteright scedilla -37 +KPX quoteright scommaaccent -37 +KPX quoteright space -74 +KPX quoteright v -20 +KPX r c -18 +KPX r cacute -18 +KPX r ccaron -18 +KPX r ccedilla -18 +KPX r comma -92 +KPX r e -18 +KPX r eacute -18 +KPX r ecaron -18 +KPX r ecircumflex -18 +KPX r edieresis -18 +KPX r edotaccent -18 +KPX r egrave -18 +KPX r emacron -18 +KPX r eogonek -18 +KPX r g -10 +KPX r gbreve -10 +KPX r gcommaaccent -10 +KPX r hyphen -37 +KPX r n -15 +KPX r nacute -15 +KPX r ncaron -15 +KPX r ncommaaccent -15 +KPX r ntilde -15 +KPX r o -18 +KPX r oacute -18 +KPX r ocircumflex -18 +KPX r odieresis -18 +KPX r ograve -18 +KPX r ohungarumlaut -18 +KPX r omacron -18 +KPX r oslash -18 +KPX r otilde -18 +KPX r p -10 +KPX r period -100 +KPX r q -18 +KPX r v -10 +KPX racute c -18 +KPX racute cacute -18 +KPX racute ccaron -18 +KPX racute ccedilla -18 +KPX racute comma -92 +KPX racute e -18 +KPX racute eacute -18 +KPX racute ecaron -18 +KPX racute ecircumflex -18 +KPX racute edieresis -18 +KPX racute edotaccent -18 +KPX racute egrave -18 +KPX racute emacron -18 +KPX racute eogonek -18 +KPX racute g -10 +KPX racute gbreve -10 +KPX racute gcommaaccent -10 +KPX racute hyphen -37 +KPX racute n -15 +KPX racute nacute -15 +KPX racute ncaron -15 +KPX racute ncommaaccent -15 +KPX racute ntilde -15 +KPX racute o -18 +KPX racute oacute -18 +KPX racute ocircumflex -18 +KPX racute odieresis -18 +KPX racute ograve -18 +KPX racute ohungarumlaut -18 +KPX racute omacron -18 +KPX racute oslash -18 +KPX racute otilde -18 +KPX racute p -10 +KPX racute period -100 +KPX racute q -18 +KPX racute v -10 +KPX rcaron c -18 +KPX rcaron cacute -18 +KPX rcaron ccaron -18 +KPX rcaron ccedilla -18 +KPX rcaron comma -92 +KPX rcaron e -18 +KPX rcaron eacute -18 +KPX rcaron ecaron -18 +KPX rcaron ecircumflex -18 +KPX rcaron edieresis -18 +KPX rcaron edotaccent -18 +KPX rcaron egrave -18 +KPX rcaron emacron -18 +KPX rcaron eogonek -18 +KPX rcaron g -10 +KPX rcaron gbreve -10 +KPX rcaron gcommaaccent -10 +KPX rcaron hyphen -37 +KPX rcaron n -15 +KPX rcaron nacute -15 +KPX rcaron ncaron -15 +KPX rcaron ncommaaccent -15 +KPX rcaron ntilde -15 +KPX rcaron o -18 +KPX rcaron oacute -18 +KPX rcaron ocircumflex -18 +KPX rcaron odieresis -18 +KPX rcaron ograve -18 +KPX rcaron ohungarumlaut -18 +KPX rcaron omacron -18 +KPX rcaron oslash -18 +KPX rcaron otilde -18 +KPX rcaron p -10 +KPX rcaron period -100 +KPX rcaron q -18 +KPX rcaron v -10 +KPX rcommaaccent c -18 +KPX rcommaaccent cacute -18 +KPX rcommaaccent ccaron -18 +KPX rcommaaccent ccedilla -18 +KPX rcommaaccent comma -92 +KPX rcommaaccent e -18 +KPX rcommaaccent eacute -18 +KPX rcommaaccent ecaron -18 +KPX rcommaaccent ecircumflex -18 +KPX rcommaaccent edieresis -18 +KPX rcommaaccent edotaccent -18 +KPX rcommaaccent egrave -18 +KPX rcommaaccent emacron -18 +KPX rcommaaccent eogonek -18 +KPX rcommaaccent g -10 +KPX rcommaaccent gbreve -10 +KPX rcommaaccent gcommaaccent -10 +KPX rcommaaccent hyphen -37 +KPX rcommaaccent n -15 +KPX rcommaaccent nacute -15 +KPX rcommaaccent ncaron -15 +KPX rcommaaccent ncommaaccent -15 +KPX rcommaaccent ntilde -15 +KPX rcommaaccent o -18 +KPX rcommaaccent oacute -18 +KPX rcommaaccent ocircumflex -18 +KPX rcommaaccent odieresis -18 +KPX rcommaaccent ograve -18 +KPX rcommaaccent ohungarumlaut -18 +KPX rcommaaccent omacron -18 +KPX rcommaaccent oslash -18 +KPX rcommaaccent otilde -18 +KPX rcommaaccent p -10 +KPX rcommaaccent period -100 +KPX rcommaaccent q -18 +KPX rcommaaccent v -10 +KPX space A -55 +KPX space Aacute -55 +KPX space Abreve -55 +KPX space Acircumflex -55 +KPX space Adieresis -55 +KPX space Agrave -55 +KPX space Amacron -55 +KPX space Aogonek -55 +KPX space Aring -55 +KPX space Atilde -55 +KPX space T -30 +KPX space Tcaron -30 +KPX space Tcommaaccent -30 +KPX space V -45 +KPX space W -30 +KPX space Y -55 +KPX space Yacute -55 +KPX space Ydieresis -55 +KPX v a -10 +KPX v aacute -10 +KPX v abreve -10 +KPX v acircumflex -10 +KPX v adieresis -10 +KPX v agrave -10 +KPX v amacron -10 +KPX v aogonek -10 +KPX v aring -10 +KPX v atilde -10 +KPX v comma -55 +KPX v e -10 +KPX v eacute -10 +KPX v ecaron -10 +KPX v ecircumflex -10 +KPX v edieresis -10 +KPX v edotaccent -10 +KPX v egrave -10 +KPX v emacron -10 +KPX v eogonek -10 +KPX v o -10 +KPX v oacute -10 +KPX v ocircumflex -10 +KPX v odieresis -10 +KPX v ograve -10 +KPX v ohungarumlaut -10 +KPX v omacron -10 +KPX v oslash -10 +KPX v otilde -10 +KPX v period -70 +KPX w comma -55 +KPX w o -10 +KPX w oacute -10 +KPX w ocircumflex -10 +KPX w odieresis -10 +KPX w ograve -10 +KPX w ohungarumlaut -10 +KPX w omacron -10 +KPX w oslash -10 +KPX w otilde -10 +KPX w period -70 +KPX y comma -55 +KPX y e -10 +KPX y eacute -10 +KPX y ecaron -10 +KPX y ecircumflex -10 +KPX y edieresis -10 +KPX y edotaccent -10 +KPX y egrave -10 +KPX y emacron -10 +KPX y eogonek -10 +KPX y o -25 +KPX y oacute -25 +KPX y ocircumflex -25 +KPX y odieresis -25 +KPX y ograve -25 +KPX y ohungarumlaut -25 +KPX y omacron -25 +KPX y oslash -25 +KPX y otilde -25 +KPX y period -70 +KPX yacute comma -55 +KPX yacute e -10 +KPX yacute eacute -10 +KPX yacute ecaron -10 +KPX yacute ecircumflex -10 +KPX yacute edieresis -10 +KPX yacute edotaccent -10 +KPX yacute egrave -10 +KPX yacute emacron -10 +KPX yacute eogonek -10 +KPX yacute o -25 +KPX yacute oacute -25 +KPX yacute ocircumflex -25 +KPX yacute odieresis -25 +KPX yacute ograve -25 +KPX yacute ohungarumlaut -25 +KPX yacute omacron -25 +KPX yacute oslash -25 +KPX yacute otilde -25 +KPX yacute period -70 +KPX ydieresis comma -55 +KPX ydieresis e -10 +KPX ydieresis eacute -10 +KPX ydieresis ecaron -10 +KPX ydieresis ecircumflex -10 +KPX ydieresis edieresis -10 +KPX ydieresis edotaccent -10 +KPX ydieresis egrave -10 +KPX ydieresis emacron -10 +KPX ydieresis eogonek -10 +KPX ydieresis o -25 +KPX ydieresis oacute -25 +KPX ydieresis ocircumflex -25 +KPX ydieresis odieresis -25 +KPX ydieresis ograve -25 +KPX ydieresis ohungarumlaut -25 +KPX ydieresis omacron -25 +KPX ydieresis oslash -25 +KPX ydieresis otilde -25 +KPX ydieresis period -70 +EndKernPairs +EndKernData +EndFontMetrics diff --git a/internal/corefont/Core14_AFMs/Times-BoldItalic.afm b/internal/corefont/Core14_AFMs/Times-BoldItalic.afm new file mode 100644 index 0000000000000000000000000000000000000000..2301dfd232647c35540f590315823c18f9633670 --- /dev/null +++ b/internal/corefont/Core14_AFMs/Times-BoldItalic.afm @@ -0,0 +1,2384 @@ +StartFontMetrics 4.1 +Comment Copyright (c) 1985, 1987, 1989, 1990, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved. +Comment Creation Date: Thu May 1 13:04:06 1997 +Comment UniqueID 43066 +Comment VMusage 45874 56899 +FontName Times-BoldItalic +FullName Times Bold Italic +FamilyName Times +Weight Bold +ItalicAngle -15 +IsFixedPitch false +CharacterSet ExtendedRoman +FontBBox -200 -218 996 921 +UnderlinePosition -100 +UnderlineThickness 50 +Version 002.000 +Notice Copyright (c) 1985, 1987, 1989, 1990, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved.Times is a trademark of Linotype-Hell AG and/or its subsidiaries. +EncodingScheme AdobeStandardEncoding +CapHeight 669 +XHeight 462 +Ascender 683 +Descender -217 +StdHW 42 +StdVW 121 +StartCharMetrics 315 +C 32 ; WX 250 ; N space ; B 0 0 0 0 ; +C 33 ; WX 389 ; N exclam ; B 67 -13 370 684 ; +C 34 ; WX 555 ; N quotedbl ; B 136 398 536 685 ; +C 35 ; WX 500 ; N numbersign ; B -33 0 533 700 ; +C 36 ; WX 500 ; N dollar ; B -20 -100 497 733 ; +C 37 ; WX 833 ; N percent ; B 39 -10 793 692 ; +C 38 ; WX 778 ; N ampersand ; B 5 -19 699 682 ; +C 39 ; WX 333 ; N quoteright ; B 98 369 302 685 ; +C 40 ; WX 333 ; N parenleft ; B 28 -179 344 685 ; +C 41 ; WX 333 ; N parenright ; B -44 -179 271 685 ; +C 42 ; WX 500 ; N asterisk ; B 65 249 456 685 ; +C 43 ; WX 570 ; N plus ; B 33 0 537 506 ; +C 44 ; WX 250 ; N comma ; B -60 -182 144 134 ; +C 45 ; WX 333 ; N hyphen ; B 2 166 271 282 ; +C 46 ; WX 250 ; N period ; B -9 -13 139 135 ; +C 47 ; WX 278 ; N slash ; B -64 -18 342 685 ; +C 48 ; WX 500 ; N zero ; B 17 -14 477 683 ; +C 49 ; WX 500 ; N one ; B 5 0 419 683 ; +C 50 ; WX 500 ; N two ; B -27 0 446 683 ; +C 51 ; WX 500 ; N three ; B -15 -13 450 683 ; +C 52 ; WX 500 ; N four ; B -15 0 503 683 ; +C 53 ; WX 500 ; N five ; B -11 -13 487 669 ; +C 54 ; WX 500 ; N six ; B 23 -15 509 679 ; +C 55 ; WX 500 ; N seven ; B 52 0 525 669 ; +C 56 ; WX 500 ; N eight ; B 3 -13 476 683 ; +C 57 ; WX 500 ; N nine ; B -12 -10 475 683 ; +C 58 ; WX 333 ; N colon ; B 23 -13 264 459 ; +C 59 ; WX 333 ; N semicolon ; B -25 -183 264 459 ; +C 60 ; WX 570 ; N less ; B 31 -8 539 514 ; +C 61 ; WX 570 ; N equal ; B 33 107 537 399 ; +C 62 ; WX 570 ; N greater ; B 31 -8 539 514 ; +C 63 ; WX 500 ; N question ; B 79 -13 470 684 ; +C 64 ; WX 832 ; N at ; B 63 -18 770 685 ; +C 65 ; WX 667 ; N A ; B -67 0 593 683 ; +C 66 ; WX 667 ; N B ; B -24 0 624 669 ; +C 67 ; WX 667 ; N C ; B 32 -18 677 685 ; +C 68 ; WX 722 ; N D ; B -46 0 685 669 ; +C 69 ; WX 667 ; N E ; B -27 0 653 669 ; +C 70 ; WX 667 ; N F ; B -13 0 660 669 ; +C 71 ; WX 722 ; N G ; B 21 -18 706 685 ; +C 72 ; WX 778 ; N H ; B -24 0 799 669 ; +C 73 ; WX 389 ; N I ; B -32 0 406 669 ; +C 74 ; WX 500 ; N J ; B -46 -99 524 669 ; +C 75 ; WX 667 ; N K ; B -21 0 702 669 ; +C 76 ; WX 611 ; N L ; B -22 0 590 669 ; +C 77 ; WX 889 ; N M ; B -29 -12 917 669 ; +C 78 ; WX 722 ; N N ; B -27 -15 748 669 ; +C 79 ; WX 722 ; N O ; B 27 -18 691 685 ; +C 80 ; WX 611 ; N P ; B -27 0 613 669 ; +C 81 ; WX 722 ; N Q ; B 27 -208 691 685 ; +C 82 ; WX 667 ; N R ; B -29 0 623 669 ; +C 83 ; WX 556 ; N S ; B 2 -18 526 685 ; +C 84 ; WX 611 ; N T ; B 50 0 650 669 ; +C 85 ; WX 722 ; N U ; B 67 -18 744 669 ; +C 86 ; WX 667 ; N V ; B 65 -18 715 669 ; +C 87 ; WX 889 ; N W ; B 65 -18 940 669 ; +C 88 ; WX 667 ; N X ; B -24 0 694 669 ; +C 89 ; WX 611 ; N Y ; B 73 0 659 669 ; +C 90 ; WX 611 ; N Z ; B -11 0 590 669 ; +C 91 ; WX 333 ; N bracketleft ; B -37 -159 362 674 ; +C 92 ; WX 278 ; N backslash ; B -1 -18 279 685 ; +C 93 ; WX 333 ; N bracketright ; B -56 -157 343 674 ; +C 94 ; WX 570 ; N asciicircum ; B 67 304 503 669 ; +C 95 ; WX 500 ; N underscore ; B 0 -125 500 -75 ; +C 96 ; WX 333 ; N quoteleft ; B 128 369 332 685 ; +C 97 ; WX 500 ; N a ; B -21 -14 455 462 ; +C 98 ; WX 500 ; N b ; B -14 -13 444 699 ; +C 99 ; WX 444 ; N c ; B -5 -13 392 462 ; +C 100 ; WX 500 ; N d ; B -21 -13 517 699 ; +C 101 ; WX 444 ; N e ; B 5 -13 398 462 ; +C 102 ; WX 333 ; N f ; B -169 -205 446 698 ; L i fi ; L l fl ; +C 103 ; WX 500 ; N g ; B -52 -203 478 462 ; +C 104 ; WX 556 ; N h ; B -13 -9 498 699 ; +C 105 ; WX 278 ; N i ; B 2 -9 263 684 ; +C 106 ; WX 278 ; N j ; B -189 -207 279 684 ; +C 107 ; WX 500 ; N k ; B -23 -8 483 699 ; +C 108 ; WX 278 ; N l ; B 2 -9 290 699 ; +C 109 ; WX 778 ; N m ; B -14 -9 722 462 ; +C 110 ; WX 556 ; N n ; B -6 -9 493 462 ; +C 111 ; WX 500 ; N o ; B -3 -13 441 462 ; +C 112 ; WX 500 ; N p ; B -120 -205 446 462 ; +C 113 ; WX 500 ; N q ; B 1 -205 471 462 ; +C 114 ; WX 389 ; N r ; B -21 0 389 462 ; +C 115 ; WX 389 ; N s ; B -19 -13 333 462 ; +C 116 ; WX 278 ; N t ; B -11 -9 281 594 ; +C 117 ; WX 556 ; N u ; B 15 -9 492 462 ; +C 118 ; WX 444 ; N v ; B 16 -13 401 462 ; +C 119 ; WX 667 ; N w ; B 16 -13 614 462 ; +C 120 ; WX 500 ; N x ; B -46 -13 469 462 ; +C 121 ; WX 444 ; N y ; B -94 -205 392 462 ; +C 122 ; WX 389 ; N z ; B -43 -78 368 449 ; +C 123 ; WX 348 ; N braceleft ; B 5 -187 436 686 ; +C 124 ; WX 220 ; N bar ; B 66 -218 154 782 ; +C 125 ; WX 348 ; N braceright ; B -129 -187 302 686 ; +C 126 ; WX 570 ; N asciitilde ; B 54 173 516 333 ; +C 161 ; WX 389 ; N exclamdown ; B 19 -205 322 492 ; +C 162 ; WX 500 ; N cent ; B 42 -143 439 576 ; +C 163 ; WX 500 ; N sterling ; B -32 -12 510 683 ; +C 164 ; WX 167 ; N fraction ; B -169 -14 324 683 ; +C 165 ; WX 500 ; N yen ; B 33 0 628 669 ; +C 166 ; WX 500 ; N florin ; B -87 -156 537 707 ; +C 167 ; WX 500 ; N section ; B 36 -143 459 685 ; +C 168 ; WX 500 ; N currency ; B -26 34 526 586 ; +C 169 ; WX 278 ; N quotesingle ; B 128 398 268 685 ; +C 170 ; WX 500 ; N quotedblleft ; B 53 369 513 685 ; +C 171 ; WX 500 ; N guillemotleft ; B 12 32 468 415 ; +C 172 ; WX 333 ; N guilsinglleft ; B 32 32 303 415 ; +C 173 ; WX 333 ; N guilsinglright ; B 10 32 281 415 ; +C 174 ; WX 556 ; N fi ; B -188 -205 514 703 ; +C 175 ; WX 556 ; N fl ; B -186 -205 553 704 ; +C 177 ; WX 500 ; N endash ; B -40 178 477 269 ; +C 178 ; WX 500 ; N dagger ; B 91 -145 494 685 ; +C 179 ; WX 500 ; N daggerdbl ; B 10 -139 493 685 ; +C 180 ; WX 250 ; N periodcentered ; B 51 257 199 405 ; +C 182 ; WX 500 ; N paragraph ; B -57 -193 562 669 ; +C 183 ; WX 350 ; N bullet ; B 0 175 350 525 ; +C 184 ; WX 333 ; N quotesinglbase ; B -5 -182 199 134 ; +C 185 ; WX 500 ; N quotedblbase ; B -57 -182 403 134 ; +C 186 ; WX 500 ; N quotedblright ; B 53 369 513 685 ; +C 187 ; WX 500 ; N guillemotright ; B 12 32 468 415 ; +C 188 ; WX 1000 ; N ellipsis ; B 40 -13 852 135 ; +C 189 ; WX 1000 ; N perthousand ; B 7 -29 996 706 ; +C 191 ; WX 500 ; N questiondown ; B 30 -205 421 492 ; +C 193 ; WX 333 ; N grave ; B 85 516 297 697 ; +C 194 ; WX 333 ; N acute ; B 139 516 379 697 ; +C 195 ; WX 333 ; N circumflex ; B 40 516 367 690 ; +C 196 ; WX 333 ; N tilde ; B 48 536 407 655 ; +C 197 ; WX 333 ; N macron ; B 51 553 393 623 ; +C 198 ; WX 333 ; N breve ; B 71 516 387 678 ; +C 199 ; WX 333 ; N dotaccent ; B 163 550 298 684 ; +C 200 ; WX 333 ; N dieresis ; B 55 550 402 684 ; +C 202 ; WX 333 ; N ring ; B 127 516 340 729 ; +C 203 ; WX 333 ; N cedilla ; B -80 -218 156 5 ; +C 205 ; WX 333 ; N hungarumlaut ; B 69 516 498 697 ; +C 206 ; WX 333 ; N ogonek ; B 15 -183 244 34 ; +C 207 ; WX 333 ; N caron ; B 79 516 411 690 ; +C 208 ; WX 1000 ; N emdash ; B -40 178 977 269 ; +C 225 ; WX 944 ; N AE ; B -64 0 918 669 ; +C 227 ; WX 266 ; N ordfeminine ; B 16 399 330 685 ; +C 232 ; WX 611 ; N Lslash ; B -22 0 590 669 ; +C 233 ; WX 722 ; N Oslash ; B 27 -125 691 764 ; +C 234 ; WX 944 ; N OE ; B 23 -8 946 677 ; +C 235 ; WX 300 ; N ordmasculine ; B 56 400 347 685 ; +C 241 ; WX 722 ; N ae ; B -5 -13 673 462 ; +C 245 ; WX 278 ; N dotlessi ; B 2 -9 238 462 ; +C 248 ; WX 278 ; N lslash ; B -7 -9 307 699 ; +C 249 ; WX 500 ; N oslash ; B -3 -119 441 560 ; +C 250 ; WX 722 ; N oe ; B 6 -13 674 462 ; +C 251 ; WX 500 ; N germandbls ; B -200 -200 473 705 ; +C -1 ; WX 389 ; N Idieresis ; B -32 0 450 862 ; +C -1 ; WX 444 ; N eacute ; B 5 -13 435 697 ; +C -1 ; WX 500 ; N abreve ; B -21 -14 471 678 ; +C -1 ; WX 556 ; N uhungarumlaut ; B 15 -9 610 697 ; +C -1 ; WX 444 ; N ecaron ; B 5 -13 467 690 ; +C -1 ; WX 611 ; N Ydieresis ; B 73 0 659 862 ; +C -1 ; WX 570 ; N divide ; B 33 -29 537 535 ; +C -1 ; WX 611 ; N Yacute ; B 73 0 659 904 ; +C -1 ; WX 667 ; N Acircumflex ; B -67 0 593 897 ; +C -1 ; WX 500 ; N aacute ; B -21 -14 463 697 ; +C -1 ; WX 722 ; N Ucircumflex ; B 67 -18 744 897 ; +C -1 ; WX 444 ; N yacute ; B -94 -205 435 697 ; +C -1 ; WX 389 ; N scommaaccent ; B -19 -218 333 462 ; +C -1 ; WX 444 ; N ecircumflex ; B 5 -13 423 690 ; +C -1 ; WX 722 ; N Uring ; B 67 -18 744 921 ; +C -1 ; WX 722 ; N Udieresis ; B 67 -18 744 862 ; +C -1 ; WX 500 ; N aogonek ; B -21 -183 455 462 ; +C -1 ; WX 722 ; N Uacute ; B 67 -18 744 904 ; +C -1 ; WX 556 ; N uogonek ; B 15 -183 492 462 ; +C -1 ; WX 667 ; N Edieresis ; B -27 0 653 862 ; +C -1 ; WX 722 ; N Dcroat ; B -31 0 700 669 ; +C -1 ; WX 250 ; N commaaccent ; B -36 -218 131 -50 ; +C -1 ; WX 747 ; N copyright ; B 30 -18 718 685 ; +C -1 ; WX 667 ; N Emacron ; B -27 0 653 830 ; +C -1 ; WX 444 ; N ccaron ; B -5 -13 467 690 ; +C -1 ; WX 500 ; N aring ; B -21 -14 455 729 ; +C -1 ; WX 722 ; N Ncommaaccent ; B -27 -218 748 669 ; +C -1 ; WX 278 ; N lacute ; B 2 -9 392 904 ; +C -1 ; WX 500 ; N agrave ; B -21 -14 455 697 ; +C -1 ; WX 611 ; N Tcommaaccent ; B 50 -218 650 669 ; +C -1 ; WX 667 ; N Cacute ; B 32 -18 677 904 ; +C -1 ; WX 500 ; N atilde ; B -21 -14 491 655 ; +C -1 ; WX 667 ; N Edotaccent ; B -27 0 653 862 ; +C -1 ; WX 389 ; N scaron ; B -19 -13 424 690 ; +C -1 ; WX 389 ; N scedilla ; B -19 -218 333 462 ; +C -1 ; WX 278 ; N iacute ; B 2 -9 352 697 ; +C -1 ; WX 494 ; N lozenge ; B 10 0 484 745 ; +C -1 ; WX 667 ; N Rcaron ; B -29 0 623 897 ; +C -1 ; WX 722 ; N Gcommaaccent ; B 21 -218 706 685 ; +C -1 ; WX 556 ; N ucircumflex ; B 15 -9 492 690 ; +C -1 ; WX 500 ; N acircumflex ; B -21 -14 455 690 ; +C -1 ; WX 667 ; N Amacron ; B -67 0 593 830 ; +C -1 ; WX 389 ; N rcaron ; B -21 0 424 690 ; +C -1 ; WX 444 ; N ccedilla ; B -5 -218 392 462 ; +C -1 ; WX 611 ; N Zdotaccent ; B -11 0 590 862 ; +C -1 ; WX 611 ; N Thorn ; B -27 0 573 669 ; +C -1 ; WX 722 ; N Omacron ; B 27 -18 691 830 ; +C -1 ; WX 667 ; N Racute ; B -29 0 623 904 ; +C -1 ; WX 556 ; N Sacute ; B 2 -18 531 904 ; +C -1 ; WX 608 ; N dcaron ; B -21 -13 675 708 ; +C -1 ; WX 722 ; N Umacron ; B 67 -18 744 830 ; +C -1 ; WX 556 ; N uring ; B 15 -9 492 729 ; +C -1 ; WX 300 ; N threesuperior ; B 17 265 321 683 ; +C -1 ; WX 722 ; N Ograve ; B 27 -18 691 904 ; +C -1 ; WX 667 ; N Agrave ; B -67 0 593 904 ; +C -1 ; WX 667 ; N Abreve ; B -67 0 593 885 ; +C -1 ; WX 570 ; N multiply ; B 48 16 522 490 ; +C -1 ; WX 556 ; N uacute ; B 15 -9 492 697 ; +C -1 ; WX 611 ; N Tcaron ; B 50 0 650 897 ; +C -1 ; WX 494 ; N partialdiff ; B 11 -21 494 750 ; +C -1 ; WX 444 ; N ydieresis ; B -94 -205 443 655 ; +C -1 ; WX 722 ; N Nacute ; B -27 -15 748 904 ; +C -1 ; WX 278 ; N icircumflex ; B -3 -9 324 690 ; +C -1 ; WX 667 ; N Ecircumflex ; B -27 0 653 897 ; +C -1 ; WX 500 ; N adieresis ; B -21 -14 476 655 ; +C -1 ; WX 444 ; N edieresis ; B 5 -13 448 655 ; +C -1 ; WX 444 ; N cacute ; B -5 -13 435 697 ; +C -1 ; WX 556 ; N nacute ; B -6 -9 493 697 ; +C -1 ; WX 556 ; N umacron ; B 15 -9 492 623 ; +C -1 ; WX 722 ; N Ncaron ; B -27 -15 748 897 ; +C -1 ; WX 389 ; N Iacute ; B -32 0 432 904 ; +C -1 ; WX 570 ; N plusminus ; B 33 0 537 506 ; +C -1 ; WX 220 ; N brokenbar ; B 66 -143 154 707 ; +C -1 ; WX 747 ; N registered ; B 30 -18 718 685 ; +C -1 ; WX 722 ; N Gbreve ; B 21 -18 706 885 ; +C -1 ; WX 389 ; N Idotaccent ; B -32 0 406 862 ; +C -1 ; WX 600 ; N summation ; B 14 -10 585 706 ; +C -1 ; WX 667 ; N Egrave ; B -27 0 653 904 ; +C -1 ; WX 389 ; N racute ; B -21 0 407 697 ; +C -1 ; WX 500 ; N omacron ; B -3 -13 462 623 ; +C -1 ; WX 611 ; N Zacute ; B -11 0 590 904 ; +C -1 ; WX 611 ; N Zcaron ; B -11 0 590 897 ; +C -1 ; WX 549 ; N greaterequal ; B 26 0 523 704 ; +C -1 ; WX 722 ; N Eth ; B -31 0 700 669 ; +C -1 ; WX 667 ; N Ccedilla ; B 32 -218 677 685 ; +C -1 ; WX 278 ; N lcommaaccent ; B -42 -218 290 699 ; +C -1 ; WX 366 ; N tcaron ; B -11 -9 434 754 ; +C -1 ; WX 444 ; N eogonek ; B 5 -183 398 462 ; +C -1 ; WX 722 ; N Uogonek ; B 67 -183 744 669 ; +C -1 ; WX 667 ; N Aacute ; B -67 0 593 904 ; +C -1 ; WX 667 ; N Adieresis ; B -67 0 593 862 ; +C -1 ; WX 444 ; N egrave ; B 5 -13 398 697 ; +C -1 ; WX 389 ; N zacute ; B -43 -78 407 697 ; +C -1 ; WX 278 ; N iogonek ; B -20 -183 263 684 ; +C -1 ; WX 722 ; N Oacute ; B 27 -18 691 904 ; +C -1 ; WX 500 ; N oacute ; B -3 -13 463 697 ; +C -1 ; WX 500 ; N amacron ; B -21 -14 467 623 ; +C -1 ; WX 389 ; N sacute ; B -19 -13 407 697 ; +C -1 ; WX 278 ; N idieresis ; B 2 -9 364 655 ; +C -1 ; WX 722 ; N Ocircumflex ; B 27 -18 691 897 ; +C -1 ; WX 722 ; N Ugrave ; B 67 -18 744 904 ; +C -1 ; WX 612 ; N Delta ; B 6 0 608 688 ; +C -1 ; WX 500 ; N thorn ; B -120 -205 446 699 ; +C -1 ; WX 300 ; N twosuperior ; B 2 274 313 683 ; +C -1 ; WX 722 ; N Odieresis ; B 27 -18 691 862 ; +C -1 ; WX 576 ; N mu ; B -60 -207 516 449 ; +C -1 ; WX 278 ; N igrave ; B 2 -9 259 697 ; +C -1 ; WX 500 ; N ohungarumlaut ; B -3 -13 582 697 ; +C -1 ; WX 667 ; N Eogonek ; B -27 -183 653 669 ; +C -1 ; WX 500 ; N dcroat ; B -21 -13 552 699 ; +C -1 ; WX 750 ; N threequarters ; B 7 -14 726 683 ; +C -1 ; WX 556 ; N Scedilla ; B 2 -218 526 685 ; +C -1 ; WX 382 ; N lcaron ; B 2 -9 448 708 ; +C -1 ; WX 667 ; N Kcommaaccent ; B -21 -218 702 669 ; +C -1 ; WX 611 ; N Lacute ; B -22 0 590 904 ; +C -1 ; WX 1000 ; N trademark ; B 32 263 968 669 ; +C -1 ; WX 444 ; N edotaccent ; B 5 -13 398 655 ; +C -1 ; WX 389 ; N Igrave ; B -32 0 406 904 ; +C -1 ; WX 389 ; N Imacron ; B -32 0 461 830 ; +C -1 ; WX 611 ; N Lcaron ; B -22 0 671 718 ; +C -1 ; WX 750 ; N onehalf ; B -9 -14 723 683 ; +C -1 ; WX 549 ; N lessequal ; B 29 0 526 704 ; +C -1 ; WX 500 ; N ocircumflex ; B -3 -13 451 690 ; +C -1 ; WX 556 ; N ntilde ; B -6 -9 504 655 ; +C -1 ; WX 722 ; N Uhungarumlaut ; B 67 -18 744 904 ; +C -1 ; WX 667 ; N Eacute ; B -27 0 653 904 ; +C -1 ; WX 444 ; N emacron ; B 5 -13 439 623 ; +C -1 ; WX 500 ; N gbreve ; B -52 -203 478 678 ; +C -1 ; WX 750 ; N onequarter ; B 7 -14 721 683 ; +C -1 ; WX 556 ; N Scaron ; B 2 -18 553 897 ; +C -1 ; WX 556 ; N Scommaaccent ; B 2 -218 526 685 ; +C -1 ; WX 722 ; N Ohungarumlaut ; B 27 -18 723 904 ; +C -1 ; WX 400 ; N degree ; B 83 397 369 683 ; +C -1 ; WX 500 ; N ograve ; B -3 -13 441 697 ; +C -1 ; WX 667 ; N Ccaron ; B 32 -18 677 897 ; +C -1 ; WX 556 ; N ugrave ; B 15 -9 492 697 ; +C -1 ; WX 549 ; N radical ; B 10 -46 512 850 ; +C -1 ; WX 722 ; N Dcaron ; B -46 0 685 897 ; +C -1 ; WX 389 ; N rcommaaccent ; B -67 -218 389 462 ; +C -1 ; WX 722 ; N Ntilde ; B -27 -15 748 862 ; +C -1 ; WX 500 ; N otilde ; B -3 -13 491 655 ; +C -1 ; WX 667 ; N Rcommaaccent ; B -29 -218 623 669 ; +C -1 ; WX 611 ; N Lcommaaccent ; B -22 -218 590 669 ; +C -1 ; WX 667 ; N Atilde ; B -67 0 593 862 ; +C -1 ; WX 667 ; N Aogonek ; B -67 -183 604 683 ; +C -1 ; WX 667 ; N Aring ; B -67 0 593 921 ; +C -1 ; WX 722 ; N Otilde ; B 27 -18 691 862 ; +C -1 ; WX 389 ; N zdotaccent ; B -43 -78 368 655 ; +C -1 ; WX 667 ; N Ecaron ; B -27 0 653 897 ; +C -1 ; WX 389 ; N Iogonek ; B -32 -183 406 669 ; +C -1 ; WX 500 ; N kcommaaccent ; B -23 -218 483 699 ; +C -1 ; WX 606 ; N minus ; B 51 209 555 297 ; +C -1 ; WX 389 ; N Icircumflex ; B -32 0 450 897 ; +C -1 ; WX 556 ; N ncaron ; B -6 -9 523 690 ; +C -1 ; WX 278 ; N tcommaaccent ; B -62 -218 281 594 ; +C -1 ; WX 606 ; N logicalnot ; B 51 108 555 399 ; +C -1 ; WX 500 ; N odieresis ; B -3 -13 471 655 ; +C -1 ; WX 556 ; N udieresis ; B 15 -9 499 655 ; +C -1 ; WX 549 ; N notequal ; B 15 -49 540 570 ; +C -1 ; WX 500 ; N gcommaaccent ; B -52 -203 478 767 ; +C -1 ; WX 500 ; N eth ; B -3 -13 454 699 ; +C -1 ; WX 389 ; N zcaron ; B -43 -78 424 690 ; +C -1 ; WX 556 ; N ncommaaccent ; B -6 -218 493 462 ; +C -1 ; WX 300 ; N onesuperior ; B 30 274 301 683 ; +C -1 ; WX 278 ; N imacron ; B 2 -9 294 623 ; +C -1 ; WX 500 ; N Euro ; B 0 0 0 0 ; +EndCharMetrics +StartKernData +StartKernPairs 2038 +KPX A C -65 +KPX A Cacute -65 +KPX A Ccaron -65 +KPX A Ccedilla -65 +KPX A G -60 +KPX A Gbreve -60 +KPX A Gcommaaccent -60 +KPX A O -50 +KPX A Oacute -50 +KPX A Ocircumflex -50 +KPX A Odieresis -50 +KPX A Ograve -50 +KPX A Ohungarumlaut -50 +KPX A Omacron -50 +KPX A Oslash -50 +KPX A Otilde -50 +KPX A Q -55 +KPX A T -55 +KPX A Tcaron -55 +KPX A Tcommaaccent -55 +KPX A U -50 +KPX A Uacute -50 +KPX A Ucircumflex -50 +KPX A Udieresis -50 +KPX A Ugrave -50 +KPX A Uhungarumlaut -50 +KPX A Umacron -50 +KPX A Uogonek -50 +KPX A Uring -50 +KPX A V -95 +KPX A W -100 +KPX A Y -70 +KPX A Yacute -70 +KPX A Ydieresis -70 +KPX A quoteright -74 +KPX A u -30 +KPX A uacute -30 +KPX A ucircumflex -30 +KPX A udieresis -30 +KPX A ugrave -30 +KPX A uhungarumlaut -30 +KPX A umacron -30 +KPX A uogonek -30 +KPX A uring -30 +KPX A v -74 +KPX A w -74 +KPX A y -74 +KPX A yacute -74 +KPX A ydieresis -74 +KPX Aacute C -65 +KPX Aacute Cacute -65 +KPX Aacute Ccaron -65 +KPX Aacute Ccedilla -65 +KPX Aacute G -60 +KPX Aacute Gbreve -60 +KPX Aacute Gcommaaccent -60 +KPX Aacute O -50 +KPX Aacute Oacute -50 +KPX Aacute Ocircumflex -50 +KPX Aacute Odieresis -50 +KPX Aacute Ograve -50 +KPX Aacute Ohungarumlaut -50 +KPX Aacute Omacron -50 +KPX Aacute Oslash -50 +KPX Aacute Otilde -50 +KPX Aacute Q -55 +KPX Aacute T -55 +KPX Aacute Tcaron -55 +KPX Aacute Tcommaaccent -55 +KPX Aacute U -50 +KPX Aacute Uacute -50 +KPX Aacute Ucircumflex -50 +KPX Aacute Udieresis -50 +KPX Aacute Ugrave -50 +KPX Aacute Uhungarumlaut -50 +KPX Aacute Umacron -50 +KPX Aacute Uogonek -50 +KPX Aacute Uring -50 +KPX Aacute V -95 +KPX Aacute W -100 +KPX Aacute Y -70 +KPX Aacute Yacute -70 +KPX Aacute Ydieresis -70 +KPX Aacute quoteright -74 +KPX Aacute u -30 +KPX Aacute uacute -30 +KPX Aacute ucircumflex -30 +KPX Aacute udieresis -30 +KPX Aacute ugrave -30 +KPX Aacute uhungarumlaut -30 +KPX Aacute umacron -30 +KPX Aacute uogonek -30 +KPX Aacute uring -30 +KPX Aacute v -74 +KPX Aacute w -74 +KPX Aacute y -74 +KPX Aacute yacute -74 +KPX Aacute ydieresis -74 +KPX Abreve C -65 +KPX Abreve Cacute -65 +KPX Abreve Ccaron -65 +KPX Abreve Ccedilla -65 +KPX Abreve G -60 +KPX Abreve Gbreve -60 +KPX Abreve Gcommaaccent -60 +KPX Abreve O -50 +KPX Abreve Oacute -50 +KPX Abreve Ocircumflex -50 +KPX Abreve Odieresis -50 +KPX Abreve Ograve -50 +KPX Abreve Ohungarumlaut -50 +KPX Abreve Omacron -50 +KPX Abreve Oslash -50 +KPX Abreve Otilde -50 +KPX Abreve Q -55 +KPX Abreve T -55 +KPX Abreve Tcaron -55 +KPX Abreve Tcommaaccent -55 +KPX Abreve U -50 +KPX Abreve Uacute -50 +KPX Abreve Ucircumflex -50 +KPX Abreve Udieresis -50 +KPX Abreve Ugrave -50 +KPX Abreve Uhungarumlaut -50 +KPX Abreve Umacron -50 +KPX Abreve Uogonek -50 +KPX Abreve Uring -50 +KPX Abreve V -95 +KPX Abreve W -100 +KPX Abreve Y -70 +KPX Abreve Yacute -70 +KPX Abreve Ydieresis -70 +KPX Abreve quoteright -74 +KPX Abreve u -30 +KPX Abreve uacute -30 +KPX Abreve ucircumflex -30 +KPX Abreve udieresis -30 +KPX Abreve ugrave -30 +KPX Abreve uhungarumlaut -30 +KPX Abreve umacron -30 +KPX Abreve uogonek -30 +KPX Abreve uring -30 +KPX Abreve v -74 +KPX Abreve w -74 +KPX Abreve y -74 +KPX Abreve yacute -74 +KPX Abreve ydieresis -74 +KPX Acircumflex C -65 +KPX Acircumflex Cacute -65 +KPX Acircumflex Ccaron -65 +KPX Acircumflex Ccedilla -65 +KPX Acircumflex G -60 +KPX Acircumflex Gbreve -60 +KPX Acircumflex Gcommaaccent -60 +KPX Acircumflex O -50 +KPX Acircumflex Oacute -50 +KPX Acircumflex Ocircumflex -50 +KPX Acircumflex Odieresis -50 +KPX Acircumflex Ograve -50 +KPX Acircumflex Ohungarumlaut -50 +KPX Acircumflex Omacron -50 +KPX Acircumflex Oslash -50 +KPX Acircumflex Otilde -50 +KPX Acircumflex Q -55 +KPX Acircumflex T -55 +KPX Acircumflex Tcaron -55 +KPX Acircumflex Tcommaaccent -55 +KPX Acircumflex U -50 +KPX Acircumflex Uacute -50 +KPX Acircumflex Ucircumflex -50 +KPX Acircumflex Udieresis -50 +KPX Acircumflex Ugrave -50 +KPX Acircumflex Uhungarumlaut -50 +KPX Acircumflex Umacron -50 +KPX Acircumflex Uogonek -50 +KPX Acircumflex Uring -50 +KPX Acircumflex V -95 +KPX Acircumflex W -100 +KPX Acircumflex Y -70 +KPX Acircumflex Yacute -70 +KPX Acircumflex Ydieresis -70 +KPX Acircumflex quoteright -74 +KPX Acircumflex u -30 +KPX Acircumflex uacute -30 +KPX Acircumflex ucircumflex -30 +KPX Acircumflex udieresis -30 +KPX Acircumflex ugrave -30 +KPX Acircumflex uhungarumlaut -30 +KPX Acircumflex umacron -30 +KPX Acircumflex uogonek -30 +KPX Acircumflex uring -30 +KPX Acircumflex v -74 +KPX Acircumflex w -74 +KPX Acircumflex y -74 +KPX Acircumflex yacute -74 +KPX Acircumflex ydieresis -74 +KPX Adieresis C -65 +KPX Adieresis Cacute -65 +KPX Adieresis Ccaron -65 +KPX Adieresis Ccedilla -65 +KPX Adieresis G -60 +KPX Adieresis Gbreve -60 +KPX Adieresis Gcommaaccent -60 +KPX Adieresis O -50 +KPX Adieresis Oacute -50 +KPX Adieresis Ocircumflex -50 +KPX Adieresis Odieresis -50 +KPX Adieresis Ograve -50 +KPX Adieresis Ohungarumlaut -50 +KPX Adieresis Omacron -50 +KPX Adieresis Oslash -50 +KPX Adieresis Otilde -50 +KPX Adieresis Q -55 +KPX Adieresis T -55 +KPX Adieresis Tcaron -55 +KPX Adieresis Tcommaaccent -55 +KPX Adieresis U -50 +KPX Adieresis Uacute -50 +KPX Adieresis Ucircumflex -50 +KPX Adieresis Udieresis -50 +KPX Adieresis Ugrave -50 +KPX Adieresis Uhungarumlaut -50 +KPX Adieresis Umacron -50 +KPX Adieresis Uogonek -50 +KPX Adieresis Uring -50 +KPX Adieresis V -95 +KPX Adieresis W -100 +KPX Adieresis Y -70 +KPX Adieresis Yacute -70 +KPX Adieresis Ydieresis -70 +KPX Adieresis quoteright -74 +KPX Adieresis u -30 +KPX Adieresis uacute -30 +KPX Adieresis ucircumflex -30 +KPX Adieresis udieresis -30 +KPX Adieresis ugrave -30 +KPX Adieresis uhungarumlaut -30 +KPX Adieresis umacron -30 +KPX Adieresis uogonek -30 +KPX Adieresis uring -30 +KPX Adieresis v -74 +KPX Adieresis w -74 +KPX Adieresis y -74 +KPX Adieresis yacute -74 +KPX Adieresis ydieresis -74 +KPX Agrave C -65 +KPX Agrave Cacute -65 +KPX Agrave Ccaron -65 +KPX Agrave Ccedilla -65 +KPX Agrave G -60 +KPX Agrave Gbreve -60 +KPX Agrave Gcommaaccent -60 +KPX Agrave O -50 +KPX Agrave Oacute -50 +KPX Agrave Ocircumflex -50 +KPX Agrave Odieresis -50 +KPX Agrave Ograve -50 +KPX Agrave Ohungarumlaut -50 +KPX Agrave Omacron -50 +KPX Agrave Oslash -50 +KPX Agrave Otilde -50 +KPX Agrave Q -55 +KPX Agrave T -55 +KPX Agrave Tcaron -55 +KPX Agrave Tcommaaccent -55 +KPX Agrave U -50 +KPX Agrave Uacute -50 +KPX Agrave Ucircumflex -50 +KPX Agrave Udieresis -50 +KPX Agrave Ugrave -50 +KPX Agrave Uhungarumlaut -50 +KPX Agrave Umacron -50 +KPX Agrave Uogonek -50 +KPX Agrave Uring -50 +KPX Agrave V -95 +KPX Agrave W -100 +KPX Agrave Y -70 +KPX Agrave Yacute -70 +KPX Agrave Ydieresis -70 +KPX Agrave quoteright -74 +KPX Agrave u -30 +KPX Agrave uacute -30 +KPX Agrave ucircumflex -30 +KPX Agrave udieresis -30 +KPX Agrave ugrave -30 +KPX Agrave uhungarumlaut -30 +KPX Agrave umacron -30 +KPX Agrave uogonek -30 +KPX Agrave uring -30 +KPX Agrave v -74 +KPX Agrave w -74 +KPX Agrave y -74 +KPX Agrave yacute -74 +KPX Agrave ydieresis -74 +KPX Amacron C -65 +KPX Amacron Cacute -65 +KPX Amacron Ccaron -65 +KPX Amacron Ccedilla -65 +KPX Amacron G -60 +KPX Amacron Gbreve -60 +KPX Amacron Gcommaaccent -60 +KPX Amacron O -50 +KPX Amacron Oacute -50 +KPX Amacron Ocircumflex -50 +KPX Amacron Odieresis -50 +KPX Amacron Ograve -50 +KPX Amacron Ohungarumlaut -50 +KPX Amacron Omacron -50 +KPX Amacron Oslash -50 +KPX Amacron Otilde -50 +KPX Amacron Q -55 +KPX Amacron T -55 +KPX Amacron Tcaron -55 +KPX Amacron Tcommaaccent -55 +KPX Amacron U -50 +KPX Amacron Uacute -50 +KPX Amacron Ucircumflex -50 +KPX Amacron Udieresis -50 +KPX Amacron Ugrave -50 +KPX Amacron Uhungarumlaut -50 +KPX Amacron Umacron -50 +KPX Amacron Uogonek -50 +KPX Amacron Uring -50 +KPX Amacron V -95 +KPX Amacron W -100 +KPX Amacron Y -70 +KPX Amacron Yacute -70 +KPX Amacron Ydieresis -70 +KPX Amacron quoteright -74 +KPX Amacron u -30 +KPX Amacron uacute -30 +KPX Amacron ucircumflex -30 +KPX Amacron udieresis -30 +KPX Amacron ugrave -30 +KPX Amacron uhungarumlaut -30 +KPX Amacron umacron -30 +KPX Amacron uogonek -30 +KPX Amacron uring -30 +KPX Amacron v -74 +KPX Amacron w -74 +KPX Amacron y -74 +KPX Amacron yacute -74 +KPX Amacron ydieresis -74 +KPX Aogonek C -65 +KPX Aogonek Cacute -65 +KPX Aogonek Ccaron -65 +KPX Aogonek Ccedilla -65 +KPX Aogonek G -60 +KPX Aogonek Gbreve -60 +KPX Aogonek Gcommaaccent -60 +KPX Aogonek O -50 +KPX Aogonek Oacute -50 +KPX Aogonek Ocircumflex -50 +KPX Aogonek Odieresis -50 +KPX Aogonek Ograve -50 +KPX Aogonek Ohungarumlaut -50 +KPX Aogonek Omacron -50 +KPX Aogonek Oslash -50 +KPX Aogonek Otilde -50 +KPX Aogonek Q -55 +KPX Aogonek T -55 +KPX Aogonek Tcaron -55 +KPX Aogonek Tcommaaccent -55 +KPX Aogonek U -50 +KPX Aogonek Uacute -50 +KPX Aogonek Ucircumflex -50 +KPX Aogonek Udieresis -50 +KPX Aogonek Ugrave -50 +KPX Aogonek Uhungarumlaut -50 +KPX Aogonek Umacron -50 +KPX Aogonek Uogonek -50 +KPX Aogonek Uring -50 +KPX Aogonek V -95 +KPX Aogonek W -100 +KPX Aogonek Y -70 +KPX Aogonek Yacute -70 +KPX Aogonek Ydieresis -70 +KPX Aogonek quoteright -74 +KPX Aogonek u -30 +KPX Aogonek uacute -30 +KPX Aogonek ucircumflex -30 +KPX Aogonek udieresis -30 +KPX Aogonek ugrave -30 +KPX Aogonek uhungarumlaut -30 +KPX Aogonek umacron -30 +KPX Aogonek uogonek -30 +KPX Aogonek uring -30 +KPX Aogonek v -74 +KPX Aogonek w -74 +KPX Aogonek y -34 +KPX Aogonek yacute -34 +KPX Aogonek ydieresis -34 +KPX Aring C -65 +KPX Aring Cacute -65 +KPX Aring Ccaron -65 +KPX Aring Ccedilla -65 +KPX Aring G -60 +KPX Aring Gbreve -60 +KPX Aring Gcommaaccent -60 +KPX Aring O -50 +KPX Aring Oacute -50 +KPX Aring Ocircumflex -50 +KPX Aring Odieresis -50 +KPX Aring Ograve -50 +KPX Aring Ohungarumlaut -50 +KPX Aring Omacron -50 +KPX Aring Oslash -50 +KPX Aring Otilde -50 +KPX Aring Q -55 +KPX Aring T -55 +KPX Aring Tcaron -55 +KPX Aring Tcommaaccent -55 +KPX Aring U -50 +KPX Aring Uacute -50 +KPX Aring Ucircumflex -50 +KPX Aring Udieresis -50 +KPX Aring Ugrave -50 +KPX Aring Uhungarumlaut -50 +KPX Aring Umacron -50 +KPX Aring Uogonek -50 +KPX Aring Uring -50 +KPX Aring V -95 +KPX Aring W -100 +KPX Aring Y -70 +KPX Aring Yacute -70 +KPX Aring Ydieresis -70 +KPX Aring quoteright -74 +KPX Aring u -30 +KPX Aring uacute -30 +KPX Aring ucircumflex -30 +KPX Aring udieresis -30 +KPX Aring ugrave -30 +KPX Aring uhungarumlaut -30 +KPX Aring umacron -30 +KPX Aring uogonek -30 +KPX Aring uring -30 +KPX Aring v -74 +KPX Aring w -74 +KPX Aring y -74 +KPX Aring yacute -74 +KPX Aring ydieresis -74 +KPX Atilde C -65 +KPX Atilde Cacute -65 +KPX Atilde Ccaron -65 +KPX Atilde Ccedilla -65 +KPX Atilde G -60 +KPX Atilde Gbreve -60 +KPX Atilde Gcommaaccent -60 +KPX Atilde O -50 +KPX Atilde Oacute -50 +KPX Atilde Ocircumflex -50 +KPX Atilde Odieresis -50 +KPX Atilde Ograve -50 +KPX Atilde Ohungarumlaut -50 +KPX Atilde Omacron -50 +KPX Atilde Oslash -50 +KPX Atilde Otilde -50 +KPX Atilde Q -55 +KPX Atilde T -55 +KPX Atilde Tcaron -55 +KPX Atilde Tcommaaccent -55 +KPX Atilde U -50 +KPX Atilde Uacute -50 +KPX Atilde Ucircumflex -50 +KPX Atilde Udieresis -50 +KPX Atilde Ugrave -50 +KPX Atilde Uhungarumlaut -50 +KPX Atilde Umacron -50 +KPX Atilde Uogonek -50 +KPX Atilde Uring -50 +KPX Atilde V -95 +KPX Atilde W -100 +KPX Atilde Y -70 +KPX Atilde Yacute -70 +KPX Atilde Ydieresis -70 +KPX Atilde quoteright -74 +KPX Atilde u -30 +KPX Atilde uacute -30 +KPX Atilde ucircumflex -30 +KPX Atilde udieresis -30 +KPX Atilde ugrave -30 +KPX Atilde uhungarumlaut -30 +KPX Atilde umacron -30 +KPX Atilde uogonek -30 +KPX Atilde uring -30 +KPX Atilde v -74 +KPX Atilde w -74 +KPX Atilde y -74 +KPX Atilde yacute -74 +KPX Atilde ydieresis -74 +KPX B A -25 +KPX B Aacute -25 +KPX B Abreve -25 +KPX B Acircumflex -25 +KPX B Adieresis -25 +KPX B Agrave -25 +KPX B Amacron -25 +KPX B Aogonek -25 +KPX B Aring -25 +KPX B Atilde -25 +KPX B U -10 +KPX B Uacute -10 +KPX B Ucircumflex -10 +KPX B Udieresis -10 +KPX B Ugrave -10 +KPX B Uhungarumlaut -10 +KPX B Umacron -10 +KPX B Uogonek -10 +KPX B Uring -10 +KPX D A -25 +KPX D Aacute -25 +KPX D Abreve -25 +KPX D Acircumflex -25 +KPX D Adieresis -25 +KPX D Agrave -25 +KPX D Amacron -25 +KPX D Aogonek -25 +KPX D Aring -25 +KPX D Atilde -25 +KPX D V -50 +KPX D W -40 +KPX D Y -50 +KPX D Yacute -50 +KPX D Ydieresis -50 +KPX Dcaron A -25 +KPX Dcaron Aacute -25 +KPX Dcaron Abreve -25 +KPX Dcaron Acircumflex -25 +KPX Dcaron Adieresis -25 +KPX Dcaron Agrave -25 +KPX Dcaron Amacron -25 +KPX Dcaron Aogonek -25 +KPX Dcaron Aring -25 +KPX Dcaron Atilde -25 +KPX Dcaron V -50 +KPX Dcaron W -40 +KPX Dcaron Y -50 +KPX Dcaron Yacute -50 +KPX Dcaron Ydieresis -50 +KPX Dcroat A -25 +KPX Dcroat Aacute -25 +KPX Dcroat Abreve -25 +KPX Dcroat Acircumflex -25 +KPX Dcroat Adieresis -25 +KPX Dcroat Agrave -25 +KPX Dcroat Amacron -25 +KPX Dcroat Aogonek -25 +KPX Dcroat Aring -25 +KPX Dcroat Atilde -25 +KPX Dcroat V -50 +KPX Dcroat W -40 +KPX Dcroat Y -50 +KPX Dcroat Yacute -50 +KPX Dcroat Ydieresis -50 +KPX F A -100 +KPX F Aacute -100 +KPX F Abreve -100 +KPX F Acircumflex -100 +KPX F Adieresis -100 +KPX F Agrave -100 +KPX F Amacron -100 +KPX F Aogonek -100 +KPX F Aring -100 +KPX F Atilde -100 +KPX F a -95 +KPX F aacute -95 +KPX F abreve -95 +KPX F acircumflex -95 +KPX F adieresis -95 +KPX F agrave -95 +KPX F amacron -95 +KPX F aogonek -95 +KPX F aring -95 +KPX F atilde -95 +KPX F comma -129 +KPX F e -100 +KPX F eacute -100 +KPX F ecaron -100 +KPX F ecircumflex -100 +KPX F edieresis -100 +KPX F edotaccent -100 +KPX F egrave -100 +KPX F emacron -100 +KPX F eogonek -100 +KPX F i -40 +KPX F iacute -40 +KPX F icircumflex -40 +KPX F idieresis -40 +KPX F igrave -40 +KPX F imacron -40 +KPX F iogonek -40 +KPX F o -70 +KPX F oacute -70 +KPX F ocircumflex -70 +KPX F odieresis -70 +KPX F ograve -70 +KPX F ohungarumlaut -70 +KPX F omacron -70 +KPX F oslash -70 +KPX F otilde -70 +KPX F period -129 +KPX F r -50 +KPX F racute -50 +KPX F rcaron -50 +KPX F rcommaaccent -50 +KPX J A -25 +KPX J Aacute -25 +KPX J Abreve -25 +KPX J Acircumflex -25 +KPX J Adieresis -25 +KPX J Agrave -25 +KPX J Amacron -25 +KPX J Aogonek -25 +KPX J Aring -25 +KPX J Atilde -25 +KPX J a -40 +KPX J aacute -40 +KPX J abreve -40 +KPX J acircumflex -40 +KPX J adieresis -40 +KPX J agrave -40 +KPX J amacron -40 +KPX J aogonek -40 +KPX J aring -40 +KPX J atilde -40 +KPX J comma -10 +KPX J e -40 +KPX J eacute -40 +KPX J ecaron -40 +KPX J ecircumflex -40 +KPX J edieresis -40 +KPX J edotaccent -40 +KPX J egrave -40 +KPX J emacron -40 +KPX J eogonek -40 +KPX J o -40 +KPX J oacute -40 +KPX J ocircumflex -40 +KPX J odieresis -40 +KPX J ograve -40 +KPX J ohungarumlaut -40 +KPX J omacron -40 +KPX J oslash -40 +KPX J otilde -40 +KPX J period -10 +KPX J u -40 +KPX J uacute -40 +KPX J ucircumflex -40 +KPX J udieresis -40 +KPX J ugrave -40 +KPX J uhungarumlaut -40 +KPX J umacron -40 +KPX J uogonek -40 +KPX J uring -40 +KPX K O -30 +KPX K Oacute -30 +KPX K Ocircumflex -30 +KPX K Odieresis -30 +KPX K Ograve -30 +KPX K Ohungarumlaut -30 +KPX K Omacron -30 +KPX K Oslash -30 +KPX K Otilde -30 +KPX K e -25 +KPX K eacute -25 +KPX K ecaron -25 +KPX K ecircumflex -25 +KPX K edieresis -25 +KPX K edotaccent -25 +KPX K egrave -25 +KPX K emacron -25 +KPX K eogonek -25 +KPX K o -25 +KPX K oacute -25 +KPX K ocircumflex -25 +KPX K odieresis -25 +KPX K ograve -25 +KPX K ohungarumlaut -25 +KPX K omacron -25 +KPX K oslash -25 +KPX K otilde -25 +KPX K u -20 +KPX K uacute -20 +KPX K ucircumflex -20 +KPX K udieresis -20 +KPX K ugrave -20 +KPX K uhungarumlaut -20 +KPX K umacron -20 +KPX K uogonek -20 +KPX K uring -20 +KPX K y -20 +KPX K yacute -20 +KPX K ydieresis -20 +KPX Kcommaaccent O -30 +KPX Kcommaaccent Oacute -30 +KPX Kcommaaccent Ocircumflex -30 +KPX Kcommaaccent Odieresis -30 +KPX Kcommaaccent Ograve -30 +KPX Kcommaaccent Ohungarumlaut -30 +KPX Kcommaaccent Omacron -30 +KPX Kcommaaccent Oslash -30 +KPX Kcommaaccent Otilde -30 +KPX Kcommaaccent e -25 +KPX Kcommaaccent eacute -25 +KPX Kcommaaccent ecaron -25 +KPX Kcommaaccent ecircumflex -25 +KPX Kcommaaccent edieresis -25 +KPX Kcommaaccent edotaccent -25 +KPX Kcommaaccent egrave -25 +KPX Kcommaaccent emacron -25 +KPX Kcommaaccent eogonek -25 +KPX Kcommaaccent o -25 +KPX Kcommaaccent oacute -25 +KPX Kcommaaccent ocircumflex -25 +KPX Kcommaaccent odieresis -25 +KPX Kcommaaccent ograve -25 +KPX Kcommaaccent ohungarumlaut -25 +KPX Kcommaaccent omacron -25 +KPX Kcommaaccent oslash -25 +KPX Kcommaaccent otilde -25 +KPX Kcommaaccent u -20 +KPX Kcommaaccent uacute -20 +KPX Kcommaaccent ucircumflex -20 +KPX Kcommaaccent udieresis -20 +KPX Kcommaaccent ugrave -20 +KPX Kcommaaccent uhungarumlaut -20 +KPX Kcommaaccent umacron -20 +KPX Kcommaaccent uogonek -20 +KPX Kcommaaccent uring -20 +KPX Kcommaaccent y -20 +KPX Kcommaaccent yacute -20 +KPX Kcommaaccent ydieresis -20 +KPX L T -18 +KPX L Tcaron -18 +KPX L Tcommaaccent -18 +KPX L V -37 +KPX L W -37 +KPX L Y -37 +KPX L Yacute -37 +KPX L Ydieresis -37 +KPX L quoteright -55 +KPX L y -37 +KPX L yacute -37 +KPX L ydieresis -37 +KPX Lacute T -18 +KPX Lacute Tcaron -18 +KPX Lacute Tcommaaccent -18 +KPX Lacute V -37 +KPX Lacute W -37 +KPX Lacute Y -37 +KPX Lacute Yacute -37 +KPX Lacute Ydieresis -37 +KPX Lacute quoteright -55 +KPX Lacute y -37 +KPX Lacute yacute -37 +KPX Lacute ydieresis -37 +KPX Lcommaaccent T -18 +KPX Lcommaaccent Tcaron -18 +KPX Lcommaaccent Tcommaaccent -18 +KPX Lcommaaccent V -37 +KPX Lcommaaccent W -37 +KPX Lcommaaccent Y -37 +KPX Lcommaaccent Yacute -37 +KPX Lcommaaccent Ydieresis -37 +KPX Lcommaaccent quoteright -55 +KPX Lcommaaccent y -37 +KPX Lcommaaccent yacute -37 +KPX Lcommaaccent ydieresis -37 +KPX Lslash T -18 +KPX Lslash Tcaron -18 +KPX Lslash Tcommaaccent -18 +KPX Lslash V -37 +KPX Lslash W -37 +KPX Lslash Y -37 +KPX Lslash Yacute -37 +KPX Lslash Ydieresis -37 +KPX Lslash quoteright -55 +KPX Lslash y -37 +KPX Lslash yacute -37 +KPX Lslash ydieresis -37 +KPX N A -30 +KPX N Aacute -30 +KPX N Abreve -30 +KPX N Acircumflex -30 +KPX N Adieresis -30 +KPX N Agrave -30 +KPX N Amacron -30 +KPX N Aogonek -30 +KPX N Aring -30 +KPX N Atilde -30 +KPX Nacute A -30 +KPX Nacute Aacute -30 +KPX Nacute Abreve -30 +KPX Nacute Acircumflex -30 +KPX Nacute Adieresis -30 +KPX Nacute Agrave -30 +KPX Nacute Amacron -30 +KPX Nacute Aogonek -30 +KPX Nacute Aring -30 +KPX Nacute Atilde -30 +KPX Ncaron A -30 +KPX Ncaron Aacute -30 +KPX Ncaron Abreve -30 +KPX Ncaron Acircumflex -30 +KPX Ncaron Adieresis -30 +KPX Ncaron Agrave -30 +KPX Ncaron Amacron -30 +KPX Ncaron Aogonek -30 +KPX Ncaron Aring -30 +KPX Ncaron Atilde -30 +KPX Ncommaaccent A -30 +KPX Ncommaaccent Aacute -30 +KPX Ncommaaccent Abreve -30 +KPX Ncommaaccent Acircumflex -30 +KPX Ncommaaccent Adieresis -30 +KPX Ncommaaccent Agrave -30 +KPX Ncommaaccent Amacron -30 +KPX Ncommaaccent Aogonek -30 +KPX Ncommaaccent Aring -30 +KPX Ncommaaccent Atilde -30 +KPX Ntilde A -30 +KPX Ntilde Aacute -30 +KPX Ntilde Abreve -30 +KPX Ntilde Acircumflex -30 +KPX Ntilde Adieresis -30 +KPX Ntilde Agrave -30 +KPX Ntilde Amacron -30 +KPX Ntilde Aogonek -30 +KPX Ntilde Aring -30 +KPX Ntilde Atilde -30 +KPX O A -40 +KPX O Aacute -40 +KPX O Abreve -40 +KPX O Acircumflex -40 +KPX O Adieresis -40 +KPX O Agrave -40 +KPX O Amacron -40 +KPX O Aogonek -40 +KPX O Aring -40 +KPX O Atilde -40 +KPX O T -40 +KPX O Tcaron -40 +KPX O Tcommaaccent -40 +KPX O V -50 +KPX O W -50 +KPX O X -40 +KPX O Y -50 +KPX O Yacute -50 +KPX O Ydieresis -50 +KPX Oacute A -40 +KPX Oacute Aacute -40 +KPX Oacute Abreve -40 +KPX Oacute Acircumflex -40 +KPX Oacute Adieresis -40 +KPX Oacute Agrave -40 +KPX Oacute Amacron -40 +KPX Oacute Aogonek -40 +KPX Oacute Aring -40 +KPX Oacute Atilde -40 +KPX Oacute T -40 +KPX Oacute Tcaron -40 +KPX Oacute Tcommaaccent -40 +KPX Oacute V -50 +KPX Oacute W -50 +KPX Oacute X -40 +KPX Oacute Y -50 +KPX Oacute Yacute -50 +KPX Oacute Ydieresis -50 +KPX Ocircumflex A -40 +KPX Ocircumflex Aacute -40 +KPX Ocircumflex Abreve -40 +KPX Ocircumflex Acircumflex -40 +KPX Ocircumflex Adieresis -40 +KPX Ocircumflex Agrave -40 +KPX Ocircumflex Amacron -40 +KPX Ocircumflex Aogonek -40 +KPX Ocircumflex Aring -40 +KPX Ocircumflex Atilde -40 +KPX Ocircumflex T -40 +KPX Ocircumflex Tcaron -40 +KPX Ocircumflex Tcommaaccent -40 +KPX Ocircumflex V -50 +KPX Ocircumflex W -50 +KPX Ocircumflex X -40 +KPX Ocircumflex Y -50 +KPX Ocircumflex Yacute -50 +KPX Ocircumflex Ydieresis -50 +KPX Odieresis A -40 +KPX Odieresis Aacute -40 +KPX Odieresis Abreve -40 +KPX Odieresis Acircumflex -40 +KPX Odieresis Adieresis -40 +KPX Odieresis Agrave -40 +KPX Odieresis Amacron -40 +KPX Odieresis Aogonek -40 +KPX Odieresis Aring -40 +KPX Odieresis Atilde -40 +KPX Odieresis T -40 +KPX Odieresis Tcaron -40 +KPX Odieresis Tcommaaccent -40 +KPX Odieresis V -50 +KPX Odieresis W -50 +KPX Odieresis X -40 +KPX Odieresis Y -50 +KPX Odieresis Yacute -50 +KPX Odieresis Ydieresis -50 +KPX Ograve A -40 +KPX Ograve Aacute -40 +KPX Ograve Abreve -40 +KPX Ograve Acircumflex -40 +KPX Ograve Adieresis -40 +KPX Ograve Agrave -40 +KPX Ograve Amacron -40 +KPX Ograve Aogonek -40 +KPX Ograve Aring -40 +KPX Ograve Atilde -40 +KPX Ograve T -40 +KPX Ograve Tcaron -40 +KPX Ograve Tcommaaccent -40 +KPX Ograve V -50 +KPX Ograve W -50 +KPX Ograve X -40 +KPX Ograve Y -50 +KPX Ograve Yacute -50 +KPX Ograve Ydieresis -50 +KPX Ohungarumlaut A -40 +KPX Ohungarumlaut Aacute -40 +KPX Ohungarumlaut Abreve -40 +KPX Ohungarumlaut Acircumflex -40 +KPX Ohungarumlaut Adieresis -40 +KPX Ohungarumlaut Agrave -40 +KPX Ohungarumlaut Amacron -40 +KPX Ohungarumlaut Aogonek -40 +KPX Ohungarumlaut Aring -40 +KPX Ohungarumlaut Atilde -40 +KPX Ohungarumlaut T -40 +KPX Ohungarumlaut Tcaron -40 +KPX Ohungarumlaut Tcommaaccent -40 +KPX Ohungarumlaut V -50 +KPX Ohungarumlaut W -50 +KPX Ohungarumlaut X -40 +KPX Ohungarumlaut Y -50 +KPX Ohungarumlaut Yacute -50 +KPX Ohungarumlaut Ydieresis -50 +KPX Omacron A -40 +KPX Omacron Aacute -40 +KPX Omacron Abreve -40 +KPX Omacron Acircumflex -40 +KPX Omacron Adieresis -40 +KPX Omacron Agrave -40 +KPX Omacron Amacron -40 +KPX Omacron Aogonek -40 +KPX Omacron Aring -40 +KPX Omacron Atilde -40 +KPX Omacron T -40 +KPX Omacron Tcaron -40 +KPX Omacron Tcommaaccent -40 +KPX Omacron V -50 +KPX Omacron W -50 +KPX Omacron X -40 +KPX Omacron Y -50 +KPX Omacron Yacute -50 +KPX Omacron Ydieresis -50 +KPX Oslash A -40 +KPX Oslash Aacute -40 +KPX Oslash Abreve -40 +KPX Oslash Acircumflex -40 +KPX Oslash Adieresis -40 +KPX Oslash Agrave -40 +KPX Oslash Amacron -40 +KPX Oslash Aogonek -40 +KPX Oslash Aring -40 +KPX Oslash Atilde -40 +KPX Oslash T -40 +KPX Oslash Tcaron -40 +KPX Oslash Tcommaaccent -40 +KPX Oslash V -50 +KPX Oslash W -50 +KPX Oslash X -40 +KPX Oslash Y -50 +KPX Oslash Yacute -50 +KPX Oslash Ydieresis -50 +KPX Otilde A -40 +KPX Otilde Aacute -40 +KPX Otilde Abreve -40 +KPX Otilde Acircumflex -40 +KPX Otilde Adieresis -40 +KPX Otilde Agrave -40 +KPX Otilde Amacron -40 +KPX Otilde Aogonek -40 +KPX Otilde Aring -40 +KPX Otilde Atilde -40 +KPX Otilde T -40 +KPX Otilde Tcaron -40 +KPX Otilde Tcommaaccent -40 +KPX Otilde V -50 +KPX Otilde W -50 +KPX Otilde X -40 +KPX Otilde Y -50 +KPX Otilde Yacute -50 +KPX Otilde Ydieresis -50 +KPX P A -85 +KPX P Aacute -85 +KPX P Abreve -85 +KPX P Acircumflex -85 +KPX P Adieresis -85 +KPX P Agrave -85 +KPX P Amacron -85 +KPX P Aogonek -85 +KPX P Aring -85 +KPX P Atilde -85 +KPX P a -40 +KPX P aacute -40 +KPX P abreve -40 +KPX P acircumflex -40 +KPX P adieresis -40 +KPX P agrave -40 +KPX P amacron -40 +KPX P aogonek -40 +KPX P aring -40 +KPX P atilde -40 +KPX P comma -129 +KPX P e -50 +KPX P eacute -50 +KPX P ecaron -50 +KPX P ecircumflex -50 +KPX P edieresis -50 +KPX P edotaccent -50 +KPX P egrave -50 +KPX P emacron -50 +KPX P eogonek -50 +KPX P o -55 +KPX P oacute -55 +KPX P ocircumflex -55 +KPX P odieresis -55 +KPX P ograve -55 +KPX P ohungarumlaut -55 +KPX P omacron -55 +KPX P oslash -55 +KPX P otilde -55 +KPX P period -129 +KPX Q U -10 +KPX Q Uacute -10 +KPX Q Ucircumflex -10 +KPX Q Udieresis -10 +KPX Q Ugrave -10 +KPX Q Uhungarumlaut -10 +KPX Q Umacron -10 +KPX Q Uogonek -10 +KPX Q Uring -10 +KPX R O -40 +KPX R Oacute -40 +KPX R Ocircumflex -40 +KPX R Odieresis -40 +KPX R Ograve -40 +KPX R Ohungarumlaut -40 +KPX R Omacron -40 +KPX R Oslash -40 +KPX R Otilde -40 +KPX R T -30 +KPX R Tcaron -30 +KPX R Tcommaaccent -30 +KPX R U -40 +KPX R Uacute -40 +KPX R Ucircumflex -40 +KPX R Udieresis -40 +KPX R Ugrave -40 +KPX R Uhungarumlaut -40 +KPX R Umacron -40 +KPX R Uogonek -40 +KPX R Uring -40 +KPX R V -18 +KPX R W -18 +KPX R Y -18 +KPX R Yacute -18 +KPX R Ydieresis -18 +KPX Racute O -40 +KPX Racute Oacute -40 +KPX Racute Ocircumflex -40 +KPX Racute Odieresis -40 +KPX Racute Ograve -40 +KPX Racute Ohungarumlaut -40 +KPX Racute Omacron -40 +KPX Racute Oslash -40 +KPX Racute Otilde -40 +KPX Racute T -30 +KPX Racute Tcaron -30 +KPX Racute Tcommaaccent -30 +KPX Racute U -40 +KPX Racute Uacute -40 +KPX Racute Ucircumflex -40 +KPX Racute Udieresis -40 +KPX Racute Ugrave -40 +KPX Racute Uhungarumlaut -40 +KPX Racute Umacron -40 +KPX Racute Uogonek -40 +KPX Racute Uring -40 +KPX Racute V -18 +KPX Racute W -18 +KPX Racute Y -18 +KPX Racute Yacute -18 +KPX Racute Ydieresis -18 +KPX Rcaron O -40 +KPX Rcaron Oacute -40 +KPX Rcaron Ocircumflex -40 +KPX Rcaron Odieresis -40 +KPX Rcaron Ograve -40 +KPX Rcaron Ohungarumlaut -40 +KPX Rcaron Omacron -40 +KPX Rcaron Oslash -40 +KPX Rcaron Otilde -40 +KPX Rcaron T -30 +KPX Rcaron Tcaron -30 +KPX Rcaron Tcommaaccent -30 +KPX Rcaron U -40 +KPX Rcaron Uacute -40 +KPX Rcaron Ucircumflex -40 +KPX Rcaron Udieresis -40 +KPX Rcaron Ugrave -40 +KPX Rcaron Uhungarumlaut -40 +KPX Rcaron Umacron -40 +KPX Rcaron Uogonek -40 +KPX Rcaron Uring -40 +KPX Rcaron V -18 +KPX Rcaron W -18 +KPX Rcaron Y -18 +KPX Rcaron Yacute -18 +KPX Rcaron Ydieresis -18 +KPX Rcommaaccent O -40 +KPX Rcommaaccent Oacute -40 +KPX Rcommaaccent Ocircumflex -40 +KPX Rcommaaccent Odieresis -40 +KPX Rcommaaccent Ograve -40 +KPX Rcommaaccent Ohungarumlaut -40 +KPX Rcommaaccent Omacron -40 +KPX Rcommaaccent Oslash -40 +KPX Rcommaaccent Otilde -40 +KPX Rcommaaccent T -30 +KPX Rcommaaccent Tcaron -30 +KPX Rcommaaccent Tcommaaccent -30 +KPX Rcommaaccent U -40 +KPX Rcommaaccent Uacute -40 +KPX Rcommaaccent Ucircumflex -40 +KPX Rcommaaccent Udieresis -40 +KPX Rcommaaccent Ugrave -40 +KPX Rcommaaccent Uhungarumlaut -40 +KPX Rcommaaccent Umacron -40 +KPX Rcommaaccent Uogonek -40 +KPX Rcommaaccent Uring -40 +KPX Rcommaaccent V -18 +KPX Rcommaaccent W -18 +KPX Rcommaaccent Y -18 +KPX Rcommaaccent Yacute -18 +KPX Rcommaaccent Ydieresis -18 +KPX T A -55 +KPX T Aacute -55 +KPX T Abreve -55 +KPX T Acircumflex -55 +KPX T Adieresis -55 +KPX T Agrave -55 +KPX T Amacron -55 +KPX T Aogonek -55 +KPX T Aring -55 +KPX T Atilde -55 +KPX T O -18 +KPX T Oacute -18 +KPX T Ocircumflex -18 +KPX T Odieresis -18 +KPX T Ograve -18 +KPX T Ohungarumlaut -18 +KPX T Omacron -18 +KPX T Oslash -18 +KPX T Otilde -18 +KPX T a -92 +KPX T aacute -92 +KPX T abreve -92 +KPX T acircumflex -92 +KPX T adieresis -92 +KPX T agrave -92 +KPX T amacron -92 +KPX T aogonek -92 +KPX T aring -92 +KPX T atilde -92 +KPX T colon -74 +KPX T comma -92 +KPX T e -92 +KPX T eacute -92 +KPX T ecaron -92 +KPX T ecircumflex -92 +KPX T edieresis -52 +KPX T edotaccent -92 +KPX T egrave -52 +KPX T emacron -52 +KPX T eogonek -92 +KPX T hyphen -92 +KPX T i -37 +KPX T iacute -37 +KPX T iogonek -37 +KPX T o -95 +KPX T oacute -95 +KPX T ocircumflex -95 +KPX T odieresis -95 +KPX T ograve -95 +KPX T ohungarumlaut -95 +KPX T omacron -95 +KPX T oslash -95 +KPX T otilde -95 +KPX T period -92 +KPX T r -37 +KPX T racute -37 +KPX T rcaron -37 +KPX T rcommaaccent -37 +KPX T semicolon -74 +KPX T u -37 +KPX T uacute -37 +KPX T ucircumflex -37 +KPX T udieresis -37 +KPX T ugrave -37 +KPX T uhungarumlaut -37 +KPX T umacron -37 +KPX T uogonek -37 +KPX T uring -37 +KPX T w -37 +KPX T y -37 +KPX T yacute -37 +KPX T ydieresis -37 +KPX Tcaron A -55 +KPX Tcaron Aacute -55 +KPX Tcaron Abreve -55 +KPX Tcaron Acircumflex -55 +KPX Tcaron Adieresis -55 +KPX Tcaron Agrave -55 +KPX Tcaron Amacron -55 +KPX Tcaron Aogonek -55 +KPX Tcaron Aring -55 +KPX Tcaron Atilde -55 +KPX Tcaron O -18 +KPX Tcaron Oacute -18 +KPX Tcaron Ocircumflex -18 +KPX Tcaron Odieresis -18 +KPX Tcaron Ograve -18 +KPX Tcaron Ohungarumlaut -18 +KPX Tcaron Omacron -18 +KPX Tcaron Oslash -18 +KPX Tcaron Otilde -18 +KPX Tcaron a -92 +KPX Tcaron aacute -92 +KPX Tcaron abreve -92 +KPX Tcaron acircumflex -92 +KPX Tcaron adieresis -92 +KPX Tcaron agrave -92 +KPX Tcaron amacron -92 +KPX Tcaron aogonek -92 +KPX Tcaron aring -92 +KPX Tcaron atilde -92 +KPX Tcaron colon -74 +KPX Tcaron comma -92 +KPX Tcaron e -92 +KPX Tcaron eacute -92 +KPX Tcaron ecaron -92 +KPX Tcaron ecircumflex -92 +KPX Tcaron edieresis -52 +KPX Tcaron edotaccent -92 +KPX Tcaron egrave -52 +KPX Tcaron emacron -52 +KPX Tcaron eogonek -92 +KPX Tcaron hyphen -92 +KPX Tcaron i -37 +KPX Tcaron iacute -37 +KPX Tcaron iogonek -37 +KPX Tcaron o -95 +KPX Tcaron oacute -95 +KPX Tcaron ocircumflex -95 +KPX Tcaron odieresis -95 +KPX Tcaron ograve -95 +KPX Tcaron ohungarumlaut -95 +KPX Tcaron omacron -95 +KPX Tcaron oslash -95 +KPX Tcaron otilde -95 +KPX Tcaron period -92 +KPX Tcaron r -37 +KPX Tcaron racute -37 +KPX Tcaron rcaron -37 +KPX Tcaron rcommaaccent -37 +KPX Tcaron semicolon -74 +KPX Tcaron u -37 +KPX Tcaron uacute -37 +KPX Tcaron ucircumflex -37 +KPX Tcaron udieresis -37 +KPX Tcaron ugrave -37 +KPX Tcaron uhungarumlaut -37 +KPX Tcaron umacron -37 +KPX Tcaron uogonek -37 +KPX Tcaron uring -37 +KPX Tcaron w -37 +KPX Tcaron y -37 +KPX Tcaron yacute -37 +KPX Tcaron ydieresis -37 +KPX Tcommaaccent A -55 +KPX Tcommaaccent Aacute -55 +KPX Tcommaaccent Abreve -55 +KPX Tcommaaccent Acircumflex -55 +KPX Tcommaaccent Adieresis -55 +KPX Tcommaaccent Agrave -55 +KPX Tcommaaccent Amacron -55 +KPX Tcommaaccent Aogonek -55 +KPX Tcommaaccent Aring -55 +KPX Tcommaaccent Atilde -55 +KPX Tcommaaccent O -18 +KPX Tcommaaccent Oacute -18 +KPX Tcommaaccent Ocircumflex -18 +KPX Tcommaaccent Odieresis -18 +KPX Tcommaaccent Ograve -18 +KPX Tcommaaccent Ohungarumlaut -18 +KPX Tcommaaccent Omacron -18 +KPX Tcommaaccent Oslash -18 +KPX Tcommaaccent Otilde -18 +KPX Tcommaaccent a -92 +KPX Tcommaaccent aacute -92 +KPX Tcommaaccent abreve -92 +KPX Tcommaaccent acircumflex -92 +KPX Tcommaaccent adieresis -92 +KPX Tcommaaccent agrave -92 +KPX Tcommaaccent amacron -92 +KPX Tcommaaccent aogonek -92 +KPX Tcommaaccent aring -92 +KPX Tcommaaccent atilde -92 +KPX Tcommaaccent colon -74 +KPX Tcommaaccent comma -92 +KPX Tcommaaccent e -92 +KPX Tcommaaccent eacute -92 +KPX Tcommaaccent ecaron -92 +KPX Tcommaaccent ecircumflex -92 +KPX Tcommaaccent edieresis -52 +KPX Tcommaaccent edotaccent -92 +KPX Tcommaaccent egrave -52 +KPX Tcommaaccent emacron -52 +KPX Tcommaaccent eogonek -92 +KPX Tcommaaccent hyphen -92 +KPX Tcommaaccent i -37 +KPX Tcommaaccent iacute -37 +KPX Tcommaaccent iogonek -37 +KPX Tcommaaccent o -95 +KPX Tcommaaccent oacute -95 +KPX Tcommaaccent ocircumflex -95 +KPX Tcommaaccent odieresis -95 +KPX Tcommaaccent ograve -95 +KPX Tcommaaccent ohungarumlaut -95 +KPX Tcommaaccent omacron -95 +KPX Tcommaaccent oslash -95 +KPX Tcommaaccent otilde -95 +KPX Tcommaaccent period -92 +KPX Tcommaaccent r -37 +KPX Tcommaaccent racute -37 +KPX Tcommaaccent rcaron -37 +KPX Tcommaaccent rcommaaccent -37 +KPX Tcommaaccent semicolon -74 +KPX Tcommaaccent u -37 +KPX Tcommaaccent uacute -37 +KPX Tcommaaccent ucircumflex -37 +KPX Tcommaaccent udieresis -37 +KPX Tcommaaccent ugrave -37 +KPX Tcommaaccent uhungarumlaut -37 +KPX Tcommaaccent umacron -37 +KPX Tcommaaccent uogonek -37 +KPX Tcommaaccent uring -37 +KPX Tcommaaccent w -37 +KPX Tcommaaccent y -37 +KPX Tcommaaccent yacute -37 +KPX Tcommaaccent ydieresis -37 +KPX U A -45 +KPX U Aacute -45 +KPX U Abreve -45 +KPX U Acircumflex -45 +KPX U Adieresis -45 +KPX U Agrave -45 +KPX U Amacron -45 +KPX U Aogonek -45 +KPX U Aring -45 +KPX U Atilde -45 +KPX Uacute A -45 +KPX Uacute Aacute -45 +KPX Uacute Abreve -45 +KPX Uacute Acircumflex -45 +KPX Uacute Adieresis -45 +KPX Uacute Agrave -45 +KPX Uacute Amacron -45 +KPX Uacute Aogonek -45 +KPX Uacute Aring -45 +KPX Uacute Atilde -45 +KPX Ucircumflex A -45 +KPX Ucircumflex Aacute -45 +KPX Ucircumflex Abreve -45 +KPX Ucircumflex Acircumflex -45 +KPX Ucircumflex Adieresis -45 +KPX Ucircumflex Agrave -45 +KPX Ucircumflex Amacron -45 +KPX Ucircumflex Aogonek -45 +KPX Ucircumflex Aring -45 +KPX Ucircumflex Atilde -45 +KPX Udieresis A -45 +KPX Udieresis Aacute -45 +KPX Udieresis Abreve -45 +KPX Udieresis Acircumflex -45 +KPX Udieresis Adieresis -45 +KPX Udieresis Agrave -45 +KPX Udieresis Amacron -45 +KPX Udieresis Aogonek -45 +KPX Udieresis Aring -45 +KPX Udieresis Atilde -45 +KPX Ugrave A -45 +KPX Ugrave Aacute -45 +KPX Ugrave Abreve -45 +KPX Ugrave Acircumflex -45 +KPX Ugrave Adieresis -45 +KPX Ugrave Agrave -45 +KPX Ugrave Amacron -45 +KPX Ugrave Aogonek -45 +KPX Ugrave Aring -45 +KPX Ugrave Atilde -45 +KPX Uhungarumlaut A -45 +KPX Uhungarumlaut Aacute -45 +KPX Uhungarumlaut Abreve -45 +KPX Uhungarumlaut Acircumflex -45 +KPX Uhungarumlaut Adieresis -45 +KPX Uhungarumlaut Agrave -45 +KPX Uhungarumlaut Amacron -45 +KPX Uhungarumlaut Aogonek -45 +KPX Uhungarumlaut Aring -45 +KPX Uhungarumlaut Atilde -45 +KPX Umacron A -45 +KPX Umacron Aacute -45 +KPX Umacron Abreve -45 +KPX Umacron Acircumflex -45 +KPX Umacron Adieresis -45 +KPX Umacron Agrave -45 +KPX Umacron Amacron -45 +KPX Umacron Aogonek -45 +KPX Umacron Aring -45 +KPX Umacron Atilde -45 +KPX Uogonek A -45 +KPX Uogonek Aacute -45 +KPX Uogonek Abreve -45 +KPX Uogonek Acircumflex -45 +KPX Uogonek Adieresis -45 +KPX Uogonek Agrave -45 +KPX Uogonek Amacron -45 +KPX Uogonek Aogonek -45 +KPX Uogonek Aring -45 +KPX Uogonek Atilde -45 +KPX Uring A -45 +KPX Uring Aacute -45 +KPX Uring Abreve -45 +KPX Uring Acircumflex -45 +KPX Uring Adieresis -45 +KPX Uring Agrave -45 +KPX Uring Amacron -45 +KPX Uring Aogonek -45 +KPX Uring Aring -45 +KPX Uring Atilde -45 +KPX V A -85 +KPX V Aacute -85 +KPX V Abreve -85 +KPX V Acircumflex -85 +KPX V Adieresis -85 +KPX V Agrave -85 +KPX V Amacron -85 +KPX V Aogonek -85 +KPX V Aring -85 +KPX V Atilde -85 +KPX V G -10 +KPX V Gbreve -10 +KPX V Gcommaaccent -10 +KPX V O -30 +KPX V Oacute -30 +KPX V Ocircumflex -30 +KPX V Odieresis -30 +KPX V Ograve -30 +KPX V Ohungarumlaut -30 +KPX V Omacron -30 +KPX V Oslash -30 +KPX V Otilde -30 +KPX V a -111 +KPX V aacute -111 +KPX V abreve -111 +KPX V acircumflex -111 +KPX V adieresis -111 +KPX V agrave -111 +KPX V amacron -111 +KPX V aogonek -111 +KPX V aring -111 +KPX V atilde -111 +KPX V colon -74 +KPX V comma -129 +KPX V e -111 +KPX V eacute -111 +KPX V ecaron -111 +KPX V ecircumflex -111 +KPX V edieresis -71 +KPX V edotaccent -111 +KPX V egrave -71 +KPX V emacron -71 +KPX V eogonek -111 +KPX V hyphen -70 +KPX V i -55 +KPX V iacute -55 +KPX V iogonek -55 +KPX V o -111 +KPX V oacute -111 +KPX V ocircumflex -111 +KPX V odieresis -111 +KPX V ograve -111 +KPX V ohungarumlaut -111 +KPX V omacron -111 +KPX V oslash -111 +KPX V otilde -111 +KPX V period -129 +KPX V semicolon -74 +KPX V u -55 +KPX V uacute -55 +KPX V ucircumflex -55 +KPX V udieresis -55 +KPX V ugrave -55 +KPX V uhungarumlaut -55 +KPX V umacron -55 +KPX V uogonek -55 +KPX V uring -55 +KPX W A -74 +KPX W Aacute -74 +KPX W Abreve -74 +KPX W Acircumflex -74 +KPX W Adieresis -74 +KPX W Agrave -74 +KPX W Amacron -74 +KPX W Aogonek -74 +KPX W Aring -74 +KPX W Atilde -74 +KPX W O -15 +KPX W Oacute -15 +KPX W Ocircumflex -15 +KPX W Odieresis -15 +KPX W Ograve -15 +KPX W Ohungarumlaut -15 +KPX W Omacron -15 +KPX W Oslash -15 +KPX W Otilde -15 +KPX W a -85 +KPX W aacute -85 +KPX W abreve -85 +KPX W acircumflex -85 +KPX W adieresis -85 +KPX W agrave -85 +KPX W amacron -85 +KPX W aogonek -85 +KPX W aring -85 +KPX W atilde -85 +KPX W colon -55 +KPX W comma -74 +KPX W e -90 +KPX W eacute -90 +KPX W ecaron -90 +KPX W ecircumflex -90 +KPX W edieresis -50 +KPX W edotaccent -90 +KPX W egrave -50 +KPX W emacron -50 +KPX W eogonek -90 +KPX W hyphen -50 +KPX W i -37 +KPX W iacute -37 +KPX W iogonek -37 +KPX W o -80 +KPX W oacute -80 +KPX W ocircumflex -80 +KPX W odieresis -80 +KPX W ograve -80 +KPX W ohungarumlaut -80 +KPX W omacron -80 +KPX W oslash -80 +KPX W otilde -80 +KPX W period -74 +KPX W semicolon -55 +KPX W u -55 +KPX W uacute -55 +KPX W ucircumflex -55 +KPX W udieresis -55 +KPX W ugrave -55 +KPX W uhungarumlaut -55 +KPX W umacron -55 +KPX W uogonek -55 +KPX W uring -55 +KPX W y -55 +KPX W yacute -55 +KPX W ydieresis -55 +KPX Y A -74 +KPX Y Aacute -74 +KPX Y Abreve -74 +KPX Y Acircumflex -74 +KPX Y Adieresis -74 +KPX Y Agrave -74 +KPX Y Amacron -74 +KPX Y Aogonek -74 +KPX Y Aring -74 +KPX Y Atilde -74 +KPX Y O -25 +KPX Y Oacute -25 +KPX Y Ocircumflex -25 +KPX Y Odieresis -25 +KPX Y Ograve -25 +KPX Y Ohungarumlaut -25 +KPX Y Omacron -25 +KPX Y Oslash -25 +KPX Y Otilde -25 +KPX Y a -92 +KPX Y aacute -92 +KPX Y abreve -92 +KPX Y acircumflex -92 +KPX Y adieresis -92 +KPX Y agrave -92 +KPX Y amacron -92 +KPX Y aogonek -92 +KPX Y aring -92 +KPX Y atilde -92 +KPX Y colon -92 +KPX Y comma -92 +KPX Y e -111 +KPX Y eacute -111 +KPX Y ecaron -111 +KPX Y ecircumflex -71 +KPX Y edieresis -71 +KPX Y edotaccent -111 +KPX Y egrave -71 +KPX Y emacron -71 +KPX Y eogonek -111 +KPX Y hyphen -92 +KPX Y i -55 +KPX Y iacute -55 +KPX Y iogonek -55 +KPX Y o -111 +KPX Y oacute -111 +KPX Y ocircumflex -111 +KPX Y odieresis -111 +KPX Y ograve -111 +KPX Y ohungarumlaut -111 +KPX Y omacron -111 +KPX Y oslash -111 +KPX Y otilde -111 +KPX Y period -74 +KPX Y semicolon -92 +KPX Y u -92 +KPX Y uacute -92 +KPX Y ucircumflex -92 +KPX Y udieresis -92 +KPX Y ugrave -92 +KPX Y uhungarumlaut -92 +KPX Y umacron -92 +KPX Y uogonek -92 +KPX Y uring -92 +KPX Yacute A -74 +KPX Yacute Aacute -74 +KPX Yacute Abreve -74 +KPX Yacute Acircumflex -74 +KPX Yacute Adieresis -74 +KPX Yacute Agrave -74 +KPX Yacute Amacron -74 +KPX Yacute Aogonek -74 +KPX Yacute Aring -74 +KPX Yacute Atilde -74 +KPX Yacute O -25 +KPX Yacute Oacute -25 +KPX Yacute Ocircumflex -25 +KPX Yacute Odieresis -25 +KPX Yacute Ograve -25 +KPX Yacute Ohungarumlaut -25 +KPX Yacute Omacron -25 +KPX Yacute Oslash -25 +KPX Yacute Otilde -25 +KPX Yacute a -92 +KPX Yacute aacute -92 +KPX Yacute abreve -92 +KPX Yacute acircumflex -92 +KPX Yacute adieresis -92 +KPX Yacute agrave -92 +KPX Yacute amacron -92 +KPX Yacute aogonek -92 +KPX Yacute aring -92 +KPX Yacute atilde -92 +KPX Yacute colon -92 +KPX Yacute comma -92 +KPX Yacute e -111 +KPX Yacute eacute -111 +KPX Yacute ecaron -111 +KPX Yacute ecircumflex -71 +KPX Yacute edieresis -71 +KPX Yacute edotaccent -111 +KPX Yacute egrave -71 +KPX Yacute emacron -71 +KPX Yacute eogonek -111 +KPX Yacute hyphen -92 +KPX Yacute i -55 +KPX Yacute iacute -55 +KPX Yacute iogonek -55 +KPX Yacute o -111 +KPX Yacute oacute -111 +KPX Yacute ocircumflex -111 +KPX Yacute odieresis -111 +KPX Yacute ograve -111 +KPX Yacute ohungarumlaut -111 +KPX Yacute omacron -111 +KPX Yacute oslash -111 +KPX Yacute otilde -111 +KPX Yacute period -74 +KPX Yacute semicolon -92 +KPX Yacute u -92 +KPX Yacute uacute -92 +KPX Yacute ucircumflex -92 +KPX Yacute udieresis -92 +KPX Yacute ugrave -92 +KPX Yacute uhungarumlaut -92 +KPX Yacute umacron -92 +KPX Yacute uogonek -92 +KPX Yacute uring -92 +KPX Ydieresis A -74 +KPX Ydieresis Aacute -74 +KPX Ydieresis Abreve -74 +KPX Ydieresis Acircumflex -74 +KPX Ydieresis Adieresis -74 +KPX Ydieresis Agrave -74 +KPX Ydieresis Amacron -74 +KPX Ydieresis Aogonek -74 +KPX Ydieresis Aring -74 +KPX Ydieresis Atilde -74 +KPX Ydieresis O -25 +KPX Ydieresis Oacute -25 +KPX Ydieresis Ocircumflex -25 +KPX Ydieresis Odieresis -25 +KPX Ydieresis Ograve -25 +KPX Ydieresis Ohungarumlaut -25 +KPX Ydieresis Omacron -25 +KPX Ydieresis Oslash -25 +KPX Ydieresis Otilde -25 +KPX Ydieresis a -92 +KPX Ydieresis aacute -92 +KPX Ydieresis abreve -92 +KPX Ydieresis acircumflex -92 +KPX Ydieresis adieresis -92 +KPX Ydieresis agrave -92 +KPX Ydieresis amacron -92 +KPX Ydieresis aogonek -92 +KPX Ydieresis aring -92 +KPX Ydieresis atilde -92 +KPX Ydieresis colon -92 +KPX Ydieresis comma -92 +KPX Ydieresis e -111 +KPX Ydieresis eacute -111 +KPX Ydieresis ecaron -111 +KPX Ydieresis ecircumflex -71 +KPX Ydieresis edieresis -71 +KPX Ydieresis edotaccent -111 +KPX Ydieresis egrave -71 +KPX Ydieresis emacron -71 +KPX Ydieresis eogonek -111 +KPX Ydieresis hyphen -92 +KPX Ydieresis i -55 +KPX Ydieresis iacute -55 +KPX Ydieresis iogonek -55 +KPX Ydieresis o -111 +KPX Ydieresis oacute -111 +KPX Ydieresis ocircumflex -111 +KPX Ydieresis odieresis -111 +KPX Ydieresis ograve -111 +KPX Ydieresis ohungarumlaut -111 +KPX Ydieresis omacron -111 +KPX Ydieresis oslash -111 +KPX Ydieresis otilde -111 +KPX Ydieresis period -74 +KPX Ydieresis semicolon -92 +KPX Ydieresis u -92 +KPX Ydieresis uacute -92 +KPX Ydieresis ucircumflex -92 +KPX Ydieresis udieresis -92 +KPX Ydieresis ugrave -92 +KPX Ydieresis uhungarumlaut -92 +KPX Ydieresis umacron -92 +KPX Ydieresis uogonek -92 +KPX Ydieresis uring -92 +KPX b b -10 +KPX b period -40 +KPX b u -20 +KPX b uacute -20 +KPX b ucircumflex -20 +KPX b udieresis -20 +KPX b ugrave -20 +KPX b uhungarumlaut -20 +KPX b umacron -20 +KPX b uogonek -20 +KPX b uring -20 +KPX c h -10 +KPX c k -10 +KPX c kcommaaccent -10 +KPX cacute h -10 +KPX cacute k -10 +KPX cacute kcommaaccent -10 +KPX ccaron h -10 +KPX ccaron k -10 +KPX ccaron kcommaaccent -10 +KPX ccedilla h -10 +KPX ccedilla k -10 +KPX ccedilla kcommaaccent -10 +KPX comma quotedblright -95 +KPX comma quoteright -95 +KPX e b -10 +KPX eacute b -10 +KPX ecaron b -10 +KPX ecircumflex b -10 +KPX edieresis b -10 +KPX edotaccent b -10 +KPX egrave b -10 +KPX emacron b -10 +KPX eogonek b -10 +KPX f comma -10 +KPX f dotlessi -30 +KPX f e -10 +KPX f eacute -10 +KPX f edotaccent -10 +KPX f eogonek -10 +KPX f f -18 +KPX f o -10 +KPX f oacute -10 +KPX f ocircumflex -10 +KPX f ograve -10 +KPX f ohungarumlaut -10 +KPX f oslash -10 +KPX f otilde -10 +KPX f period -10 +KPX f quoteright 55 +KPX k e -30 +KPX k eacute -30 +KPX k ecaron -30 +KPX k ecircumflex -30 +KPX k edieresis -30 +KPX k edotaccent -30 +KPX k egrave -30 +KPX k emacron -30 +KPX k eogonek -30 +KPX k o -10 +KPX k oacute -10 +KPX k ocircumflex -10 +KPX k odieresis -10 +KPX k ograve -10 +KPX k ohungarumlaut -10 +KPX k omacron -10 +KPX k oslash -10 +KPX k otilde -10 +KPX kcommaaccent e -30 +KPX kcommaaccent eacute -30 +KPX kcommaaccent ecaron -30 +KPX kcommaaccent ecircumflex -30 +KPX kcommaaccent edieresis -30 +KPX kcommaaccent edotaccent -30 +KPX kcommaaccent egrave -30 +KPX kcommaaccent emacron -30 +KPX kcommaaccent eogonek -30 +KPX kcommaaccent o -10 +KPX kcommaaccent oacute -10 +KPX kcommaaccent ocircumflex -10 +KPX kcommaaccent odieresis -10 +KPX kcommaaccent ograve -10 +KPX kcommaaccent ohungarumlaut -10 +KPX kcommaaccent omacron -10 +KPX kcommaaccent oslash -10 +KPX kcommaaccent otilde -10 +KPX n v -40 +KPX nacute v -40 +KPX ncaron v -40 +KPX ncommaaccent v -40 +KPX ntilde v -40 +KPX o v -15 +KPX o w -25 +KPX o x -10 +KPX o y -10 +KPX o yacute -10 +KPX o ydieresis -10 +KPX oacute v -15 +KPX oacute w -25 +KPX oacute x -10 +KPX oacute y -10 +KPX oacute yacute -10 +KPX oacute ydieresis -10 +KPX ocircumflex v -15 +KPX ocircumflex w -25 +KPX ocircumflex x -10 +KPX ocircumflex y -10 +KPX ocircumflex yacute -10 +KPX ocircumflex ydieresis -10 +KPX odieresis v -15 +KPX odieresis w -25 +KPX odieresis x -10 +KPX odieresis y -10 +KPX odieresis yacute -10 +KPX odieresis ydieresis -10 +KPX ograve v -15 +KPX ograve w -25 +KPX ograve x -10 +KPX ograve y -10 +KPX ograve yacute -10 +KPX ograve ydieresis -10 +KPX ohungarumlaut v -15 +KPX ohungarumlaut w -25 +KPX ohungarumlaut x -10 +KPX ohungarumlaut y -10 +KPX ohungarumlaut yacute -10 +KPX ohungarumlaut ydieresis -10 +KPX omacron v -15 +KPX omacron w -25 +KPX omacron x -10 +KPX omacron y -10 +KPX omacron yacute -10 +KPX omacron ydieresis -10 +KPX oslash v -15 +KPX oslash w -25 +KPX oslash x -10 +KPX oslash y -10 +KPX oslash yacute -10 +KPX oslash ydieresis -10 +KPX otilde v -15 +KPX otilde w -25 +KPX otilde x -10 +KPX otilde y -10 +KPX otilde yacute -10 +KPX otilde ydieresis -10 +KPX period quotedblright -95 +KPX period quoteright -95 +KPX quoteleft quoteleft -74 +KPX quoteright d -15 +KPX quoteright dcroat -15 +KPX quoteright quoteright -74 +KPX quoteright r -15 +KPX quoteright racute -15 +KPX quoteright rcaron -15 +KPX quoteright rcommaaccent -15 +KPX quoteright s -74 +KPX quoteright sacute -74 +KPX quoteright scaron -74 +KPX quoteright scedilla -74 +KPX quoteright scommaaccent -74 +KPX quoteright space -74 +KPX quoteright t -37 +KPX quoteright tcommaaccent -37 +KPX quoteright v -15 +KPX r comma -65 +KPX r period -65 +KPX racute comma -65 +KPX racute period -65 +KPX rcaron comma -65 +KPX rcaron period -65 +KPX rcommaaccent comma -65 +KPX rcommaaccent period -65 +KPX space A -37 +KPX space Aacute -37 +KPX space Abreve -37 +KPX space Acircumflex -37 +KPX space Adieresis -37 +KPX space Agrave -37 +KPX space Amacron -37 +KPX space Aogonek -37 +KPX space Aring -37 +KPX space Atilde -37 +KPX space V -70 +KPX space W -70 +KPX space Y -70 +KPX space Yacute -70 +KPX space Ydieresis -70 +KPX v comma -37 +KPX v e -15 +KPX v eacute -15 +KPX v ecaron -15 +KPX v ecircumflex -15 +KPX v edieresis -15 +KPX v edotaccent -15 +KPX v egrave -15 +KPX v emacron -15 +KPX v eogonek -15 +KPX v o -15 +KPX v oacute -15 +KPX v ocircumflex -15 +KPX v odieresis -15 +KPX v ograve -15 +KPX v ohungarumlaut -15 +KPX v omacron -15 +KPX v oslash -15 +KPX v otilde -15 +KPX v period -37 +KPX w a -10 +KPX w aacute -10 +KPX w abreve -10 +KPX w acircumflex -10 +KPX w adieresis -10 +KPX w agrave -10 +KPX w amacron -10 +KPX w aogonek -10 +KPX w aring -10 +KPX w atilde -10 +KPX w comma -37 +KPX w e -10 +KPX w eacute -10 +KPX w ecaron -10 +KPX w ecircumflex -10 +KPX w edieresis -10 +KPX w edotaccent -10 +KPX w egrave -10 +KPX w emacron -10 +KPX w eogonek -10 +KPX w o -15 +KPX w oacute -15 +KPX w ocircumflex -15 +KPX w odieresis -15 +KPX w ograve -15 +KPX w ohungarumlaut -15 +KPX w omacron -15 +KPX w oslash -15 +KPX w otilde -15 +KPX w period -37 +KPX x e -10 +KPX x eacute -10 +KPX x ecaron -10 +KPX x ecircumflex -10 +KPX x edieresis -10 +KPX x edotaccent -10 +KPX x egrave -10 +KPX x emacron -10 +KPX x eogonek -10 +KPX y comma -37 +KPX y period -37 +KPX yacute comma -37 +KPX yacute period -37 +KPX ydieresis comma -37 +KPX ydieresis period -37 +EndKernPairs +EndKernData +EndFontMetrics diff --git a/internal/corefont/Core14_AFMs/Times-Italic.afm b/internal/corefont/Core14_AFMs/Times-Italic.afm new file mode 100644 index 0000000000000000000000000000000000000000..b0eaee40fc6ab16103375431fea06c22f648f471 --- /dev/null +++ b/internal/corefont/Core14_AFMs/Times-Italic.afm @@ -0,0 +1,2667 @@ +StartFontMetrics 4.1 +Comment Copyright (c) 1985, 1987, 1989, 1990, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved. +Comment Creation Date: Thu May 1 12:56:55 1997 +Comment UniqueID 43067 +Comment VMusage 47727 58752 +FontName Times-Italic +FullName Times Italic +FamilyName Times +Weight Medium +ItalicAngle -15.5 +IsFixedPitch false +CharacterSet ExtendedRoman +FontBBox -169 -217 1010 883 +UnderlinePosition -100 +UnderlineThickness 50 +Version 002.000 +Notice Copyright (c) 1985, 1987, 1989, 1990, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved.Times is a trademark of Linotype-Hell AG and/or its subsidiaries. +EncodingScheme AdobeStandardEncoding +CapHeight 653 +XHeight 441 +Ascender 683 +Descender -217 +StdHW 32 +StdVW 76 +StartCharMetrics 315 +C 32 ; WX 250 ; N space ; B 0 0 0 0 ; +C 33 ; WX 333 ; N exclam ; B 39 -11 302 667 ; +C 34 ; WX 420 ; N quotedbl ; B 144 421 432 666 ; +C 35 ; WX 500 ; N numbersign ; B 2 0 540 676 ; +C 36 ; WX 500 ; N dollar ; B 31 -89 497 731 ; +C 37 ; WX 833 ; N percent ; B 79 -13 790 676 ; +C 38 ; WX 778 ; N ampersand ; B 76 -18 723 666 ; +C 39 ; WX 333 ; N quoteright ; B 151 436 290 666 ; +C 40 ; WX 333 ; N parenleft ; B 42 -181 315 669 ; +C 41 ; WX 333 ; N parenright ; B 16 -180 289 669 ; +C 42 ; WX 500 ; N asterisk ; B 128 255 492 666 ; +C 43 ; WX 675 ; N plus ; B 86 0 590 506 ; +C 44 ; WX 250 ; N comma ; B -4 -129 135 101 ; +C 45 ; WX 333 ; N hyphen ; B 49 192 282 255 ; +C 46 ; WX 250 ; N period ; B 27 -11 138 100 ; +C 47 ; WX 278 ; N slash ; B -65 -18 386 666 ; +C 48 ; WX 500 ; N zero ; B 32 -7 497 676 ; +C 49 ; WX 500 ; N one ; B 49 0 409 676 ; +C 50 ; WX 500 ; N two ; B 12 0 452 676 ; +C 51 ; WX 500 ; N three ; B 15 -7 465 676 ; +C 52 ; WX 500 ; N four ; B 1 0 479 676 ; +C 53 ; WX 500 ; N five ; B 15 -7 491 666 ; +C 54 ; WX 500 ; N six ; B 30 -7 521 686 ; +C 55 ; WX 500 ; N seven ; B 75 -8 537 666 ; +C 56 ; WX 500 ; N eight ; B 30 -7 493 676 ; +C 57 ; WX 500 ; N nine ; B 23 -17 492 676 ; +C 58 ; WX 333 ; N colon ; B 50 -11 261 441 ; +C 59 ; WX 333 ; N semicolon ; B 27 -129 261 441 ; +C 60 ; WX 675 ; N less ; B 84 -8 592 514 ; +C 61 ; WX 675 ; N equal ; B 86 120 590 386 ; +C 62 ; WX 675 ; N greater ; B 84 -8 592 514 ; +C 63 ; WX 500 ; N question ; B 132 -12 472 664 ; +C 64 ; WX 920 ; N at ; B 118 -18 806 666 ; +C 65 ; WX 611 ; N A ; B -51 0 564 668 ; +C 66 ; WX 611 ; N B ; B -8 0 588 653 ; +C 67 ; WX 667 ; N C ; B 66 -18 689 666 ; +C 68 ; WX 722 ; N D ; B -8 0 700 653 ; +C 69 ; WX 611 ; N E ; B -1 0 634 653 ; +C 70 ; WX 611 ; N F ; B 8 0 645 653 ; +C 71 ; WX 722 ; N G ; B 52 -18 722 666 ; +C 72 ; WX 722 ; N H ; B -8 0 767 653 ; +C 73 ; WX 333 ; N I ; B -8 0 384 653 ; +C 74 ; WX 444 ; N J ; B -6 -18 491 653 ; +C 75 ; WX 667 ; N K ; B 7 0 722 653 ; +C 76 ; WX 556 ; N L ; B -8 0 559 653 ; +C 77 ; WX 833 ; N M ; B -18 0 873 653 ; +C 78 ; WX 667 ; N N ; B -20 -15 727 653 ; +C 79 ; WX 722 ; N O ; B 60 -18 699 666 ; +C 80 ; WX 611 ; N P ; B 0 0 605 653 ; +C 81 ; WX 722 ; N Q ; B 59 -182 699 666 ; +C 82 ; WX 611 ; N R ; B -13 0 588 653 ; +C 83 ; WX 500 ; N S ; B 17 -18 508 667 ; +C 84 ; WX 556 ; N T ; B 59 0 633 653 ; +C 85 ; WX 722 ; N U ; B 102 -18 765 653 ; +C 86 ; WX 611 ; N V ; B 76 -18 688 653 ; +C 87 ; WX 833 ; N W ; B 71 -18 906 653 ; +C 88 ; WX 611 ; N X ; B -29 0 655 653 ; +C 89 ; WX 556 ; N Y ; B 78 0 633 653 ; +C 90 ; WX 556 ; N Z ; B -6 0 606 653 ; +C 91 ; WX 389 ; N bracketleft ; B 21 -153 391 663 ; +C 92 ; WX 278 ; N backslash ; B -41 -18 319 666 ; +C 93 ; WX 389 ; N bracketright ; B 12 -153 382 663 ; +C 94 ; WX 422 ; N asciicircum ; B 0 301 422 666 ; +C 95 ; WX 500 ; N underscore ; B 0 -125 500 -75 ; +C 96 ; WX 333 ; N quoteleft ; B 171 436 310 666 ; +C 97 ; WX 500 ; N a ; B 17 -11 476 441 ; +C 98 ; WX 500 ; N b ; B 23 -11 473 683 ; +C 99 ; WX 444 ; N c ; B 30 -11 425 441 ; +C 100 ; WX 500 ; N d ; B 15 -13 527 683 ; +C 101 ; WX 444 ; N e ; B 31 -11 412 441 ; +C 102 ; WX 278 ; N f ; B -147 -207 424 678 ; L i fi ; L l fl ; +C 103 ; WX 500 ; N g ; B 8 -206 472 441 ; +C 104 ; WX 500 ; N h ; B 19 -9 478 683 ; +C 105 ; WX 278 ; N i ; B 49 -11 264 654 ; +C 106 ; WX 278 ; N j ; B -124 -207 276 654 ; +C 107 ; WX 444 ; N k ; B 14 -11 461 683 ; +C 108 ; WX 278 ; N l ; B 41 -11 279 683 ; +C 109 ; WX 722 ; N m ; B 12 -9 704 441 ; +C 110 ; WX 500 ; N n ; B 14 -9 474 441 ; +C 111 ; WX 500 ; N o ; B 27 -11 468 441 ; +C 112 ; WX 500 ; N p ; B -75 -205 469 441 ; +C 113 ; WX 500 ; N q ; B 25 -209 483 441 ; +C 114 ; WX 389 ; N r ; B 45 0 412 441 ; +C 115 ; WX 389 ; N s ; B 16 -13 366 442 ; +C 116 ; WX 278 ; N t ; B 37 -11 296 546 ; +C 117 ; WX 500 ; N u ; B 42 -11 475 441 ; +C 118 ; WX 444 ; N v ; B 21 -18 426 441 ; +C 119 ; WX 667 ; N w ; B 16 -18 648 441 ; +C 120 ; WX 444 ; N x ; B -27 -11 447 441 ; +C 121 ; WX 444 ; N y ; B -24 -206 426 441 ; +C 122 ; WX 389 ; N z ; B -2 -81 380 428 ; +C 123 ; WX 400 ; N braceleft ; B 51 -177 407 687 ; +C 124 ; WX 275 ; N bar ; B 105 -217 171 783 ; +C 125 ; WX 400 ; N braceright ; B -7 -177 349 687 ; +C 126 ; WX 541 ; N asciitilde ; B 40 183 502 323 ; +C 161 ; WX 389 ; N exclamdown ; B 59 -205 322 473 ; +C 162 ; WX 500 ; N cent ; B 77 -143 472 560 ; +C 163 ; WX 500 ; N sterling ; B 10 -6 517 670 ; +C 164 ; WX 167 ; N fraction ; B -169 -10 337 676 ; +C 165 ; WX 500 ; N yen ; B 27 0 603 653 ; +C 166 ; WX 500 ; N florin ; B 25 -182 507 682 ; +C 167 ; WX 500 ; N section ; B 53 -162 461 666 ; +C 168 ; WX 500 ; N currency ; B -22 53 522 597 ; +C 169 ; WX 214 ; N quotesingle ; B 132 421 241 666 ; +C 170 ; WX 556 ; N quotedblleft ; B 166 436 514 666 ; +C 171 ; WX 500 ; N guillemotleft ; B 53 37 445 403 ; +C 172 ; WX 333 ; N guilsinglleft ; B 51 37 281 403 ; +C 173 ; WX 333 ; N guilsinglright ; B 52 37 282 403 ; +C 174 ; WX 500 ; N fi ; B -141 -207 481 681 ; +C 175 ; WX 500 ; N fl ; B -141 -204 518 682 ; +C 177 ; WX 500 ; N endash ; B -6 197 505 243 ; +C 178 ; WX 500 ; N dagger ; B 101 -159 488 666 ; +C 179 ; WX 500 ; N daggerdbl ; B 22 -143 491 666 ; +C 180 ; WX 250 ; N periodcentered ; B 70 199 181 310 ; +C 182 ; WX 523 ; N paragraph ; B 55 -123 616 653 ; +C 183 ; WX 350 ; N bullet ; B 40 191 310 461 ; +C 184 ; WX 333 ; N quotesinglbase ; B 44 -129 183 101 ; +C 185 ; WX 556 ; N quotedblbase ; B 57 -129 405 101 ; +C 186 ; WX 556 ; N quotedblright ; B 151 436 499 666 ; +C 187 ; WX 500 ; N guillemotright ; B 55 37 447 403 ; +C 188 ; WX 889 ; N ellipsis ; B 57 -11 762 100 ; +C 189 ; WX 1000 ; N perthousand ; B 25 -19 1010 706 ; +C 191 ; WX 500 ; N questiondown ; B 28 -205 368 471 ; +C 193 ; WX 333 ; N grave ; B 121 492 311 664 ; +C 194 ; WX 333 ; N acute ; B 180 494 403 664 ; +C 195 ; WX 333 ; N circumflex ; B 91 492 385 661 ; +C 196 ; WX 333 ; N tilde ; B 100 517 427 624 ; +C 197 ; WX 333 ; N macron ; B 99 532 411 583 ; +C 198 ; WX 333 ; N breve ; B 117 492 418 650 ; +C 199 ; WX 333 ; N dotaccent ; B 207 548 305 646 ; +C 200 ; WX 333 ; N dieresis ; B 107 548 405 646 ; +C 202 ; WX 333 ; N ring ; B 155 492 355 691 ; +C 203 ; WX 333 ; N cedilla ; B -30 -217 182 0 ; +C 205 ; WX 333 ; N hungarumlaut ; B 93 494 486 664 ; +C 206 ; WX 333 ; N ogonek ; B 20 -169 203 40 ; +C 207 ; WX 333 ; N caron ; B 121 492 426 661 ; +C 208 ; WX 889 ; N emdash ; B -6 197 894 243 ; +C 225 ; WX 889 ; N AE ; B -27 0 911 653 ; +C 227 ; WX 276 ; N ordfeminine ; B 42 406 352 676 ; +C 232 ; WX 556 ; N Lslash ; B -8 0 559 653 ; +C 233 ; WX 722 ; N Oslash ; B 60 -105 699 722 ; +C 234 ; WX 944 ; N OE ; B 49 -8 964 666 ; +C 235 ; WX 310 ; N ordmasculine ; B 67 406 362 676 ; +C 241 ; WX 667 ; N ae ; B 23 -11 640 441 ; +C 245 ; WX 278 ; N dotlessi ; B 49 -11 235 441 ; +C 248 ; WX 278 ; N lslash ; B 41 -11 312 683 ; +C 249 ; WX 500 ; N oslash ; B 28 -135 469 554 ; +C 250 ; WX 667 ; N oe ; B 20 -12 646 441 ; +C 251 ; WX 500 ; N germandbls ; B -168 -207 493 679 ; +C -1 ; WX 333 ; N Idieresis ; B -8 0 435 818 ; +C -1 ; WX 444 ; N eacute ; B 31 -11 459 664 ; +C -1 ; WX 500 ; N abreve ; B 17 -11 502 650 ; +C -1 ; WX 500 ; N uhungarumlaut ; B 42 -11 580 664 ; +C -1 ; WX 444 ; N ecaron ; B 31 -11 482 661 ; +C -1 ; WX 556 ; N Ydieresis ; B 78 0 633 818 ; +C -1 ; WX 675 ; N divide ; B 86 -11 590 517 ; +C -1 ; WX 556 ; N Yacute ; B 78 0 633 876 ; +C -1 ; WX 611 ; N Acircumflex ; B -51 0 564 873 ; +C -1 ; WX 500 ; N aacute ; B 17 -11 487 664 ; +C -1 ; WX 722 ; N Ucircumflex ; B 102 -18 765 873 ; +C -1 ; WX 444 ; N yacute ; B -24 -206 459 664 ; +C -1 ; WX 389 ; N scommaaccent ; B 16 -217 366 442 ; +C -1 ; WX 444 ; N ecircumflex ; B 31 -11 441 661 ; +C -1 ; WX 722 ; N Uring ; B 102 -18 765 883 ; +C -1 ; WX 722 ; N Udieresis ; B 102 -18 765 818 ; +C -1 ; WX 500 ; N aogonek ; B 17 -169 476 441 ; +C -1 ; WX 722 ; N Uacute ; B 102 -18 765 876 ; +C -1 ; WX 500 ; N uogonek ; B 42 -169 477 441 ; +C -1 ; WX 611 ; N Edieresis ; B -1 0 634 818 ; +C -1 ; WX 722 ; N Dcroat ; B -8 0 700 653 ; +C -1 ; WX 250 ; N commaaccent ; B 8 -217 133 -50 ; +C -1 ; WX 760 ; N copyright ; B 41 -18 719 666 ; +C -1 ; WX 611 ; N Emacron ; B -1 0 634 795 ; +C -1 ; WX 444 ; N ccaron ; B 30 -11 482 661 ; +C -1 ; WX 500 ; N aring ; B 17 -11 476 691 ; +C -1 ; WX 667 ; N Ncommaaccent ; B -20 -187 727 653 ; +C -1 ; WX 278 ; N lacute ; B 41 -11 395 876 ; +C -1 ; WX 500 ; N agrave ; B 17 -11 476 664 ; +C -1 ; WX 556 ; N Tcommaaccent ; B 59 -217 633 653 ; +C -1 ; WX 667 ; N Cacute ; B 66 -18 690 876 ; +C -1 ; WX 500 ; N atilde ; B 17 -11 511 624 ; +C -1 ; WX 611 ; N Edotaccent ; B -1 0 634 818 ; +C -1 ; WX 389 ; N scaron ; B 16 -13 454 661 ; +C -1 ; WX 389 ; N scedilla ; B 16 -217 366 442 ; +C -1 ; WX 278 ; N iacute ; B 49 -11 355 664 ; +C -1 ; WX 471 ; N lozenge ; B 13 0 459 724 ; +C -1 ; WX 611 ; N Rcaron ; B -13 0 588 873 ; +C -1 ; WX 722 ; N Gcommaaccent ; B 52 -217 722 666 ; +C -1 ; WX 500 ; N ucircumflex ; B 42 -11 475 661 ; +C -1 ; WX 500 ; N acircumflex ; B 17 -11 476 661 ; +C -1 ; WX 611 ; N Amacron ; B -51 0 564 795 ; +C -1 ; WX 389 ; N rcaron ; B 45 0 434 661 ; +C -1 ; WX 444 ; N ccedilla ; B 30 -217 425 441 ; +C -1 ; WX 556 ; N Zdotaccent ; B -6 0 606 818 ; +C -1 ; WX 611 ; N Thorn ; B 0 0 569 653 ; +C -1 ; WX 722 ; N Omacron ; B 60 -18 699 795 ; +C -1 ; WX 611 ; N Racute ; B -13 0 588 876 ; +C -1 ; WX 500 ; N Sacute ; B 17 -18 508 876 ; +C -1 ; WX 544 ; N dcaron ; B 15 -13 658 683 ; +C -1 ; WX 722 ; N Umacron ; B 102 -18 765 795 ; +C -1 ; WX 500 ; N uring ; B 42 -11 475 691 ; +C -1 ; WX 300 ; N threesuperior ; B 43 268 339 676 ; +C -1 ; WX 722 ; N Ograve ; B 60 -18 699 876 ; +C -1 ; WX 611 ; N Agrave ; B -51 0 564 876 ; +C -1 ; WX 611 ; N Abreve ; B -51 0 564 862 ; +C -1 ; WX 675 ; N multiply ; B 93 8 582 497 ; +C -1 ; WX 500 ; N uacute ; B 42 -11 477 664 ; +C -1 ; WX 556 ; N Tcaron ; B 59 0 633 873 ; +C -1 ; WX 476 ; N partialdiff ; B 17 -38 459 710 ; +C -1 ; WX 444 ; N ydieresis ; B -24 -206 441 606 ; +C -1 ; WX 667 ; N Nacute ; B -20 -15 727 876 ; +C -1 ; WX 278 ; N icircumflex ; B 33 -11 327 661 ; +C -1 ; WX 611 ; N Ecircumflex ; B -1 0 634 873 ; +C -1 ; WX 500 ; N adieresis ; B 17 -11 489 606 ; +C -1 ; WX 444 ; N edieresis ; B 31 -11 451 606 ; +C -1 ; WX 444 ; N cacute ; B 30 -11 459 664 ; +C -1 ; WX 500 ; N nacute ; B 14 -9 477 664 ; +C -1 ; WX 500 ; N umacron ; B 42 -11 485 583 ; +C -1 ; WX 667 ; N Ncaron ; B -20 -15 727 873 ; +C -1 ; WX 333 ; N Iacute ; B -8 0 433 876 ; +C -1 ; WX 675 ; N plusminus ; B 86 0 590 506 ; +C -1 ; WX 275 ; N brokenbar ; B 105 -142 171 708 ; +C -1 ; WX 760 ; N registered ; B 41 -18 719 666 ; +C -1 ; WX 722 ; N Gbreve ; B 52 -18 722 862 ; +C -1 ; WX 333 ; N Idotaccent ; B -8 0 384 818 ; +C -1 ; WX 600 ; N summation ; B 15 -10 585 706 ; +C -1 ; WX 611 ; N Egrave ; B -1 0 634 876 ; +C -1 ; WX 389 ; N racute ; B 45 0 431 664 ; +C -1 ; WX 500 ; N omacron ; B 27 -11 495 583 ; +C -1 ; WX 556 ; N Zacute ; B -6 0 606 876 ; +C -1 ; WX 556 ; N Zcaron ; B -6 0 606 873 ; +C -1 ; WX 549 ; N greaterequal ; B 26 0 523 658 ; +C -1 ; WX 722 ; N Eth ; B -8 0 700 653 ; +C -1 ; WX 667 ; N Ccedilla ; B 66 -217 689 666 ; +C -1 ; WX 278 ; N lcommaaccent ; B 22 -217 279 683 ; +C -1 ; WX 300 ; N tcaron ; B 37 -11 407 681 ; +C -1 ; WX 444 ; N eogonek ; B 31 -169 412 441 ; +C -1 ; WX 722 ; N Uogonek ; B 102 -184 765 653 ; +C -1 ; WX 611 ; N Aacute ; B -51 0 564 876 ; +C -1 ; WX 611 ; N Adieresis ; B -51 0 564 818 ; +C -1 ; WX 444 ; N egrave ; B 31 -11 412 664 ; +C -1 ; WX 389 ; N zacute ; B -2 -81 431 664 ; +C -1 ; WX 278 ; N iogonek ; B 49 -169 264 654 ; +C -1 ; WX 722 ; N Oacute ; B 60 -18 699 876 ; +C -1 ; WX 500 ; N oacute ; B 27 -11 487 664 ; +C -1 ; WX 500 ; N amacron ; B 17 -11 495 583 ; +C -1 ; WX 389 ; N sacute ; B 16 -13 431 664 ; +C -1 ; WX 278 ; N idieresis ; B 49 -11 352 606 ; +C -1 ; WX 722 ; N Ocircumflex ; B 60 -18 699 873 ; +C -1 ; WX 722 ; N Ugrave ; B 102 -18 765 876 ; +C -1 ; WX 612 ; N Delta ; B 6 0 608 688 ; +C -1 ; WX 500 ; N thorn ; B -75 -205 469 683 ; +C -1 ; WX 300 ; N twosuperior ; B 33 271 324 676 ; +C -1 ; WX 722 ; N Odieresis ; B 60 -18 699 818 ; +C -1 ; WX 500 ; N mu ; B -30 -209 497 428 ; +C -1 ; WX 278 ; N igrave ; B 49 -11 284 664 ; +C -1 ; WX 500 ; N ohungarumlaut ; B 27 -11 590 664 ; +C -1 ; WX 611 ; N Eogonek ; B -1 -169 634 653 ; +C -1 ; WX 500 ; N dcroat ; B 15 -13 572 683 ; +C -1 ; WX 750 ; N threequarters ; B 23 -10 736 676 ; +C -1 ; WX 500 ; N Scedilla ; B 17 -217 508 667 ; +C -1 ; WX 300 ; N lcaron ; B 41 -11 407 683 ; +C -1 ; WX 667 ; N Kcommaaccent ; B 7 -217 722 653 ; +C -1 ; WX 556 ; N Lacute ; B -8 0 559 876 ; +C -1 ; WX 980 ; N trademark ; B 30 247 957 653 ; +C -1 ; WX 444 ; N edotaccent ; B 31 -11 412 606 ; +C -1 ; WX 333 ; N Igrave ; B -8 0 384 876 ; +C -1 ; WX 333 ; N Imacron ; B -8 0 441 795 ; +C -1 ; WX 611 ; N Lcaron ; B -8 0 586 653 ; +C -1 ; WX 750 ; N onehalf ; B 34 -10 749 676 ; +C -1 ; WX 549 ; N lessequal ; B 26 0 523 658 ; +C -1 ; WX 500 ; N ocircumflex ; B 27 -11 468 661 ; +C -1 ; WX 500 ; N ntilde ; B 14 -9 476 624 ; +C -1 ; WX 722 ; N Uhungarumlaut ; B 102 -18 765 876 ; +C -1 ; WX 611 ; N Eacute ; B -1 0 634 876 ; +C -1 ; WX 444 ; N emacron ; B 31 -11 457 583 ; +C -1 ; WX 500 ; N gbreve ; B 8 -206 487 650 ; +C -1 ; WX 750 ; N onequarter ; B 33 -10 736 676 ; +C -1 ; WX 500 ; N Scaron ; B 17 -18 520 873 ; +C -1 ; WX 500 ; N Scommaaccent ; B 17 -217 508 667 ; +C -1 ; WX 722 ; N Ohungarumlaut ; B 60 -18 699 876 ; +C -1 ; WX 400 ; N degree ; B 101 390 387 676 ; +C -1 ; WX 500 ; N ograve ; B 27 -11 468 664 ; +C -1 ; WX 667 ; N Ccaron ; B 66 -18 689 873 ; +C -1 ; WX 500 ; N ugrave ; B 42 -11 475 664 ; +C -1 ; WX 453 ; N radical ; B 2 -60 452 768 ; +C -1 ; WX 722 ; N Dcaron ; B -8 0 700 873 ; +C -1 ; WX 389 ; N rcommaaccent ; B -3 -217 412 441 ; +C -1 ; WX 667 ; N Ntilde ; B -20 -15 727 836 ; +C -1 ; WX 500 ; N otilde ; B 27 -11 496 624 ; +C -1 ; WX 611 ; N Rcommaaccent ; B -13 -187 588 653 ; +C -1 ; WX 556 ; N Lcommaaccent ; B -8 -217 559 653 ; +C -1 ; WX 611 ; N Atilde ; B -51 0 566 836 ; +C -1 ; WX 611 ; N Aogonek ; B -51 -169 566 668 ; +C -1 ; WX 611 ; N Aring ; B -51 0 564 883 ; +C -1 ; WX 722 ; N Otilde ; B 60 -18 699 836 ; +C -1 ; WX 389 ; N zdotaccent ; B -2 -81 380 606 ; +C -1 ; WX 611 ; N Ecaron ; B -1 0 634 873 ; +C -1 ; WX 333 ; N Iogonek ; B -8 -169 384 653 ; +C -1 ; WX 444 ; N kcommaaccent ; B 14 -187 461 683 ; +C -1 ; WX 675 ; N minus ; B 86 220 590 286 ; +C -1 ; WX 333 ; N Icircumflex ; B -8 0 425 873 ; +C -1 ; WX 500 ; N ncaron ; B 14 -9 510 661 ; +C -1 ; WX 278 ; N tcommaaccent ; B 2 -217 296 546 ; +C -1 ; WX 675 ; N logicalnot ; B 86 108 590 386 ; +C -1 ; WX 500 ; N odieresis ; B 27 -11 489 606 ; +C -1 ; WX 500 ; N udieresis ; B 42 -11 479 606 ; +C -1 ; WX 549 ; N notequal ; B 12 -29 537 541 ; +C -1 ; WX 500 ; N gcommaaccent ; B 8 -206 472 706 ; +C -1 ; WX 500 ; N eth ; B 27 -11 482 683 ; +C -1 ; WX 389 ; N zcaron ; B -2 -81 434 661 ; +C -1 ; WX 500 ; N ncommaaccent ; B 14 -187 474 441 ; +C -1 ; WX 300 ; N onesuperior ; B 43 271 284 676 ; +C -1 ; WX 278 ; N imacron ; B 46 -11 311 583 ; +C -1 ; WX 500 ; N Euro ; B 0 0 0 0 ; +EndCharMetrics +StartKernData +StartKernPairs 2321 +KPX A C -30 +KPX A Cacute -30 +KPX A Ccaron -30 +KPX A Ccedilla -30 +KPX A G -35 +KPX A Gbreve -35 +KPX A Gcommaaccent -35 +KPX A O -40 +KPX A Oacute -40 +KPX A Ocircumflex -40 +KPX A Odieresis -40 +KPX A Ograve -40 +KPX A Ohungarumlaut -40 +KPX A Omacron -40 +KPX A Oslash -40 +KPX A Otilde -40 +KPX A Q -40 +KPX A T -37 +KPX A Tcaron -37 +KPX A Tcommaaccent -37 +KPX A U -50 +KPX A Uacute -50 +KPX A Ucircumflex -50 +KPX A Udieresis -50 +KPX A Ugrave -50 +KPX A Uhungarumlaut -50 +KPX A Umacron -50 +KPX A Uogonek -50 +KPX A Uring -50 +KPX A V -105 +KPX A W -95 +KPX A Y -55 +KPX A Yacute -55 +KPX A Ydieresis -55 +KPX A quoteright -37 +KPX A u -20 +KPX A uacute -20 +KPX A ucircumflex -20 +KPX A udieresis -20 +KPX A ugrave -20 +KPX A uhungarumlaut -20 +KPX A umacron -20 +KPX A uogonek -20 +KPX A uring -20 +KPX A v -55 +KPX A w -55 +KPX A y -55 +KPX A yacute -55 +KPX A ydieresis -55 +KPX Aacute C -30 +KPX Aacute Cacute -30 +KPX Aacute Ccaron -30 +KPX Aacute Ccedilla -30 +KPX Aacute G -35 +KPX Aacute Gbreve -35 +KPX Aacute Gcommaaccent -35 +KPX Aacute O -40 +KPX Aacute Oacute -40 +KPX Aacute Ocircumflex -40 +KPX Aacute Odieresis -40 +KPX Aacute Ograve -40 +KPX Aacute Ohungarumlaut -40 +KPX Aacute Omacron -40 +KPX Aacute Oslash -40 +KPX Aacute Otilde -40 +KPX Aacute Q -40 +KPX Aacute T -37 +KPX Aacute Tcaron -37 +KPX Aacute Tcommaaccent -37 +KPX Aacute U -50 +KPX Aacute Uacute -50 +KPX Aacute Ucircumflex -50 +KPX Aacute Udieresis -50 +KPX Aacute Ugrave -50 +KPX Aacute Uhungarumlaut -50 +KPX Aacute Umacron -50 +KPX Aacute Uogonek -50 +KPX Aacute Uring -50 +KPX Aacute V -105 +KPX Aacute W -95 +KPX Aacute Y -55 +KPX Aacute Yacute -55 +KPX Aacute Ydieresis -55 +KPX Aacute quoteright -37 +KPX Aacute u -20 +KPX Aacute uacute -20 +KPX Aacute ucircumflex -20 +KPX Aacute udieresis -20 +KPX Aacute ugrave -20 +KPX Aacute uhungarumlaut -20 +KPX Aacute umacron -20 +KPX Aacute uogonek -20 +KPX Aacute uring -20 +KPX Aacute v -55 +KPX Aacute w -55 +KPX Aacute y -55 +KPX Aacute yacute -55 +KPX Aacute ydieresis -55 +KPX Abreve C -30 +KPX Abreve Cacute -30 +KPX Abreve Ccaron -30 +KPX Abreve Ccedilla -30 +KPX Abreve G -35 +KPX Abreve Gbreve -35 +KPX Abreve Gcommaaccent -35 +KPX Abreve O -40 +KPX Abreve Oacute -40 +KPX Abreve Ocircumflex -40 +KPX Abreve Odieresis -40 +KPX Abreve Ograve -40 +KPX Abreve Ohungarumlaut -40 +KPX Abreve Omacron -40 +KPX Abreve Oslash -40 +KPX Abreve Otilde -40 +KPX Abreve Q -40 +KPX Abreve T -37 +KPX Abreve Tcaron -37 +KPX Abreve Tcommaaccent -37 +KPX Abreve U -50 +KPX Abreve Uacute -50 +KPX Abreve Ucircumflex -50 +KPX Abreve Udieresis -50 +KPX Abreve Ugrave -50 +KPX Abreve Uhungarumlaut -50 +KPX Abreve Umacron -50 +KPX Abreve Uogonek -50 +KPX Abreve Uring -50 +KPX Abreve V -105 +KPX Abreve W -95 +KPX Abreve Y -55 +KPX Abreve Yacute -55 +KPX Abreve Ydieresis -55 +KPX Abreve quoteright -37 +KPX Abreve u -20 +KPX Abreve uacute -20 +KPX Abreve ucircumflex -20 +KPX Abreve udieresis -20 +KPX Abreve ugrave -20 +KPX Abreve uhungarumlaut -20 +KPX Abreve umacron -20 +KPX Abreve uogonek -20 +KPX Abreve uring -20 +KPX Abreve v -55 +KPX Abreve w -55 +KPX Abreve y -55 +KPX Abreve yacute -55 +KPX Abreve ydieresis -55 +KPX Acircumflex C -30 +KPX Acircumflex Cacute -30 +KPX Acircumflex Ccaron -30 +KPX Acircumflex Ccedilla -30 +KPX Acircumflex G -35 +KPX Acircumflex Gbreve -35 +KPX Acircumflex Gcommaaccent -35 +KPX Acircumflex O -40 +KPX Acircumflex Oacute -40 +KPX Acircumflex Ocircumflex -40 +KPX Acircumflex Odieresis -40 +KPX Acircumflex Ograve -40 +KPX Acircumflex Ohungarumlaut -40 +KPX Acircumflex Omacron -40 +KPX Acircumflex Oslash -40 +KPX Acircumflex Otilde -40 +KPX Acircumflex Q -40 +KPX Acircumflex T -37 +KPX Acircumflex Tcaron -37 +KPX Acircumflex Tcommaaccent -37 +KPX Acircumflex U -50 +KPX Acircumflex Uacute -50 +KPX Acircumflex Ucircumflex -50 +KPX Acircumflex Udieresis -50 +KPX Acircumflex Ugrave -50 +KPX Acircumflex Uhungarumlaut -50 +KPX Acircumflex Umacron -50 +KPX Acircumflex Uogonek -50 +KPX Acircumflex Uring -50 +KPX Acircumflex V -105 +KPX Acircumflex W -95 +KPX Acircumflex Y -55 +KPX Acircumflex Yacute -55 +KPX Acircumflex Ydieresis -55 +KPX Acircumflex quoteright -37 +KPX Acircumflex u -20 +KPX Acircumflex uacute -20 +KPX Acircumflex ucircumflex -20 +KPX Acircumflex udieresis -20 +KPX Acircumflex ugrave -20 +KPX Acircumflex uhungarumlaut -20 +KPX Acircumflex umacron -20 +KPX Acircumflex uogonek -20 +KPX Acircumflex uring -20 +KPX Acircumflex v -55 +KPX Acircumflex w -55 +KPX Acircumflex y -55 +KPX Acircumflex yacute -55 +KPX Acircumflex ydieresis -55 +KPX Adieresis C -30 +KPX Adieresis Cacute -30 +KPX Adieresis Ccaron -30 +KPX Adieresis Ccedilla -30 +KPX Adieresis G -35 +KPX Adieresis Gbreve -35 +KPX Adieresis Gcommaaccent -35 +KPX Adieresis O -40 +KPX Adieresis Oacute -40 +KPX Adieresis Ocircumflex -40 +KPX Adieresis Odieresis -40 +KPX Adieresis Ograve -40 +KPX Adieresis Ohungarumlaut -40 +KPX Adieresis Omacron -40 +KPX Adieresis Oslash -40 +KPX Adieresis Otilde -40 +KPX Adieresis Q -40 +KPX Adieresis T -37 +KPX Adieresis Tcaron -37 +KPX Adieresis Tcommaaccent -37 +KPX Adieresis U -50 +KPX Adieresis Uacute -50 +KPX Adieresis Ucircumflex -50 +KPX Adieresis Udieresis -50 +KPX Adieresis Ugrave -50 +KPX Adieresis Uhungarumlaut -50 +KPX Adieresis Umacron -50 +KPX Adieresis Uogonek -50 +KPX Adieresis Uring -50 +KPX Adieresis V -105 +KPX Adieresis W -95 +KPX Adieresis Y -55 +KPX Adieresis Yacute -55 +KPX Adieresis Ydieresis -55 +KPX Adieresis quoteright -37 +KPX Adieresis u -20 +KPX Adieresis uacute -20 +KPX Adieresis ucircumflex -20 +KPX Adieresis udieresis -20 +KPX Adieresis ugrave -20 +KPX Adieresis uhungarumlaut -20 +KPX Adieresis umacron -20 +KPX Adieresis uogonek -20 +KPX Adieresis uring -20 +KPX Adieresis v -55 +KPX Adieresis w -55 +KPX Adieresis y -55 +KPX Adieresis yacute -55 +KPX Adieresis ydieresis -55 +KPX Agrave C -30 +KPX Agrave Cacute -30 +KPX Agrave Ccaron -30 +KPX Agrave Ccedilla -30 +KPX Agrave G -35 +KPX Agrave Gbreve -35 +KPX Agrave Gcommaaccent -35 +KPX Agrave O -40 +KPX Agrave Oacute -40 +KPX Agrave Ocircumflex -40 +KPX Agrave Odieresis -40 +KPX Agrave Ograve -40 +KPX Agrave Ohungarumlaut -40 +KPX Agrave Omacron -40 +KPX Agrave Oslash -40 +KPX Agrave Otilde -40 +KPX Agrave Q -40 +KPX Agrave T -37 +KPX Agrave Tcaron -37 +KPX Agrave Tcommaaccent -37 +KPX Agrave U -50 +KPX Agrave Uacute -50 +KPX Agrave Ucircumflex -50 +KPX Agrave Udieresis -50 +KPX Agrave Ugrave -50 +KPX Agrave Uhungarumlaut -50 +KPX Agrave Umacron -50 +KPX Agrave Uogonek -50 +KPX Agrave Uring -50 +KPX Agrave V -105 +KPX Agrave W -95 +KPX Agrave Y -55 +KPX Agrave Yacute -55 +KPX Agrave Ydieresis -55 +KPX Agrave quoteright -37 +KPX Agrave u -20 +KPX Agrave uacute -20 +KPX Agrave ucircumflex -20 +KPX Agrave udieresis -20 +KPX Agrave ugrave -20 +KPX Agrave uhungarumlaut -20 +KPX Agrave umacron -20 +KPX Agrave uogonek -20 +KPX Agrave uring -20 +KPX Agrave v -55 +KPX Agrave w -55 +KPX Agrave y -55 +KPX Agrave yacute -55 +KPX Agrave ydieresis -55 +KPX Amacron C -30 +KPX Amacron Cacute -30 +KPX Amacron Ccaron -30 +KPX Amacron Ccedilla -30 +KPX Amacron G -35 +KPX Amacron Gbreve -35 +KPX Amacron Gcommaaccent -35 +KPX Amacron O -40 +KPX Amacron Oacute -40 +KPX Amacron Ocircumflex -40 +KPX Amacron Odieresis -40 +KPX Amacron Ograve -40 +KPX Amacron Ohungarumlaut -40 +KPX Amacron Omacron -40 +KPX Amacron Oslash -40 +KPX Amacron Otilde -40 +KPX Amacron Q -40 +KPX Amacron T -37 +KPX Amacron Tcaron -37 +KPX Amacron Tcommaaccent -37 +KPX Amacron U -50 +KPX Amacron Uacute -50 +KPX Amacron Ucircumflex -50 +KPX Amacron Udieresis -50 +KPX Amacron Ugrave -50 +KPX Amacron Uhungarumlaut -50 +KPX Amacron Umacron -50 +KPX Amacron Uogonek -50 +KPX Amacron Uring -50 +KPX Amacron V -105 +KPX Amacron W -95 +KPX Amacron Y -55 +KPX Amacron Yacute -55 +KPX Amacron Ydieresis -55 +KPX Amacron quoteright -37 +KPX Amacron u -20 +KPX Amacron uacute -20 +KPX Amacron ucircumflex -20 +KPX Amacron udieresis -20 +KPX Amacron ugrave -20 +KPX Amacron uhungarumlaut -20 +KPX Amacron umacron -20 +KPX Amacron uogonek -20 +KPX Amacron uring -20 +KPX Amacron v -55 +KPX Amacron w -55 +KPX Amacron y -55 +KPX Amacron yacute -55 +KPX Amacron ydieresis -55 +KPX Aogonek C -30 +KPX Aogonek Cacute -30 +KPX Aogonek Ccaron -30 +KPX Aogonek Ccedilla -30 +KPX Aogonek G -35 +KPX Aogonek Gbreve -35 +KPX Aogonek Gcommaaccent -35 +KPX Aogonek O -40 +KPX Aogonek Oacute -40 +KPX Aogonek Ocircumflex -40 +KPX Aogonek Odieresis -40 +KPX Aogonek Ograve -40 +KPX Aogonek Ohungarumlaut -40 +KPX Aogonek Omacron -40 +KPX Aogonek Oslash -40 +KPX Aogonek Otilde -40 +KPX Aogonek Q -40 +KPX Aogonek T -37 +KPX Aogonek Tcaron -37 +KPX Aogonek Tcommaaccent -37 +KPX Aogonek U -50 +KPX Aogonek Uacute -50 +KPX Aogonek Ucircumflex -50 +KPX Aogonek Udieresis -50 +KPX Aogonek Ugrave -50 +KPX Aogonek Uhungarumlaut -50 +KPX Aogonek Umacron -50 +KPX Aogonek Uogonek -50 +KPX Aogonek Uring -50 +KPX Aogonek V -105 +KPX Aogonek W -95 +KPX Aogonek Y -55 +KPX Aogonek Yacute -55 +KPX Aogonek Ydieresis -55 +KPX Aogonek quoteright -37 +KPX Aogonek u -20 +KPX Aogonek uacute -20 +KPX Aogonek ucircumflex -20 +KPX Aogonek udieresis -20 +KPX Aogonek ugrave -20 +KPX Aogonek uhungarumlaut -20 +KPX Aogonek umacron -20 +KPX Aogonek uogonek -20 +KPX Aogonek uring -20 +KPX Aogonek v -55 +KPX Aogonek w -55 +KPX Aogonek y -55 +KPX Aogonek yacute -55 +KPX Aogonek ydieresis -55 +KPX Aring C -30 +KPX Aring Cacute -30 +KPX Aring Ccaron -30 +KPX Aring Ccedilla -30 +KPX Aring G -35 +KPX Aring Gbreve -35 +KPX Aring Gcommaaccent -35 +KPX Aring O -40 +KPX Aring Oacute -40 +KPX Aring Ocircumflex -40 +KPX Aring Odieresis -40 +KPX Aring Ograve -40 +KPX Aring Ohungarumlaut -40 +KPX Aring Omacron -40 +KPX Aring Oslash -40 +KPX Aring Otilde -40 +KPX Aring Q -40 +KPX Aring T -37 +KPX Aring Tcaron -37 +KPX Aring Tcommaaccent -37 +KPX Aring U -50 +KPX Aring Uacute -50 +KPX Aring Ucircumflex -50 +KPX Aring Udieresis -50 +KPX Aring Ugrave -50 +KPX Aring Uhungarumlaut -50 +KPX Aring Umacron -50 +KPX Aring Uogonek -50 +KPX Aring Uring -50 +KPX Aring V -105 +KPX Aring W -95 +KPX Aring Y -55 +KPX Aring Yacute -55 +KPX Aring Ydieresis -55 +KPX Aring quoteright -37 +KPX Aring u -20 +KPX Aring uacute -20 +KPX Aring ucircumflex -20 +KPX Aring udieresis -20 +KPX Aring ugrave -20 +KPX Aring uhungarumlaut -20 +KPX Aring umacron -20 +KPX Aring uogonek -20 +KPX Aring uring -20 +KPX Aring v -55 +KPX Aring w -55 +KPX Aring y -55 +KPX Aring yacute -55 +KPX Aring ydieresis -55 +KPX Atilde C -30 +KPX Atilde Cacute -30 +KPX Atilde Ccaron -30 +KPX Atilde Ccedilla -30 +KPX Atilde G -35 +KPX Atilde Gbreve -35 +KPX Atilde Gcommaaccent -35 +KPX Atilde O -40 +KPX Atilde Oacute -40 +KPX Atilde Ocircumflex -40 +KPX Atilde Odieresis -40 +KPX Atilde Ograve -40 +KPX Atilde Ohungarumlaut -40 +KPX Atilde Omacron -40 +KPX Atilde Oslash -40 +KPX Atilde Otilde -40 +KPX Atilde Q -40 +KPX Atilde T -37 +KPX Atilde Tcaron -37 +KPX Atilde Tcommaaccent -37 +KPX Atilde U -50 +KPX Atilde Uacute -50 +KPX Atilde Ucircumflex -50 +KPX Atilde Udieresis -50 +KPX Atilde Ugrave -50 +KPX Atilde Uhungarumlaut -50 +KPX Atilde Umacron -50 +KPX Atilde Uogonek -50 +KPX Atilde Uring -50 +KPX Atilde V -105 +KPX Atilde W -95 +KPX Atilde Y -55 +KPX Atilde Yacute -55 +KPX Atilde Ydieresis -55 +KPX Atilde quoteright -37 +KPX Atilde u -20 +KPX Atilde uacute -20 +KPX Atilde ucircumflex -20 +KPX Atilde udieresis -20 +KPX Atilde ugrave -20 +KPX Atilde uhungarumlaut -20 +KPX Atilde umacron -20 +KPX Atilde uogonek -20 +KPX Atilde uring -20 +KPX Atilde v -55 +KPX Atilde w -55 +KPX Atilde y -55 +KPX Atilde yacute -55 +KPX Atilde ydieresis -55 +KPX B A -25 +KPX B Aacute -25 +KPX B Abreve -25 +KPX B Acircumflex -25 +KPX B Adieresis -25 +KPX B Agrave -25 +KPX B Amacron -25 +KPX B Aogonek -25 +KPX B Aring -25 +KPX B Atilde -25 +KPX B U -10 +KPX B Uacute -10 +KPX B Ucircumflex -10 +KPX B Udieresis -10 +KPX B Ugrave -10 +KPX B Uhungarumlaut -10 +KPX B Umacron -10 +KPX B Uogonek -10 +KPX B Uring -10 +KPX D A -35 +KPX D Aacute -35 +KPX D Abreve -35 +KPX D Acircumflex -35 +KPX D Adieresis -35 +KPX D Agrave -35 +KPX D Amacron -35 +KPX D Aogonek -35 +KPX D Aring -35 +KPX D Atilde -35 +KPX D V -40 +KPX D W -40 +KPX D Y -40 +KPX D Yacute -40 +KPX D Ydieresis -40 +KPX Dcaron A -35 +KPX Dcaron Aacute -35 +KPX Dcaron Abreve -35 +KPX Dcaron Acircumflex -35 +KPX Dcaron Adieresis -35 +KPX Dcaron Agrave -35 +KPX Dcaron Amacron -35 +KPX Dcaron Aogonek -35 +KPX Dcaron Aring -35 +KPX Dcaron Atilde -35 +KPX Dcaron V -40 +KPX Dcaron W -40 +KPX Dcaron Y -40 +KPX Dcaron Yacute -40 +KPX Dcaron Ydieresis -40 +KPX Dcroat A -35 +KPX Dcroat Aacute -35 +KPX Dcroat Abreve -35 +KPX Dcroat Acircumflex -35 +KPX Dcroat Adieresis -35 +KPX Dcroat Agrave -35 +KPX Dcroat Amacron -35 +KPX Dcroat Aogonek -35 +KPX Dcroat Aring -35 +KPX Dcroat Atilde -35 +KPX Dcroat V -40 +KPX Dcroat W -40 +KPX Dcroat Y -40 +KPX Dcroat Yacute -40 +KPX Dcroat Ydieresis -40 +KPX F A -115 +KPX F Aacute -115 +KPX F Abreve -115 +KPX F Acircumflex -115 +KPX F Adieresis -115 +KPX F Agrave -115 +KPX F Amacron -115 +KPX F Aogonek -115 +KPX F Aring -115 +KPX F Atilde -115 +KPX F a -75 +KPX F aacute -75 +KPX F abreve -75 +KPX F acircumflex -75 +KPX F adieresis -75 +KPX F agrave -75 +KPX F amacron -75 +KPX F aogonek -75 +KPX F aring -75 +KPX F atilde -75 +KPX F comma -135 +KPX F e -75 +KPX F eacute -75 +KPX F ecaron -75 +KPX F ecircumflex -75 +KPX F edieresis -75 +KPX F edotaccent -75 +KPX F egrave -75 +KPX F emacron -75 +KPX F eogonek -75 +KPX F i -45 +KPX F iacute -45 +KPX F icircumflex -45 +KPX F idieresis -45 +KPX F igrave -45 +KPX F imacron -45 +KPX F iogonek -45 +KPX F o -105 +KPX F oacute -105 +KPX F ocircumflex -105 +KPX F odieresis -105 +KPX F ograve -105 +KPX F ohungarumlaut -105 +KPX F omacron -105 +KPX F oslash -105 +KPX F otilde -105 +KPX F period -135 +KPX F r -55 +KPX F racute -55 +KPX F rcaron -55 +KPX F rcommaaccent -55 +KPX J A -40 +KPX J Aacute -40 +KPX J Abreve -40 +KPX J Acircumflex -40 +KPX J Adieresis -40 +KPX J Agrave -40 +KPX J Amacron -40 +KPX J Aogonek -40 +KPX J Aring -40 +KPX J Atilde -40 +KPX J a -35 +KPX J aacute -35 +KPX J abreve -35 +KPX J acircumflex -35 +KPX J adieresis -35 +KPX J agrave -35 +KPX J amacron -35 +KPX J aogonek -35 +KPX J aring -35 +KPX J atilde -35 +KPX J comma -25 +KPX J e -25 +KPX J eacute -25 +KPX J ecaron -25 +KPX J ecircumflex -25 +KPX J edieresis -25 +KPX J edotaccent -25 +KPX J egrave -25 +KPX J emacron -25 +KPX J eogonek -25 +KPX J o -25 +KPX J oacute -25 +KPX J ocircumflex -25 +KPX J odieresis -25 +KPX J ograve -25 +KPX J ohungarumlaut -25 +KPX J omacron -25 +KPX J oslash -25 +KPX J otilde -25 +KPX J period -25 +KPX J u -35 +KPX J uacute -35 +KPX J ucircumflex -35 +KPX J udieresis -35 +KPX J ugrave -35 +KPX J uhungarumlaut -35 +KPX J umacron -35 +KPX J uogonek -35 +KPX J uring -35 +KPX K O -50 +KPX K Oacute -50 +KPX K Ocircumflex -50 +KPX K Odieresis -50 +KPX K Ograve -50 +KPX K Ohungarumlaut -50 +KPX K Omacron -50 +KPX K Oslash -50 +KPX K Otilde -50 +KPX K e -35 +KPX K eacute -35 +KPX K ecaron -35 +KPX K ecircumflex -35 +KPX K edieresis -35 +KPX K edotaccent -35 +KPX K egrave -35 +KPX K emacron -35 +KPX K eogonek -35 +KPX K o -40 +KPX K oacute -40 +KPX K ocircumflex -40 +KPX K odieresis -40 +KPX K ograve -40 +KPX K ohungarumlaut -40 +KPX K omacron -40 +KPX K oslash -40 +KPX K otilde -40 +KPX K u -40 +KPX K uacute -40 +KPX K ucircumflex -40 +KPX K udieresis -40 +KPX K ugrave -40 +KPX K uhungarumlaut -40 +KPX K umacron -40 +KPX K uogonek -40 +KPX K uring -40 +KPX K y -40 +KPX K yacute -40 +KPX K ydieresis -40 +KPX Kcommaaccent O -50 +KPX Kcommaaccent Oacute -50 +KPX Kcommaaccent Ocircumflex -50 +KPX Kcommaaccent Odieresis -50 +KPX Kcommaaccent Ograve -50 +KPX Kcommaaccent Ohungarumlaut -50 +KPX Kcommaaccent Omacron -50 +KPX Kcommaaccent Oslash -50 +KPX Kcommaaccent Otilde -50 +KPX Kcommaaccent e -35 +KPX Kcommaaccent eacute -35 +KPX Kcommaaccent ecaron -35 +KPX Kcommaaccent ecircumflex -35 +KPX Kcommaaccent edieresis -35 +KPX Kcommaaccent edotaccent -35 +KPX Kcommaaccent egrave -35 +KPX Kcommaaccent emacron -35 +KPX Kcommaaccent eogonek -35 +KPX Kcommaaccent o -40 +KPX Kcommaaccent oacute -40 +KPX Kcommaaccent ocircumflex -40 +KPX Kcommaaccent odieresis -40 +KPX Kcommaaccent ograve -40 +KPX Kcommaaccent ohungarumlaut -40 +KPX Kcommaaccent omacron -40 +KPX Kcommaaccent oslash -40 +KPX Kcommaaccent otilde -40 +KPX Kcommaaccent u -40 +KPX Kcommaaccent uacute -40 +KPX Kcommaaccent ucircumflex -40 +KPX Kcommaaccent udieresis -40 +KPX Kcommaaccent ugrave -40 +KPX Kcommaaccent uhungarumlaut -40 +KPX Kcommaaccent umacron -40 +KPX Kcommaaccent uogonek -40 +KPX Kcommaaccent uring -40 +KPX Kcommaaccent y -40 +KPX Kcommaaccent yacute -40 +KPX Kcommaaccent ydieresis -40 +KPX L T -20 +KPX L Tcaron -20 +KPX L Tcommaaccent -20 +KPX L V -55 +KPX L W -55 +KPX L Y -20 +KPX L Yacute -20 +KPX L Ydieresis -20 +KPX L quoteright -37 +KPX L y -30 +KPX L yacute -30 +KPX L ydieresis -30 +KPX Lacute T -20 +KPX Lacute Tcaron -20 +KPX Lacute Tcommaaccent -20 +KPX Lacute V -55 +KPX Lacute W -55 +KPX Lacute Y -20 +KPX Lacute Yacute -20 +KPX Lacute Ydieresis -20 +KPX Lacute quoteright -37 +KPX Lacute y -30 +KPX Lacute yacute -30 +KPX Lacute ydieresis -30 +KPX Lcommaaccent T -20 +KPX Lcommaaccent Tcaron -20 +KPX Lcommaaccent Tcommaaccent -20 +KPX Lcommaaccent V -55 +KPX Lcommaaccent W -55 +KPX Lcommaaccent Y -20 +KPX Lcommaaccent Yacute -20 +KPX Lcommaaccent Ydieresis -20 +KPX Lcommaaccent quoteright -37 +KPX Lcommaaccent y -30 +KPX Lcommaaccent yacute -30 +KPX Lcommaaccent ydieresis -30 +KPX Lslash T -20 +KPX Lslash Tcaron -20 +KPX Lslash Tcommaaccent -20 +KPX Lslash V -55 +KPX Lslash W -55 +KPX Lslash Y -20 +KPX Lslash Yacute -20 +KPX Lslash Ydieresis -20 +KPX Lslash quoteright -37 +KPX Lslash y -30 +KPX Lslash yacute -30 +KPX Lslash ydieresis -30 +KPX N A -27 +KPX N Aacute -27 +KPX N Abreve -27 +KPX N Acircumflex -27 +KPX N Adieresis -27 +KPX N Agrave -27 +KPX N Amacron -27 +KPX N Aogonek -27 +KPX N Aring -27 +KPX N Atilde -27 +KPX Nacute A -27 +KPX Nacute Aacute -27 +KPX Nacute Abreve -27 +KPX Nacute Acircumflex -27 +KPX Nacute Adieresis -27 +KPX Nacute Agrave -27 +KPX Nacute Amacron -27 +KPX Nacute Aogonek -27 +KPX Nacute Aring -27 +KPX Nacute Atilde -27 +KPX Ncaron A -27 +KPX Ncaron Aacute -27 +KPX Ncaron Abreve -27 +KPX Ncaron Acircumflex -27 +KPX Ncaron Adieresis -27 +KPX Ncaron Agrave -27 +KPX Ncaron Amacron -27 +KPX Ncaron Aogonek -27 +KPX Ncaron Aring -27 +KPX Ncaron Atilde -27 +KPX Ncommaaccent A -27 +KPX Ncommaaccent Aacute -27 +KPX Ncommaaccent Abreve -27 +KPX Ncommaaccent Acircumflex -27 +KPX Ncommaaccent Adieresis -27 +KPX Ncommaaccent Agrave -27 +KPX Ncommaaccent Amacron -27 +KPX Ncommaaccent Aogonek -27 +KPX Ncommaaccent Aring -27 +KPX Ncommaaccent Atilde -27 +KPX Ntilde A -27 +KPX Ntilde Aacute -27 +KPX Ntilde Abreve -27 +KPX Ntilde Acircumflex -27 +KPX Ntilde Adieresis -27 +KPX Ntilde Agrave -27 +KPX Ntilde Amacron -27 +KPX Ntilde Aogonek -27 +KPX Ntilde Aring -27 +KPX Ntilde Atilde -27 +KPX O A -55 +KPX O Aacute -55 +KPX O Abreve -55 +KPX O Acircumflex -55 +KPX O Adieresis -55 +KPX O Agrave -55 +KPX O Amacron -55 +KPX O Aogonek -55 +KPX O Aring -55 +KPX O Atilde -55 +KPX O T -40 +KPX O Tcaron -40 +KPX O Tcommaaccent -40 +KPX O V -50 +KPX O W -50 +KPX O X -40 +KPX O Y -50 +KPX O Yacute -50 +KPX O Ydieresis -50 +KPX Oacute A -55 +KPX Oacute Aacute -55 +KPX Oacute Abreve -55 +KPX Oacute Acircumflex -55 +KPX Oacute Adieresis -55 +KPX Oacute Agrave -55 +KPX Oacute Amacron -55 +KPX Oacute Aogonek -55 +KPX Oacute Aring -55 +KPX Oacute Atilde -55 +KPX Oacute T -40 +KPX Oacute Tcaron -40 +KPX Oacute Tcommaaccent -40 +KPX Oacute V -50 +KPX Oacute W -50 +KPX Oacute X -40 +KPX Oacute Y -50 +KPX Oacute Yacute -50 +KPX Oacute Ydieresis -50 +KPX Ocircumflex A -55 +KPX Ocircumflex Aacute -55 +KPX Ocircumflex Abreve -55 +KPX Ocircumflex Acircumflex -55 +KPX Ocircumflex Adieresis -55 +KPX Ocircumflex Agrave -55 +KPX Ocircumflex Amacron -55 +KPX Ocircumflex Aogonek -55 +KPX Ocircumflex Aring -55 +KPX Ocircumflex Atilde -55 +KPX Ocircumflex T -40 +KPX Ocircumflex Tcaron -40 +KPX Ocircumflex Tcommaaccent -40 +KPX Ocircumflex V -50 +KPX Ocircumflex W -50 +KPX Ocircumflex X -40 +KPX Ocircumflex Y -50 +KPX Ocircumflex Yacute -50 +KPX Ocircumflex Ydieresis -50 +KPX Odieresis A -55 +KPX Odieresis Aacute -55 +KPX Odieresis Abreve -55 +KPX Odieresis Acircumflex -55 +KPX Odieresis Adieresis -55 +KPX Odieresis Agrave -55 +KPX Odieresis Amacron -55 +KPX Odieresis Aogonek -55 +KPX Odieresis Aring -55 +KPX Odieresis Atilde -55 +KPX Odieresis T -40 +KPX Odieresis Tcaron -40 +KPX Odieresis Tcommaaccent -40 +KPX Odieresis V -50 +KPX Odieresis W -50 +KPX Odieresis X -40 +KPX Odieresis Y -50 +KPX Odieresis Yacute -50 +KPX Odieresis Ydieresis -50 +KPX Ograve A -55 +KPX Ograve Aacute -55 +KPX Ograve Abreve -55 +KPX Ograve Acircumflex -55 +KPX Ograve Adieresis -55 +KPX Ograve Agrave -55 +KPX Ograve Amacron -55 +KPX Ograve Aogonek -55 +KPX Ograve Aring -55 +KPX Ograve Atilde -55 +KPX Ograve T -40 +KPX Ograve Tcaron -40 +KPX Ograve Tcommaaccent -40 +KPX Ograve V -50 +KPX Ograve W -50 +KPX Ograve X -40 +KPX Ograve Y -50 +KPX Ograve Yacute -50 +KPX Ograve Ydieresis -50 +KPX Ohungarumlaut A -55 +KPX Ohungarumlaut Aacute -55 +KPX Ohungarumlaut Abreve -55 +KPX Ohungarumlaut Acircumflex -55 +KPX Ohungarumlaut Adieresis -55 +KPX Ohungarumlaut Agrave -55 +KPX Ohungarumlaut Amacron -55 +KPX Ohungarumlaut Aogonek -55 +KPX Ohungarumlaut Aring -55 +KPX Ohungarumlaut Atilde -55 +KPX Ohungarumlaut T -40 +KPX Ohungarumlaut Tcaron -40 +KPX Ohungarumlaut Tcommaaccent -40 +KPX Ohungarumlaut V -50 +KPX Ohungarumlaut W -50 +KPX Ohungarumlaut X -40 +KPX Ohungarumlaut Y -50 +KPX Ohungarumlaut Yacute -50 +KPX Ohungarumlaut Ydieresis -50 +KPX Omacron A -55 +KPX Omacron Aacute -55 +KPX Omacron Abreve -55 +KPX Omacron Acircumflex -55 +KPX Omacron Adieresis -55 +KPX Omacron Agrave -55 +KPX Omacron Amacron -55 +KPX Omacron Aogonek -55 +KPX Omacron Aring -55 +KPX Omacron Atilde -55 +KPX Omacron T -40 +KPX Omacron Tcaron -40 +KPX Omacron Tcommaaccent -40 +KPX Omacron V -50 +KPX Omacron W -50 +KPX Omacron X -40 +KPX Omacron Y -50 +KPX Omacron Yacute -50 +KPX Omacron Ydieresis -50 +KPX Oslash A -55 +KPX Oslash Aacute -55 +KPX Oslash Abreve -55 +KPX Oslash Acircumflex -55 +KPX Oslash Adieresis -55 +KPX Oslash Agrave -55 +KPX Oslash Amacron -55 +KPX Oslash Aogonek -55 +KPX Oslash Aring -55 +KPX Oslash Atilde -55 +KPX Oslash T -40 +KPX Oslash Tcaron -40 +KPX Oslash Tcommaaccent -40 +KPX Oslash V -50 +KPX Oslash W -50 +KPX Oslash X -40 +KPX Oslash Y -50 +KPX Oslash Yacute -50 +KPX Oslash Ydieresis -50 +KPX Otilde A -55 +KPX Otilde Aacute -55 +KPX Otilde Abreve -55 +KPX Otilde Acircumflex -55 +KPX Otilde Adieresis -55 +KPX Otilde Agrave -55 +KPX Otilde Amacron -55 +KPX Otilde Aogonek -55 +KPX Otilde Aring -55 +KPX Otilde Atilde -55 +KPX Otilde T -40 +KPX Otilde Tcaron -40 +KPX Otilde Tcommaaccent -40 +KPX Otilde V -50 +KPX Otilde W -50 +KPX Otilde X -40 +KPX Otilde Y -50 +KPX Otilde Yacute -50 +KPX Otilde Ydieresis -50 +KPX P A -90 +KPX P Aacute -90 +KPX P Abreve -90 +KPX P Acircumflex -90 +KPX P Adieresis -90 +KPX P Agrave -90 +KPX P Amacron -90 +KPX P Aogonek -90 +KPX P Aring -90 +KPX P Atilde -90 +KPX P a -80 +KPX P aacute -80 +KPX P abreve -80 +KPX P acircumflex -80 +KPX P adieresis -80 +KPX P agrave -80 +KPX P amacron -80 +KPX P aogonek -80 +KPX P aring -80 +KPX P atilde -80 +KPX P comma -135 +KPX P e -80 +KPX P eacute -80 +KPX P ecaron -80 +KPX P ecircumflex -80 +KPX P edieresis -80 +KPX P edotaccent -80 +KPX P egrave -80 +KPX P emacron -80 +KPX P eogonek -80 +KPX P o -80 +KPX P oacute -80 +KPX P ocircumflex -80 +KPX P odieresis -80 +KPX P ograve -80 +KPX P ohungarumlaut -80 +KPX P omacron -80 +KPX P oslash -80 +KPX P otilde -80 +KPX P period -135 +KPX Q U -10 +KPX Q Uacute -10 +KPX Q Ucircumflex -10 +KPX Q Udieresis -10 +KPX Q Ugrave -10 +KPX Q Uhungarumlaut -10 +KPX Q Umacron -10 +KPX Q Uogonek -10 +KPX Q Uring -10 +KPX R O -40 +KPX R Oacute -40 +KPX R Ocircumflex -40 +KPX R Odieresis -40 +KPX R Ograve -40 +KPX R Ohungarumlaut -40 +KPX R Omacron -40 +KPX R Oslash -40 +KPX R Otilde -40 +KPX R U -40 +KPX R Uacute -40 +KPX R Ucircumflex -40 +KPX R Udieresis -40 +KPX R Ugrave -40 +KPX R Uhungarumlaut -40 +KPX R Umacron -40 +KPX R Uogonek -40 +KPX R Uring -40 +KPX R V -18 +KPX R W -18 +KPX R Y -18 +KPX R Yacute -18 +KPX R Ydieresis -18 +KPX Racute O -40 +KPX Racute Oacute -40 +KPX Racute Ocircumflex -40 +KPX Racute Odieresis -40 +KPX Racute Ograve -40 +KPX Racute Ohungarumlaut -40 +KPX Racute Omacron -40 +KPX Racute Oslash -40 +KPX Racute Otilde -40 +KPX Racute U -40 +KPX Racute Uacute -40 +KPX Racute Ucircumflex -40 +KPX Racute Udieresis -40 +KPX Racute Ugrave -40 +KPX Racute Uhungarumlaut -40 +KPX Racute Umacron -40 +KPX Racute Uogonek -40 +KPX Racute Uring -40 +KPX Racute V -18 +KPX Racute W -18 +KPX Racute Y -18 +KPX Racute Yacute -18 +KPX Racute Ydieresis -18 +KPX Rcaron O -40 +KPX Rcaron Oacute -40 +KPX Rcaron Ocircumflex -40 +KPX Rcaron Odieresis -40 +KPX Rcaron Ograve -40 +KPX Rcaron Ohungarumlaut -40 +KPX Rcaron Omacron -40 +KPX Rcaron Oslash -40 +KPX Rcaron Otilde -40 +KPX Rcaron U -40 +KPX Rcaron Uacute -40 +KPX Rcaron Ucircumflex -40 +KPX Rcaron Udieresis -40 +KPX Rcaron Ugrave -40 +KPX Rcaron Uhungarumlaut -40 +KPX Rcaron Umacron -40 +KPX Rcaron Uogonek -40 +KPX Rcaron Uring -40 +KPX Rcaron V -18 +KPX Rcaron W -18 +KPX Rcaron Y -18 +KPX Rcaron Yacute -18 +KPX Rcaron Ydieresis -18 +KPX Rcommaaccent O -40 +KPX Rcommaaccent Oacute -40 +KPX Rcommaaccent Ocircumflex -40 +KPX Rcommaaccent Odieresis -40 +KPX Rcommaaccent Ograve -40 +KPX Rcommaaccent Ohungarumlaut -40 +KPX Rcommaaccent Omacron -40 +KPX Rcommaaccent Oslash -40 +KPX Rcommaaccent Otilde -40 +KPX Rcommaaccent U -40 +KPX Rcommaaccent Uacute -40 +KPX Rcommaaccent Ucircumflex -40 +KPX Rcommaaccent Udieresis -40 +KPX Rcommaaccent Ugrave -40 +KPX Rcommaaccent Uhungarumlaut -40 +KPX Rcommaaccent Umacron -40 +KPX Rcommaaccent Uogonek -40 +KPX Rcommaaccent Uring -40 +KPX Rcommaaccent V -18 +KPX Rcommaaccent W -18 +KPX Rcommaaccent Y -18 +KPX Rcommaaccent Yacute -18 +KPX Rcommaaccent Ydieresis -18 +KPX T A -50 +KPX T Aacute -50 +KPX T Abreve -50 +KPX T Acircumflex -50 +KPX T Adieresis -50 +KPX T Agrave -50 +KPX T Amacron -50 +KPX T Aogonek -50 +KPX T Aring -50 +KPX T Atilde -50 +KPX T O -18 +KPX T Oacute -18 +KPX T Ocircumflex -18 +KPX T Odieresis -18 +KPX T Ograve -18 +KPX T Ohungarumlaut -18 +KPX T Omacron -18 +KPX T Oslash -18 +KPX T Otilde -18 +KPX T a -92 +KPX T aacute -92 +KPX T abreve -92 +KPX T acircumflex -92 +KPX T adieresis -92 +KPX T agrave -92 +KPX T amacron -92 +KPX T aogonek -92 +KPX T aring -92 +KPX T atilde -92 +KPX T colon -55 +KPX T comma -74 +KPX T e -92 +KPX T eacute -92 +KPX T ecaron -92 +KPX T ecircumflex -52 +KPX T edieresis -52 +KPX T edotaccent -92 +KPX T egrave -52 +KPX T emacron -52 +KPX T eogonek -92 +KPX T hyphen -74 +KPX T i -55 +KPX T iacute -55 +KPX T iogonek -55 +KPX T o -92 +KPX T oacute -92 +KPX T ocircumflex -92 +KPX T odieresis -92 +KPX T ograve -92 +KPX T ohungarumlaut -92 +KPX T omacron -92 +KPX T oslash -92 +KPX T otilde -92 +KPX T period -74 +KPX T r -55 +KPX T racute -55 +KPX T rcaron -55 +KPX T rcommaaccent -55 +KPX T semicolon -65 +KPX T u -55 +KPX T uacute -55 +KPX T ucircumflex -55 +KPX T udieresis -55 +KPX T ugrave -55 +KPX T uhungarumlaut -55 +KPX T umacron -55 +KPX T uogonek -55 +KPX T uring -55 +KPX T w -74 +KPX T y -74 +KPX T yacute -74 +KPX T ydieresis -34 +KPX Tcaron A -50 +KPX Tcaron Aacute -50 +KPX Tcaron Abreve -50 +KPX Tcaron Acircumflex -50 +KPX Tcaron Adieresis -50 +KPX Tcaron Agrave -50 +KPX Tcaron Amacron -50 +KPX Tcaron Aogonek -50 +KPX Tcaron Aring -50 +KPX Tcaron Atilde -50 +KPX Tcaron O -18 +KPX Tcaron Oacute -18 +KPX Tcaron Ocircumflex -18 +KPX Tcaron Odieresis -18 +KPX Tcaron Ograve -18 +KPX Tcaron Ohungarumlaut -18 +KPX Tcaron Omacron -18 +KPX Tcaron Oslash -18 +KPX Tcaron Otilde -18 +KPX Tcaron a -92 +KPX Tcaron aacute -92 +KPX Tcaron abreve -92 +KPX Tcaron acircumflex -92 +KPX Tcaron adieresis -92 +KPX Tcaron agrave -92 +KPX Tcaron amacron -92 +KPX Tcaron aogonek -92 +KPX Tcaron aring -92 +KPX Tcaron atilde -92 +KPX Tcaron colon -55 +KPX Tcaron comma -74 +KPX Tcaron e -92 +KPX Tcaron eacute -92 +KPX Tcaron ecaron -92 +KPX Tcaron ecircumflex -52 +KPX Tcaron edieresis -52 +KPX Tcaron edotaccent -92 +KPX Tcaron egrave -52 +KPX Tcaron emacron -52 +KPX Tcaron eogonek -92 +KPX Tcaron hyphen -74 +KPX Tcaron i -55 +KPX Tcaron iacute -55 +KPX Tcaron iogonek -55 +KPX Tcaron o -92 +KPX Tcaron oacute -92 +KPX Tcaron ocircumflex -92 +KPX Tcaron odieresis -92 +KPX Tcaron ograve -92 +KPX Tcaron ohungarumlaut -92 +KPX Tcaron omacron -92 +KPX Tcaron oslash -92 +KPX Tcaron otilde -92 +KPX Tcaron period -74 +KPX Tcaron r -55 +KPX Tcaron racute -55 +KPX Tcaron rcaron -55 +KPX Tcaron rcommaaccent -55 +KPX Tcaron semicolon -65 +KPX Tcaron u -55 +KPX Tcaron uacute -55 +KPX Tcaron ucircumflex -55 +KPX Tcaron udieresis -55 +KPX Tcaron ugrave -55 +KPX Tcaron uhungarumlaut -55 +KPX Tcaron umacron -55 +KPX Tcaron uogonek -55 +KPX Tcaron uring -55 +KPX Tcaron w -74 +KPX Tcaron y -74 +KPX Tcaron yacute -74 +KPX Tcaron ydieresis -34 +KPX Tcommaaccent A -50 +KPX Tcommaaccent Aacute -50 +KPX Tcommaaccent Abreve -50 +KPX Tcommaaccent Acircumflex -50 +KPX Tcommaaccent Adieresis -50 +KPX Tcommaaccent Agrave -50 +KPX Tcommaaccent Amacron -50 +KPX Tcommaaccent Aogonek -50 +KPX Tcommaaccent Aring -50 +KPX Tcommaaccent Atilde -50 +KPX Tcommaaccent O -18 +KPX Tcommaaccent Oacute -18 +KPX Tcommaaccent Ocircumflex -18 +KPX Tcommaaccent Odieresis -18 +KPX Tcommaaccent Ograve -18 +KPX Tcommaaccent Ohungarumlaut -18 +KPX Tcommaaccent Omacron -18 +KPX Tcommaaccent Oslash -18 +KPX Tcommaaccent Otilde -18 +KPX Tcommaaccent a -92 +KPX Tcommaaccent aacute -92 +KPX Tcommaaccent abreve -92 +KPX Tcommaaccent acircumflex -92 +KPX Tcommaaccent adieresis -92 +KPX Tcommaaccent agrave -92 +KPX Tcommaaccent amacron -92 +KPX Tcommaaccent aogonek -92 +KPX Tcommaaccent aring -92 +KPX Tcommaaccent atilde -92 +KPX Tcommaaccent colon -55 +KPX Tcommaaccent comma -74 +KPX Tcommaaccent e -92 +KPX Tcommaaccent eacute -92 +KPX Tcommaaccent ecaron -92 +KPX Tcommaaccent ecircumflex -52 +KPX Tcommaaccent edieresis -52 +KPX Tcommaaccent edotaccent -92 +KPX Tcommaaccent egrave -52 +KPX Tcommaaccent emacron -52 +KPX Tcommaaccent eogonek -92 +KPX Tcommaaccent hyphen -74 +KPX Tcommaaccent i -55 +KPX Tcommaaccent iacute -55 +KPX Tcommaaccent iogonek -55 +KPX Tcommaaccent o -92 +KPX Tcommaaccent oacute -92 +KPX Tcommaaccent ocircumflex -92 +KPX Tcommaaccent odieresis -92 +KPX Tcommaaccent ograve -92 +KPX Tcommaaccent ohungarumlaut -92 +KPX Tcommaaccent omacron -92 +KPX Tcommaaccent oslash -92 +KPX Tcommaaccent otilde -92 +KPX Tcommaaccent period -74 +KPX Tcommaaccent r -55 +KPX Tcommaaccent racute -55 +KPX Tcommaaccent rcaron -55 +KPX Tcommaaccent rcommaaccent -55 +KPX Tcommaaccent semicolon -65 +KPX Tcommaaccent u -55 +KPX Tcommaaccent uacute -55 +KPX Tcommaaccent ucircumflex -55 +KPX Tcommaaccent udieresis -55 +KPX Tcommaaccent ugrave -55 +KPX Tcommaaccent uhungarumlaut -55 +KPX Tcommaaccent umacron -55 +KPX Tcommaaccent uogonek -55 +KPX Tcommaaccent uring -55 +KPX Tcommaaccent w -74 +KPX Tcommaaccent y -74 +KPX Tcommaaccent yacute -74 +KPX Tcommaaccent ydieresis -34 +KPX U A -40 +KPX U Aacute -40 +KPX U Abreve -40 +KPX U Acircumflex -40 +KPX U Adieresis -40 +KPX U Agrave -40 +KPX U Amacron -40 +KPX U Aogonek -40 +KPX U Aring -40 +KPX U Atilde -40 +KPX U comma -25 +KPX U period -25 +KPX Uacute A -40 +KPX Uacute Aacute -40 +KPX Uacute Abreve -40 +KPX Uacute Acircumflex -40 +KPX Uacute Adieresis -40 +KPX Uacute Agrave -40 +KPX Uacute Amacron -40 +KPX Uacute Aogonek -40 +KPX Uacute Aring -40 +KPX Uacute Atilde -40 +KPX Uacute comma -25 +KPX Uacute period -25 +KPX Ucircumflex A -40 +KPX Ucircumflex Aacute -40 +KPX Ucircumflex Abreve -40 +KPX Ucircumflex Acircumflex -40 +KPX Ucircumflex Adieresis -40 +KPX Ucircumflex Agrave -40 +KPX Ucircumflex Amacron -40 +KPX Ucircumflex Aogonek -40 +KPX Ucircumflex Aring -40 +KPX Ucircumflex Atilde -40 +KPX Ucircumflex comma -25 +KPX Ucircumflex period -25 +KPX Udieresis A -40 +KPX Udieresis Aacute -40 +KPX Udieresis Abreve -40 +KPX Udieresis Acircumflex -40 +KPX Udieresis Adieresis -40 +KPX Udieresis Agrave -40 +KPX Udieresis Amacron -40 +KPX Udieresis Aogonek -40 +KPX Udieresis Aring -40 +KPX Udieresis Atilde -40 +KPX Udieresis comma -25 +KPX Udieresis period -25 +KPX Ugrave A -40 +KPX Ugrave Aacute -40 +KPX Ugrave Abreve -40 +KPX Ugrave Acircumflex -40 +KPX Ugrave Adieresis -40 +KPX Ugrave Agrave -40 +KPX Ugrave Amacron -40 +KPX Ugrave Aogonek -40 +KPX Ugrave Aring -40 +KPX Ugrave Atilde -40 +KPX Ugrave comma -25 +KPX Ugrave period -25 +KPX Uhungarumlaut A -40 +KPX Uhungarumlaut Aacute -40 +KPX Uhungarumlaut Abreve -40 +KPX Uhungarumlaut Acircumflex -40 +KPX Uhungarumlaut Adieresis -40 +KPX Uhungarumlaut Agrave -40 +KPX Uhungarumlaut Amacron -40 +KPX Uhungarumlaut Aogonek -40 +KPX Uhungarumlaut Aring -40 +KPX Uhungarumlaut Atilde -40 +KPX Uhungarumlaut comma -25 +KPX Uhungarumlaut period -25 +KPX Umacron A -40 +KPX Umacron Aacute -40 +KPX Umacron Abreve -40 +KPX Umacron Acircumflex -40 +KPX Umacron Adieresis -40 +KPX Umacron Agrave -40 +KPX Umacron Amacron -40 +KPX Umacron Aogonek -40 +KPX Umacron Aring -40 +KPX Umacron Atilde -40 +KPX Umacron comma -25 +KPX Umacron period -25 +KPX Uogonek A -40 +KPX Uogonek Aacute -40 +KPX Uogonek Abreve -40 +KPX Uogonek Acircumflex -40 +KPX Uogonek Adieresis -40 +KPX Uogonek Agrave -40 +KPX Uogonek Amacron -40 +KPX Uogonek Aogonek -40 +KPX Uogonek Aring -40 +KPX Uogonek Atilde -40 +KPX Uogonek comma -25 +KPX Uogonek period -25 +KPX Uring A -40 +KPX Uring Aacute -40 +KPX Uring Abreve -40 +KPX Uring Acircumflex -40 +KPX Uring Adieresis -40 +KPX Uring Agrave -40 +KPX Uring Amacron -40 +KPX Uring Aogonek -40 +KPX Uring Aring -40 +KPX Uring Atilde -40 +KPX Uring comma -25 +KPX Uring period -25 +KPX V A -60 +KPX V Aacute -60 +KPX V Abreve -60 +KPX V Acircumflex -60 +KPX V Adieresis -60 +KPX V Agrave -60 +KPX V Amacron -60 +KPX V Aogonek -60 +KPX V Aring -60 +KPX V Atilde -60 +KPX V O -30 +KPX V Oacute -30 +KPX V Ocircumflex -30 +KPX V Odieresis -30 +KPX V Ograve -30 +KPX V Ohungarumlaut -30 +KPX V Omacron -30 +KPX V Oslash -30 +KPX V Otilde -30 +KPX V a -111 +KPX V aacute -111 +KPX V abreve -111 +KPX V acircumflex -111 +KPX V adieresis -111 +KPX V agrave -111 +KPX V amacron -111 +KPX V aogonek -111 +KPX V aring -111 +KPX V atilde -111 +KPX V colon -65 +KPX V comma -129 +KPX V e -111 +KPX V eacute -111 +KPX V ecaron -111 +KPX V ecircumflex -111 +KPX V edieresis -71 +KPX V edotaccent -111 +KPX V egrave -71 +KPX V emacron -71 +KPX V eogonek -111 +KPX V hyphen -55 +KPX V i -74 +KPX V iacute -74 +KPX V icircumflex -34 +KPX V idieresis -34 +KPX V igrave -34 +KPX V imacron -34 +KPX V iogonek -74 +KPX V o -111 +KPX V oacute -111 +KPX V ocircumflex -111 +KPX V odieresis -111 +KPX V ograve -111 +KPX V ohungarumlaut -111 +KPX V omacron -111 +KPX V oslash -111 +KPX V otilde -111 +KPX V period -129 +KPX V semicolon -74 +KPX V u -74 +KPX V uacute -74 +KPX V ucircumflex -74 +KPX V udieresis -74 +KPX V ugrave -74 +KPX V uhungarumlaut -74 +KPX V umacron -74 +KPX V uogonek -74 +KPX V uring -74 +KPX W A -60 +KPX W Aacute -60 +KPX W Abreve -60 +KPX W Acircumflex -60 +KPX W Adieresis -60 +KPX W Agrave -60 +KPX W Amacron -60 +KPX W Aogonek -60 +KPX W Aring -60 +KPX W Atilde -60 +KPX W O -25 +KPX W Oacute -25 +KPX W Ocircumflex -25 +KPX W Odieresis -25 +KPX W Ograve -25 +KPX W Ohungarumlaut -25 +KPX W Omacron -25 +KPX W Oslash -25 +KPX W Otilde -25 +KPX W a -92 +KPX W aacute -92 +KPX W abreve -92 +KPX W acircumflex -92 +KPX W adieresis -92 +KPX W agrave -92 +KPX W amacron -92 +KPX W aogonek -92 +KPX W aring -92 +KPX W atilde -92 +KPX W colon -65 +KPX W comma -92 +KPX W e -92 +KPX W eacute -92 +KPX W ecaron -92 +KPX W ecircumflex -92 +KPX W edieresis -52 +KPX W edotaccent -92 +KPX W egrave -52 +KPX W emacron -52 +KPX W eogonek -92 +KPX W hyphen -37 +KPX W i -55 +KPX W iacute -55 +KPX W iogonek -55 +KPX W o -92 +KPX W oacute -92 +KPX W ocircumflex -92 +KPX W odieresis -92 +KPX W ograve -92 +KPX W ohungarumlaut -92 +KPX W omacron -92 +KPX W oslash -92 +KPX W otilde -92 +KPX W period -92 +KPX W semicolon -65 +KPX W u -55 +KPX W uacute -55 +KPX W ucircumflex -55 +KPX W udieresis -55 +KPX W ugrave -55 +KPX W uhungarumlaut -55 +KPX W umacron -55 +KPX W uogonek -55 +KPX W uring -55 +KPX W y -70 +KPX W yacute -70 +KPX W ydieresis -70 +KPX Y A -50 +KPX Y Aacute -50 +KPX Y Abreve -50 +KPX Y Acircumflex -50 +KPX Y Adieresis -50 +KPX Y Agrave -50 +KPX Y Amacron -50 +KPX Y Aogonek -50 +KPX Y Aring -50 +KPX Y Atilde -50 +KPX Y O -15 +KPX Y Oacute -15 +KPX Y Ocircumflex -15 +KPX Y Odieresis -15 +KPX Y Ograve -15 +KPX Y Ohungarumlaut -15 +KPX Y Omacron -15 +KPX Y Oslash -15 +KPX Y Otilde -15 +KPX Y a -92 +KPX Y aacute -92 +KPX Y abreve -92 +KPX Y acircumflex -92 +KPX Y adieresis -92 +KPX Y agrave -92 +KPX Y amacron -92 +KPX Y aogonek -92 +KPX Y aring -92 +KPX Y atilde -92 +KPX Y colon -65 +KPX Y comma -92 +KPX Y e -92 +KPX Y eacute -92 +KPX Y ecaron -92 +KPX Y ecircumflex -92 +KPX Y edieresis -52 +KPX Y edotaccent -92 +KPX Y egrave -52 +KPX Y emacron -52 +KPX Y eogonek -92 +KPX Y hyphen -74 +KPX Y i -74 +KPX Y iacute -74 +KPX Y icircumflex -34 +KPX Y idieresis -34 +KPX Y igrave -34 +KPX Y imacron -34 +KPX Y iogonek -74 +KPX Y o -92 +KPX Y oacute -92 +KPX Y ocircumflex -92 +KPX Y odieresis -92 +KPX Y ograve -92 +KPX Y ohungarumlaut -92 +KPX Y omacron -92 +KPX Y oslash -92 +KPX Y otilde -92 +KPX Y period -92 +KPX Y semicolon -65 +KPX Y u -92 +KPX Y uacute -92 +KPX Y ucircumflex -92 +KPX Y udieresis -92 +KPX Y ugrave -92 +KPX Y uhungarumlaut -92 +KPX Y umacron -92 +KPX Y uogonek -92 +KPX Y uring -92 +KPX Yacute A -50 +KPX Yacute Aacute -50 +KPX Yacute Abreve -50 +KPX Yacute Acircumflex -50 +KPX Yacute Adieresis -50 +KPX Yacute Agrave -50 +KPX Yacute Amacron -50 +KPX Yacute Aogonek -50 +KPX Yacute Aring -50 +KPX Yacute Atilde -50 +KPX Yacute O -15 +KPX Yacute Oacute -15 +KPX Yacute Ocircumflex -15 +KPX Yacute Odieresis -15 +KPX Yacute Ograve -15 +KPX Yacute Ohungarumlaut -15 +KPX Yacute Omacron -15 +KPX Yacute Oslash -15 +KPX Yacute Otilde -15 +KPX Yacute a -92 +KPX Yacute aacute -92 +KPX Yacute abreve -92 +KPX Yacute acircumflex -92 +KPX Yacute adieresis -92 +KPX Yacute agrave -92 +KPX Yacute amacron -92 +KPX Yacute aogonek -92 +KPX Yacute aring -92 +KPX Yacute atilde -92 +KPX Yacute colon -65 +KPX Yacute comma -92 +KPX Yacute e -92 +KPX Yacute eacute -92 +KPX Yacute ecaron -92 +KPX Yacute ecircumflex -92 +KPX Yacute edieresis -52 +KPX Yacute edotaccent -92 +KPX Yacute egrave -52 +KPX Yacute emacron -52 +KPX Yacute eogonek -92 +KPX Yacute hyphen -74 +KPX Yacute i -74 +KPX Yacute iacute -74 +KPX Yacute icircumflex -34 +KPX Yacute idieresis -34 +KPX Yacute igrave -34 +KPX Yacute imacron -34 +KPX Yacute iogonek -74 +KPX Yacute o -92 +KPX Yacute oacute -92 +KPX Yacute ocircumflex -92 +KPX Yacute odieresis -92 +KPX Yacute ograve -92 +KPX Yacute ohungarumlaut -92 +KPX Yacute omacron -92 +KPX Yacute oslash -92 +KPX Yacute otilde -92 +KPX Yacute period -92 +KPX Yacute semicolon -65 +KPX Yacute u -92 +KPX Yacute uacute -92 +KPX Yacute ucircumflex -92 +KPX Yacute udieresis -92 +KPX Yacute ugrave -92 +KPX Yacute uhungarumlaut -92 +KPX Yacute umacron -92 +KPX Yacute uogonek -92 +KPX Yacute uring -92 +KPX Ydieresis A -50 +KPX Ydieresis Aacute -50 +KPX Ydieresis Abreve -50 +KPX Ydieresis Acircumflex -50 +KPX Ydieresis Adieresis -50 +KPX Ydieresis Agrave -50 +KPX Ydieresis Amacron -50 +KPX Ydieresis Aogonek -50 +KPX Ydieresis Aring -50 +KPX Ydieresis Atilde -50 +KPX Ydieresis O -15 +KPX Ydieresis Oacute -15 +KPX Ydieresis Ocircumflex -15 +KPX Ydieresis Odieresis -15 +KPX Ydieresis Ograve -15 +KPX Ydieresis Ohungarumlaut -15 +KPX Ydieresis Omacron -15 +KPX Ydieresis Oslash -15 +KPX Ydieresis Otilde -15 +KPX Ydieresis a -92 +KPX Ydieresis aacute -92 +KPX Ydieresis abreve -92 +KPX Ydieresis acircumflex -92 +KPX Ydieresis adieresis -92 +KPX Ydieresis agrave -92 +KPX Ydieresis amacron -92 +KPX Ydieresis aogonek -92 +KPX Ydieresis aring -92 +KPX Ydieresis atilde -92 +KPX Ydieresis colon -65 +KPX Ydieresis comma -92 +KPX Ydieresis e -92 +KPX Ydieresis eacute -92 +KPX Ydieresis ecaron -92 +KPX Ydieresis ecircumflex -92 +KPX Ydieresis edieresis -52 +KPX Ydieresis edotaccent -92 +KPX Ydieresis egrave -52 +KPX Ydieresis emacron -52 +KPX Ydieresis eogonek -92 +KPX Ydieresis hyphen -74 +KPX Ydieresis i -74 +KPX Ydieresis iacute -74 +KPX Ydieresis icircumflex -34 +KPX Ydieresis idieresis -34 +KPX Ydieresis igrave -34 +KPX Ydieresis imacron -34 +KPX Ydieresis iogonek -74 +KPX Ydieresis o -92 +KPX Ydieresis oacute -92 +KPX Ydieresis ocircumflex -92 +KPX Ydieresis odieresis -92 +KPX Ydieresis ograve -92 +KPX Ydieresis ohungarumlaut -92 +KPX Ydieresis omacron -92 +KPX Ydieresis oslash -92 +KPX Ydieresis otilde -92 +KPX Ydieresis period -92 +KPX Ydieresis semicolon -65 +KPX Ydieresis u -92 +KPX Ydieresis uacute -92 +KPX Ydieresis ucircumflex -92 +KPX Ydieresis udieresis -92 +KPX Ydieresis ugrave -92 +KPX Ydieresis uhungarumlaut -92 +KPX Ydieresis umacron -92 +KPX Ydieresis uogonek -92 +KPX Ydieresis uring -92 +KPX a g -10 +KPX a gbreve -10 +KPX a gcommaaccent -10 +KPX aacute g -10 +KPX aacute gbreve -10 +KPX aacute gcommaaccent -10 +KPX abreve g -10 +KPX abreve gbreve -10 +KPX abreve gcommaaccent -10 +KPX acircumflex g -10 +KPX acircumflex gbreve -10 +KPX acircumflex gcommaaccent -10 +KPX adieresis g -10 +KPX adieresis gbreve -10 +KPX adieresis gcommaaccent -10 +KPX agrave g -10 +KPX agrave gbreve -10 +KPX agrave gcommaaccent -10 +KPX amacron g -10 +KPX amacron gbreve -10 +KPX amacron gcommaaccent -10 +KPX aogonek g -10 +KPX aogonek gbreve -10 +KPX aogonek gcommaaccent -10 +KPX aring g -10 +KPX aring gbreve -10 +KPX aring gcommaaccent -10 +KPX atilde g -10 +KPX atilde gbreve -10 +KPX atilde gcommaaccent -10 +KPX b period -40 +KPX b u -20 +KPX b uacute -20 +KPX b ucircumflex -20 +KPX b udieresis -20 +KPX b ugrave -20 +KPX b uhungarumlaut -20 +KPX b umacron -20 +KPX b uogonek -20 +KPX b uring -20 +KPX c h -15 +KPX c k -20 +KPX c kcommaaccent -20 +KPX cacute h -15 +KPX cacute k -20 +KPX cacute kcommaaccent -20 +KPX ccaron h -15 +KPX ccaron k -20 +KPX ccaron kcommaaccent -20 +KPX ccedilla h -15 +KPX ccedilla k -20 +KPX ccedilla kcommaaccent -20 +KPX comma quotedblright -140 +KPX comma quoteright -140 +KPX e comma -10 +KPX e g -40 +KPX e gbreve -40 +KPX e gcommaaccent -40 +KPX e period -15 +KPX e v -15 +KPX e w -15 +KPX e x -20 +KPX e y -30 +KPX e yacute -30 +KPX e ydieresis -30 +KPX eacute comma -10 +KPX eacute g -40 +KPX eacute gbreve -40 +KPX eacute gcommaaccent -40 +KPX eacute period -15 +KPX eacute v -15 +KPX eacute w -15 +KPX eacute x -20 +KPX eacute y -30 +KPX eacute yacute -30 +KPX eacute ydieresis -30 +KPX ecaron comma -10 +KPX ecaron g -40 +KPX ecaron gbreve -40 +KPX ecaron gcommaaccent -40 +KPX ecaron period -15 +KPX ecaron v -15 +KPX ecaron w -15 +KPX ecaron x -20 +KPX ecaron y -30 +KPX ecaron yacute -30 +KPX ecaron ydieresis -30 +KPX ecircumflex comma -10 +KPX ecircumflex g -40 +KPX ecircumflex gbreve -40 +KPX ecircumflex gcommaaccent -40 +KPX ecircumflex period -15 +KPX ecircumflex v -15 +KPX ecircumflex w -15 +KPX ecircumflex x -20 +KPX ecircumflex y -30 +KPX ecircumflex yacute -30 +KPX ecircumflex ydieresis -30 +KPX edieresis comma -10 +KPX edieresis g -40 +KPX edieresis gbreve -40 +KPX edieresis gcommaaccent -40 +KPX edieresis period -15 +KPX edieresis v -15 +KPX edieresis w -15 +KPX edieresis x -20 +KPX edieresis y -30 +KPX edieresis yacute -30 +KPX edieresis ydieresis -30 +KPX edotaccent comma -10 +KPX edotaccent g -40 +KPX edotaccent gbreve -40 +KPX edotaccent gcommaaccent -40 +KPX edotaccent period -15 +KPX edotaccent v -15 +KPX edotaccent w -15 +KPX edotaccent x -20 +KPX edotaccent y -30 +KPX edotaccent yacute -30 +KPX edotaccent ydieresis -30 +KPX egrave comma -10 +KPX egrave g -40 +KPX egrave gbreve -40 +KPX egrave gcommaaccent -40 +KPX egrave period -15 +KPX egrave v -15 +KPX egrave w -15 +KPX egrave x -20 +KPX egrave y -30 +KPX egrave yacute -30 +KPX egrave ydieresis -30 +KPX emacron comma -10 +KPX emacron g -40 +KPX emacron gbreve -40 +KPX emacron gcommaaccent -40 +KPX emacron period -15 +KPX emacron v -15 +KPX emacron w -15 +KPX emacron x -20 +KPX emacron y -30 +KPX emacron yacute -30 +KPX emacron ydieresis -30 +KPX eogonek comma -10 +KPX eogonek g -40 +KPX eogonek gbreve -40 +KPX eogonek gcommaaccent -40 +KPX eogonek period -15 +KPX eogonek v -15 +KPX eogonek w -15 +KPX eogonek x -20 +KPX eogonek y -30 +KPX eogonek yacute -30 +KPX eogonek ydieresis -30 +KPX f comma -10 +KPX f dotlessi -60 +KPX f f -18 +KPX f i -20 +KPX f iogonek -20 +KPX f period -15 +KPX f quoteright 92 +KPX g comma -10 +KPX g e -10 +KPX g eacute -10 +KPX g ecaron -10 +KPX g ecircumflex -10 +KPX g edieresis -10 +KPX g edotaccent -10 +KPX g egrave -10 +KPX g emacron -10 +KPX g eogonek -10 +KPX g g -10 +KPX g gbreve -10 +KPX g gcommaaccent -10 +KPX g period -15 +KPX gbreve comma -10 +KPX gbreve e -10 +KPX gbreve eacute -10 +KPX gbreve ecaron -10 +KPX gbreve ecircumflex -10 +KPX gbreve edieresis -10 +KPX gbreve edotaccent -10 +KPX gbreve egrave -10 +KPX gbreve emacron -10 +KPX gbreve eogonek -10 +KPX gbreve g -10 +KPX gbreve gbreve -10 +KPX gbreve gcommaaccent -10 +KPX gbreve period -15 +KPX gcommaaccent comma -10 +KPX gcommaaccent e -10 +KPX gcommaaccent eacute -10 +KPX gcommaaccent ecaron -10 +KPX gcommaaccent ecircumflex -10 +KPX gcommaaccent edieresis -10 +KPX gcommaaccent edotaccent -10 +KPX gcommaaccent egrave -10 +KPX gcommaaccent emacron -10 +KPX gcommaaccent eogonek -10 +KPX gcommaaccent g -10 +KPX gcommaaccent gbreve -10 +KPX gcommaaccent gcommaaccent -10 +KPX gcommaaccent period -15 +KPX k e -10 +KPX k eacute -10 +KPX k ecaron -10 +KPX k ecircumflex -10 +KPX k edieresis -10 +KPX k edotaccent -10 +KPX k egrave -10 +KPX k emacron -10 +KPX k eogonek -10 +KPX k o -10 +KPX k oacute -10 +KPX k ocircumflex -10 +KPX k odieresis -10 +KPX k ograve -10 +KPX k ohungarumlaut -10 +KPX k omacron -10 +KPX k oslash -10 +KPX k otilde -10 +KPX k y -10 +KPX k yacute -10 +KPX k ydieresis -10 +KPX kcommaaccent e -10 +KPX kcommaaccent eacute -10 +KPX kcommaaccent ecaron -10 +KPX kcommaaccent ecircumflex -10 +KPX kcommaaccent edieresis -10 +KPX kcommaaccent edotaccent -10 +KPX kcommaaccent egrave -10 +KPX kcommaaccent emacron -10 +KPX kcommaaccent eogonek -10 +KPX kcommaaccent o -10 +KPX kcommaaccent oacute -10 +KPX kcommaaccent ocircumflex -10 +KPX kcommaaccent odieresis -10 +KPX kcommaaccent ograve -10 +KPX kcommaaccent ohungarumlaut -10 +KPX kcommaaccent omacron -10 +KPX kcommaaccent oslash -10 +KPX kcommaaccent otilde -10 +KPX kcommaaccent y -10 +KPX kcommaaccent yacute -10 +KPX kcommaaccent ydieresis -10 +KPX n v -40 +KPX nacute v -40 +KPX ncaron v -40 +KPX ncommaaccent v -40 +KPX ntilde v -40 +KPX o g -10 +KPX o gbreve -10 +KPX o gcommaaccent -10 +KPX o v -10 +KPX oacute g -10 +KPX oacute gbreve -10 +KPX oacute gcommaaccent -10 +KPX oacute v -10 +KPX ocircumflex g -10 +KPX ocircumflex gbreve -10 +KPX ocircumflex gcommaaccent -10 +KPX ocircumflex v -10 +KPX odieresis g -10 +KPX odieresis gbreve -10 +KPX odieresis gcommaaccent -10 +KPX odieresis v -10 +KPX ograve g -10 +KPX ograve gbreve -10 +KPX ograve gcommaaccent -10 +KPX ograve v -10 +KPX ohungarumlaut g -10 +KPX ohungarumlaut gbreve -10 +KPX ohungarumlaut gcommaaccent -10 +KPX ohungarumlaut v -10 +KPX omacron g -10 +KPX omacron gbreve -10 +KPX omacron gcommaaccent -10 +KPX omacron v -10 +KPX oslash g -10 +KPX oslash gbreve -10 +KPX oslash gcommaaccent -10 +KPX oslash v -10 +KPX otilde g -10 +KPX otilde gbreve -10 +KPX otilde gcommaaccent -10 +KPX otilde v -10 +KPX period quotedblright -140 +KPX period quoteright -140 +KPX quoteleft quoteleft -111 +KPX quoteright d -25 +KPX quoteright dcroat -25 +KPX quoteright quoteright -111 +KPX quoteright r -25 +KPX quoteright racute -25 +KPX quoteright rcaron -25 +KPX quoteright rcommaaccent -25 +KPX quoteright s -40 +KPX quoteright sacute -40 +KPX quoteright scaron -40 +KPX quoteright scedilla -40 +KPX quoteright scommaaccent -40 +KPX quoteright space -111 +KPX quoteright t -30 +KPX quoteright tcommaaccent -30 +KPX quoteright v -10 +KPX r a -15 +KPX r aacute -15 +KPX r abreve -15 +KPX r acircumflex -15 +KPX r adieresis -15 +KPX r agrave -15 +KPX r amacron -15 +KPX r aogonek -15 +KPX r aring -15 +KPX r atilde -15 +KPX r c -37 +KPX r cacute -37 +KPX r ccaron -37 +KPX r ccedilla -37 +KPX r comma -111 +KPX r d -37 +KPX r dcroat -37 +KPX r e -37 +KPX r eacute -37 +KPX r ecaron -37 +KPX r ecircumflex -37 +KPX r edieresis -37 +KPX r edotaccent -37 +KPX r egrave -37 +KPX r emacron -37 +KPX r eogonek -37 +KPX r g -37 +KPX r gbreve -37 +KPX r gcommaaccent -37 +KPX r hyphen -20 +KPX r o -45 +KPX r oacute -45 +KPX r ocircumflex -45 +KPX r odieresis -45 +KPX r ograve -45 +KPX r ohungarumlaut -45 +KPX r omacron -45 +KPX r oslash -45 +KPX r otilde -45 +KPX r period -111 +KPX r q -37 +KPX r s -10 +KPX r sacute -10 +KPX r scaron -10 +KPX r scedilla -10 +KPX r scommaaccent -10 +KPX racute a -15 +KPX racute aacute -15 +KPX racute abreve -15 +KPX racute acircumflex -15 +KPX racute adieresis -15 +KPX racute agrave -15 +KPX racute amacron -15 +KPX racute aogonek -15 +KPX racute aring -15 +KPX racute atilde -15 +KPX racute c -37 +KPX racute cacute -37 +KPX racute ccaron -37 +KPX racute ccedilla -37 +KPX racute comma -111 +KPX racute d -37 +KPX racute dcroat -37 +KPX racute e -37 +KPX racute eacute -37 +KPX racute ecaron -37 +KPX racute ecircumflex -37 +KPX racute edieresis -37 +KPX racute edotaccent -37 +KPX racute egrave -37 +KPX racute emacron -37 +KPX racute eogonek -37 +KPX racute g -37 +KPX racute gbreve -37 +KPX racute gcommaaccent -37 +KPX racute hyphen -20 +KPX racute o -45 +KPX racute oacute -45 +KPX racute ocircumflex -45 +KPX racute odieresis -45 +KPX racute ograve -45 +KPX racute ohungarumlaut -45 +KPX racute omacron -45 +KPX racute oslash -45 +KPX racute otilde -45 +KPX racute period -111 +KPX racute q -37 +KPX racute s -10 +KPX racute sacute -10 +KPX racute scaron -10 +KPX racute scedilla -10 +KPX racute scommaaccent -10 +KPX rcaron a -15 +KPX rcaron aacute -15 +KPX rcaron abreve -15 +KPX rcaron acircumflex -15 +KPX rcaron adieresis -15 +KPX rcaron agrave -15 +KPX rcaron amacron -15 +KPX rcaron aogonek -15 +KPX rcaron aring -15 +KPX rcaron atilde -15 +KPX rcaron c -37 +KPX rcaron cacute -37 +KPX rcaron ccaron -37 +KPX rcaron ccedilla -37 +KPX rcaron comma -111 +KPX rcaron d -37 +KPX rcaron dcroat -37 +KPX rcaron e -37 +KPX rcaron eacute -37 +KPX rcaron ecaron -37 +KPX rcaron ecircumflex -37 +KPX rcaron edieresis -37 +KPX rcaron edotaccent -37 +KPX rcaron egrave -37 +KPX rcaron emacron -37 +KPX rcaron eogonek -37 +KPX rcaron g -37 +KPX rcaron gbreve -37 +KPX rcaron gcommaaccent -37 +KPX rcaron hyphen -20 +KPX rcaron o -45 +KPX rcaron oacute -45 +KPX rcaron ocircumflex -45 +KPX rcaron odieresis -45 +KPX rcaron ograve -45 +KPX rcaron ohungarumlaut -45 +KPX rcaron omacron -45 +KPX rcaron oslash -45 +KPX rcaron otilde -45 +KPX rcaron period -111 +KPX rcaron q -37 +KPX rcaron s -10 +KPX rcaron sacute -10 +KPX rcaron scaron -10 +KPX rcaron scedilla -10 +KPX rcaron scommaaccent -10 +KPX rcommaaccent a -15 +KPX rcommaaccent aacute -15 +KPX rcommaaccent abreve -15 +KPX rcommaaccent acircumflex -15 +KPX rcommaaccent adieresis -15 +KPX rcommaaccent agrave -15 +KPX rcommaaccent amacron -15 +KPX rcommaaccent aogonek -15 +KPX rcommaaccent aring -15 +KPX rcommaaccent atilde -15 +KPX rcommaaccent c -37 +KPX rcommaaccent cacute -37 +KPX rcommaaccent ccaron -37 +KPX rcommaaccent ccedilla -37 +KPX rcommaaccent comma -111 +KPX rcommaaccent d -37 +KPX rcommaaccent dcroat -37 +KPX rcommaaccent e -37 +KPX rcommaaccent eacute -37 +KPX rcommaaccent ecaron -37 +KPX rcommaaccent ecircumflex -37 +KPX rcommaaccent edieresis -37 +KPX rcommaaccent edotaccent -37 +KPX rcommaaccent egrave -37 +KPX rcommaaccent emacron -37 +KPX rcommaaccent eogonek -37 +KPX rcommaaccent g -37 +KPX rcommaaccent gbreve -37 +KPX rcommaaccent gcommaaccent -37 +KPX rcommaaccent hyphen -20 +KPX rcommaaccent o -45 +KPX rcommaaccent oacute -45 +KPX rcommaaccent ocircumflex -45 +KPX rcommaaccent odieresis -45 +KPX rcommaaccent ograve -45 +KPX rcommaaccent ohungarumlaut -45 +KPX rcommaaccent omacron -45 +KPX rcommaaccent oslash -45 +KPX rcommaaccent otilde -45 +KPX rcommaaccent period -111 +KPX rcommaaccent q -37 +KPX rcommaaccent s -10 +KPX rcommaaccent sacute -10 +KPX rcommaaccent scaron -10 +KPX rcommaaccent scedilla -10 +KPX rcommaaccent scommaaccent -10 +KPX space A -18 +KPX space Aacute -18 +KPX space Abreve -18 +KPX space Acircumflex -18 +KPX space Adieresis -18 +KPX space Agrave -18 +KPX space Amacron -18 +KPX space Aogonek -18 +KPX space Aring -18 +KPX space Atilde -18 +KPX space T -18 +KPX space Tcaron -18 +KPX space Tcommaaccent -18 +KPX space V -35 +KPX space W -40 +KPX space Y -75 +KPX space Yacute -75 +KPX space Ydieresis -75 +KPX v comma -74 +KPX v period -74 +KPX w comma -74 +KPX w period -74 +KPX y comma -55 +KPX y period -55 +KPX yacute comma -55 +KPX yacute period -55 +KPX ydieresis comma -55 +KPX ydieresis period -55 +EndKernPairs +EndKernData +EndFontMetrics diff --git a/internal/corefont/Core14_AFMs/Times-Roman.afm b/internal/corefont/Core14_AFMs/Times-Roman.afm new file mode 100644 index 0000000000000000000000000000000000000000..a0953f2802cbde4ca95c212f21195b55c0bc5a42 --- /dev/null +++ b/internal/corefont/Core14_AFMs/Times-Roman.afm @@ -0,0 +1,2419 @@ +StartFontMetrics 4.1 +Comment Copyright (c) 1985, 1987, 1989, 1990, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved. +Comment Creation Date: Thu May 1 12:49:17 1997 +Comment UniqueID 43068 +Comment VMusage 43909 54934 +FontName Times-Roman +FullName Times Roman +FamilyName Times +Weight Roman +ItalicAngle 0 +IsFixedPitch false +CharacterSet ExtendedRoman +FontBBox -168 -218 1000 898 +UnderlinePosition -100 +UnderlineThickness 50 +Version 002.000 +Notice Copyright (c) 1985, 1987, 1989, 1990, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved.Times is a trademark of Linotype-Hell AG and/or its subsidiaries. +EncodingScheme AdobeStandardEncoding +CapHeight 662 +XHeight 450 +Ascender 683 +Descender -217 +StdHW 28 +StdVW 84 +StartCharMetrics 315 +C 32 ; WX 250 ; N space ; B 0 0 0 0 ; +C 33 ; WX 333 ; N exclam ; B 130 -9 238 676 ; +C 34 ; WX 408 ; N quotedbl ; B 77 431 331 676 ; +C 35 ; WX 500 ; N numbersign ; B 5 0 496 662 ; +C 36 ; WX 500 ; N dollar ; B 44 -87 457 727 ; +C 37 ; WX 833 ; N percent ; B 61 -13 772 676 ; +C 38 ; WX 778 ; N ampersand ; B 42 -13 750 676 ; +C 39 ; WX 333 ; N quoteright ; B 79 433 218 676 ; +C 40 ; WX 333 ; N parenleft ; B 48 -177 304 676 ; +C 41 ; WX 333 ; N parenright ; B 29 -177 285 676 ; +C 42 ; WX 500 ; N asterisk ; B 69 265 432 676 ; +C 43 ; WX 564 ; N plus ; B 30 0 534 506 ; +C 44 ; WX 250 ; N comma ; B 56 -141 195 102 ; +C 45 ; WX 333 ; N hyphen ; B 39 194 285 257 ; +C 46 ; WX 250 ; N period ; B 70 -11 181 100 ; +C 47 ; WX 278 ; N slash ; B -9 -14 287 676 ; +C 48 ; WX 500 ; N zero ; B 24 -14 476 676 ; +C 49 ; WX 500 ; N one ; B 111 0 394 676 ; +C 50 ; WX 500 ; N two ; B 30 0 475 676 ; +C 51 ; WX 500 ; N three ; B 43 -14 431 676 ; +C 52 ; WX 500 ; N four ; B 12 0 472 676 ; +C 53 ; WX 500 ; N five ; B 32 -14 438 688 ; +C 54 ; WX 500 ; N six ; B 34 -14 468 684 ; +C 55 ; WX 500 ; N seven ; B 20 -8 449 662 ; +C 56 ; WX 500 ; N eight ; B 56 -14 445 676 ; +C 57 ; WX 500 ; N nine ; B 30 -22 459 676 ; +C 58 ; WX 278 ; N colon ; B 81 -11 192 459 ; +C 59 ; WX 278 ; N semicolon ; B 80 -141 219 459 ; +C 60 ; WX 564 ; N less ; B 28 -8 536 514 ; +C 61 ; WX 564 ; N equal ; B 30 120 534 386 ; +C 62 ; WX 564 ; N greater ; B 28 -8 536 514 ; +C 63 ; WX 444 ; N question ; B 68 -8 414 676 ; +C 64 ; WX 921 ; N at ; B 116 -14 809 676 ; +C 65 ; WX 722 ; N A ; B 15 0 706 674 ; +C 66 ; WX 667 ; N B ; B 17 0 593 662 ; +C 67 ; WX 667 ; N C ; B 28 -14 633 676 ; +C 68 ; WX 722 ; N D ; B 16 0 685 662 ; +C 69 ; WX 611 ; N E ; B 12 0 597 662 ; +C 70 ; WX 556 ; N F ; B 12 0 546 662 ; +C 71 ; WX 722 ; N G ; B 32 -14 709 676 ; +C 72 ; WX 722 ; N H ; B 19 0 702 662 ; +C 73 ; WX 333 ; N I ; B 18 0 315 662 ; +C 74 ; WX 389 ; N J ; B 10 -14 370 662 ; +C 75 ; WX 722 ; N K ; B 34 0 723 662 ; +C 76 ; WX 611 ; N L ; B 12 0 598 662 ; +C 77 ; WX 889 ; N M ; B 12 0 863 662 ; +C 78 ; WX 722 ; N N ; B 12 -11 707 662 ; +C 79 ; WX 722 ; N O ; B 34 -14 688 676 ; +C 80 ; WX 556 ; N P ; B 16 0 542 662 ; +C 81 ; WX 722 ; N Q ; B 34 -178 701 676 ; +C 82 ; WX 667 ; N R ; B 17 0 659 662 ; +C 83 ; WX 556 ; N S ; B 42 -14 491 676 ; +C 84 ; WX 611 ; N T ; B 17 0 593 662 ; +C 85 ; WX 722 ; N U ; B 14 -14 705 662 ; +C 86 ; WX 722 ; N V ; B 16 -11 697 662 ; +C 87 ; WX 944 ; N W ; B 5 -11 932 662 ; +C 88 ; WX 722 ; N X ; B 10 0 704 662 ; +C 89 ; WX 722 ; N Y ; B 22 0 703 662 ; +C 90 ; WX 611 ; N Z ; B 9 0 597 662 ; +C 91 ; WX 333 ; N bracketleft ; B 88 -156 299 662 ; +C 92 ; WX 278 ; N backslash ; B -9 -14 287 676 ; +C 93 ; WX 333 ; N bracketright ; B 34 -156 245 662 ; +C 94 ; WX 469 ; N asciicircum ; B 24 297 446 662 ; +C 95 ; WX 500 ; N underscore ; B 0 -125 500 -75 ; +C 96 ; WX 333 ; N quoteleft ; B 115 433 254 676 ; +C 97 ; WX 444 ; N a ; B 37 -10 442 460 ; +C 98 ; WX 500 ; N b ; B 3 -10 468 683 ; +C 99 ; WX 444 ; N c ; B 25 -10 412 460 ; +C 100 ; WX 500 ; N d ; B 27 -10 491 683 ; +C 101 ; WX 444 ; N e ; B 25 -10 424 460 ; +C 102 ; WX 333 ; N f ; B 20 0 383 683 ; L i fi ; L l fl ; +C 103 ; WX 500 ; N g ; B 28 -218 470 460 ; +C 104 ; WX 500 ; N h ; B 9 0 487 683 ; +C 105 ; WX 278 ; N i ; B 16 0 253 683 ; +C 106 ; WX 278 ; N j ; B -70 -218 194 683 ; +C 107 ; WX 500 ; N k ; B 7 0 505 683 ; +C 108 ; WX 278 ; N l ; B 19 0 257 683 ; +C 109 ; WX 778 ; N m ; B 16 0 775 460 ; +C 110 ; WX 500 ; N n ; B 16 0 485 460 ; +C 111 ; WX 500 ; N o ; B 29 -10 470 460 ; +C 112 ; WX 500 ; N p ; B 5 -217 470 460 ; +C 113 ; WX 500 ; N q ; B 24 -217 488 460 ; +C 114 ; WX 333 ; N r ; B 5 0 335 460 ; +C 115 ; WX 389 ; N s ; B 51 -10 348 460 ; +C 116 ; WX 278 ; N t ; B 13 -10 279 579 ; +C 117 ; WX 500 ; N u ; B 9 -10 479 450 ; +C 118 ; WX 500 ; N v ; B 19 -14 477 450 ; +C 119 ; WX 722 ; N w ; B 21 -14 694 450 ; +C 120 ; WX 500 ; N x ; B 17 0 479 450 ; +C 121 ; WX 500 ; N y ; B 14 -218 475 450 ; +C 122 ; WX 444 ; N z ; B 27 0 418 450 ; +C 123 ; WX 480 ; N braceleft ; B 100 -181 350 680 ; +C 124 ; WX 200 ; N bar ; B 67 -218 133 782 ; +C 125 ; WX 480 ; N braceright ; B 130 -181 380 680 ; +C 126 ; WX 541 ; N asciitilde ; B 40 183 502 323 ; +C 161 ; WX 333 ; N exclamdown ; B 97 -218 205 467 ; +C 162 ; WX 500 ; N cent ; B 53 -138 448 579 ; +C 163 ; WX 500 ; N sterling ; B 12 -8 490 676 ; +C 164 ; WX 167 ; N fraction ; B -168 -14 331 676 ; +C 165 ; WX 500 ; N yen ; B -53 0 512 662 ; +C 166 ; WX 500 ; N florin ; B 7 -189 490 676 ; +C 167 ; WX 500 ; N section ; B 70 -148 426 676 ; +C 168 ; WX 500 ; N currency ; B -22 58 522 602 ; +C 169 ; WX 180 ; N quotesingle ; B 48 431 133 676 ; +C 170 ; WX 444 ; N quotedblleft ; B 43 433 414 676 ; +C 171 ; WX 500 ; N guillemotleft ; B 42 33 456 416 ; +C 172 ; WX 333 ; N guilsinglleft ; B 63 33 285 416 ; +C 173 ; WX 333 ; N guilsinglright ; B 48 33 270 416 ; +C 174 ; WX 556 ; N fi ; B 31 0 521 683 ; +C 175 ; WX 556 ; N fl ; B 32 0 521 683 ; +C 177 ; WX 500 ; N endash ; B 0 201 500 250 ; +C 178 ; WX 500 ; N dagger ; B 59 -149 442 676 ; +C 179 ; WX 500 ; N daggerdbl ; B 58 -153 442 676 ; +C 180 ; WX 250 ; N periodcentered ; B 70 199 181 310 ; +C 182 ; WX 453 ; N paragraph ; B -22 -154 450 662 ; +C 183 ; WX 350 ; N bullet ; B 40 196 310 466 ; +C 184 ; WX 333 ; N quotesinglbase ; B 79 -141 218 102 ; +C 185 ; WX 444 ; N quotedblbase ; B 45 -141 416 102 ; +C 186 ; WX 444 ; N quotedblright ; B 30 433 401 676 ; +C 187 ; WX 500 ; N guillemotright ; B 44 33 458 416 ; +C 188 ; WX 1000 ; N ellipsis ; B 111 -11 888 100 ; +C 189 ; WX 1000 ; N perthousand ; B 7 -19 994 706 ; +C 191 ; WX 444 ; N questiondown ; B 30 -218 376 466 ; +C 193 ; WX 333 ; N grave ; B 19 507 242 678 ; +C 194 ; WX 333 ; N acute ; B 93 507 317 678 ; +C 195 ; WX 333 ; N circumflex ; B 11 507 322 674 ; +C 196 ; WX 333 ; N tilde ; B 1 532 331 638 ; +C 197 ; WX 333 ; N macron ; B 11 547 322 601 ; +C 198 ; WX 333 ; N breve ; B 26 507 307 664 ; +C 199 ; WX 333 ; N dotaccent ; B 118 581 216 681 ; +C 200 ; WX 333 ; N dieresis ; B 18 581 315 681 ; +C 202 ; WX 333 ; N ring ; B 67 512 266 711 ; +C 203 ; WX 333 ; N cedilla ; B 52 -215 261 0 ; +C 205 ; WX 333 ; N hungarumlaut ; B -3 507 377 678 ; +C 206 ; WX 333 ; N ogonek ; B 62 -165 243 0 ; +C 207 ; WX 333 ; N caron ; B 11 507 322 674 ; +C 208 ; WX 1000 ; N emdash ; B 0 201 1000 250 ; +C 225 ; WX 889 ; N AE ; B 0 0 863 662 ; +C 227 ; WX 276 ; N ordfeminine ; B 4 394 270 676 ; +C 232 ; WX 611 ; N Lslash ; B 12 0 598 662 ; +C 233 ; WX 722 ; N Oslash ; B 34 -80 688 734 ; +C 234 ; WX 889 ; N OE ; B 30 -6 885 668 ; +C 235 ; WX 310 ; N ordmasculine ; B 6 394 304 676 ; +C 241 ; WX 667 ; N ae ; B 38 -10 632 460 ; +C 245 ; WX 278 ; N dotlessi ; B 16 0 253 460 ; +C 248 ; WX 278 ; N lslash ; B 19 0 259 683 ; +C 249 ; WX 500 ; N oslash ; B 29 -112 470 551 ; +C 250 ; WX 722 ; N oe ; B 30 -10 690 460 ; +C 251 ; WX 500 ; N germandbls ; B 12 -9 468 683 ; +C -1 ; WX 333 ; N Idieresis ; B 18 0 315 835 ; +C -1 ; WX 444 ; N eacute ; B 25 -10 424 678 ; +C -1 ; WX 444 ; N abreve ; B 37 -10 442 664 ; +C -1 ; WX 500 ; N uhungarumlaut ; B 9 -10 501 678 ; +C -1 ; WX 444 ; N ecaron ; B 25 -10 424 674 ; +C -1 ; WX 722 ; N Ydieresis ; B 22 0 703 835 ; +C -1 ; WX 564 ; N divide ; B 30 -10 534 516 ; +C -1 ; WX 722 ; N Yacute ; B 22 0 703 890 ; +C -1 ; WX 722 ; N Acircumflex ; B 15 0 706 886 ; +C -1 ; WX 444 ; N aacute ; B 37 -10 442 678 ; +C -1 ; WX 722 ; N Ucircumflex ; B 14 -14 705 886 ; +C -1 ; WX 500 ; N yacute ; B 14 -218 475 678 ; +C -1 ; WX 389 ; N scommaaccent ; B 51 -218 348 460 ; +C -1 ; WX 444 ; N ecircumflex ; B 25 -10 424 674 ; +C -1 ; WX 722 ; N Uring ; B 14 -14 705 898 ; +C -1 ; WX 722 ; N Udieresis ; B 14 -14 705 835 ; +C -1 ; WX 444 ; N aogonek ; B 37 -165 469 460 ; +C -1 ; WX 722 ; N Uacute ; B 14 -14 705 890 ; +C -1 ; WX 500 ; N uogonek ; B 9 -155 487 450 ; +C -1 ; WX 611 ; N Edieresis ; B 12 0 597 835 ; +C -1 ; WX 722 ; N Dcroat ; B 16 0 685 662 ; +C -1 ; WX 250 ; N commaaccent ; B 59 -218 184 -50 ; +C -1 ; WX 760 ; N copyright ; B 38 -14 722 676 ; +C -1 ; WX 611 ; N Emacron ; B 12 0 597 813 ; +C -1 ; WX 444 ; N ccaron ; B 25 -10 412 674 ; +C -1 ; WX 444 ; N aring ; B 37 -10 442 711 ; +C -1 ; WX 722 ; N Ncommaaccent ; B 12 -198 707 662 ; +C -1 ; WX 278 ; N lacute ; B 19 0 290 890 ; +C -1 ; WX 444 ; N agrave ; B 37 -10 442 678 ; +C -1 ; WX 611 ; N Tcommaaccent ; B 17 -218 593 662 ; +C -1 ; WX 667 ; N Cacute ; B 28 -14 633 890 ; +C -1 ; WX 444 ; N atilde ; B 37 -10 442 638 ; +C -1 ; WX 611 ; N Edotaccent ; B 12 0 597 835 ; +C -1 ; WX 389 ; N scaron ; B 39 -10 350 674 ; +C -1 ; WX 389 ; N scedilla ; B 51 -215 348 460 ; +C -1 ; WX 278 ; N iacute ; B 16 0 290 678 ; +C -1 ; WX 471 ; N lozenge ; B 13 0 459 724 ; +C -1 ; WX 667 ; N Rcaron ; B 17 0 659 886 ; +C -1 ; WX 722 ; N Gcommaaccent ; B 32 -218 709 676 ; +C -1 ; WX 500 ; N ucircumflex ; B 9 -10 479 674 ; +C -1 ; WX 444 ; N acircumflex ; B 37 -10 442 674 ; +C -1 ; WX 722 ; N Amacron ; B 15 0 706 813 ; +C -1 ; WX 333 ; N rcaron ; B 5 0 335 674 ; +C -1 ; WX 444 ; N ccedilla ; B 25 -215 412 460 ; +C -1 ; WX 611 ; N Zdotaccent ; B 9 0 597 835 ; +C -1 ; WX 556 ; N Thorn ; B 16 0 542 662 ; +C -1 ; WX 722 ; N Omacron ; B 34 -14 688 813 ; +C -1 ; WX 667 ; N Racute ; B 17 0 659 890 ; +C -1 ; WX 556 ; N Sacute ; B 42 -14 491 890 ; +C -1 ; WX 588 ; N dcaron ; B 27 -10 589 695 ; +C -1 ; WX 722 ; N Umacron ; B 14 -14 705 813 ; +C -1 ; WX 500 ; N uring ; B 9 -10 479 711 ; +C -1 ; WX 300 ; N threesuperior ; B 15 262 291 676 ; +C -1 ; WX 722 ; N Ograve ; B 34 -14 688 890 ; +C -1 ; WX 722 ; N Agrave ; B 15 0 706 890 ; +C -1 ; WX 722 ; N Abreve ; B 15 0 706 876 ; +C -1 ; WX 564 ; N multiply ; B 38 8 527 497 ; +C -1 ; WX 500 ; N uacute ; B 9 -10 479 678 ; +C -1 ; WX 611 ; N Tcaron ; B 17 0 593 886 ; +C -1 ; WX 476 ; N partialdiff ; B 17 -38 459 710 ; +C -1 ; WX 500 ; N ydieresis ; B 14 -218 475 623 ; +C -1 ; WX 722 ; N Nacute ; B 12 -11 707 890 ; +C -1 ; WX 278 ; N icircumflex ; B -16 0 295 674 ; +C -1 ; WX 611 ; N Ecircumflex ; B 12 0 597 886 ; +C -1 ; WX 444 ; N adieresis ; B 37 -10 442 623 ; +C -1 ; WX 444 ; N edieresis ; B 25 -10 424 623 ; +C -1 ; WX 444 ; N cacute ; B 25 -10 413 678 ; +C -1 ; WX 500 ; N nacute ; B 16 0 485 678 ; +C -1 ; WX 500 ; N umacron ; B 9 -10 479 601 ; +C -1 ; WX 722 ; N Ncaron ; B 12 -11 707 886 ; +C -1 ; WX 333 ; N Iacute ; B 18 0 317 890 ; +C -1 ; WX 564 ; N plusminus ; B 30 0 534 506 ; +C -1 ; WX 200 ; N brokenbar ; B 67 -143 133 707 ; +C -1 ; WX 760 ; N registered ; B 38 -14 722 676 ; +C -1 ; WX 722 ; N Gbreve ; B 32 -14 709 876 ; +C -1 ; WX 333 ; N Idotaccent ; B 18 0 315 835 ; +C -1 ; WX 600 ; N summation ; B 15 -10 585 706 ; +C -1 ; WX 611 ; N Egrave ; B 12 0 597 890 ; +C -1 ; WX 333 ; N racute ; B 5 0 335 678 ; +C -1 ; WX 500 ; N omacron ; B 29 -10 470 601 ; +C -1 ; WX 611 ; N Zacute ; B 9 0 597 890 ; +C -1 ; WX 611 ; N Zcaron ; B 9 0 597 886 ; +C -1 ; WX 549 ; N greaterequal ; B 26 0 523 666 ; +C -1 ; WX 722 ; N Eth ; B 16 0 685 662 ; +C -1 ; WX 667 ; N Ccedilla ; B 28 -215 633 676 ; +C -1 ; WX 278 ; N lcommaaccent ; B 19 -218 257 683 ; +C -1 ; WX 326 ; N tcaron ; B 13 -10 318 722 ; +C -1 ; WX 444 ; N eogonek ; B 25 -165 424 460 ; +C -1 ; WX 722 ; N Uogonek ; B 14 -165 705 662 ; +C -1 ; WX 722 ; N Aacute ; B 15 0 706 890 ; +C -1 ; WX 722 ; N Adieresis ; B 15 0 706 835 ; +C -1 ; WX 444 ; N egrave ; B 25 -10 424 678 ; +C -1 ; WX 444 ; N zacute ; B 27 0 418 678 ; +C -1 ; WX 278 ; N iogonek ; B 16 -165 265 683 ; +C -1 ; WX 722 ; N Oacute ; B 34 -14 688 890 ; +C -1 ; WX 500 ; N oacute ; B 29 -10 470 678 ; +C -1 ; WX 444 ; N amacron ; B 37 -10 442 601 ; +C -1 ; WX 389 ; N sacute ; B 51 -10 348 678 ; +C -1 ; WX 278 ; N idieresis ; B -9 0 288 623 ; +C -1 ; WX 722 ; N Ocircumflex ; B 34 -14 688 886 ; +C -1 ; WX 722 ; N Ugrave ; B 14 -14 705 890 ; +C -1 ; WX 612 ; N Delta ; B 6 0 608 688 ; +C -1 ; WX 500 ; N thorn ; B 5 -217 470 683 ; +C -1 ; WX 300 ; N twosuperior ; B 1 270 296 676 ; +C -1 ; WX 722 ; N Odieresis ; B 34 -14 688 835 ; +C -1 ; WX 500 ; N mu ; B 36 -218 512 450 ; +C -1 ; WX 278 ; N igrave ; B -8 0 253 678 ; +C -1 ; WX 500 ; N ohungarumlaut ; B 29 -10 491 678 ; +C -1 ; WX 611 ; N Eogonek ; B 12 -165 597 662 ; +C -1 ; WX 500 ; N dcroat ; B 27 -10 500 683 ; +C -1 ; WX 750 ; N threequarters ; B 15 -14 718 676 ; +C -1 ; WX 556 ; N Scedilla ; B 42 -215 491 676 ; +C -1 ; WX 344 ; N lcaron ; B 19 0 347 695 ; +C -1 ; WX 722 ; N Kcommaaccent ; B 34 -198 723 662 ; +C -1 ; WX 611 ; N Lacute ; B 12 0 598 890 ; +C -1 ; WX 980 ; N trademark ; B 30 256 957 662 ; +C -1 ; WX 444 ; N edotaccent ; B 25 -10 424 623 ; +C -1 ; WX 333 ; N Igrave ; B 18 0 315 890 ; +C -1 ; WX 333 ; N Imacron ; B 11 0 322 813 ; +C -1 ; WX 611 ; N Lcaron ; B 12 0 598 676 ; +C -1 ; WX 750 ; N onehalf ; B 31 -14 746 676 ; +C -1 ; WX 549 ; N lessequal ; B 26 0 523 666 ; +C -1 ; WX 500 ; N ocircumflex ; B 29 -10 470 674 ; +C -1 ; WX 500 ; N ntilde ; B 16 0 485 638 ; +C -1 ; WX 722 ; N Uhungarumlaut ; B 14 -14 705 890 ; +C -1 ; WX 611 ; N Eacute ; B 12 0 597 890 ; +C -1 ; WX 444 ; N emacron ; B 25 -10 424 601 ; +C -1 ; WX 500 ; N gbreve ; B 28 -218 470 664 ; +C -1 ; WX 750 ; N onequarter ; B 37 -14 718 676 ; +C -1 ; WX 556 ; N Scaron ; B 42 -14 491 886 ; +C -1 ; WX 556 ; N Scommaaccent ; B 42 -218 491 676 ; +C -1 ; WX 722 ; N Ohungarumlaut ; B 34 -14 688 890 ; +C -1 ; WX 400 ; N degree ; B 57 390 343 676 ; +C -1 ; WX 500 ; N ograve ; B 29 -10 470 678 ; +C -1 ; WX 667 ; N Ccaron ; B 28 -14 633 886 ; +C -1 ; WX 500 ; N ugrave ; B 9 -10 479 678 ; +C -1 ; WX 453 ; N radical ; B 2 -60 452 768 ; +C -1 ; WX 722 ; N Dcaron ; B 16 0 685 886 ; +C -1 ; WX 333 ; N rcommaaccent ; B 5 -218 335 460 ; +C -1 ; WX 722 ; N Ntilde ; B 12 -11 707 850 ; +C -1 ; WX 500 ; N otilde ; B 29 -10 470 638 ; +C -1 ; WX 667 ; N Rcommaaccent ; B 17 -198 659 662 ; +C -1 ; WX 611 ; N Lcommaaccent ; B 12 -218 598 662 ; +C -1 ; WX 722 ; N Atilde ; B 15 0 706 850 ; +C -1 ; WX 722 ; N Aogonek ; B 15 -165 738 674 ; +C -1 ; WX 722 ; N Aring ; B 15 0 706 898 ; +C -1 ; WX 722 ; N Otilde ; B 34 -14 688 850 ; +C -1 ; WX 444 ; N zdotaccent ; B 27 0 418 623 ; +C -1 ; WX 611 ; N Ecaron ; B 12 0 597 886 ; +C -1 ; WX 333 ; N Iogonek ; B 18 -165 315 662 ; +C -1 ; WX 500 ; N kcommaaccent ; B 7 -218 505 683 ; +C -1 ; WX 564 ; N minus ; B 30 220 534 286 ; +C -1 ; WX 333 ; N Icircumflex ; B 11 0 322 886 ; +C -1 ; WX 500 ; N ncaron ; B 16 0 485 674 ; +C -1 ; WX 278 ; N tcommaaccent ; B 13 -218 279 579 ; +C -1 ; WX 564 ; N logicalnot ; B 30 108 534 386 ; +C -1 ; WX 500 ; N odieresis ; B 29 -10 470 623 ; +C -1 ; WX 500 ; N udieresis ; B 9 -10 479 623 ; +C -1 ; WX 549 ; N notequal ; B 12 -31 537 547 ; +C -1 ; WX 500 ; N gcommaaccent ; B 28 -218 470 749 ; +C -1 ; WX 500 ; N eth ; B 29 -10 471 686 ; +C -1 ; WX 444 ; N zcaron ; B 27 0 418 674 ; +C -1 ; WX 500 ; N ncommaaccent ; B 16 -218 485 460 ; +C -1 ; WX 300 ; N onesuperior ; B 57 270 248 676 ; +C -1 ; WX 278 ; N imacron ; B 6 0 271 601 ; +C -1 ; WX 500 ; N Euro ; B 0 0 0 0 ; +EndCharMetrics +StartKernData +StartKernPairs 2073 +KPX A C -40 +KPX A Cacute -40 +KPX A Ccaron -40 +KPX A Ccedilla -40 +KPX A G -40 +KPX A Gbreve -40 +KPX A Gcommaaccent -40 +KPX A O -55 +KPX A Oacute -55 +KPX A Ocircumflex -55 +KPX A Odieresis -55 +KPX A Ograve -55 +KPX A Ohungarumlaut -55 +KPX A Omacron -55 +KPX A Oslash -55 +KPX A Otilde -55 +KPX A Q -55 +KPX A T -111 +KPX A Tcaron -111 +KPX A Tcommaaccent -111 +KPX A U -55 +KPX A Uacute -55 +KPX A Ucircumflex -55 +KPX A Udieresis -55 +KPX A Ugrave -55 +KPX A Uhungarumlaut -55 +KPX A Umacron -55 +KPX A Uogonek -55 +KPX A Uring -55 +KPX A V -135 +KPX A W -90 +KPX A Y -105 +KPX A Yacute -105 +KPX A Ydieresis -105 +KPX A quoteright -111 +KPX A v -74 +KPX A w -92 +KPX A y -92 +KPX A yacute -92 +KPX A ydieresis -92 +KPX Aacute C -40 +KPX Aacute Cacute -40 +KPX Aacute Ccaron -40 +KPX Aacute Ccedilla -40 +KPX Aacute G -40 +KPX Aacute Gbreve -40 +KPX Aacute Gcommaaccent -40 +KPX Aacute O -55 +KPX Aacute Oacute -55 +KPX Aacute Ocircumflex -55 +KPX Aacute Odieresis -55 +KPX Aacute Ograve -55 +KPX Aacute Ohungarumlaut -55 +KPX Aacute Omacron -55 +KPX Aacute Oslash -55 +KPX Aacute Otilde -55 +KPX Aacute Q -55 +KPX Aacute T -111 +KPX Aacute Tcaron -111 +KPX Aacute Tcommaaccent -111 +KPX Aacute U -55 +KPX Aacute Uacute -55 +KPX Aacute Ucircumflex -55 +KPX Aacute Udieresis -55 +KPX Aacute Ugrave -55 +KPX Aacute Uhungarumlaut -55 +KPX Aacute Umacron -55 +KPX Aacute Uogonek -55 +KPX Aacute Uring -55 +KPX Aacute V -135 +KPX Aacute W -90 +KPX Aacute Y -105 +KPX Aacute Yacute -105 +KPX Aacute Ydieresis -105 +KPX Aacute quoteright -111 +KPX Aacute v -74 +KPX Aacute w -92 +KPX Aacute y -92 +KPX Aacute yacute -92 +KPX Aacute ydieresis -92 +KPX Abreve C -40 +KPX Abreve Cacute -40 +KPX Abreve Ccaron -40 +KPX Abreve Ccedilla -40 +KPX Abreve G -40 +KPX Abreve Gbreve -40 +KPX Abreve Gcommaaccent -40 +KPX Abreve O -55 +KPX Abreve Oacute -55 +KPX Abreve Ocircumflex -55 +KPX Abreve Odieresis -55 +KPX Abreve Ograve -55 +KPX Abreve Ohungarumlaut -55 +KPX Abreve Omacron -55 +KPX Abreve Oslash -55 +KPX Abreve Otilde -55 +KPX Abreve Q -55 +KPX Abreve T -111 +KPX Abreve Tcaron -111 +KPX Abreve Tcommaaccent -111 +KPX Abreve U -55 +KPX Abreve Uacute -55 +KPX Abreve Ucircumflex -55 +KPX Abreve Udieresis -55 +KPX Abreve Ugrave -55 +KPX Abreve Uhungarumlaut -55 +KPX Abreve Umacron -55 +KPX Abreve Uogonek -55 +KPX Abreve Uring -55 +KPX Abreve V -135 +KPX Abreve W -90 +KPX Abreve Y -105 +KPX Abreve Yacute -105 +KPX Abreve Ydieresis -105 +KPX Abreve quoteright -111 +KPX Abreve v -74 +KPX Abreve w -92 +KPX Abreve y -92 +KPX Abreve yacute -92 +KPX Abreve ydieresis -92 +KPX Acircumflex C -40 +KPX Acircumflex Cacute -40 +KPX Acircumflex Ccaron -40 +KPX Acircumflex Ccedilla -40 +KPX Acircumflex G -40 +KPX Acircumflex Gbreve -40 +KPX Acircumflex Gcommaaccent -40 +KPX Acircumflex O -55 +KPX Acircumflex Oacute -55 +KPX Acircumflex Ocircumflex -55 +KPX Acircumflex Odieresis -55 +KPX Acircumflex Ograve -55 +KPX Acircumflex Ohungarumlaut -55 +KPX Acircumflex Omacron -55 +KPX Acircumflex Oslash -55 +KPX Acircumflex Otilde -55 +KPX Acircumflex Q -55 +KPX Acircumflex T -111 +KPX Acircumflex Tcaron -111 +KPX Acircumflex Tcommaaccent -111 +KPX Acircumflex U -55 +KPX Acircumflex Uacute -55 +KPX Acircumflex Ucircumflex -55 +KPX Acircumflex Udieresis -55 +KPX Acircumflex Ugrave -55 +KPX Acircumflex Uhungarumlaut -55 +KPX Acircumflex Umacron -55 +KPX Acircumflex Uogonek -55 +KPX Acircumflex Uring -55 +KPX Acircumflex V -135 +KPX Acircumflex W -90 +KPX Acircumflex Y -105 +KPX Acircumflex Yacute -105 +KPX Acircumflex Ydieresis -105 +KPX Acircumflex quoteright -111 +KPX Acircumflex v -74 +KPX Acircumflex w -92 +KPX Acircumflex y -92 +KPX Acircumflex yacute -92 +KPX Acircumflex ydieresis -92 +KPX Adieresis C -40 +KPX Adieresis Cacute -40 +KPX Adieresis Ccaron -40 +KPX Adieresis Ccedilla -40 +KPX Adieresis G -40 +KPX Adieresis Gbreve -40 +KPX Adieresis Gcommaaccent -40 +KPX Adieresis O -55 +KPX Adieresis Oacute -55 +KPX Adieresis Ocircumflex -55 +KPX Adieresis Odieresis -55 +KPX Adieresis Ograve -55 +KPX Adieresis Ohungarumlaut -55 +KPX Adieresis Omacron -55 +KPX Adieresis Oslash -55 +KPX Adieresis Otilde -55 +KPX Adieresis Q -55 +KPX Adieresis T -111 +KPX Adieresis Tcaron -111 +KPX Adieresis Tcommaaccent -111 +KPX Adieresis U -55 +KPX Adieresis Uacute -55 +KPX Adieresis Ucircumflex -55 +KPX Adieresis Udieresis -55 +KPX Adieresis Ugrave -55 +KPX Adieresis Uhungarumlaut -55 +KPX Adieresis Umacron -55 +KPX Adieresis Uogonek -55 +KPX Adieresis Uring -55 +KPX Adieresis V -135 +KPX Adieresis W -90 +KPX Adieresis Y -105 +KPX Adieresis Yacute -105 +KPX Adieresis Ydieresis -105 +KPX Adieresis quoteright -111 +KPX Adieresis v -74 +KPX Adieresis w -92 +KPX Adieresis y -92 +KPX Adieresis yacute -92 +KPX Adieresis ydieresis -92 +KPX Agrave C -40 +KPX Agrave Cacute -40 +KPX Agrave Ccaron -40 +KPX Agrave Ccedilla -40 +KPX Agrave G -40 +KPX Agrave Gbreve -40 +KPX Agrave Gcommaaccent -40 +KPX Agrave O -55 +KPX Agrave Oacute -55 +KPX Agrave Ocircumflex -55 +KPX Agrave Odieresis -55 +KPX Agrave Ograve -55 +KPX Agrave Ohungarumlaut -55 +KPX Agrave Omacron -55 +KPX Agrave Oslash -55 +KPX Agrave Otilde -55 +KPX Agrave Q -55 +KPX Agrave T -111 +KPX Agrave Tcaron -111 +KPX Agrave Tcommaaccent -111 +KPX Agrave U -55 +KPX Agrave Uacute -55 +KPX Agrave Ucircumflex -55 +KPX Agrave Udieresis -55 +KPX Agrave Ugrave -55 +KPX Agrave Uhungarumlaut -55 +KPX Agrave Umacron -55 +KPX Agrave Uogonek -55 +KPX Agrave Uring -55 +KPX Agrave V -135 +KPX Agrave W -90 +KPX Agrave Y -105 +KPX Agrave Yacute -105 +KPX Agrave Ydieresis -105 +KPX Agrave quoteright -111 +KPX Agrave v -74 +KPX Agrave w -92 +KPX Agrave y -92 +KPX Agrave yacute -92 +KPX Agrave ydieresis -92 +KPX Amacron C -40 +KPX Amacron Cacute -40 +KPX Amacron Ccaron -40 +KPX Amacron Ccedilla -40 +KPX Amacron G -40 +KPX Amacron Gbreve -40 +KPX Amacron Gcommaaccent -40 +KPX Amacron O -55 +KPX Amacron Oacute -55 +KPX Amacron Ocircumflex -55 +KPX Amacron Odieresis -55 +KPX Amacron Ograve -55 +KPX Amacron Ohungarumlaut -55 +KPX Amacron Omacron -55 +KPX Amacron Oslash -55 +KPX Amacron Otilde -55 +KPX Amacron Q -55 +KPX Amacron T -111 +KPX Amacron Tcaron -111 +KPX Amacron Tcommaaccent -111 +KPX Amacron U -55 +KPX Amacron Uacute -55 +KPX Amacron Ucircumflex -55 +KPX Amacron Udieresis -55 +KPX Amacron Ugrave -55 +KPX Amacron Uhungarumlaut -55 +KPX Amacron Umacron -55 +KPX Amacron Uogonek -55 +KPX Amacron Uring -55 +KPX Amacron V -135 +KPX Amacron W -90 +KPX Amacron Y -105 +KPX Amacron Yacute -105 +KPX Amacron Ydieresis -105 +KPX Amacron quoteright -111 +KPX Amacron v -74 +KPX Amacron w -92 +KPX Amacron y -92 +KPX Amacron yacute -92 +KPX Amacron ydieresis -92 +KPX Aogonek C -40 +KPX Aogonek Cacute -40 +KPX Aogonek Ccaron -40 +KPX Aogonek Ccedilla -40 +KPX Aogonek G -40 +KPX Aogonek Gbreve -40 +KPX Aogonek Gcommaaccent -40 +KPX Aogonek O -55 +KPX Aogonek Oacute -55 +KPX Aogonek Ocircumflex -55 +KPX Aogonek Odieresis -55 +KPX Aogonek Ograve -55 +KPX Aogonek Ohungarumlaut -55 +KPX Aogonek Omacron -55 +KPX Aogonek Oslash -55 +KPX Aogonek Otilde -55 +KPX Aogonek Q -55 +KPX Aogonek T -111 +KPX Aogonek Tcaron -111 +KPX Aogonek Tcommaaccent -111 +KPX Aogonek U -55 +KPX Aogonek Uacute -55 +KPX Aogonek Ucircumflex -55 +KPX Aogonek Udieresis -55 +KPX Aogonek Ugrave -55 +KPX Aogonek Uhungarumlaut -55 +KPX Aogonek Umacron -55 +KPX Aogonek Uogonek -55 +KPX Aogonek Uring -55 +KPX Aogonek V -135 +KPX Aogonek W -90 +KPX Aogonek Y -105 +KPX Aogonek Yacute -105 +KPX Aogonek Ydieresis -105 +KPX Aogonek quoteright -111 +KPX Aogonek v -74 +KPX Aogonek w -52 +KPX Aogonek y -52 +KPX Aogonek yacute -52 +KPX Aogonek ydieresis -52 +KPX Aring C -40 +KPX Aring Cacute -40 +KPX Aring Ccaron -40 +KPX Aring Ccedilla -40 +KPX Aring G -40 +KPX Aring Gbreve -40 +KPX Aring Gcommaaccent -40 +KPX Aring O -55 +KPX Aring Oacute -55 +KPX Aring Ocircumflex -55 +KPX Aring Odieresis -55 +KPX Aring Ograve -55 +KPX Aring Ohungarumlaut -55 +KPX Aring Omacron -55 +KPX Aring Oslash -55 +KPX Aring Otilde -55 +KPX Aring Q -55 +KPX Aring T -111 +KPX Aring Tcaron -111 +KPX Aring Tcommaaccent -111 +KPX Aring U -55 +KPX Aring Uacute -55 +KPX Aring Ucircumflex -55 +KPX Aring Udieresis -55 +KPX Aring Ugrave -55 +KPX Aring Uhungarumlaut -55 +KPX Aring Umacron -55 +KPX Aring Uogonek -55 +KPX Aring Uring -55 +KPX Aring V -135 +KPX Aring W -90 +KPX Aring Y -105 +KPX Aring Yacute -105 +KPX Aring Ydieresis -105 +KPX Aring quoteright -111 +KPX Aring v -74 +KPX Aring w -92 +KPX Aring y -92 +KPX Aring yacute -92 +KPX Aring ydieresis -92 +KPX Atilde C -40 +KPX Atilde Cacute -40 +KPX Atilde Ccaron -40 +KPX Atilde Ccedilla -40 +KPX Atilde G -40 +KPX Atilde Gbreve -40 +KPX Atilde Gcommaaccent -40 +KPX Atilde O -55 +KPX Atilde Oacute -55 +KPX Atilde Ocircumflex -55 +KPX Atilde Odieresis -55 +KPX Atilde Ograve -55 +KPX Atilde Ohungarumlaut -55 +KPX Atilde Omacron -55 +KPX Atilde Oslash -55 +KPX Atilde Otilde -55 +KPX Atilde Q -55 +KPX Atilde T -111 +KPX Atilde Tcaron -111 +KPX Atilde Tcommaaccent -111 +KPX Atilde U -55 +KPX Atilde Uacute -55 +KPX Atilde Ucircumflex -55 +KPX Atilde Udieresis -55 +KPX Atilde Ugrave -55 +KPX Atilde Uhungarumlaut -55 +KPX Atilde Umacron -55 +KPX Atilde Uogonek -55 +KPX Atilde Uring -55 +KPX Atilde V -135 +KPX Atilde W -90 +KPX Atilde Y -105 +KPX Atilde Yacute -105 +KPX Atilde Ydieresis -105 +KPX Atilde quoteright -111 +KPX Atilde v -74 +KPX Atilde w -92 +KPX Atilde y -92 +KPX Atilde yacute -92 +KPX Atilde ydieresis -92 +KPX B A -35 +KPX B Aacute -35 +KPX B Abreve -35 +KPX B Acircumflex -35 +KPX B Adieresis -35 +KPX B Agrave -35 +KPX B Amacron -35 +KPX B Aogonek -35 +KPX B Aring -35 +KPX B Atilde -35 +KPX B U -10 +KPX B Uacute -10 +KPX B Ucircumflex -10 +KPX B Udieresis -10 +KPX B Ugrave -10 +KPX B Uhungarumlaut -10 +KPX B Umacron -10 +KPX B Uogonek -10 +KPX B Uring -10 +KPX D A -40 +KPX D Aacute -40 +KPX D Abreve -40 +KPX D Acircumflex -40 +KPX D Adieresis -40 +KPX D Agrave -40 +KPX D Amacron -40 +KPX D Aogonek -40 +KPX D Aring -40 +KPX D Atilde -40 +KPX D V -40 +KPX D W -30 +KPX D Y -55 +KPX D Yacute -55 +KPX D Ydieresis -55 +KPX Dcaron A -40 +KPX Dcaron Aacute -40 +KPX Dcaron Abreve -40 +KPX Dcaron Acircumflex -40 +KPX Dcaron Adieresis -40 +KPX Dcaron Agrave -40 +KPX Dcaron Amacron -40 +KPX Dcaron Aogonek -40 +KPX Dcaron Aring -40 +KPX Dcaron Atilde -40 +KPX Dcaron V -40 +KPX Dcaron W -30 +KPX Dcaron Y -55 +KPX Dcaron Yacute -55 +KPX Dcaron Ydieresis -55 +KPX Dcroat A -40 +KPX Dcroat Aacute -40 +KPX Dcroat Abreve -40 +KPX Dcroat Acircumflex -40 +KPX Dcroat Adieresis -40 +KPX Dcroat Agrave -40 +KPX Dcroat Amacron -40 +KPX Dcroat Aogonek -40 +KPX Dcroat Aring -40 +KPX Dcroat Atilde -40 +KPX Dcroat V -40 +KPX Dcroat W -30 +KPX Dcroat Y -55 +KPX Dcroat Yacute -55 +KPX Dcroat Ydieresis -55 +KPX F A -74 +KPX F Aacute -74 +KPX F Abreve -74 +KPX F Acircumflex -74 +KPX F Adieresis -74 +KPX F Agrave -74 +KPX F Amacron -74 +KPX F Aogonek -74 +KPX F Aring -74 +KPX F Atilde -74 +KPX F a -15 +KPX F aacute -15 +KPX F abreve -15 +KPX F acircumflex -15 +KPX F adieresis -15 +KPX F agrave -15 +KPX F amacron -15 +KPX F aogonek -15 +KPX F aring -15 +KPX F atilde -15 +KPX F comma -80 +KPX F o -15 +KPX F oacute -15 +KPX F ocircumflex -15 +KPX F odieresis -15 +KPX F ograve -15 +KPX F ohungarumlaut -15 +KPX F omacron -15 +KPX F oslash -15 +KPX F otilde -15 +KPX F period -80 +KPX J A -60 +KPX J Aacute -60 +KPX J Abreve -60 +KPX J Acircumflex -60 +KPX J Adieresis -60 +KPX J Agrave -60 +KPX J Amacron -60 +KPX J Aogonek -60 +KPX J Aring -60 +KPX J Atilde -60 +KPX K O -30 +KPX K Oacute -30 +KPX K Ocircumflex -30 +KPX K Odieresis -30 +KPX K Ograve -30 +KPX K Ohungarumlaut -30 +KPX K Omacron -30 +KPX K Oslash -30 +KPX K Otilde -30 +KPX K e -25 +KPX K eacute -25 +KPX K ecaron -25 +KPX K ecircumflex -25 +KPX K edieresis -25 +KPX K edotaccent -25 +KPX K egrave -25 +KPX K emacron -25 +KPX K eogonek -25 +KPX K o -35 +KPX K oacute -35 +KPX K ocircumflex -35 +KPX K odieresis -35 +KPX K ograve -35 +KPX K ohungarumlaut -35 +KPX K omacron -35 +KPX K oslash -35 +KPX K otilde -35 +KPX K u -15 +KPX K uacute -15 +KPX K ucircumflex -15 +KPX K udieresis -15 +KPX K ugrave -15 +KPX K uhungarumlaut -15 +KPX K umacron -15 +KPX K uogonek -15 +KPX K uring -15 +KPX K y -25 +KPX K yacute -25 +KPX K ydieresis -25 +KPX Kcommaaccent O -30 +KPX Kcommaaccent Oacute -30 +KPX Kcommaaccent Ocircumflex -30 +KPX Kcommaaccent Odieresis -30 +KPX Kcommaaccent Ograve -30 +KPX Kcommaaccent Ohungarumlaut -30 +KPX Kcommaaccent Omacron -30 +KPX Kcommaaccent Oslash -30 +KPX Kcommaaccent Otilde -30 +KPX Kcommaaccent e -25 +KPX Kcommaaccent eacute -25 +KPX Kcommaaccent ecaron -25 +KPX Kcommaaccent ecircumflex -25 +KPX Kcommaaccent edieresis -25 +KPX Kcommaaccent edotaccent -25 +KPX Kcommaaccent egrave -25 +KPX Kcommaaccent emacron -25 +KPX Kcommaaccent eogonek -25 +KPX Kcommaaccent o -35 +KPX Kcommaaccent oacute -35 +KPX Kcommaaccent ocircumflex -35 +KPX Kcommaaccent odieresis -35 +KPX Kcommaaccent ograve -35 +KPX Kcommaaccent ohungarumlaut -35 +KPX Kcommaaccent omacron -35 +KPX Kcommaaccent oslash -35 +KPX Kcommaaccent otilde -35 +KPX Kcommaaccent u -15 +KPX Kcommaaccent uacute -15 +KPX Kcommaaccent ucircumflex -15 +KPX Kcommaaccent udieresis -15 +KPX Kcommaaccent ugrave -15 +KPX Kcommaaccent uhungarumlaut -15 +KPX Kcommaaccent umacron -15 +KPX Kcommaaccent uogonek -15 +KPX Kcommaaccent uring -15 +KPX Kcommaaccent y -25 +KPX Kcommaaccent yacute -25 +KPX Kcommaaccent ydieresis -25 +KPX L T -92 +KPX L Tcaron -92 +KPX L Tcommaaccent -92 +KPX L V -100 +KPX L W -74 +KPX L Y -100 +KPX L Yacute -100 +KPX L Ydieresis -100 +KPX L quoteright -92 +KPX L y -55 +KPX L yacute -55 +KPX L ydieresis -55 +KPX Lacute T -92 +KPX Lacute Tcaron -92 +KPX Lacute Tcommaaccent -92 +KPX Lacute V -100 +KPX Lacute W -74 +KPX Lacute Y -100 +KPX Lacute Yacute -100 +KPX Lacute Ydieresis -100 +KPX Lacute quoteright -92 +KPX Lacute y -55 +KPX Lacute yacute -55 +KPX Lacute ydieresis -55 +KPX Lcaron quoteright -92 +KPX Lcaron y -55 +KPX Lcaron yacute -55 +KPX Lcaron ydieresis -55 +KPX Lcommaaccent T -92 +KPX Lcommaaccent Tcaron -92 +KPX Lcommaaccent Tcommaaccent -92 +KPX Lcommaaccent V -100 +KPX Lcommaaccent W -74 +KPX Lcommaaccent Y -100 +KPX Lcommaaccent Yacute -100 +KPX Lcommaaccent Ydieresis -100 +KPX Lcommaaccent quoteright -92 +KPX Lcommaaccent y -55 +KPX Lcommaaccent yacute -55 +KPX Lcommaaccent ydieresis -55 +KPX Lslash T -92 +KPX Lslash Tcaron -92 +KPX Lslash Tcommaaccent -92 +KPX Lslash V -100 +KPX Lslash W -74 +KPX Lslash Y -100 +KPX Lslash Yacute -100 +KPX Lslash Ydieresis -100 +KPX Lslash quoteright -92 +KPX Lslash y -55 +KPX Lslash yacute -55 +KPX Lslash ydieresis -55 +KPX N A -35 +KPX N Aacute -35 +KPX N Abreve -35 +KPX N Acircumflex -35 +KPX N Adieresis -35 +KPX N Agrave -35 +KPX N Amacron -35 +KPX N Aogonek -35 +KPX N Aring -35 +KPX N Atilde -35 +KPX Nacute A -35 +KPX Nacute Aacute -35 +KPX Nacute Abreve -35 +KPX Nacute Acircumflex -35 +KPX Nacute Adieresis -35 +KPX Nacute Agrave -35 +KPX Nacute Amacron -35 +KPX Nacute Aogonek -35 +KPX Nacute Aring -35 +KPX Nacute Atilde -35 +KPX Ncaron A -35 +KPX Ncaron Aacute -35 +KPX Ncaron Abreve -35 +KPX Ncaron Acircumflex -35 +KPX Ncaron Adieresis -35 +KPX Ncaron Agrave -35 +KPX Ncaron Amacron -35 +KPX Ncaron Aogonek -35 +KPX Ncaron Aring -35 +KPX Ncaron Atilde -35 +KPX Ncommaaccent A -35 +KPX Ncommaaccent Aacute -35 +KPX Ncommaaccent Abreve -35 +KPX Ncommaaccent Acircumflex -35 +KPX Ncommaaccent Adieresis -35 +KPX Ncommaaccent Agrave -35 +KPX Ncommaaccent Amacron -35 +KPX Ncommaaccent Aogonek -35 +KPX Ncommaaccent Aring -35 +KPX Ncommaaccent Atilde -35 +KPX Ntilde A -35 +KPX Ntilde Aacute -35 +KPX Ntilde Abreve -35 +KPX Ntilde Acircumflex -35 +KPX Ntilde Adieresis -35 +KPX Ntilde Agrave -35 +KPX Ntilde Amacron -35 +KPX Ntilde Aogonek -35 +KPX Ntilde Aring -35 +KPX Ntilde Atilde -35 +KPX O A -35 +KPX O Aacute -35 +KPX O Abreve -35 +KPX O Acircumflex -35 +KPX O Adieresis -35 +KPX O Agrave -35 +KPX O Amacron -35 +KPX O Aogonek -35 +KPX O Aring -35 +KPX O Atilde -35 +KPX O T -40 +KPX O Tcaron -40 +KPX O Tcommaaccent -40 +KPX O V -50 +KPX O W -35 +KPX O X -40 +KPX O Y -50 +KPX O Yacute -50 +KPX O Ydieresis -50 +KPX Oacute A -35 +KPX Oacute Aacute -35 +KPX Oacute Abreve -35 +KPX Oacute Acircumflex -35 +KPX Oacute Adieresis -35 +KPX Oacute Agrave -35 +KPX Oacute Amacron -35 +KPX Oacute Aogonek -35 +KPX Oacute Aring -35 +KPX Oacute Atilde -35 +KPX Oacute T -40 +KPX Oacute Tcaron -40 +KPX Oacute Tcommaaccent -40 +KPX Oacute V -50 +KPX Oacute W -35 +KPX Oacute X -40 +KPX Oacute Y -50 +KPX Oacute Yacute -50 +KPX Oacute Ydieresis -50 +KPX Ocircumflex A -35 +KPX Ocircumflex Aacute -35 +KPX Ocircumflex Abreve -35 +KPX Ocircumflex Acircumflex -35 +KPX Ocircumflex Adieresis -35 +KPX Ocircumflex Agrave -35 +KPX Ocircumflex Amacron -35 +KPX Ocircumflex Aogonek -35 +KPX Ocircumflex Aring -35 +KPX Ocircumflex Atilde -35 +KPX Ocircumflex T -40 +KPX Ocircumflex Tcaron -40 +KPX Ocircumflex Tcommaaccent -40 +KPX Ocircumflex V -50 +KPX Ocircumflex W -35 +KPX Ocircumflex X -40 +KPX Ocircumflex Y -50 +KPX Ocircumflex Yacute -50 +KPX Ocircumflex Ydieresis -50 +KPX Odieresis A -35 +KPX Odieresis Aacute -35 +KPX Odieresis Abreve -35 +KPX Odieresis Acircumflex -35 +KPX Odieresis Adieresis -35 +KPX Odieresis Agrave -35 +KPX Odieresis Amacron -35 +KPX Odieresis Aogonek -35 +KPX Odieresis Aring -35 +KPX Odieresis Atilde -35 +KPX Odieresis T -40 +KPX Odieresis Tcaron -40 +KPX Odieresis Tcommaaccent -40 +KPX Odieresis V -50 +KPX Odieresis W -35 +KPX Odieresis X -40 +KPX Odieresis Y -50 +KPX Odieresis Yacute -50 +KPX Odieresis Ydieresis -50 +KPX Ograve A -35 +KPX Ograve Aacute -35 +KPX Ograve Abreve -35 +KPX Ograve Acircumflex -35 +KPX Ograve Adieresis -35 +KPX Ograve Agrave -35 +KPX Ograve Amacron -35 +KPX Ograve Aogonek -35 +KPX Ograve Aring -35 +KPX Ograve Atilde -35 +KPX Ograve T -40 +KPX Ograve Tcaron -40 +KPX Ograve Tcommaaccent -40 +KPX Ograve V -50 +KPX Ograve W -35 +KPX Ograve X -40 +KPX Ograve Y -50 +KPX Ograve Yacute -50 +KPX Ograve Ydieresis -50 +KPX Ohungarumlaut A -35 +KPX Ohungarumlaut Aacute -35 +KPX Ohungarumlaut Abreve -35 +KPX Ohungarumlaut Acircumflex -35 +KPX Ohungarumlaut Adieresis -35 +KPX Ohungarumlaut Agrave -35 +KPX Ohungarumlaut Amacron -35 +KPX Ohungarumlaut Aogonek -35 +KPX Ohungarumlaut Aring -35 +KPX Ohungarumlaut Atilde -35 +KPX Ohungarumlaut T -40 +KPX Ohungarumlaut Tcaron -40 +KPX Ohungarumlaut Tcommaaccent -40 +KPX Ohungarumlaut V -50 +KPX Ohungarumlaut W -35 +KPX Ohungarumlaut X -40 +KPX Ohungarumlaut Y -50 +KPX Ohungarumlaut Yacute -50 +KPX Ohungarumlaut Ydieresis -50 +KPX Omacron A -35 +KPX Omacron Aacute -35 +KPX Omacron Abreve -35 +KPX Omacron Acircumflex -35 +KPX Omacron Adieresis -35 +KPX Omacron Agrave -35 +KPX Omacron Amacron -35 +KPX Omacron Aogonek -35 +KPX Omacron Aring -35 +KPX Omacron Atilde -35 +KPX Omacron T -40 +KPX Omacron Tcaron -40 +KPX Omacron Tcommaaccent -40 +KPX Omacron V -50 +KPX Omacron W -35 +KPX Omacron X -40 +KPX Omacron Y -50 +KPX Omacron Yacute -50 +KPX Omacron Ydieresis -50 +KPX Oslash A -35 +KPX Oslash Aacute -35 +KPX Oslash Abreve -35 +KPX Oslash Acircumflex -35 +KPX Oslash Adieresis -35 +KPX Oslash Agrave -35 +KPX Oslash Amacron -35 +KPX Oslash Aogonek -35 +KPX Oslash Aring -35 +KPX Oslash Atilde -35 +KPX Oslash T -40 +KPX Oslash Tcaron -40 +KPX Oslash Tcommaaccent -40 +KPX Oslash V -50 +KPX Oslash W -35 +KPX Oslash X -40 +KPX Oslash Y -50 +KPX Oslash Yacute -50 +KPX Oslash Ydieresis -50 +KPX Otilde A -35 +KPX Otilde Aacute -35 +KPX Otilde Abreve -35 +KPX Otilde Acircumflex -35 +KPX Otilde Adieresis -35 +KPX Otilde Agrave -35 +KPX Otilde Amacron -35 +KPX Otilde Aogonek -35 +KPX Otilde Aring -35 +KPX Otilde Atilde -35 +KPX Otilde T -40 +KPX Otilde Tcaron -40 +KPX Otilde Tcommaaccent -40 +KPX Otilde V -50 +KPX Otilde W -35 +KPX Otilde X -40 +KPX Otilde Y -50 +KPX Otilde Yacute -50 +KPX Otilde Ydieresis -50 +KPX P A -92 +KPX P Aacute -92 +KPX P Abreve -92 +KPX P Acircumflex -92 +KPX P Adieresis -92 +KPX P Agrave -92 +KPX P Amacron -92 +KPX P Aogonek -92 +KPX P Aring -92 +KPX P Atilde -92 +KPX P a -15 +KPX P aacute -15 +KPX P abreve -15 +KPX P acircumflex -15 +KPX P adieresis -15 +KPX P agrave -15 +KPX P amacron -15 +KPX P aogonek -15 +KPX P aring -15 +KPX P atilde -15 +KPX P comma -111 +KPX P period -111 +KPX Q U -10 +KPX Q Uacute -10 +KPX Q Ucircumflex -10 +KPX Q Udieresis -10 +KPX Q Ugrave -10 +KPX Q Uhungarumlaut -10 +KPX Q Umacron -10 +KPX Q Uogonek -10 +KPX Q Uring -10 +KPX R O -40 +KPX R Oacute -40 +KPX R Ocircumflex -40 +KPX R Odieresis -40 +KPX R Ograve -40 +KPX R Ohungarumlaut -40 +KPX R Omacron -40 +KPX R Oslash -40 +KPX R Otilde -40 +KPX R T -60 +KPX R Tcaron -60 +KPX R Tcommaaccent -60 +KPX R U -40 +KPX R Uacute -40 +KPX R Ucircumflex -40 +KPX R Udieresis -40 +KPX R Ugrave -40 +KPX R Uhungarumlaut -40 +KPX R Umacron -40 +KPX R Uogonek -40 +KPX R Uring -40 +KPX R V -80 +KPX R W -55 +KPX R Y -65 +KPX R Yacute -65 +KPX R Ydieresis -65 +KPX Racute O -40 +KPX Racute Oacute -40 +KPX Racute Ocircumflex -40 +KPX Racute Odieresis -40 +KPX Racute Ograve -40 +KPX Racute Ohungarumlaut -40 +KPX Racute Omacron -40 +KPX Racute Oslash -40 +KPX Racute Otilde -40 +KPX Racute T -60 +KPX Racute Tcaron -60 +KPX Racute Tcommaaccent -60 +KPX Racute U -40 +KPX Racute Uacute -40 +KPX Racute Ucircumflex -40 +KPX Racute Udieresis -40 +KPX Racute Ugrave -40 +KPX Racute Uhungarumlaut -40 +KPX Racute Umacron -40 +KPX Racute Uogonek -40 +KPX Racute Uring -40 +KPX Racute V -80 +KPX Racute W -55 +KPX Racute Y -65 +KPX Racute Yacute -65 +KPX Racute Ydieresis -65 +KPX Rcaron O -40 +KPX Rcaron Oacute -40 +KPX Rcaron Ocircumflex -40 +KPX Rcaron Odieresis -40 +KPX Rcaron Ograve -40 +KPX Rcaron Ohungarumlaut -40 +KPX Rcaron Omacron -40 +KPX Rcaron Oslash -40 +KPX Rcaron Otilde -40 +KPX Rcaron T -60 +KPX Rcaron Tcaron -60 +KPX Rcaron Tcommaaccent -60 +KPX Rcaron U -40 +KPX Rcaron Uacute -40 +KPX Rcaron Ucircumflex -40 +KPX Rcaron Udieresis -40 +KPX Rcaron Ugrave -40 +KPX Rcaron Uhungarumlaut -40 +KPX Rcaron Umacron -40 +KPX Rcaron Uogonek -40 +KPX Rcaron Uring -40 +KPX Rcaron V -80 +KPX Rcaron W -55 +KPX Rcaron Y -65 +KPX Rcaron Yacute -65 +KPX Rcaron Ydieresis -65 +KPX Rcommaaccent O -40 +KPX Rcommaaccent Oacute -40 +KPX Rcommaaccent Ocircumflex -40 +KPX Rcommaaccent Odieresis -40 +KPX Rcommaaccent Ograve -40 +KPX Rcommaaccent Ohungarumlaut -40 +KPX Rcommaaccent Omacron -40 +KPX Rcommaaccent Oslash -40 +KPX Rcommaaccent Otilde -40 +KPX Rcommaaccent T -60 +KPX Rcommaaccent Tcaron -60 +KPX Rcommaaccent Tcommaaccent -60 +KPX Rcommaaccent U -40 +KPX Rcommaaccent Uacute -40 +KPX Rcommaaccent Ucircumflex -40 +KPX Rcommaaccent Udieresis -40 +KPX Rcommaaccent Ugrave -40 +KPX Rcommaaccent Uhungarumlaut -40 +KPX Rcommaaccent Umacron -40 +KPX Rcommaaccent Uogonek -40 +KPX Rcommaaccent Uring -40 +KPX Rcommaaccent V -80 +KPX Rcommaaccent W -55 +KPX Rcommaaccent Y -65 +KPX Rcommaaccent Yacute -65 +KPX Rcommaaccent Ydieresis -65 +KPX T A -93 +KPX T Aacute -93 +KPX T Abreve -93 +KPX T Acircumflex -93 +KPX T Adieresis -93 +KPX T Agrave -93 +KPX T Amacron -93 +KPX T Aogonek -93 +KPX T Aring -93 +KPX T Atilde -93 +KPX T O -18 +KPX T Oacute -18 +KPX T Ocircumflex -18 +KPX T Odieresis -18 +KPX T Ograve -18 +KPX T Ohungarumlaut -18 +KPX T Omacron -18 +KPX T Oslash -18 +KPX T Otilde -18 +KPX T a -80 +KPX T aacute -80 +KPX T abreve -80 +KPX T acircumflex -80 +KPX T adieresis -40 +KPX T agrave -40 +KPX T amacron -40 +KPX T aogonek -80 +KPX T aring -80 +KPX T atilde -40 +KPX T colon -50 +KPX T comma -74 +KPX T e -70 +KPX T eacute -70 +KPX T ecaron -70 +KPX T ecircumflex -70 +KPX T edieresis -30 +KPX T edotaccent -70 +KPX T egrave -70 +KPX T emacron -30 +KPX T eogonek -70 +KPX T hyphen -92 +KPX T i -35 +KPX T iacute -35 +KPX T iogonek -35 +KPX T o -80 +KPX T oacute -80 +KPX T ocircumflex -80 +KPX T odieresis -80 +KPX T ograve -80 +KPX T ohungarumlaut -80 +KPX T omacron -80 +KPX T oslash -80 +KPX T otilde -80 +KPX T period -74 +KPX T r -35 +KPX T racute -35 +KPX T rcaron -35 +KPX T rcommaaccent -35 +KPX T semicolon -55 +KPX T u -45 +KPX T uacute -45 +KPX T ucircumflex -45 +KPX T udieresis -45 +KPX T ugrave -45 +KPX T uhungarumlaut -45 +KPX T umacron -45 +KPX T uogonek -45 +KPX T uring -45 +KPX T w -80 +KPX T y -80 +KPX T yacute -80 +KPX T ydieresis -80 +KPX Tcaron A -93 +KPX Tcaron Aacute -93 +KPX Tcaron Abreve -93 +KPX Tcaron Acircumflex -93 +KPX Tcaron Adieresis -93 +KPX Tcaron Agrave -93 +KPX Tcaron Amacron -93 +KPX Tcaron Aogonek -93 +KPX Tcaron Aring -93 +KPX Tcaron Atilde -93 +KPX Tcaron O -18 +KPX Tcaron Oacute -18 +KPX Tcaron Ocircumflex -18 +KPX Tcaron Odieresis -18 +KPX Tcaron Ograve -18 +KPX Tcaron Ohungarumlaut -18 +KPX Tcaron Omacron -18 +KPX Tcaron Oslash -18 +KPX Tcaron Otilde -18 +KPX Tcaron a -80 +KPX Tcaron aacute -80 +KPX Tcaron abreve -80 +KPX Tcaron acircumflex -80 +KPX Tcaron adieresis -40 +KPX Tcaron agrave -40 +KPX Tcaron amacron -40 +KPX Tcaron aogonek -80 +KPX Tcaron aring -80 +KPX Tcaron atilde -40 +KPX Tcaron colon -50 +KPX Tcaron comma -74 +KPX Tcaron e -70 +KPX Tcaron eacute -70 +KPX Tcaron ecaron -70 +KPX Tcaron ecircumflex -30 +KPX Tcaron edieresis -30 +KPX Tcaron edotaccent -70 +KPX Tcaron egrave -70 +KPX Tcaron emacron -30 +KPX Tcaron eogonek -70 +KPX Tcaron hyphen -92 +KPX Tcaron i -35 +KPX Tcaron iacute -35 +KPX Tcaron iogonek -35 +KPX Tcaron o -80 +KPX Tcaron oacute -80 +KPX Tcaron ocircumflex -80 +KPX Tcaron odieresis -80 +KPX Tcaron ograve -80 +KPX Tcaron ohungarumlaut -80 +KPX Tcaron omacron -80 +KPX Tcaron oslash -80 +KPX Tcaron otilde -80 +KPX Tcaron period -74 +KPX Tcaron r -35 +KPX Tcaron racute -35 +KPX Tcaron rcaron -35 +KPX Tcaron rcommaaccent -35 +KPX Tcaron semicolon -55 +KPX Tcaron u -45 +KPX Tcaron uacute -45 +KPX Tcaron ucircumflex -45 +KPX Tcaron udieresis -45 +KPX Tcaron ugrave -45 +KPX Tcaron uhungarumlaut -45 +KPX Tcaron umacron -45 +KPX Tcaron uogonek -45 +KPX Tcaron uring -45 +KPX Tcaron w -80 +KPX Tcaron y -80 +KPX Tcaron yacute -80 +KPX Tcaron ydieresis -80 +KPX Tcommaaccent A -93 +KPX Tcommaaccent Aacute -93 +KPX Tcommaaccent Abreve -93 +KPX Tcommaaccent Acircumflex -93 +KPX Tcommaaccent Adieresis -93 +KPX Tcommaaccent Agrave -93 +KPX Tcommaaccent Amacron -93 +KPX Tcommaaccent Aogonek -93 +KPX Tcommaaccent Aring -93 +KPX Tcommaaccent Atilde -93 +KPX Tcommaaccent O -18 +KPX Tcommaaccent Oacute -18 +KPX Tcommaaccent Ocircumflex -18 +KPX Tcommaaccent Odieresis -18 +KPX Tcommaaccent Ograve -18 +KPX Tcommaaccent Ohungarumlaut -18 +KPX Tcommaaccent Omacron -18 +KPX Tcommaaccent Oslash -18 +KPX Tcommaaccent Otilde -18 +KPX Tcommaaccent a -80 +KPX Tcommaaccent aacute -80 +KPX Tcommaaccent abreve -80 +KPX Tcommaaccent acircumflex -80 +KPX Tcommaaccent adieresis -40 +KPX Tcommaaccent agrave -40 +KPX Tcommaaccent amacron -40 +KPX Tcommaaccent aogonek -80 +KPX Tcommaaccent aring -80 +KPX Tcommaaccent atilde -40 +KPX Tcommaaccent colon -50 +KPX Tcommaaccent comma -74 +KPX Tcommaaccent e -70 +KPX Tcommaaccent eacute -70 +KPX Tcommaaccent ecaron -70 +KPX Tcommaaccent ecircumflex -30 +KPX Tcommaaccent edieresis -30 +KPX Tcommaaccent edotaccent -70 +KPX Tcommaaccent egrave -30 +KPX Tcommaaccent emacron -70 +KPX Tcommaaccent eogonek -70 +KPX Tcommaaccent hyphen -92 +KPX Tcommaaccent i -35 +KPX Tcommaaccent iacute -35 +KPX Tcommaaccent iogonek -35 +KPX Tcommaaccent o -80 +KPX Tcommaaccent oacute -80 +KPX Tcommaaccent ocircumflex -80 +KPX Tcommaaccent odieresis -80 +KPX Tcommaaccent ograve -80 +KPX Tcommaaccent ohungarumlaut -80 +KPX Tcommaaccent omacron -80 +KPX Tcommaaccent oslash -80 +KPX Tcommaaccent otilde -80 +KPX Tcommaaccent period -74 +KPX Tcommaaccent r -35 +KPX Tcommaaccent racute -35 +KPX Tcommaaccent rcaron -35 +KPX Tcommaaccent rcommaaccent -35 +KPX Tcommaaccent semicolon -55 +KPX Tcommaaccent u -45 +KPX Tcommaaccent uacute -45 +KPX Tcommaaccent ucircumflex -45 +KPX Tcommaaccent udieresis -45 +KPX Tcommaaccent ugrave -45 +KPX Tcommaaccent uhungarumlaut -45 +KPX Tcommaaccent umacron -45 +KPX Tcommaaccent uogonek -45 +KPX Tcommaaccent uring -45 +KPX Tcommaaccent w -80 +KPX Tcommaaccent y -80 +KPX Tcommaaccent yacute -80 +KPX Tcommaaccent ydieresis -80 +KPX U A -40 +KPX U Aacute -40 +KPX U Abreve -40 +KPX U Acircumflex -40 +KPX U Adieresis -40 +KPX U Agrave -40 +KPX U Amacron -40 +KPX U Aogonek -40 +KPX U Aring -40 +KPX U Atilde -40 +KPX Uacute A -40 +KPX Uacute Aacute -40 +KPX Uacute Abreve -40 +KPX Uacute Acircumflex -40 +KPX Uacute Adieresis -40 +KPX Uacute Agrave -40 +KPX Uacute Amacron -40 +KPX Uacute Aogonek -40 +KPX Uacute Aring -40 +KPX Uacute Atilde -40 +KPX Ucircumflex A -40 +KPX Ucircumflex Aacute -40 +KPX Ucircumflex Abreve -40 +KPX Ucircumflex Acircumflex -40 +KPX Ucircumflex Adieresis -40 +KPX Ucircumflex Agrave -40 +KPX Ucircumflex Amacron -40 +KPX Ucircumflex Aogonek -40 +KPX Ucircumflex Aring -40 +KPX Ucircumflex Atilde -40 +KPX Udieresis A -40 +KPX Udieresis Aacute -40 +KPX Udieresis Abreve -40 +KPX Udieresis Acircumflex -40 +KPX Udieresis Adieresis -40 +KPX Udieresis Agrave -40 +KPX Udieresis Amacron -40 +KPX Udieresis Aogonek -40 +KPX Udieresis Aring -40 +KPX Udieresis Atilde -40 +KPX Ugrave A -40 +KPX Ugrave Aacute -40 +KPX Ugrave Abreve -40 +KPX Ugrave Acircumflex -40 +KPX Ugrave Adieresis -40 +KPX Ugrave Agrave -40 +KPX Ugrave Amacron -40 +KPX Ugrave Aogonek -40 +KPX Ugrave Aring -40 +KPX Ugrave Atilde -40 +KPX Uhungarumlaut A -40 +KPX Uhungarumlaut Aacute -40 +KPX Uhungarumlaut Abreve -40 +KPX Uhungarumlaut Acircumflex -40 +KPX Uhungarumlaut Adieresis -40 +KPX Uhungarumlaut Agrave -40 +KPX Uhungarumlaut Amacron -40 +KPX Uhungarumlaut Aogonek -40 +KPX Uhungarumlaut Aring -40 +KPX Uhungarumlaut Atilde -40 +KPX Umacron A -40 +KPX Umacron Aacute -40 +KPX Umacron Abreve -40 +KPX Umacron Acircumflex -40 +KPX Umacron Adieresis -40 +KPX Umacron Agrave -40 +KPX Umacron Amacron -40 +KPX Umacron Aogonek -40 +KPX Umacron Aring -40 +KPX Umacron Atilde -40 +KPX Uogonek A -40 +KPX Uogonek Aacute -40 +KPX Uogonek Abreve -40 +KPX Uogonek Acircumflex -40 +KPX Uogonek Adieresis -40 +KPX Uogonek Agrave -40 +KPX Uogonek Amacron -40 +KPX Uogonek Aogonek -40 +KPX Uogonek Aring -40 +KPX Uogonek Atilde -40 +KPX Uring A -40 +KPX Uring Aacute -40 +KPX Uring Abreve -40 +KPX Uring Acircumflex -40 +KPX Uring Adieresis -40 +KPX Uring Agrave -40 +KPX Uring Amacron -40 +KPX Uring Aogonek -40 +KPX Uring Aring -40 +KPX Uring Atilde -40 +KPX V A -135 +KPX V Aacute -135 +KPX V Abreve -135 +KPX V Acircumflex -135 +KPX V Adieresis -135 +KPX V Agrave -135 +KPX V Amacron -135 +KPX V Aogonek -135 +KPX V Aring -135 +KPX V Atilde -135 +KPX V G -15 +KPX V Gbreve -15 +KPX V Gcommaaccent -15 +KPX V O -40 +KPX V Oacute -40 +KPX V Ocircumflex -40 +KPX V Odieresis -40 +KPX V Ograve -40 +KPX V Ohungarumlaut -40 +KPX V Omacron -40 +KPX V Oslash -40 +KPX V Otilde -40 +KPX V a -111 +KPX V aacute -111 +KPX V abreve -111 +KPX V acircumflex -71 +KPX V adieresis -71 +KPX V agrave -71 +KPX V amacron -71 +KPX V aogonek -111 +KPX V aring -111 +KPX V atilde -71 +KPX V colon -74 +KPX V comma -129 +KPX V e -111 +KPX V eacute -111 +KPX V ecaron -71 +KPX V ecircumflex -71 +KPX V edieresis -71 +KPX V edotaccent -111 +KPX V egrave -71 +KPX V emacron -71 +KPX V eogonek -111 +KPX V hyphen -100 +KPX V i -60 +KPX V iacute -60 +KPX V icircumflex -20 +KPX V idieresis -20 +KPX V igrave -20 +KPX V imacron -20 +KPX V iogonek -60 +KPX V o -129 +KPX V oacute -129 +KPX V ocircumflex -129 +KPX V odieresis -89 +KPX V ograve -89 +KPX V ohungarumlaut -129 +KPX V omacron -89 +KPX V oslash -129 +KPX V otilde -89 +KPX V period -129 +KPX V semicolon -74 +KPX V u -75 +KPX V uacute -75 +KPX V ucircumflex -75 +KPX V udieresis -75 +KPX V ugrave -75 +KPX V uhungarumlaut -75 +KPX V umacron -75 +KPX V uogonek -75 +KPX V uring -75 +KPX W A -120 +KPX W Aacute -120 +KPX W Abreve -120 +KPX W Acircumflex -120 +KPX W Adieresis -120 +KPX W Agrave -120 +KPX W Amacron -120 +KPX W Aogonek -120 +KPX W Aring -120 +KPX W Atilde -120 +KPX W O -10 +KPX W Oacute -10 +KPX W Ocircumflex -10 +KPX W Odieresis -10 +KPX W Ograve -10 +KPX W Ohungarumlaut -10 +KPX W Omacron -10 +KPX W Oslash -10 +KPX W Otilde -10 +KPX W a -80 +KPX W aacute -80 +KPX W abreve -80 +KPX W acircumflex -80 +KPX W adieresis -80 +KPX W agrave -80 +KPX W amacron -80 +KPX W aogonek -80 +KPX W aring -80 +KPX W atilde -80 +KPX W colon -37 +KPX W comma -92 +KPX W e -80 +KPX W eacute -80 +KPX W ecaron -80 +KPX W ecircumflex -80 +KPX W edieresis -40 +KPX W edotaccent -80 +KPX W egrave -40 +KPX W emacron -40 +KPX W eogonek -80 +KPX W hyphen -65 +KPX W i -40 +KPX W iacute -40 +KPX W iogonek -40 +KPX W o -80 +KPX W oacute -80 +KPX W ocircumflex -80 +KPX W odieresis -80 +KPX W ograve -80 +KPX W ohungarumlaut -80 +KPX W omacron -80 +KPX W oslash -80 +KPX W otilde -80 +KPX W period -92 +KPX W semicolon -37 +KPX W u -50 +KPX W uacute -50 +KPX W ucircumflex -50 +KPX W udieresis -50 +KPX W ugrave -50 +KPX W uhungarumlaut -50 +KPX W umacron -50 +KPX W uogonek -50 +KPX W uring -50 +KPX W y -73 +KPX W yacute -73 +KPX W ydieresis -73 +KPX Y A -120 +KPX Y Aacute -120 +KPX Y Abreve -120 +KPX Y Acircumflex -120 +KPX Y Adieresis -120 +KPX Y Agrave -120 +KPX Y Amacron -120 +KPX Y Aogonek -120 +KPX Y Aring -120 +KPX Y Atilde -120 +KPX Y O -30 +KPX Y Oacute -30 +KPX Y Ocircumflex -30 +KPX Y Odieresis -30 +KPX Y Ograve -30 +KPX Y Ohungarumlaut -30 +KPX Y Omacron -30 +KPX Y Oslash -30 +KPX Y Otilde -30 +KPX Y a -100 +KPX Y aacute -100 +KPX Y abreve -100 +KPX Y acircumflex -100 +KPX Y adieresis -60 +KPX Y agrave -60 +KPX Y amacron -60 +KPX Y aogonek -100 +KPX Y aring -100 +KPX Y atilde -60 +KPX Y colon -92 +KPX Y comma -129 +KPX Y e -100 +KPX Y eacute -100 +KPX Y ecaron -100 +KPX Y ecircumflex -100 +KPX Y edieresis -60 +KPX Y edotaccent -100 +KPX Y egrave -60 +KPX Y emacron -60 +KPX Y eogonek -100 +KPX Y hyphen -111 +KPX Y i -55 +KPX Y iacute -55 +KPX Y iogonek -55 +KPX Y o -110 +KPX Y oacute -110 +KPX Y ocircumflex -110 +KPX Y odieresis -70 +KPX Y ograve -70 +KPX Y ohungarumlaut -110 +KPX Y omacron -70 +KPX Y oslash -110 +KPX Y otilde -70 +KPX Y period -129 +KPX Y semicolon -92 +KPX Y u -111 +KPX Y uacute -111 +KPX Y ucircumflex -111 +KPX Y udieresis -71 +KPX Y ugrave -71 +KPX Y uhungarumlaut -111 +KPX Y umacron -71 +KPX Y uogonek -111 +KPX Y uring -111 +KPX Yacute A -120 +KPX Yacute Aacute -120 +KPX Yacute Abreve -120 +KPX Yacute Acircumflex -120 +KPX Yacute Adieresis -120 +KPX Yacute Agrave -120 +KPX Yacute Amacron -120 +KPX Yacute Aogonek -120 +KPX Yacute Aring -120 +KPX Yacute Atilde -120 +KPX Yacute O -30 +KPX Yacute Oacute -30 +KPX Yacute Ocircumflex -30 +KPX Yacute Odieresis -30 +KPX Yacute Ograve -30 +KPX Yacute Ohungarumlaut -30 +KPX Yacute Omacron -30 +KPX Yacute Oslash -30 +KPX Yacute Otilde -30 +KPX Yacute a -100 +KPX Yacute aacute -100 +KPX Yacute abreve -100 +KPX Yacute acircumflex -100 +KPX Yacute adieresis -60 +KPX Yacute agrave -60 +KPX Yacute amacron -60 +KPX Yacute aogonek -100 +KPX Yacute aring -100 +KPX Yacute atilde -60 +KPX Yacute colon -92 +KPX Yacute comma -129 +KPX Yacute e -100 +KPX Yacute eacute -100 +KPX Yacute ecaron -100 +KPX Yacute ecircumflex -100 +KPX Yacute edieresis -60 +KPX Yacute edotaccent -100 +KPX Yacute egrave -60 +KPX Yacute emacron -60 +KPX Yacute eogonek -100 +KPX Yacute hyphen -111 +KPX Yacute i -55 +KPX Yacute iacute -55 +KPX Yacute iogonek -55 +KPX Yacute o -110 +KPX Yacute oacute -110 +KPX Yacute ocircumflex -110 +KPX Yacute odieresis -70 +KPX Yacute ograve -70 +KPX Yacute ohungarumlaut -110 +KPX Yacute omacron -70 +KPX Yacute oslash -110 +KPX Yacute otilde -70 +KPX Yacute period -129 +KPX Yacute semicolon -92 +KPX Yacute u -111 +KPX Yacute uacute -111 +KPX Yacute ucircumflex -111 +KPX Yacute udieresis -71 +KPX Yacute ugrave -71 +KPX Yacute uhungarumlaut -111 +KPX Yacute umacron -71 +KPX Yacute uogonek -111 +KPX Yacute uring -111 +KPX Ydieresis A -120 +KPX Ydieresis Aacute -120 +KPX Ydieresis Abreve -120 +KPX Ydieresis Acircumflex -120 +KPX Ydieresis Adieresis -120 +KPX Ydieresis Agrave -120 +KPX Ydieresis Amacron -120 +KPX Ydieresis Aogonek -120 +KPX Ydieresis Aring -120 +KPX Ydieresis Atilde -120 +KPX Ydieresis O -30 +KPX Ydieresis Oacute -30 +KPX Ydieresis Ocircumflex -30 +KPX Ydieresis Odieresis -30 +KPX Ydieresis Ograve -30 +KPX Ydieresis Ohungarumlaut -30 +KPX Ydieresis Omacron -30 +KPX Ydieresis Oslash -30 +KPX Ydieresis Otilde -30 +KPX Ydieresis a -100 +KPX Ydieresis aacute -100 +KPX Ydieresis abreve -100 +KPX Ydieresis acircumflex -100 +KPX Ydieresis adieresis -60 +KPX Ydieresis agrave -60 +KPX Ydieresis amacron -60 +KPX Ydieresis aogonek -100 +KPX Ydieresis aring -100 +KPX Ydieresis atilde -100 +KPX Ydieresis colon -92 +KPX Ydieresis comma -129 +KPX Ydieresis e -100 +KPX Ydieresis eacute -100 +KPX Ydieresis ecaron -100 +KPX Ydieresis ecircumflex -100 +KPX Ydieresis edieresis -60 +KPX Ydieresis edotaccent -100 +KPX Ydieresis egrave -60 +KPX Ydieresis emacron -60 +KPX Ydieresis eogonek -100 +KPX Ydieresis hyphen -111 +KPX Ydieresis i -55 +KPX Ydieresis iacute -55 +KPX Ydieresis iogonek -55 +KPX Ydieresis o -110 +KPX Ydieresis oacute -110 +KPX Ydieresis ocircumflex -110 +KPX Ydieresis odieresis -70 +KPX Ydieresis ograve -70 +KPX Ydieresis ohungarumlaut -110 +KPX Ydieresis omacron -70 +KPX Ydieresis oslash -110 +KPX Ydieresis otilde -70 +KPX Ydieresis period -129 +KPX Ydieresis semicolon -92 +KPX Ydieresis u -111 +KPX Ydieresis uacute -111 +KPX Ydieresis ucircumflex -111 +KPX Ydieresis udieresis -71 +KPX Ydieresis ugrave -71 +KPX Ydieresis uhungarumlaut -111 +KPX Ydieresis umacron -71 +KPX Ydieresis uogonek -111 +KPX Ydieresis uring -111 +KPX a v -20 +KPX a w -15 +KPX aacute v -20 +KPX aacute w -15 +KPX abreve v -20 +KPX abreve w -15 +KPX acircumflex v -20 +KPX acircumflex w -15 +KPX adieresis v -20 +KPX adieresis w -15 +KPX agrave v -20 +KPX agrave w -15 +KPX amacron v -20 +KPX amacron w -15 +KPX aogonek v -20 +KPX aogonek w -15 +KPX aring v -20 +KPX aring w -15 +KPX atilde v -20 +KPX atilde w -15 +KPX b period -40 +KPX b u -20 +KPX b uacute -20 +KPX b ucircumflex -20 +KPX b udieresis -20 +KPX b ugrave -20 +KPX b uhungarumlaut -20 +KPX b umacron -20 +KPX b uogonek -20 +KPX b uring -20 +KPX b v -15 +KPX c y -15 +KPX c yacute -15 +KPX c ydieresis -15 +KPX cacute y -15 +KPX cacute yacute -15 +KPX cacute ydieresis -15 +KPX ccaron y -15 +KPX ccaron yacute -15 +KPX ccaron ydieresis -15 +KPX ccedilla y -15 +KPX ccedilla yacute -15 +KPX ccedilla ydieresis -15 +KPX comma quotedblright -70 +KPX comma quoteright -70 +KPX e g -15 +KPX e gbreve -15 +KPX e gcommaaccent -15 +KPX e v -25 +KPX e w -25 +KPX e x -15 +KPX e y -15 +KPX e yacute -15 +KPX e ydieresis -15 +KPX eacute g -15 +KPX eacute gbreve -15 +KPX eacute gcommaaccent -15 +KPX eacute v -25 +KPX eacute w -25 +KPX eacute x -15 +KPX eacute y -15 +KPX eacute yacute -15 +KPX eacute ydieresis -15 +KPX ecaron g -15 +KPX ecaron gbreve -15 +KPX ecaron gcommaaccent -15 +KPX ecaron v -25 +KPX ecaron w -25 +KPX ecaron x -15 +KPX ecaron y -15 +KPX ecaron yacute -15 +KPX ecaron ydieresis -15 +KPX ecircumflex g -15 +KPX ecircumflex gbreve -15 +KPX ecircumflex gcommaaccent -15 +KPX ecircumflex v -25 +KPX ecircumflex w -25 +KPX ecircumflex x -15 +KPX ecircumflex y -15 +KPX ecircumflex yacute -15 +KPX ecircumflex ydieresis -15 +KPX edieresis g -15 +KPX edieresis gbreve -15 +KPX edieresis gcommaaccent -15 +KPX edieresis v -25 +KPX edieresis w -25 +KPX edieresis x -15 +KPX edieresis y -15 +KPX edieresis yacute -15 +KPX edieresis ydieresis -15 +KPX edotaccent g -15 +KPX edotaccent gbreve -15 +KPX edotaccent gcommaaccent -15 +KPX edotaccent v -25 +KPX edotaccent w -25 +KPX edotaccent x -15 +KPX edotaccent y -15 +KPX edotaccent yacute -15 +KPX edotaccent ydieresis -15 +KPX egrave g -15 +KPX egrave gbreve -15 +KPX egrave gcommaaccent -15 +KPX egrave v -25 +KPX egrave w -25 +KPX egrave x -15 +KPX egrave y -15 +KPX egrave yacute -15 +KPX egrave ydieresis -15 +KPX emacron g -15 +KPX emacron gbreve -15 +KPX emacron gcommaaccent -15 +KPX emacron v -25 +KPX emacron w -25 +KPX emacron x -15 +KPX emacron y -15 +KPX emacron yacute -15 +KPX emacron ydieresis -15 +KPX eogonek g -15 +KPX eogonek gbreve -15 +KPX eogonek gcommaaccent -15 +KPX eogonek v -25 +KPX eogonek w -25 +KPX eogonek x -15 +KPX eogonek y -15 +KPX eogonek yacute -15 +KPX eogonek ydieresis -15 +KPX f a -10 +KPX f aacute -10 +KPX f abreve -10 +KPX f acircumflex -10 +KPX f adieresis -10 +KPX f agrave -10 +KPX f amacron -10 +KPX f aogonek -10 +KPX f aring -10 +KPX f atilde -10 +KPX f dotlessi -50 +KPX f f -25 +KPX f i -20 +KPX f iacute -20 +KPX f quoteright 55 +KPX g a -5 +KPX g aacute -5 +KPX g abreve -5 +KPX g acircumflex -5 +KPX g adieresis -5 +KPX g agrave -5 +KPX g amacron -5 +KPX g aogonek -5 +KPX g aring -5 +KPX g atilde -5 +KPX gbreve a -5 +KPX gbreve aacute -5 +KPX gbreve abreve -5 +KPX gbreve acircumflex -5 +KPX gbreve adieresis -5 +KPX gbreve agrave -5 +KPX gbreve amacron -5 +KPX gbreve aogonek -5 +KPX gbreve aring -5 +KPX gbreve atilde -5 +KPX gcommaaccent a -5 +KPX gcommaaccent aacute -5 +KPX gcommaaccent abreve -5 +KPX gcommaaccent acircumflex -5 +KPX gcommaaccent adieresis -5 +KPX gcommaaccent agrave -5 +KPX gcommaaccent amacron -5 +KPX gcommaaccent aogonek -5 +KPX gcommaaccent aring -5 +KPX gcommaaccent atilde -5 +KPX h y -5 +KPX h yacute -5 +KPX h ydieresis -5 +KPX i v -25 +KPX iacute v -25 +KPX icircumflex v -25 +KPX idieresis v -25 +KPX igrave v -25 +KPX imacron v -25 +KPX iogonek v -25 +KPX k e -10 +KPX k eacute -10 +KPX k ecaron -10 +KPX k ecircumflex -10 +KPX k edieresis -10 +KPX k edotaccent -10 +KPX k egrave -10 +KPX k emacron -10 +KPX k eogonek -10 +KPX k o -10 +KPX k oacute -10 +KPX k ocircumflex -10 +KPX k odieresis -10 +KPX k ograve -10 +KPX k ohungarumlaut -10 +KPX k omacron -10 +KPX k oslash -10 +KPX k otilde -10 +KPX k y -15 +KPX k yacute -15 +KPX k ydieresis -15 +KPX kcommaaccent e -10 +KPX kcommaaccent eacute -10 +KPX kcommaaccent ecaron -10 +KPX kcommaaccent ecircumflex -10 +KPX kcommaaccent edieresis -10 +KPX kcommaaccent edotaccent -10 +KPX kcommaaccent egrave -10 +KPX kcommaaccent emacron -10 +KPX kcommaaccent eogonek -10 +KPX kcommaaccent o -10 +KPX kcommaaccent oacute -10 +KPX kcommaaccent ocircumflex -10 +KPX kcommaaccent odieresis -10 +KPX kcommaaccent ograve -10 +KPX kcommaaccent ohungarumlaut -10 +KPX kcommaaccent omacron -10 +KPX kcommaaccent oslash -10 +KPX kcommaaccent otilde -10 +KPX kcommaaccent y -15 +KPX kcommaaccent yacute -15 +KPX kcommaaccent ydieresis -15 +KPX l w -10 +KPX lacute w -10 +KPX lcommaaccent w -10 +KPX lslash w -10 +KPX n v -40 +KPX n y -15 +KPX n yacute -15 +KPX n ydieresis -15 +KPX nacute v -40 +KPX nacute y -15 +KPX nacute yacute -15 +KPX nacute ydieresis -15 +KPX ncaron v -40 +KPX ncaron y -15 +KPX ncaron yacute -15 +KPX ncaron ydieresis -15 +KPX ncommaaccent v -40 +KPX ncommaaccent y -15 +KPX ncommaaccent yacute -15 +KPX ncommaaccent ydieresis -15 +KPX ntilde v -40 +KPX ntilde y -15 +KPX ntilde yacute -15 +KPX ntilde ydieresis -15 +KPX o v -15 +KPX o w -25 +KPX o y -10 +KPX o yacute -10 +KPX o ydieresis -10 +KPX oacute v -15 +KPX oacute w -25 +KPX oacute y -10 +KPX oacute yacute -10 +KPX oacute ydieresis -10 +KPX ocircumflex v -15 +KPX ocircumflex w -25 +KPX ocircumflex y -10 +KPX ocircumflex yacute -10 +KPX ocircumflex ydieresis -10 +KPX odieresis v -15 +KPX odieresis w -25 +KPX odieresis y -10 +KPX odieresis yacute -10 +KPX odieresis ydieresis -10 +KPX ograve v -15 +KPX ograve w -25 +KPX ograve y -10 +KPX ograve yacute -10 +KPX ograve ydieresis -10 +KPX ohungarumlaut v -15 +KPX ohungarumlaut w -25 +KPX ohungarumlaut y -10 +KPX ohungarumlaut yacute -10 +KPX ohungarumlaut ydieresis -10 +KPX omacron v -15 +KPX omacron w -25 +KPX omacron y -10 +KPX omacron yacute -10 +KPX omacron ydieresis -10 +KPX oslash v -15 +KPX oslash w -25 +KPX oslash y -10 +KPX oslash yacute -10 +KPX oslash ydieresis -10 +KPX otilde v -15 +KPX otilde w -25 +KPX otilde y -10 +KPX otilde yacute -10 +KPX otilde ydieresis -10 +KPX p y -10 +KPX p yacute -10 +KPX p ydieresis -10 +KPX period quotedblright -70 +KPX period quoteright -70 +KPX quotedblleft A -80 +KPX quotedblleft Aacute -80 +KPX quotedblleft Abreve -80 +KPX quotedblleft Acircumflex -80 +KPX quotedblleft Adieresis -80 +KPX quotedblleft Agrave -80 +KPX quotedblleft Amacron -80 +KPX quotedblleft Aogonek -80 +KPX quotedblleft Aring -80 +KPX quotedblleft Atilde -80 +KPX quoteleft A -80 +KPX quoteleft Aacute -80 +KPX quoteleft Abreve -80 +KPX quoteleft Acircumflex -80 +KPX quoteleft Adieresis -80 +KPX quoteleft Agrave -80 +KPX quoteleft Amacron -80 +KPX quoteleft Aogonek -80 +KPX quoteleft Aring -80 +KPX quoteleft Atilde -80 +KPX quoteleft quoteleft -74 +KPX quoteright d -50 +KPX quoteright dcroat -50 +KPX quoteright l -10 +KPX quoteright lacute -10 +KPX quoteright lcommaaccent -10 +KPX quoteright lslash -10 +KPX quoteright quoteright -74 +KPX quoteright r -50 +KPX quoteright racute -50 +KPX quoteright rcaron -50 +KPX quoteright rcommaaccent -50 +KPX quoteright s -55 +KPX quoteright sacute -55 +KPX quoteright scaron -55 +KPX quoteright scedilla -55 +KPX quoteright scommaaccent -55 +KPX quoteright space -74 +KPX quoteright t -18 +KPX quoteright tcommaaccent -18 +KPX quoteright v -50 +KPX r comma -40 +KPX r g -18 +KPX r gbreve -18 +KPX r gcommaaccent -18 +KPX r hyphen -20 +KPX r period -55 +KPX racute comma -40 +KPX racute g -18 +KPX racute gbreve -18 +KPX racute gcommaaccent -18 +KPX racute hyphen -20 +KPX racute period -55 +KPX rcaron comma -40 +KPX rcaron g -18 +KPX rcaron gbreve -18 +KPX rcaron gcommaaccent -18 +KPX rcaron hyphen -20 +KPX rcaron period -55 +KPX rcommaaccent comma -40 +KPX rcommaaccent g -18 +KPX rcommaaccent gbreve -18 +KPX rcommaaccent gcommaaccent -18 +KPX rcommaaccent hyphen -20 +KPX rcommaaccent period -55 +KPX space A -55 +KPX space Aacute -55 +KPX space Abreve -55 +KPX space Acircumflex -55 +KPX space Adieresis -55 +KPX space Agrave -55 +KPX space Amacron -55 +KPX space Aogonek -55 +KPX space Aring -55 +KPX space Atilde -55 +KPX space T -18 +KPX space Tcaron -18 +KPX space Tcommaaccent -18 +KPX space V -50 +KPX space W -30 +KPX space Y -90 +KPX space Yacute -90 +KPX space Ydieresis -90 +KPX v a -25 +KPX v aacute -25 +KPX v abreve -25 +KPX v acircumflex -25 +KPX v adieresis -25 +KPX v agrave -25 +KPX v amacron -25 +KPX v aogonek -25 +KPX v aring -25 +KPX v atilde -25 +KPX v comma -65 +KPX v e -15 +KPX v eacute -15 +KPX v ecaron -15 +KPX v ecircumflex -15 +KPX v edieresis -15 +KPX v edotaccent -15 +KPX v egrave -15 +KPX v emacron -15 +KPX v eogonek -15 +KPX v o -20 +KPX v oacute -20 +KPX v ocircumflex -20 +KPX v odieresis -20 +KPX v ograve -20 +KPX v ohungarumlaut -20 +KPX v omacron -20 +KPX v oslash -20 +KPX v otilde -20 +KPX v period -65 +KPX w a -10 +KPX w aacute -10 +KPX w abreve -10 +KPX w acircumflex -10 +KPX w adieresis -10 +KPX w agrave -10 +KPX w amacron -10 +KPX w aogonek -10 +KPX w aring -10 +KPX w atilde -10 +KPX w comma -65 +KPX w o -10 +KPX w oacute -10 +KPX w ocircumflex -10 +KPX w odieresis -10 +KPX w ograve -10 +KPX w ohungarumlaut -10 +KPX w omacron -10 +KPX w oslash -10 +KPX w otilde -10 +KPX w period -65 +KPX x e -15 +KPX x eacute -15 +KPX x ecaron -15 +KPX x ecircumflex -15 +KPX x edieresis -15 +KPX x edotaccent -15 +KPX x egrave -15 +KPX x emacron -15 +KPX x eogonek -15 +KPX y comma -65 +KPX y period -65 +KPX yacute comma -65 +KPX yacute period -65 +KPX ydieresis comma -65 +KPX ydieresis period -65 +EndKernPairs +EndKernData +EndFontMetrics diff --git a/internal/corefont/Core14_AFMs/ZapfDingbats.afm b/internal/corefont/Core14_AFMs/ZapfDingbats.afm new file mode 100644 index 0000000000000000000000000000000000000000..b2745053e4086c1a5ea7b74e460e62526b64009a --- /dev/null +++ b/internal/corefont/Core14_AFMs/ZapfDingbats.afm @@ -0,0 +1,225 @@ +StartFontMetrics 4.1 +Comment Copyright (c) 1985, 1987, 1988, 1989, 1997 Adobe Systems Incorporated. All Rights Reserved. +Comment Creation Date: Thu May 1 15:14:13 1997 +Comment UniqueID 43082 +Comment VMusage 45775 55535 +FontName ZapfDingbats +FullName ITC Zapf Dingbats +FamilyName ZapfDingbats +Weight Medium +ItalicAngle 0 +IsFixedPitch false +CharacterSet Special +FontBBox -1 -143 981 820 +UnderlinePosition -100 +UnderlineThickness 50 +Version 002.000 +Notice Copyright (c) 1985, 1987, 1988, 1989, 1997 Adobe Systems Incorporated. All Rights Reserved.ITC Zapf Dingbats is a registered trademark of International Typeface Corporation. +EncodingScheme FontSpecific +StdHW 28 +StdVW 90 +StartCharMetrics 202 +C 32 ; WX 278 ; N space ; B 0 0 0 0 ; +C 33 ; WX 974 ; N a1 ; B 35 72 939 621 ; +C 34 ; WX 961 ; N a2 ; B 35 81 927 611 ; +C 35 ; WX 974 ; N a202 ; B 35 72 939 621 ; +C 36 ; WX 980 ; N a3 ; B 35 0 945 692 ; +C 37 ; WX 719 ; N a4 ; B 34 139 685 566 ; +C 38 ; WX 789 ; N a5 ; B 35 -14 755 705 ; +C 39 ; WX 790 ; N a119 ; B 35 -14 755 705 ; +C 40 ; WX 791 ; N a118 ; B 35 -13 761 705 ; +C 41 ; WX 690 ; N a117 ; B 34 138 655 553 ; +C 42 ; WX 960 ; N a11 ; B 35 123 925 568 ; +C 43 ; WX 939 ; N a12 ; B 35 134 904 559 ; +C 44 ; WX 549 ; N a13 ; B 29 -11 516 705 ; +C 45 ; WX 855 ; N a14 ; B 34 59 820 632 ; +C 46 ; WX 911 ; N a15 ; B 35 50 876 642 ; +C 47 ; WX 933 ; N a16 ; B 35 139 899 550 ; +C 48 ; WX 911 ; N a105 ; B 35 50 876 642 ; +C 49 ; WX 945 ; N a17 ; B 35 139 909 553 ; +C 50 ; WX 974 ; N a18 ; B 35 104 938 587 ; +C 51 ; WX 755 ; N a19 ; B 34 -13 721 705 ; +C 52 ; WX 846 ; N a20 ; B 36 -14 811 705 ; +C 53 ; WX 762 ; N a21 ; B 35 0 727 692 ; +C 54 ; WX 761 ; N a22 ; B 35 0 727 692 ; +C 55 ; WX 571 ; N a23 ; B -1 -68 571 661 ; +C 56 ; WX 677 ; N a24 ; B 36 -13 642 705 ; +C 57 ; WX 763 ; N a25 ; B 35 0 728 692 ; +C 58 ; WX 760 ; N a26 ; B 35 0 726 692 ; +C 59 ; WX 759 ; N a27 ; B 35 0 725 692 ; +C 60 ; WX 754 ; N a28 ; B 35 0 720 692 ; +C 61 ; WX 494 ; N a6 ; B 35 0 460 692 ; +C 62 ; WX 552 ; N a7 ; B 35 0 517 692 ; +C 63 ; WX 537 ; N a8 ; B 35 0 503 692 ; +C 64 ; WX 577 ; N a9 ; B 35 96 542 596 ; +C 65 ; WX 692 ; N a10 ; B 35 -14 657 705 ; +C 66 ; WX 786 ; N a29 ; B 35 -14 751 705 ; +C 67 ; WX 788 ; N a30 ; B 35 -14 752 705 ; +C 68 ; WX 788 ; N a31 ; B 35 -14 753 705 ; +C 69 ; WX 790 ; N a32 ; B 35 -14 756 705 ; +C 70 ; WX 793 ; N a33 ; B 35 -13 759 705 ; +C 71 ; WX 794 ; N a34 ; B 35 -13 759 705 ; +C 72 ; WX 816 ; N a35 ; B 35 -14 782 705 ; +C 73 ; WX 823 ; N a36 ; B 35 -14 787 705 ; +C 74 ; WX 789 ; N a37 ; B 35 -14 754 705 ; +C 75 ; WX 841 ; N a38 ; B 35 -14 807 705 ; +C 76 ; WX 823 ; N a39 ; B 35 -14 789 705 ; +C 77 ; WX 833 ; N a40 ; B 35 -14 798 705 ; +C 78 ; WX 816 ; N a41 ; B 35 -13 782 705 ; +C 79 ; WX 831 ; N a42 ; B 35 -14 796 705 ; +C 80 ; WX 923 ; N a43 ; B 35 -14 888 705 ; +C 81 ; WX 744 ; N a44 ; B 35 0 710 692 ; +C 82 ; WX 723 ; N a45 ; B 35 0 688 692 ; +C 83 ; WX 749 ; N a46 ; B 35 0 714 692 ; +C 84 ; WX 790 ; N a47 ; B 34 -14 756 705 ; +C 85 ; WX 792 ; N a48 ; B 35 -14 758 705 ; +C 86 ; WX 695 ; N a49 ; B 35 -14 661 706 ; +C 87 ; WX 776 ; N a50 ; B 35 -6 741 699 ; +C 88 ; WX 768 ; N a51 ; B 35 -7 734 699 ; +C 89 ; WX 792 ; N a52 ; B 35 -14 757 705 ; +C 90 ; WX 759 ; N a53 ; B 35 0 725 692 ; +C 91 ; WX 707 ; N a54 ; B 35 -13 672 704 ; +C 92 ; WX 708 ; N a55 ; B 35 -14 672 705 ; +C 93 ; WX 682 ; N a56 ; B 35 -14 647 705 ; +C 94 ; WX 701 ; N a57 ; B 35 -14 666 705 ; +C 95 ; WX 826 ; N a58 ; B 35 -14 791 705 ; +C 96 ; WX 815 ; N a59 ; B 35 -14 780 705 ; +C 97 ; WX 789 ; N a60 ; B 35 -14 754 705 ; +C 98 ; WX 789 ; N a61 ; B 35 -14 754 705 ; +C 99 ; WX 707 ; N a62 ; B 34 -14 673 705 ; +C 100 ; WX 687 ; N a63 ; B 36 0 651 692 ; +C 101 ; WX 696 ; N a64 ; B 35 0 661 691 ; +C 102 ; WX 689 ; N a65 ; B 35 0 655 692 ; +C 103 ; WX 786 ; N a66 ; B 34 -14 751 705 ; +C 104 ; WX 787 ; N a67 ; B 35 -14 752 705 ; +C 105 ; WX 713 ; N a68 ; B 35 -14 678 705 ; +C 106 ; WX 791 ; N a69 ; B 35 -14 756 705 ; +C 107 ; WX 785 ; N a70 ; B 36 -14 751 705 ; +C 108 ; WX 791 ; N a71 ; B 35 -14 757 705 ; +C 109 ; WX 873 ; N a72 ; B 35 -14 838 705 ; +C 110 ; WX 761 ; N a73 ; B 35 0 726 692 ; +C 111 ; WX 762 ; N a74 ; B 35 0 727 692 ; +C 112 ; WX 762 ; N a203 ; B 35 0 727 692 ; +C 113 ; WX 759 ; N a75 ; B 35 0 725 692 ; +C 114 ; WX 759 ; N a204 ; B 35 0 725 692 ; +C 115 ; WX 892 ; N a76 ; B 35 0 858 705 ; +C 116 ; WX 892 ; N a77 ; B 35 -14 858 692 ; +C 117 ; WX 788 ; N a78 ; B 35 -14 754 705 ; +C 118 ; WX 784 ; N a79 ; B 35 -14 749 705 ; +C 119 ; WX 438 ; N a81 ; B 35 -14 403 705 ; +C 120 ; WX 138 ; N a82 ; B 35 0 104 692 ; +C 121 ; WX 277 ; N a83 ; B 35 0 242 692 ; +C 122 ; WX 415 ; N a84 ; B 35 0 380 692 ; +C 123 ; WX 392 ; N a97 ; B 35 263 357 705 ; +C 124 ; WX 392 ; N a98 ; B 34 263 357 705 ; +C 125 ; WX 668 ; N a99 ; B 35 263 633 705 ; +C 126 ; WX 668 ; N a100 ; B 36 263 634 705 ; +C 128 ; WX 390 ; N a89 ; B 35 -14 356 705 ; +C 129 ; WX 390 ; N a90 ; B 35 -14 355 705 ; +C 130 ; WX 317 ; N a93 ; B 35 0 283 692 ; +C 131 ; WX 317 ; N a94 ; B 35 0 283 692 ; +C 132 ; WX 276 ; N a91 ; B 35 0 242 692 ; +C 133 ; WX 276 ; N a92 ; B 35 0 242 692 ; +C 134 ; WX 509 ; N a205 ; B 35 0 475 692 ; +C 135 ; WX 509 ; N a85 ; B 35 0 475 692 ; +C 136 ; WX 410 ; N a206 ; B 35 0 375 692 ; +C 137 ; WX 410 ; N a86 ; B 35 0 375 692 ; +C 138 ; WX 234 ; N a87 ; B 35 -14 199 705 ; +C 139 ; WX 234 ; N a88 ; B 35 -14 199 705 ; +C 140 ; WX 334 ; N a95 ; B 35 0 299 692 ; +C 141 ; WX 334 ; N a96 ; B 35 0 299 692 ; +C 161 ; WX 732 ; N a101 ; B 35 -143 697 806 ; +C 162 ; WX 544 ; N a102 ; B 56 -14 488 706 ; +C 163 ; WX 544 ; N a103 ; B 34 -14 508 705 ; +C 164 ; WX 910 ; N a104 ; B 35 40 875 651 ; +C 165 ; WX 667 ; N a106 ; B 35 -14 633 705 ; +C 166 ; WX 760 ; N a107 ; B 35 -14 726 705 ; +C 167 ; WX 760 ; N a108 ; B 0 121 758 569 ; +C 168 ; WX 776 ; N a112 ; B 35 0 741 705 ; +C 169 ; WX 595 ; N a111 ; B 34 -14 560 705 ; +C 170 ; WX 694 ; N a110 ; B 35 -14 659 705 ; +C 171 ; WX 626 ; N a109 ; B 34 0 591 705 ; +C 172 ; WX 788 ; N a120 ; B 35 -14 754 705 ; +C 173 ; WX 788 ; N a121 ; B 35 -14 754 705 ; +C 174 ; WX 788 ; N a122 ; B 35 -14 754 705 ; +C 175 ; WX 788 ; N a123 ; B 35 -14 754 705 ; +C 176 ; WX 788 ; N a124 ; B 35 -14 754 705 ; +C 177 ; WX 788 ; N a125 ; B 35 -14 754 705 ; +C 178 ; WX 788 ; N a126 ; B 35 -14 754 705 ; +C 179 ; WX 788 ; N a127 ; B 35 -14 754 705 ; +C 180 ; WX 788 ; N a128 ; B 35 -14 754 705 ; +C 181 ; WX 788 ; N a129 ; B 35 -14 754 705 ; +C 182 ; WX 788 ; N a130 ; B 35 -14 754 705 ; +C 183 ; WX 788 ; N a131 ; B 35 -14 754 705 ; +C 184 ; WX 788 ; N a132 ; B 35 -14 754 705 ; +C 185 ; WX 788 ; N a133 ; B 35 -14 754 705 ; +C 186 ; WX 788 ; N a134 ; B 35 -14 754 705 ; +C 187 ; WX 788 ; N a135 ; B 35 -14 754 705 ; +C 188 ; WX 788 ; N a136 ; B 35 -14 754 705 ; +C 189 ; WX 788 ; N a137 ; B 35 -14 754 705 ; +C 190 ; WX 788 ; N a138 ; B 35 -14 754 705 ; +C 191 ; WX 788 ; N a139 ; B 35 -14 754 705 ; +C 192 ; WX 788 ; N a140 ; B 35 -14 754 705 ; +C 193 ; WX 788 ; N a141 ; B 35 -14 754 705 ; +C 194 ; WX 788 ; N a142 ; B 35 -14 754 705 ; +C 195 ; WX 788 ; N a143 ; B 35 -14 754 705 ; +C 196 ; WX 788 ; N a144 ; B 35 -14 754 705 ; +C 197 ; WX 788 ; N a145 ; B 35 -14 754 705 ; +C 198 ; WX 788 ; N a146 ; B 35 -14 754 705 ; +C 199 ; WX 788 ; N a147 ; B 35 -14 754 705 ; +C 200 ; WX 788 ; N a148 ; B 35 -14 754 705 ; +C 201 ; WX 788 ; N a149 ; B 35 -14 754 705 ; +C 202 ; WX 788 ; N a150 ; B 35 -14 754 705 ; +C 203 ; WX 788 ; N a151 ; B 35 -14 754 705 ; +C 204 ; WX 788 ; N a152 ; B 35 -14 754 705 ; +C 205 ; WX 788 ; N a153 ; B 35 -14 754 705 ; +C 206 ; WX 788 ; N a154 ; B 35 -14 754 705 ; +C 207 ; WX 788 ; N a155 ; B 35 -14 754 705 ; +C 208 ; WX 788 ; N a156 ; B 35 -14 754 705 ; +C 209 ; WX 788 ; N a157 ; B 35 -14 754 705 ; +C 210 ; WX 788 ; N a158 ; B 35 -14 754 705 ; +C 211 ; WX 788 ; N a159 ; B 35 -14 754 705 ; +C 212 ; WX 894 ; N a160 ; B 35 58 860 634 ; +C 213 ; WX 838 ; N a161 ; B 35 152 803 540 ; +C 214 ; WX 1016 ; N a163 ; B 34 152 981 540 ; +C 215 ; WX 458 ; N a164 ; B 35 -127 422 820 ; +C 216 ; WX 748 ; N a196 ; B 35 94 698 597 ; +C 217 ; WX 924 ; N a165 ; B 35 140 890 552 ; +C 218 ; WX 748 ; N a192 ; B 35 94 698 597 ; +C 219 ; WX 918 ; N a166 ; B 35 166 884 526 ; +C 220 ; WX 927 ; N a167 ; B 35 32 892 660 ; +C 221 ; WX 928 ; N a168 ; B 35 129 891 562 ; +C 222 ; WX 928 ; N a169 ; B 35 128 893 563 ; +C 223 ; WX 834 ; N a170 ; B 35 155 799 537 ; +C 224 ; WX 873 ; N a171 ; B 35 93 838 599 ; +C 225 ; WX 828 ; N a172 ; B 35 104 791 588 ; +C 226 ; WX 924 ; N a173 ; B 35 98 889 594 ; +C 227 ; WX 924 ; N a162 ; B 35 98 889 594 ; +C 228 ; WX 917 ; N a174 ; B 35 0 882 692 ; +C 229 ; WX 930 ; N a175 ; B 35 84 896 608 ; +C 230 ; WX 931 ; N a176 ; B 35 84 896 608 ; +C 231 ; WX 463 ; N a177 ; B 35 -99 429 791 ; +C 232 ; WX 883 ; N a178 ; B 35 71 848 623 ; +C 233 ; WX 836 ; N a179 ; B 35 44 802 648 ; +C 234 ; WX 836 ; N a193 ; B 35 44 802 648 ; +C 235 ; WX 867 ; N a180 ; B 35 101 832 591 ; +C 236 ; WX 867 ; N a199 ; B 35 101 832 591 ; +C 237 ; WX 696 ; N a181 ; B 35 44 661 648 ; +C 238 ; WX 696 ; N a200 ; B 35 44 661 648 ; +C 239 ; WX 874 ; N a182 ; B 35 77 840 619 ; +C 241 ; WX 874 ; N a201 ; B 35 73 840 615 ; +C 242 ; WX 760 ; N a183 ; B 35 0 725 692 ; +C 243 ; WX 946 ; N a184 ; B 35 160 911 533 ; +C 244 ; WX 771 ; N a197 ; B 34 37 736 655 ; +C 245 ; WX 865 ; N a185 ; B 35 207 830 481 ; +C 246 ; WX 771 ; N a194 ; B 34 37 736 655 ; +C 247 ; WX 888 ; N a198 ; B 34 -19 853 712 ; +C 248 ; WX 967 ; N a186 ; B 35 124 932 568 ; +C 249 ; WX 888 ; N a195 ; B 34 -19 853 712 ; +C 250 ; WX 831 ; N a187 ; B 35 113 796 579 ; +C 251 ; WX 873 ; N a188 ; B 36 118 838 578 ; +C 252 ; WX 927 ; N a189 ; B 35 150 891 542 ; +C 253 ; WX 970 ; N a190 ; B 35 76 931 616 ; +C 254 ; WX 918 ; N a191 ; B 34 99 884 593 ; +EndCharMetrics +EndFontMetrics diff --git a/internal/corefont/metrics/gen.go b/internal/corefont/metrics/gen.go new file mode 100644 index 0000000000000000000000000000000000000000..45163b9b09ae9deb4aff1c6fc3089228537d25a9 --- /dev/null +++ b/internal/corefont/metrics/gen.go @@ -0,0 +1,810 @@ +// Copyright 2019 The pdfcpu Authors. +// +// 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 +// +// 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. + +//go:build ignore +// +build ignore + +package main + +import ( + "bufio" + "bytes" + "flag" + "fmt" + "go/format" + "log" + "os" + "path/filepath" + "sort" + "strconv" + "strings" +) + +var debug = flag.Bool("debug", false, "") + +func main() { + flag.Parse() + + // Generate standard.go. + { + w := &bytes.Buffer{} + w.WriteString(header) + writeWinAnsiGlyphMap(w) + writeSymbolGlyphMap(w) + writeZapfDingbatsGlyphMap(w) + writeCoreFontMetrics(w) + finish(w, "standard.go") + } + +} + +func writeWinAnsiGlyphMap(w *bytes.Buffer) { + s := `// WinAnsiGlyphMap is a glyph lookup table for CP1252 character codes. + // See Annex D.2 Latin Character Set and Encodings. + var WinAnsiGlyphMap = map[int]string { + ` + writeGlyphMap(w, s, winAnsiGlyphMap) +} + +func writeSymbolGlyphMap(w *bytes.Buffer) { + s := `// SymbolGlyphMap is a glyph lookup table for Symbol character codes. + // See Annex D.5 Symbol Set and Encoding. + var SymbolGlyphMap = map[int]string { + ` + writeGlyphMap(w, s, symbolGlyphMap) + +} + +func writeZapfDingbatsGlyphMap(w *bytes.Buffer) { + s := `// ZapfDingbatsGlyphMap is a glyph lookup table for ZapfDingbats character codes. + // See Annex D.6 ZapfDingbats Set and Encoding + var ZapfDingbatsGlyphMap = map[int]string { + ` + writeGlyphMap(w, s, zapfDingbatsGlyphMap) +} + +func writeGlyphMap(w *bytes.Buffer, varDec string, m map[int]string) { + w.WriteString(varDec) + keys := make([]int, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Ints(keys) + for _, k := range keys { + fmt.Fprintf(w, "%d: \"%s\", // %#U\n", k, m[k], rune(k)) + } + w.WriteString("}\n\n") +} + +func writeCoreFontMetrics(w *bytes.Buffer) { + s := `type fontMetrics struct { + FBox *types.Rectangle // font box + W map[string]int // glyph widths + } + + // CoreFontMetrics represents font metrics for the Adobe standard type 1 core fonts. + var CoreFontMetrics = map[string]fontMetrics{ + ` + w.WriteString(s) + dir := "../Core14_AFMs" + files, err := os.ReadDir(dir) + if err != nil { + log.Fatal(err) + } + for _, f := range files { + if !strings.HasSuffix(f.Name(), ".afm") { + continue + } + writeFontMetrics(w, dir, f.Name()) + } + w.WriteString("}") +} + +func writeFontBBox(w *bytes.Buffer, ss []string) { + if len(ss) != 5 { + panic("corrupt .afm file!") + } + f1, err := strconv.ParseFloat(ss[1], 64) + if err != nil { + log.Fatal(err) + } + f2, err := strconv.ParseFloat(ss[2], 64) + if err != nil { + log.Fatal(err) + } + f3, err := strconv.ParseFloat(ss[3], 64) + if err != nil { + log.Fatal(err) + } + f4, err := strconv.ParseFloat(ss[4], 64) + if err != nil { + log.Fatal(err) + } + fmt.Fprintf(w, "types.NewRectangle(%.1f, %.1f, %.1f, %.1f),\n", f1, f2, f3, f4) +} + +func writeFontMetrics(w *bytes.Buffer, dir, fileName string) { + fmt.Fprintf(w, "\"%s\": {\n", fileName[:len(fileName)-4]) + f, err := os.Open(filepath.Join(dir, fileName)) + if err != nil { + log.Fatal(err) + } + defer f.Close() + s := bufio.NewScanner(f) + isHeader := true + var headerDigested bool + for s.Scan() { + ss := strings.Fields(s.Text()) + if isHeader { + switch ss[0] { + case "FontBBox": + writeFontBBox(w, ss) + headerDigested = true + case "StartCharMetrics": + if !headerDigested { + panic("corrupt .afm file!") + } + isHeader = false + w.WriteString("map[string]int{") + } + continue + } + switch ss[0] { + case "C": + if len(ss) < 8 { + panic("corrupt .afm file!") + } + i, err := strconv.Atoi(ss[4]) + if err != nil { + log.Fatal(err) + } + fmt.Fprintf(w, "\"%s\": %d, ", ss[7], i) + case "EndCharMetrics": + w.WriteString("},\n") + break + } + } + if err := s.Err(); err != nil { + log.Fatal(err) + } + w.WriteString("\n},\n") +} + +const header = `// generated by "go run gen.go". DO NOT EDIT. + +package metrics + +import ( + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) +` + +func finish(w *bytes.Buffer, filename string) { + if *debug { + os.Stdout.Write(w.Bytes()) + return + } + out, err := format.Source(w.Bytes()) + if err != nil { + log.Fatalf("format.Source: %v", err) + } + if err := os.WriteFile(filename, out, 0660); err != nil { + log.Fatalf("os.WriteFile: %v", err) + } +} + +// See Annex D.2 Latin Character Set and Encodings +var winAnsiGlyphMap = map[int]string{ + 0101: "A", + 0306: "AE", + 0301: "Aacute", + 0302: "Acircumflex", + 0304: "Adieresis", + 0300: "Agrave", + 0305: "Aring", + 0303: "Atilde", + 0102: "B", + 0103: "C", + 0307: "Ccedilla", + 0104: "D", + 0105: "E", + 0311: "Eacute", + 0312: "Ecircumflex", + 0313: "Edieresis", + 0310: "Egrave", + 0320: "Eth", + 0200: "Euro", + 0106: "F", + 0107: "G", + 0110: "H", + 0111: "I", + 0315: "Iacute", + 0316: "Icircumflex", + 0317: "Idieresis", + 0314: "Igrave", + 0112: "J", + 0113: "K", + 0114: "L", + 0115: "M", + 0116: "N", + 0321: "Ntilde", + 0117: "O", + 0214: "OE", + 0323: "Oacute", + 0324: "Ocircumflex", + 0326: "Odieresis", + 0322: "Ograve", + 0330: "Oslash", + 0325: "Otilde", + 0120: "P", + 0121: "Q", + 0122: "R", + 0123: "S", + 0212: "Scaron", + 0124: "T", + 0336: "Thorn", + 0125: "U", + 0332: "Uacute", + 0333: "Ucircumflex", + 0334: "Udieresis", + 0331: "Ugrave", + 0126: "V", + 0127: "W", + 0130: "X", + 0131: "Y", + 0335: "Yacute", + 0237: "Ydieresis", + 0132: "Z", + 0216: "Zcaron", + 0141: "a", + 0341: "aacute", + 0342: "acircumflex", + 0264: "acute", + 0344: "adieresis", + 0346: "ae", + 0340: "agrave", + 0046: "ampersand", + 0345: "aring", + 0136: "asciicircum", + 0176: "asciitilde", + 0052: "asterisk", + 0100: "at", + 0343: "atilde", + 0142: "b", + 0134: "backslash", + 0174: "bar", + 0173: "braceleft", + 0175: "braceright", + 0133: "bracketleft", + 0135: "bracketright", + 0246: "brokenbar", + 0225: "bullet", + 0143: "c", + 0347: "ccedilla", + 0270: "cedilla", + 0242: "cent", + 0210: "circumflex", + 0072: "colon", + 0054: "comma", + 0251: "copyright", + 0244: "currency", + 0144: "d", + 0206: "dagger", + 0207: "daggerdbl", + 0260: "degree", + 0250: "dieresis", + 0367: "divide", + 0044: "dollar", + 0145: "e", + 0351: "eacute", + 0352: "ecircumflex", + 0353: "edieresis", + 0350: "egrave", + 0070: "eight", + 0205: "ellipsis", + 0227: "emdash", + 0226: "endash", + 0075: "equal", + 0360: "eth", + 0041: "exclam", + 0241: "exclamdown", + 0146: "f", + 0065: "five", + 0203: "florin", + 0064: "four", + 0147: "g", + 0337: "germandbls", + 0140: "grave", + 0076: "greater", + 0253: "guillemotleft", + 0273: "guillemotright", + 0213: "guilsinglleft", + 0233: "guilsinglright", + 0150: "h", + 0055: "hyphen", + 0151: "i", + 0355: "iacute", + 0356: "icircumflex", + 0357: "idieresis", + 0354: "igrave", + 0152: "j", + 0153: "k", + 0154: "l", + 0074: "less", + 0254: "logicalnot", + 0155: "m", + 0257: "macron", + 0265: "mu", + 0327: "multiply", + 0156: "n", + 0071: "nine", + 0361: "ntilde", + 0043: "numbersign", + 0157: "o", + 0363: "oacute", + 0364: "ocircumflex", + 0366: "odieresis", + 0234: "oe", + 0362: "ograve", + 0061: "one", + 0275: "onehalf", + 0274: "onequarter", + 0271: "onesuperior", + 0252: "ordfeminine", + 0272: "ordmasculine", + 0370: "oslash", + 0365: "otilde", + 0160: "p", + 0266: "paragraph", + 0050: "parenleft", + 0051: "parenright", + 0045: "percent", + 0056: "period", + 0267: "periodcentered", + 0211: "perthousand", + 0053: "plus", + 0261: "plusminus", + 0161: "q", + 0077: "question", + 0277: "questiondown", + 0042: "quotedbl", + 0204: "quotedblbase", + 0223: "quotedblleft", + 0224: "quotedblright", + 0221: "quoteleft", + 0222: "quoteright", + 0202: "quotesinglbase", + 0047: "quotesingle", + 0162: "r", + 0256: "registered", + 0163: "s", + 0232: "scaron", + 0247: "section", + 0073: "semicolon", + 0067: "seven", + 0066: "six", + 0057: "slash", + 0040: "space", + 0243: "sterling", + 0164: "t", + 0376: "thorn", + 0063: "three", + 0276: "threequarters", + 0263: "threesuperior", + 0230: "tilde", + 0231: "trademark", + 0062: "two", + 0262: "twosuperior", + 0165: "u", + 0372: "uacute", + 0373: "ucircumflex", + 0374: "udieresis", + 0371: "ugrave", + 0137: "underscore", + 0166: "v", + 0167: "w", + 0170: "x", + 0171: "y", + 0375: "yacute", + 0377: "ydieresis", + 0245: "yen", + 0172: "z", + 0236: "zcaron", + 0060: "zero", +} + +// See Annex D.5 Symbol Set and Encoding +var symbolGlyphMap = map[int]string{ + 0101: "Alpha", + 0102: "Beta", + 0103: "Chi", + 0104: "Delta", + 0105: "Epsilon", + 0110: "Eta", + 0240: "Euro", + 0107: "Gamma", + 0301: "Ifraktur", + 0111: "Iota", + 0113: "Kappa", + 0114: "Lambda", + 0115: "Mu", + 0116: "Nu", + 0127: "Omega", + 0117: "Omicron", + 0106: "Phi", + 0120: "Pi", + 0131: "Psi", + 0302: "Rfraktur", + 0122: "Rho", + 0123: "Sigma", + 0124: "Tau", + 0121: "Theta", + 0125: "Upsilon", + 0241: "Upsilon1", + 0130: "Xi", + 0132: "Zeta", + 0300: "aleph", + 0141: "alpha", + 0046: "ampersand", + 0320: "angle", + 0341: "angleleft", + 0361: "angleright", + 0273: "approxequal", + 0253: "arrowboth", + 0333: "arrowdblboth", + 0337: "arrowdbldown", + 0334: "arrowdblleft", + 0336: "arrowdblright", + 0335: "arrowdblup", + 0257: "arrowdown", + 0276: "arrowhorizex", + 0254: "arrowleft", + 0256: "arrowright", + 0255: "arrowup", + 0275: "arrowvertex", + 0052: "asteriskmath", + 0174: "bar", + 0142: "beta", + 0173: "braceleft", + 0175: "braceright", + 0354: "bracelefttp", + 0355: "braceleftmid", + 0356: "braceleftbt", + 0374: "bracerighttp", + 0375: "bracerightmid", + 0376: "bracerightbt", + 0357: "braceex", + 0133: "bracketleft", + 0135: "bracketright", + 0351: "bracketlefttp", + 0352: "bracketleftex", + 0353: "bracketleftbt", + 0371: "bracketrighttp", + 0372: "bracketrightex", + 0373: "bracketrightbt", + 0267: "bullet", + 0277: "carriagereturn", + 0143: "chi", + 0304: "circlemultiply", + 0305: "circleplus", + 0247: "club", + 0072: "colon", + 0054: "comma", + 0100: "congruent", + 0343: "copyrightsans", + 0323: "copyrightserif", + 0260: "degree", + 0144: "delta", + 0250: "diamond", + 0270: "divide", + 0327: "dotmath", + 0070: "eight", + 0316: "element", + 0274: "ellipsis", + 0306: "emptyset", + 0145: "epsilon", + 0075: "equal", + 0272: "equivalence", + 0150: "eta", + 0041: "exclam", + 0044: "existential", + 0065: "five", + 0246: "florin", + 0064: "four", + 0244: "fraction", + 0147: "gamma", + 0321: "gradient", + 0076: "greater", + 0263: "greaterequal", + 0251: "heart", + 0245: "infinity", + 0362: "integral", + 0363: "integraltp", + 0364: "integralex", + 0365: "integralbt", + 0307: "intersection", + 0151: "iota", + 0153: "kappa", + 0154: "lambda", + 0074: "less", + 0243: "lessequal", + 0331: "logicaland", + 0330: "logicalnot", + 0332: "logicalor", + 0340: "lozenge", + 0055: "minus", + 0242: "minute", + 0155: "mu", + 0264: "multiply", + 0071: "nine", + 0317: "notelement", + 0271: "notequal", + 0313: "notsubset", + 0156: "nu", + 0043: "numbersign", + 0167: "omega", + 0166: "omega1", + 0157: "omicron", + 0061: "one", + 0050: "parenleft", + 0051: "parenright", + 0346: "parenlefttp", + 0347: "parenleftex", + 0350: "parenleftbt", + 0366: "parenrighttp", + 0367: "parenrightex", + 0370: "parenrightbt", + 0266: "partialdiff", + 0045: "percent", + 0056: "period", + 0136: "perpendicular", + 0146: "phi", + 0152: "phi1", + 0160: "pi", + 0053: "plus", + 0261: "plusminus", + 0325: "product", + 0314: "propersubset", + 0311: "propersuperset", + 0265: "proportional", + 0171: "psi", + 0077: "question", + 0326: "radical", + 0140: "radicalex", + 0315: "reflexsubset", + 0312: "reflexsuperset", + 0342: "registersans", + 0322: "registerserif", + 0162: "rho", + 0262: "second", + 0073: "semicolon", + 0067: "seven", + 0163: "sigma", + 0126: "sigma1", + 0176: "similar", + 0066: "six", + 0057: "slash", + 0040: "space", + 0252: "spade", + 0047: "suchthat", + 0345: "summation", + 0164: "tau", + 0134: "therefore", + 0161: "theta", + 0112: "theta1", + 0063: "three", + 0344: "trademarksans", + 0324: "trademarkserif", + 0062: "two", + 0137: "underscore", + 0310: "union", + 0042: "universal", + 0165: "upsilon", + 0303: "weierstrass", + 0170: "xi", + 0060: "zero", + 0172: "zeta", +} + +// See Annex D.6 ZapfDingbats Set and Encoding +var zapfDingbatsGlyphMap = map[int]string{ + 0040: "space", + 0041: "a1", + 0042: "a2", + 0043: "a202", + 0044: "a3", + 0045: "a4", + 0046: "a5", + 0047: "a119", + 0050: "a118", + 0051: "a117", + 0052: "a11", + 0053: "a12", + 0054: "a13", + 0055: "a14", + 0056: "a15", + 0057: "a16", + 0060: "a105", + 0061: "a17", + 0062: "a18", + 0063: "a19", + 0064: "a20", + 0065: "a21", + 0066: "a22", + 0067: "a23", + 0070: "a24", + 0071: "a25", + 0072: "a26", + 0073: "a27", + 0074: "a28", + 0075: "a6", + 0076: "a7", + 0077: "a8", + 0100: "a9", + 0101: "a10", + 0102: "a29", + 0103: "a30", + 0104: "a31", + 0105: "a32", + 0106: "a33", + 0107: "a34", + 0110: "a35", + 0111: "a36", + 0112: "a37", + 0113: "a38", + 0114: "a39", + 0115: "a40", + 0116: "a41", + 0117: "a42", + 0120: "a43", + 0121: "a44", + 0122: "a45", + 0123: "a46", + 0124: "a47", + 0125: "a48", + 0126: "a49", + 0127: "a50", + 0130: "a51", + 0131: "a52", + 0132: "a53", + 0133: "a54", + 0134: "a55", + 0135: "a56", + 0136: "a57", + 0137: "a58", + 0140: "a59", + 0141: "a60", + 0142: "a61", + 0143: "a62", + 0144: "a63", + 0145: "a64", + 0146: "a65", + 0147: "a66", + 0150: "a67", + 0151: "a68", + 0152: "a69", + 0153: "a70", + 0154: "a71", + 0155: "a72", + 0156: "a73", + 0157: "a74", + 0160: "a203", + 0161: "a75", + 0162: "a204", + 0163: "a76", + 0164: "a77", + 0165: "a78", + 0166: "a79", + 0167: "a81", + 0170: "a82", + 0171: "a83", + 0172: "a84", + 0173: "a97", + 0174: "a98", + 0175: "a99", + 0176: "a100", + 0241: "a101", + 0242: "a102", + 0243: "a103", + 0244: "a104", + 0245: "a106", + 0246: "a107", + 0247: "a108", + 0250: "a112", + 0251: "a111", + 0252: "a110", + 0253: "a109", + 0254: "a120", + 0255: "a121", + 0256: "a122", + 0257: "a123", + 0260: "a124", + 0261: "a125", + 0262: "a126", + 0263: "a127", + 0264: "a128", + 0265: "a129", + 0266: "a130", + 0267: "a131", + 0270: "a132", + 0271: "a133", + 0272: "a134", + 0273: "a135", + 0274: "a136", + 0275: "a137", + 0276: "a138", + 0277: "a139", + 0300: "a140", + 0301: "a141", + 0302: "a142", + 0303: "a143", + 0304: "a144", + 0305: "a145", + 0306: "a146", + 0307: "a147", + 0310: "a148", + 0311: "a149", + 0312: "a150", + 0313: "a151", + 0314: "a152", + 0315: "a153", + 0316: "a154", + 0317: "a155", + 0320: "a156", + 0321: "a157", + 0322: "a158", + 0323: "a159", + 0324: "a160", + 0325: "a161", + 0326: "a163", + 0327: "a164", + 0330: "a196", + 0331: "a165", + 0332: "a192", + 0333: "a166", + 0334: "a167", + 0335: "a168", + 0336: "a169", + 0337: "a170", + 0340: "a171", + 0341: "a172", + 0342: "a173", + 0343: "a162", + 0344: "a174", + 0345: "a175", + 0346: "a176", + 0347: "a177", + 0350: "a178", + 0351: "a179", + 0352: "a193", + 0353: "a180", + 0354: "a199", + 0355: "a181", + 0356: "a200", + 0357: "a182", + 0361: "a201", + 0362: "a183", + 0363: "a184", + 0364: "a197", + 0365: "a185", + 0366: "a194", + 0367: "a198", + 0370: "a186", + 0371: "a195", + 0372: "a187", + 0373: "a188", + 0374: "a189", + 0375: "a190", + 0376: "a191", +} diff --git a/internal/corefont/metrics/metrics.go b/internal/corefont/metrics/metrics.go new file mode 100644 index 0000000000000000000000000000000000000000..19102b5b9823432532f6eb892919da279adf22fd --- /dev/null +++ b/internal/corefont/metrics/metrics.go @@ -0,0 +1,55 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 metrics provides font metrics for the PDF standard fonts. +package metrics + +// The PostScript names of the 14 Type 1 fonts, aka the PDF core font set, are as follows: +// +// Times-Roman, +// Helvetica, +// Courier, +// Symbol, +// Times-Bold, +// Helvetica-Bold, +// Courier-Bold, +// ZapfDingbats, +// Times-Italic, +// Helvetica- Oblique, +// Courier-Oblique, +// Times-BoldItalic, +// Helvetica-BoldOblique, +// Courier-BoldOblique + +// CoreFontCharWidth returns the character width for fontName and c in glyph space units. +func CoreFontCharWidth(fontName string, c int) int { + var m map[int]string + switch fontName { + case "Symbol": + m = SymbolGlyphMap + case "ZapfDingbats": + m = ZapfDingbatsGlyphMap + default: + m = WinAnsiGlyphMap + } + glyphName := m[c] + fm := CoreFontMetrics[fontName] + w, ok := fm.W[glyphName] + if !ok { + w = 1000 //m.W["bullet"] + } + return w +} diff --git a/internal/corefont/metrics/standard.go b/internal/corefont/metrics/standard.go new file mode 100644 index 0000000000000000000000000000000000000000..2bcf9b86b4decf82db88776d273055c59c4e68ba --- /dev/null +++ b/internal/corefont/metrics/standard.go @@ -0,0 +1,678 @@ +// generated by "go run gen.go". DO NOT EDIT. + +package metrics + +import "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + +// WinAnsiGlyphMap is a glyph lookup table for CP1252 character codes. +// See Annex D.2 Latin Character Set and Encodings. +var WinAnsiGlyphMap = map[int]string{ + 32: "space", // U+0020 ' ' + 33: "exclam", // U+0021 '!' + 34: "quotedbl", // U+0022 '"' + 35: "numbersign", // U+0023 '#' + 36: "dollar", // U+0024 '$' + 37: "percent", // U+0025 '%' + 38: "ampersand", // U+0026 '&' + 39: "quotesingle", // U+0027 ''' + 40: "parenleft", // U+0028 '(' + 41: "parenright", // U+0029 ')' + 42: "asterisk", // U+002A '*' + 43: "plus", // U+002B '+' + 44: "comma", // U+002C ',' + 45: "hyphen", // U+002D '-' + 46: "period", // U+002E '.' + 47: "slash", // U+002F '/' + 48: "zero", // U+0030 '0' + 49: "one", // U+0031 '1' + 50: "two", // U+0032 '2' + 51: "three", // U+0033 '3' + 52: "four", // U+0034 '4' + 53: "five", // U+0035 '5' + 54: "six", // U+0036 '6' + 55: "seven", // U+0037 '7' + 56: "eight", // U+0038 '8' + 57: "nine", // U+0039 '9' + 58: "colon", // U+003A ':' + 59: "semicolon", // U+003B ';' + 60: "less", // U+003C '<' + 61: "equal", // U+003D '=' + 62: "greater", // U+003E '>' + 63: "question", // U+003F '?' + 64: "at", // U+0040 '@' + 65: "A", // U+0041 'A' + 66: "B", // U+0042 'B' + 67: "C", // U+0043 'C' + 68: "D", // U+0044 'D' + 69: "E", // U+0045 'E' + 70: "F", // U+0046 'F' + 71: "G", // U+0047 'G' + 72: "H", // U+0048 'H' + 73: "I", // U+0049 'I' + 74: "J", // U+004A 'J' + 75: "K", // U+004B 'K' + 76: "L", // U+004C 'L' + 77: "M", // U+004D 'M' + 78: "N", // U+004E 'N' + 79: "O", // U+004F 'O' + 80: "P", // U+0050 'P' + 81: "Q", // U+0051 'Q' + 82: "R", // U+0052 'R' + 83: "S", // U+0053 'S' + 84: "T", // U+0054 'T' + 85: "U", // U+0055 'U' + 86: "V", // U+0056 'V' + 87: "W", // U+0057 'W' + 88: "X", // U+0058 'X' + 89: "Y", // U+0059 'Y' + 90: "Z", // U+005A 'Z' + 91: "bracketleft", // U+005B '[' + 92: "backslash", // U+005C '\' + 93: "bracketright", // U+005D ']' + 94: "asciicircum", // U+005E '^' + 95: "underscore", // U+005F '_' + 96: "grave", // U+0060 '`' + 97: "a", // U+0061 'a' + 98: "b", // U+0062 'b' + 99: "c", // U+0063 'c' + 100: "d", // U+0064 'd' + 101: "e", // U+0065 'e' + 102: "f", // U+0066 'f' + 103: "g", // U+0067 'g' + 104: "h", // U+0068 'h' + 105: "i", // U+0069 'i' + 106: "j", // U+006A 'j' + 107: "k", // U+006B 'k' + 108: "l", // U+006C 'l' + 109: "m", // U+006D 'm' + 110: "n", // U+006E 'n' + 111: "o", // U+006F 'o' + 112: "p", // U+0070 'p' + 113: "q", // U+0071 'q' + 114: "r", // U+0072 'r' + 115: "s", // U+0073 's' + 116: "t", // U+0074 't' + 117: "u", // U+0075 'u' + 118: "v", // U+0076 'v' + 119: "w", // U+0077 'w' + 120: "x", // U+0078 'x' + 121: "y", // U+0079 'y' + 122: "z", // U+007A 'z' + 123: "braceleft", // U+007B '{' + 124: "bar", // U+007C '|' + 125: "braceright", // U+007D '}' + 126: "asciitilde", // U+007E '~' + 128: "Euro", // U+0080 + 130: "quotesinglbase", // U+0082 + 131: "florin", // U+0083 + 132: "quotedblbase", // U+0084 + 133: "ellipsis", // U+0085 + 134: "dagger", // U+0086 + 135: "daggerdbl", // U+0087 + 136: "circumflex", // U+0088 + 137: "perthousand", // U+0089 + 138: "Scaron", // U+008A + 139: "guilsinglleft", // U+008B + 140: "OE", // U+008C + 142: "Zcaron", // U+008E + 145: "quoteleft", // U+0091 + 146: "quoteright", // U+0092 + 147: "quotedblleft", // U+0093 + 148: "quotedblright", // U+0094 + 149: "bullet", // U+0095 + 150: "endash", // U+0096 + 151: "emdash", // U+0097 + 152: "tilde", // U+0098 + 153: "trademark", // U+0099 + 154: "scaron", // U+009A + 155: "guilsinglright", // U+009B + 156: "oe", // U+009C + 158: "zcaron", // U+009E + 159: "Ydieresis", // U+009F + 161: "exclamdown", // U+00A1 '¡' + 162: "cent", // U+00A2 '¢' + 163: "sterling", // U+00A3 '£' + 164: "currency", // U+00A4 '¤' + 165: "yen", // U+00A5 '¥' + 166: "brokenbar", // U+00A6 '¦' + 167: "section", // U+00A7 '§' + 168: "dieresis", // U+00A8 '¨' + 169: "copyright", // U+00A9 '©' + 170: "ordfeminine", // U+00AA 'ª' + 171: "guillemotleft", // U+00AB '«' + 172: "logicalnot", // U+00AC '¬' + 174: "registered", // U+00AE '®' + 175: "macron", // U+00AF '¯' + 176: "degree", // U+00B0 '°' + 177: "plusminus", // U+00B1 '±' + 178: "twosuperior", // U+00B2 '²' + 179: "threesuperior", // U+00B3 '³' + 180: "acute", // U+00B4 '´' + 181: "mu", // U+00B5 'µ' + 182: "paragraph", // U+00B6 '¶' + 183: "periodcentered", // U+00B7 '·' + 184: "cedilla", // U+00B8 '¸' + 185: "onesuperior", // U+00B9 '¹' + 186: "ordmasculine", // U+00BA 'º' + 187: "guillemotright", // U+00BB '»' + 188: "onequarter", // U+00BC '¼' + 189: "onehalf", // U+00BD '½' + 190: "threequarters", // U+00BE '¾' + 191: "questiondown", // U+00BF '¿' + 192: "Agrave", // U+00C0 'À' + 193: "Aacute", // U+00C1 'Á' + 194: "Acircumflex", // U+00C2 'Â' + 195: "Atilde", // U+00C3 'Ã' + 196: "Adieresis", // U+00C4 'Ä' + 197: "Aring", // U+00C5 'Å' + 198: "AE", // U+00C6 'Æ' + 199: "Ccedilla", // U+00C7 'Ç' + 200: "Egrave", // U+00C8 'È' + 201: "Eacute", // U+00C9 'É' + 202: "Ecircumflex", // U+00CA 'Ê' + 203: "Edieresis", // U+00CB 'Ë' + 204: "Igrave", // U+00CC 'Ì' + 205: "Iacute", // U+00CD 'Í' + 206: "Icircumflex", // U+00CE 'Î' + 207: "Idieresis", // U+00CF 'Ï' + 208: "Eth", // U+00D0 'Ð' + 209: "Ntilde", // U+00D1 'Ñ' + 210: "Ograve", // U+00D2 'Ò' + 211: "Oacute", // U+00D3 'Ó' + 212: "Ocircumflex", // U+00D4 'Ô' + 213: "Otilde", // U+00D5 'Õ' + 214: "Odieresis", // U+00D6 'Ö' + 215: "multiply", // U+00D7 '×' + 216: "Oslash", // U+00D8 'Ø' + 217: "Ugrave", // U+00D9 'Ù' + 218: "Uacute", // U+00DA 'Ú' + 219: "Ucircumflex", // U+00DB 'Û' + 220: "Udieresis", // U+00DC 'Ü' + 221: "Yacute", // U+00DD 'Ý' + 222: "Thorn", // U+00DE 'Þ' + 223: "germandbls", // U+00DF 'ß' + 224: "agrave", // U+00E0 'à' + 225: "aacute", // U+00E1 'á' + 226: "acircumflex", // U+00E2 'â' + 227: "atilde", // U+00E3 'ã' + 228: "adieresis", // U+00E4 'ä' + 229: "aring", // U+00E5 'å' + 230: "ae", // U+00E6 'æ' + 231: "ccedilla", // U+00E7 'ç' + 232: "egrave", // U+00E8 'è' + 233: "eacute", // U+00E9 'é' + 234: "ecircumflex", // U+00EA 'ê' + 235: "edieresis", // U+00EB 'ë' + 236: "igrave", // U+00EC 'ì' + 237: "iacute", // U+00ED 'í' + 238: "icircumflex", // U+00EE 'î' + 239: "idieresis", // U+00EF 'ï' + 240: "eth", // U+00F0 'ð' + 241: "ntilde", // U+00F1 'ñ' + 242: "ograve", // U+00F2 'ò' + 243: "oacute", // U+00F3 'ó' + 244: "ocircumflex", // U+00F4 'ô' + 245: "otilde", // U+00F5 'õ' + 246: "odieresis", // U+00F6 'ö' + 247: "divide", // U+00F7 '÷' + 248: "oslash", // U+00F8 'ø' + 249: "ugrave", // U+00F9 'ù' + 250: "uacute", // U+00FA 'ú' + 251: "ucircumflex", // U+00FB 'û' + 252: "udieresis", // U+00FC 'ü' + 253: "yacute", // U+00FD 'ý' + 254: "thorn", // U+00FE 'þ' + 255: "ydieresis", // U+00FF 'ÿ' +} + +// SymbolGlyphMap is a glyph lookup table for Symbol character codes. +// See Annex D.5 Symbol Set and Encoding. +var SymbolGlyphMap = map[int]string{ + 32: "space", // U+0020 ' ' + 33: "exclam", // U+0021 '!' + 34: "universal", // U+0022 '"' + 35: "numbersign", // U+0023 '#' + 36: "existential", // U+0024 '$' + 37: "percent", // U+0025 '%' + 38: "ampersand", // U+0026 '&' + 39: "suchthat", // U+0027 ''' + 40: "parenleft", // U+0028 '(' + 41: "parenright", // U+0029 ')' + 42: "asteriskmath", // U+002A '*' + 43: "plus", // U+002B '+' + 44: "comma", // U+002C ',' + 45: "minus", // U+002D '-' + 46: "period", // U+002E '.' + 47: "slash", // U+002F '/' + 48: "zero", // U+0030 '0' + 49: "one", // U+0031 '1' + 50: "two", // U+0032 '2' + 51: "three", // U+0033 '3' + 52: "four", // U+0034 '4' + 53: "five", // U+0035 '5' + 54: "six", // U+0036 '6' + 55: "seven", // U+0037 '7' + 56: "eight", // U+0038 '8' + 57: "nine", // U+0039 '9' + 58: "colon", // U+003A ':' + 59: "semicolon", // U+003B ';' + 60: "less", // U+003C '<' + 61: "equal", // U+003D '=' + 62: "greater", // U+003E '>' + 63: "question", // U+003F '?' + 64: "congruent", // U+0040 '@' + 65: "Alpha", // U+0041 'A' + 66: "Beta", // U+0042 'B' + 67: "Chi", // U+0043 'C' + 68: "Delta", // U+0044 'D' + 69: "Epsilon", // U+0045 'E' + 70: "Phi", // U+0046 'F' + 71: "Gamma", // U+0047 'G' + 72: "Eta", // U+0048 'H' + 73: "Iota", // U+0049 'I' + 74: "theta1", // U+004A 'J' + 75: "Kappa", // U+004B 'K' + 76: "Lambda", // U+004C 'L' + 77: "Mu", // U+004D 'M' + 78: "Nu", // U+004E 'N' + 79: "Omicron", // U+004F 'O' + 80: "Pi", // U+0050 'P' + 81: "Theta", // U+0051 'Q' + 82: "Rho", // U+0052 'R' + 83: "Sigma", // U+0053 'S' + 84: "Tau", // U+0054 'T' + 85: "Upsilon", // U+0055 'U' + 86: "sigma1", // U+0056 'V' + 87: "Omega", // U+0057 'W' + 88: "Xi", // U+0058 'X' + 89: "Psi", // U+0059 'Y' + 90: "Zeta", // U+005A 'Z' + 91: "bracketleft", // U+005B '[' + 92: "therefore", // U+005C '\' + 93: "bracketright", // U+005D ']' + 94: "perpendicular", // U+005E '^' + 95: "underscore", // U+005F '_' + 96: "radicalex", // U+0060 '`' + 97: "alpha", // U+0061 'a' + 98: "beta", // U+0062 'b' + 99: "chi", // U+0063 'c' + 100: "delta", // U+0064 'd' + 101: "epsilon", // U+0065 'e' + 102: "phi", // U+0066 'f' + 103: "gamma", // U+0067 'g' + 104: "eta", // U+0068 'h' + 105: "iota", // U+0069 'i' + 106: "phi1", // U+006A 'j' + 107: "kappa", // U+006B 'k' + 108: "lambda", // U+006C 'l' + 109: "mu", // U+006D 'm' + 110: "nu", // U+006E 'n' + 111: "omicron", // U+006F 'o' + 112: "pi", // U+0070 'p' + 113: "theta", // U+0071 'q' + 114: "rho", // U+0072 'r' + 115: "sigma", // U+0073 's' + 116: "tau", // U+0074 't' + 117: "upsilon", // U+0075 'u' + 118: "omega1", // U+0076 'v' + 119: "omega", // U+0077 'w' + 120: "xi", // U+0078 'x' + 121: "psi", // U+0079 'y' + 122: "zeta", // U+007A 'z' + 123: "braceleft", // U+007B '{' + 124: "bar", // U+007C '|' + 125: "braceright", // U+007D '}' + 126: "similar", // U+007E '~' + 160: "Euro", // U+00A0 + 161: "Upsilon1", // U+00A1 '¡' + 162: "minute", // U+00A2 '¢' + 163: "lessequal", // U+00A3 '£' + 164: "fraction", // U+00A4 '¤' + 165: "infinity", // U+00A5 '¥' + 166: "florin", // U+00A6 '¦' + 167: "club", // U+00A7 '§' + 168: "diamond", // U+00A8 '¨' + 169: "heart", // U+00A9 '©' + 170: "spade", // U+00AA 'ª' + 171: "arrowboth", // U+00AB '«' + 172: "arrowleft", // U+00AC '¬' + 173: "arrowup", // U+00AD + 174: "arrowright", // U+00AE '®' + 175: "arrowdown", // U+00AF '¯' + 176: "degree", // U+00B0 '°' + 177: "plusminus", // U+00B1 '±' + 178: "second", // U+00B2 '²' + 179: "greaterequal", // U+00B3 '³' + 180: "multiply", // U+00B4 '´' + 181: "proportional", // U+00B5 'µ' + 182: "partialdiff", // U+00B6 '¶' + 183: "bullet", // U+00B7 '·' + 184: "divide", // U+00B8 '¸' + 185: "notequal", // U+00B9 '¹' + 186: "equivalence", // U+00BA 'º' + 187: "approxequal", // U+00BB '»' + 188: "ellipsis", // U+00BC '¼' + 189: "arrowvertex", // U+00BD '½' + 190: "arrowhorizex", // U+00BE '¾' + 191: "carriagereturn", // U+00BF '¿' + 192: "aleph", // U+00C0 'À' + 193: "Ifraktur", // U+00C1 'Á' + 194: "Rfraktur", // U+00C2 'Â' + 195: "weierstrass", // U+00C3 'Ã' + 196: "circlemultiply", // U+00C4 'Ä' + 197: "circleplus", // U+00C5 'Å' + 198: "emptyset", // U+00C6 'Æ' + 199: "intersection", // U+00C7 'Ç' + 200: "union", // U+00C8 'È' + 201: "propersuperset", // U+00C9 'É' + 202: "reflexsuperset", // U+00CA 'Ê' + 203: "notsubset", // U+00CB 'Ë' + 204: "propersubset", // U+00CC 'Ì' + 205: "reflexsubset", // U+00CD 'Í' + 206: "element", // U+00CE 'Î' + 207: "notelement", // U+00CF 'Ï' + 208: "angle", // U+00D0 'Ð' + 209: "gradient", // U+00D1 'Ñ' + 210: "registerserif", // U+00D2 'Ò' + 211: "copyrightserif", // U+00D3 'Ó' + 212: "trademarkserif", // U+00D4 'Ô' + 213: "product", // U+00D5 'Õ' + 214: "radical", // U+00D6 'Ö' + 215: "dotmath", // U+00D7 '×' + 216: "logicalnot", // U+00D8 'Ø' + 217: "logicaland", // U+00D9 'Ù' + 218: "logicalor", // U+00DA 'Ú' + 219: "arrowdblboth", // U+00DB 'Û' + 220: "arrowdblleft", // U+00DC 'Ü' + 221: "arrowdblup", // U+00DD 'Ý' + 222: "arrowdblright", // U+00DE 'Þ' + 223: "arrowdbldown", // U+00DF 'ß' + 224: "lozenge", // U+00E0 'à' + 225: "angleleft", // U+00E1 'á' + 226: "registersans", // U+00E2 'â' + 227: "copyrightsans", // U+00E3 'ã' + 228: "trademarksans", // U+00E4 'ä' + 229: "summation", // U+00E5 'å' + 230: "parenlefttp", // U+00E6 'æ' + 231: "parenleftex", // U+00E7 'ç' + 232: "parenleftbt", // U+00E8 'è' + 233: "bracketlefttp", // U+00E9 'é' + 234: "bracketleftex", // U+00EA 'ê' + 235: "bracketleftbt", // U+00EB 'ë' + 236: "bracelefttp", // U+00EC 'ì' + 237: "braceleftmid", // U+00ED 'í' + 238: "braceleftbt", // U+00EE 'î' + 239: "braceex", // U+00EF 'ï' + 241: "angleright", // U+00F1 'ñ' + 242: "integral", // U+00F2 'ò' + 243: "integraltp", // U+00F3 'ó' + 244: "integralex", // U+00F4 'ô' + 245: "integralbt", // U+00F5 'õ' + 246: "parenrighttp", // U+00F6 'ö' + 247: "parenrightex", // U+00F7 '÷' + 248: "parenrightbt", // U+00F8 'ø' + 249: "bracketrighttp", // U+00F9 'ù' + 250: "bracketrightex", // U+00FA 'ú' + 251: "bracketrightbt", // U+00FB 'û' + 252: "bracerighttp", // U+00FC 'ü' + 253: "bracerightmid", // U+00FD 'ý' + 254: "bracerightbt", // U+00FE 'þ' +} + +// ZapfDingbatsGlyphMap is a glyph lookup table for ZapfDingbats character codes. +// See Annex D.6 ZapfDingbats Set and Encoding +var ZapfDingbatsGlyphMap = map[int]string{ + 32: "space", // U+0020 ' ' + 33: "a1", // U+0021 '!' + 34: "a2", // U+0022 '"' + 35: "a202", // U+0023 '#' + 36: "a3", // U+0024 '$' + 37: "a4", // U+0025 '%' + 38: "a5", // U+0026 '&' + 39: "a119", // U+0027 ''' + 40: "a118", // U+0028 '(' + 41: "a117", // U+0029 ')' + 42: "a11", // U+002A '*' + 43: "a12", // U+002B '+' + 44: "a13", // U+002C ',' + 45: "a14", // U+002D '-' + 46: "a15", // U+002E '.' + 47: "a16", // U+002F '/' + 48: "a105", // U+0030 '0' + 49: "a17", // U+0031 '1' + 50: "a18", // U+0032 '2' + 51: "a19", // U+0033 '3' + 52: "a20", // U+0034 '4' + 53: "a21", // U+0035 '5' + 54: "a22", // U+0036 '6' + 55: "a23", // U+0037 '7' + 56: "a24", // U+0038 '8' + 57: "a25", // U+0039 '9' + 58: "a26", // U+003A ':' + 59: "a27", // U+003B ';' + 60: "a28", // U+003C '<' + 61: "a6", // U+003D '=' + 62: "a7", // U+003E '>' + 63: "a8", // U+003F '?' + 64: "a9", // U+0040 '@' + 65: "a10", // U+0041 'A' + 66: "a29", // U+0042 'B' + 67: "a30", // U+0043 'C' + 68: "a31", // U+0044 'D' + 69: "a32", // U+0045 'E' + 70: "a33", // U+0046 'F' + 71: "a34", // U+0047 'G' + 72: "a35", // U+0048 'H' + 73: "a36", // U+0049 'I' + 74: "a37", // U+004A 'J' + 75: "a38", // U+004B 'K' + 76: "a39", // U+004C 'L' + 77: "a40", // U+004D 'M' + 78: "a41", // U+004E 'N' + 79: "a42", // U+004F 'O' + 80: "a43", // U+0050 'P' + 81: "a44", // U+0051 'Q' + 82: "a45", // U+0052 'R' + 83: "a46", // U+0053 'S' + 84: "a47", // U+0054 'T' + 85: "a48", // U+0055 'U' + 86: "a49", // U+0056 'V' + 87: "a50", // U+0057 'W' + 88: "a51", // U+0058 'X' + 89: "a52", // U+0059 'Y' + 90: "a53", // U+005A 'Z' + 91: "a54", // U+005B '[' + 92: "a55", // U+005C '\' + 93: "a56", // U+005D ']' + 94: "a57", // U+005E '^' + 95: "a58", // U+005F '_' + 96: "a59", // U+0060 '`' + 97: "a60", // U+0061 'a' + 98: "a61", // U+0062 'b' + 99: "a62", // U+0063 'c' + 100: "a63", // U+0064 'd' + 101: "a64", // U+0065 'e' + 102: "a65", // U+0066 'f' + 103: "a66", // U+0067 'g' + 104: "a67", // U+0068 'h' + 105: "a68", // U+0069 'i' + 106: "a69", // U+006A 'j' + 107: "a70", // U+006B 'k' + 108: "a71", // U+006C 'l' + 109: "a72", // U+006D 'm' + 110: "a73", // U+006E 'n' + 111: "a74", // U+006F 'o' + 112: "a203", // U+0070 'p' + 113: "a75", // U+0071 'q' + 114: "a204", // U+0072 'r' + 115: "a76", // U+0073 's' + 116: "a77", // U+0074 't' + 117: "a78", // U+0075 'u' + 118: "a79", // U+0076 'v' + 119: "a81", // U+0077 'w' + 120: "a82", // U+0078 'x' + 121: "a83", // U+0079 'y' + 122: "a84", // U+007A 'z' + 123: "a97", // U+007B '{' + 124: "a98", // U+007C '|' + 125: "a99", // U+007D '}' + 126: "a100", // U+007E '~' + 161: "a101", // U+00A1 '¡' + 162: "a102", // U+00A2 '¢' + 163: "a103", // U+00A3 '£' + 164: "a104", // U+00A4 '¤' + 165: "a106", // U+00A5 '¥' + 166: "a107", // U+00A6 '¦' + 167: "a108", // U+00A7 '§' + 168: "a112", // U+00A8 '¨' + 169: "a111", // U+00A9 '©' + 170: "a110", // U+00AA 'ª' + 171: "a109", // U+00AB '«' + 172: "a120", // U+00AC '¬' + 173: "a121", // U+00AD + 174: "a122", // U+00AE '®' + 175: "a123", // U+00AF '¯' + 176: "a124", // U+00B0 '°' + 177: "a125", // U+00B1 '±' + 178: "a126", // U+00B2 '²' + 179: "a127", // U+00B3 '³' + 180: "a128", // U+00B4 '´' + 181: "a129", // U+00B5 'µ' + 182: "a130", // U+00B6 '¶' + 183: "a131", // U+00B7 '·' + 184: "a132", // U+00B8 '¸' + 185: "a133", // U+00B9 '¹' + 186: "a134", // U+00BA 'º' + 187: "a135", // U+00BB '»' + 188: "a136", // U+00BC '¼' + 189: "a137", // U+00BD '½' + 190: "a138", // U+00BE '¾' + 191: "a139", // U+00BF '¿' + 192: "a140", // U+00C0 'À' + 193: "a141", // U+00C1 'Á' + 194: "a142", // U+00C2 'Â' + 195: "a143", // U+00C3 'Ã' + 196: "a144", // U+00C4 'Ä' + 197: "a145", // U+00C5 'Å' + 198: "a146", // U+00C6 'Æ' + 199: "a147", // U+00C7 'Ç' + 200: "a148", // U+00C8 'È' + 201: "a149", // U+00C9 'É' + 202: "a150", // U+00CA 'Ê' + 203: "a151", // U+00CB 'Ë' + 204: "a152", // U+00CC 'Ì' + 205: "a153", // U+00CD 'Í' + 206: "a154", // U+00CE 'Î' + 207: "a155", // U+00CF 'Ï' + 208: "a156", // U+00D0 'Ð' + 209: "a157", // U+00D1 'Ñ' + 210: "a158", // U+00D2 'Ò' + 211: "a159", // U+00D3 'Ó' + 212: "a160", // U+00D4 'Ô' + 213: "a161", // U+00D5 'Õ' + 214: "a163", // U+00D6 'Ö' + 215: "a164", // U+00D7 '×' + 216: "a196", // U+00D8 'Ø' + 217: "a165", // U+00D9 'Ù' + 218: "a192", // U+00DA 'Ú' + 219: "a166", // U+00DB 'Û' + 220: "a167", // U+00DC 'Ü' + 221: "a168", // U+00DD 'Ý' + 222: "a169", // U+00DE 'Þ' + 223: "a170", // U+00DF 'ß' + 224: "a171", // U+00E0 'à' + 225: "a172", // U+00E1 'á' + 226: "a173", // U+00E2 'â' + 227: "a162", // U+00E3 'ã' + 228: "a174", // U+00E4 'ä' + 229: "a175", // U+00E5 'å' + 230: "a176", // U+00E6 'æ' + 231: "a177", // U+00E7 'ç' + 232: "a178", // U+00E8 'è' + 233: "a179", // U+00E9 'é' + 234: "a193", // U+00EA 'ê' + 235: "a180", // U+00EB 'ë' + 236: "a199", // U+00EC 'ì' + 237: "a181", // U+00ED 'í' + 238: "a200", // U+00EE 'î' + 239: "a182", // U+00EF 'ï' + 241: "a201", // U+00F1 'ñ' + 242: "a183", // U+00F2 'ò' + 243: "a184", // U+00F3 'ó' + 244: "a197", // U+00F4 'ô' + 245: "a185", // U+00F5 'õ' + 246: "a194", // U+00F6 'ö' + 247: "a198", // U+00F7 '÷' + 248: "a186", // U+00F8 'ø' + 249: "a195", // U+00F9 'ù' + 250: "a187", // U+00FA 'ú' + 251: "a188", // U+00FB 'û' + 252: "a189", // U+00FC 'ü' + 253: "a190", // U+00FD 'ý' + 254: "a191", // U+00FE 'þ' +} + +type fontMetrics struct { + FBox *types.Rectangle // font box + W map[string]int // glyph widths +} + +// CoreFontMetrics represents font metrics for the Adobe standard type 1 core fonts. +var CoreFontMetrics = map[string]fontMetrics{ + "Courier-Bold": { + types.NewRectangle(-113.0, -250.0, 749.0, 801.0), + map[string]int{"space": 600, "exclam": 600, "quotedbl": 600, "numbersign": 600, "dollar": 600, "percent": 600, "ampersand": 600, "quoteright": 600, "parenleft": 600, "parenright": 600, "asterisk": 600, "plus": 600, "comma": 600, "hyphen": 600, "period": 600, "slash": 600, "zero": 600, "one": 600, "two": 600, "three": 600, "four": 600, "five": 600, "six": 600, "seven": 600, "eight": 600, "nine": 600, "colon": 600, "semicolon": 600, "less": 600, "equal": 600, "greater": 600, "question": 600, "at": 600, "A": 600, "B": 600, "C": 600, "D": 600, "E": 600, "F": 600, "G": 600, "H": 600, "I": 600, "J": 600, "K": 600, "L": 600, "M": 600, "N": 600, "O": 600, "P": 600, "Q": 600, "R": 600, "S": 600, "T": 600, "U": 600, "V": 600, "W": 600, "X": 600, "Y": 600, "Z": 600, "bracketleft": 600, "backslash": 600, "bracketright": 600, "asciicircum": 600, "underscore": 600, "quoteleft": 600, "a": 600, "b": 600, "c": 600, "d": 600, "e": 600, "f": 600, "g": 600, "h": 600, "i": 600, "j": 600, "k": 600, "l": 600, "m": 600, "n": 600, "o": 600, "p": 600, "q": 600, "r": 600, "s": 600, "t": 600, "u": 600, "v": 600, "w": 600, "x": 600, "y": 600, "z": 600, "braceleft": 600, "bar": 600, "braceright": 600, "asciitilde": 600, "exclamdown": 600, "cent": 600, "sterling": 600, "fraction": 600, "yen": 600, "florin": 600, "section": 600, "currency": 600, "quotesingle": 600, "quotedblleft": 600, "guillemotleft": 600, "guilsinglleft": 600, "guilsinglright": 600, "fi": 600, "fl": 600, "endash": 600, "dagger": 600, "daggerdbl": 600, "periodcentered": 600, "paragraph": 600, "bullet": 600, "quotesinglbase": 600, "quotedblbase": 600, "quotedblright": 600, "guillemotright": 600, "ellipsis": 600, "perthousand": 600, "questiondown": 600, "grave": 600, "acute": 600, "circumflex": 600, "tilde": 600, "macron": 600, "breve": 600, "dotaccent": 600, "dieresis": 600, "ring": 600, "cedilla": 600, "hungarumlaut": 600, "ogonek": 600, "caron": 600, "emdash": 600, "AE": 600, "ordfeminine": 600, "Lslash": 600, "Oslash": 600, "OE": 600, "ordmasculine": 600, "ae": 600, "dotlessi": 600, "lslash": 600, "oslash": 600, "oe": 600, "germandbls": 600, "Idieresis": 600, "eacute": 600, "abreve": 600, "uhungarumlaut": 600, "ecaron": 600, "Ydieresis": 600, "divide": 600, "Yacute": 600, "Acircumflex": 600, "aacute": 600, "Ucircumflex": 600, "yacute": 600, "scommaaccent": 600, "ecircumflex": 600, "Uring": 600, "Udieresis": 600, "aogonek": 600, "Uacute": 600, "uogonek": 600, "Edieresis": 600, "Dcroat": 600, "commaaccent": 600, "copyright": 600, "Emacron": 600, "ccaron": 600, "aring": 600, "Ncommaaccent": 600, "lacute": 600, "agrave": 600, "Tcommaaccent": 600, "Cacute": 600, "atilde": 600, "Edotaccent": 600, "scaron": 600, "scedilla": 600, "iacute": 600, "lozenge": 600, "Rcaron": 600, "Gcommaaccent": 600, "ucircumflex": 600, "acircumflex": 600, "Amacron": 600, "rcaron": 600, "ccedilla": 600, "Zdotaccent": 600, "Thorn": 600, "Omacron": 600, "Racute": 600, "Sacute": 600, "dcaron": 600, "Umacron": 600, "uring": 600, "threesuperior": 600, "Ograve": 600, "Agrave": 600, "Abreve": 600, "multiply": 600, "uacute": 600, "Tcaron": 600, "partialdiff": 600, "ydieresis": 600, "Nacute": 600, "icircumflex": 600, "Ecircumflex": 600, "adieresis": 600, "edieresis": 600, "cacute": 600, "nacute": 600, "umacron": 600, "Ncaron": 600, "Iacute": 600, "plusminus": 600, "brokenbar": 600, "registered": 600, "Gbreve": 600, "Idotaccent": 600, "summation": 600, "Egrave": 600, "racute": 600, "omacron": 600, "Zacute": 600, "Zcaron": 600, "greaterequal": 600, "Eth": 600, "Ccedilla": 600, "lcommaaccent": 600, "tcaron": 600, "eogonek": 600, "Uogonek": 600, "Aacute": 600, "Adieresis": 600, "egrave": 600, "zacute": 600, "iogonek": 600, "Oacute": 600, "oacute": 600, "amacron": 600, "sacute": 600, "idieresis": 600, "Ocircumflex": 600, "Ugrave": 600, "Delta": 600, "thorn": 600, "twosuperior": 600, "Odieresis": 600, "mu": 600, "igrave": 600, "ohungarumlaut": 600, "Eogonek": 600, "dcroat": 600, "threequarters": 600, "Scedilla": 600, "lcaron": 600, "Kcommaaccent": 600, "Lacute": 600, "trademark": 600, "edotaccent": 600, "Igrave": 600, "Imacron": 600, "Lcaron": 600, "onehalf": 600, "lessequal": 600, "ocircumflex": 600, "ntilde": 600, "Uhungarumlaut": 600, "Eacute": 600, "emacron": 600, "gbreve": 600, "onequarter": 600, "Scaron": 600, "Scommaaccent": 600, "Ohungarumlaut": 600, "degree": 600, "ograve": 600, "Ccaron": 600, "ugrave": 600, "radical": 600, "Dcaron": 600, "rcommaaccent": 600, "Ntilde": 600, "otilde": 600, "Rcommaaccent": 600, "Lcommaaccent": 600, "Atilde": 600, "Aogonek": 600, "Aring": 600, "Otilde": 600, "zdotaccent": 600, "Ecaron": 600, "Iogonek": 600, "kcommaaccent": 600, "minus": 600, "Icircumflex": 600, "ncaron": 600, "tcommaaccent": 600, "logicalnot": 600, "odieresis": 600, "udieresis": 600, "notequal": 600, "gcommaaccent": 600, "eth": 600, "zcaron": 600, "ncommaaccent": 600, "onesuperior": 600, "imacron": 600, "Euro": 600}, + }, + "Courier-BoldOblique": { + types.NewRectangle(-57.0, -250.0, 869.0, 801.0), + map[string]int{"space": 600, "exclam": 600, "quotedbl": 600, "numbersign": 600, "dollar": 600, "percent": 600, "ampersand": 600, "quoteright": 600, "parenleft": 600, "parenright": 600, "asterisk": 600, "plus": 600, "comma": 600, "hyphen": 600, "period": 600, "slash": 600, "zero": 600, "one": 600, "two": 600, "three": 600, "four": 600, "five": 600, "six": 600, "seven": 600, "eight": 600, "nine": 600, "colon": 600, "semicolon": 600, "less": 600, "equal": 600, "greater": 600, "question": 600, "at": 600, "A": 600, "B": 600, "C": 600, "D": 600, "E": 600, "F": 600, "G": 600, "H": 600, "I": 600, "J": 600, "K": 600, "L": 600, "M": 600, "N": 600, "O": 600, "P": 600, "Q": 600, "R": 600, "S": 600, "T": 600, "U": 600, "V": 600, "W": 600, "X": 600, "Y": 600, "Z": 600, "bracketleft": 600, "backslash": 600, "bracketright": 600, "asciicircum": 600, "underscore": 600, "quoteleft": 600, "a": 600, "b": 600, "c": 600, "d": 600, "e": 600, "f": 600, "g": 600, "h": 600, "i": 600, "j": 600, "k": 600, "l": 600, "m": 600, "n": 600, "o": 600, "p": 600, "q": 600, "r": 600, "s": 600, "t": 600, "u": 600, "v": 600, "w": 600, "x": 600, "y": 600, "z": 600, "braceleft": 600, "bar": 600, "braceright": 600, "asciitilde": 600, "exclamdown": 600, "cent": 600, "sterling": 600, "fraction": 600, "yen": 600, "florin": 600, "section": 600, "currency": 600, "quotesingle": 600, "quotedblleft": 600, "guillemotleft": 600, "guilsinglleft": 600, "guilsinglright": 600, "fi": 600, "fl": 600, "endash": 600, "dagger": 600, "daggerdbl": 600, "periodcentered": 600, "paragraph": 600, "bullet": 600, "quotesinglbase": 600, "quotedblbase": 600, "quotedblright": 600, "guillemotright": 600, "ellipsis": 600, "perthousand": 600, "questiondown": 600, "grave": 600, "acute": 600, "circumflex": 600, "tilde": 600, "macron": 600, "breve": 600, "dotaccent": 600, "dieresis": 600, "ring": 600, "cedilla": 600, "hungarumlaut": 600, "ogonek": 600, "caron": 600, "emdash": 600, "AE": 600, "ordfeminine": 600, "Lslash": 600, "Oslash": 600, "OE": 600, "ordmasculine": 600, "ae": 600, "dotlessi": 600, "lslash": 600, "oslash": 600, "oe": 600, "germandbls": 600, "Idieresis": 600, "eacute": 600, "abreve": 600, "uhungarumlaut": 600, "ecaron": 600, "Ydieresis": 600, "divide": 600, "Yacute": 600, "Acircumflex": 600, "aacute": 600, "Ucircumflex": 600, "yacute": 600, "scommaaccent": 600, "ecircumflex": 600, "Uring": 600, "Udieresis": 600, "aogonek": 600, "Uacute": 600, "uogonek": 600, "Edieresis": 600, "Dcroat": 600, "commaaccent": 600, "copyright": 600, "Emacron": 600, "ccaron": 600, "aring": 600, "Ncommaaccent": 600, "lacute": 600, "agrave": 600, "Tcommaaccent": 600, "Cacute": 600, "atilde": 600, "Edotaccent": 600, "scaron": 600, "scedilla": 600, "iacute": 600, "lozenge": 600, "Rcaron": 600, "Gcommaaccent": 600, "ucircumflex": 600, "acircumflex": 600, "Amacron": 600, "rcaron": 600, "ccedilla": 600, "Zdotaccent": 600, "Thorn": 600, "Omacron": 600, "Racute": 600, "Sacute": 600, "dcaron": 600, "Umacron": 600, "uring": 600, "threesuperior": 600, "Ograve": 600, "Agrave": 600, "Abreve": 600, "multiply": 600, "uacute": 600, "Tcaron": 600, "partialdiff": 600, "ydieresis": 600, "Nacute": 600, "icircumflex": 600, "Ecircumflex": 600, "adieresis": 600, "edieresis": 600, "cacute": 600, "nacute": 600, "umacron": 600, "Ncaron": 600, "Iacute": 600, "plusminus": 600, "brokenbar": 600, "registered": 600, "Gbreve": 600, "Idotaccent": 600, "summation": 600, "Egrave": 600, "racute": 600, "omacron": 600, "Zacute": 600, "Zcaron": 600, "greaterequal": 600, "Eth": 600, "Ccedilla": 600, "lcommaaccent": 600, "tcaron": 600, "eogonek": 600, "Uogonek": 600, "Aacute": 600, "Adieresis": 600, "egrave": 600, "zacute": 600, "iogonek": 600, "Oacute": 600, "oacute": 600, "amacron": 600, "sacute": 600, "idieresis": 600, "Ocircumflex": 600, "Ugrave": 600, "Delta": 600, "thorn": 600, "twosuperior": 600, "Odieresis": 600, "mu": 600, "igrave": 600, "ohungarumlaut": 600, "Eogonek": 600, "dcroat": 600, "threequarters": 600, "Scedilla": 600, "lcaron": 600, "Kcommaaccent": 600, "Lacute": 600, "trademark": 600, "edotaccent": 600, "Igrave": 600, "Imacron": 600, "Lcaron": 600, "onehalf": 600, "lessequal": 600, "ocircumflex": 600, "ntilde": 600, "Uhungarumlaut": 600, "Eacute": 600, "emacron": 600, "gbreve": 600, "onequarter": 600, "Scaron": 600, "Scommaaccent": 600, "Ohungarumlaut": 600, "degree": 600, "ograve": 600, "Ccaron": 600, "ugrave": 600, "radical": 600, "Dcaron": 600, "rcommaaccent": 600, "Ntilde": 600, "otilde": 600, "Rcommaaccent": 600, "Lcommaaccent": 600, "Atilde": 600, "Aogonek": 600, "Aring": 600, "Otilde": 600, "zdotaccent": 600, "Ecaron": 600, "Iogonek": 600, "kcommaaccent": 600, "minus": 600, "Icircumflex": 600, "ncaron": 600, "tcommaaccent": 600, "logicalnot": 600, "odieresis": 600, "udieresis": 600, "notequal": 600, "gcommaaccent": 600, "eth": 600, "zcaron": 600, "ncommaaccent": 600, "onesuperior": 600, "imacron": 600, "Euro": 600}, + }, + "Courier-Oblique": { + types.NewRectangle(-27.0, -250.0, 849.0, 805.0), + map[string]int{"space": 600, "exclam": 600, "quotedbl": 600, "numbersign": 600, "dollar": 600, "percent": 600, "ampersand": 600, "quoteright": 600, "parenleft": 600, "parenright": 600, "asterisk": 600, "plus": 600, "comma": 600, "hyphen": 600, "period": 600, "slash": 600, "zero": 600, "one": 600, "two": 600, "three": 600, "four": 600, "five": 600, "six": 600, "seven": 600, "eight": 600, "nine": 600, "colon": 600, "semicolon": 600, "less": 600, "equal": 600, "greater": 600, "question": 600, "at": 600, "A": 600, "B": 600, "C": 600, "D": 600, "E": 600, "F": 600, "G": 600, "H": 600, "I": 600, "J": 600, "K": 600, "L": 600, "M": 600, "N": 600, "O": 600, "P": 600, "Q": 600, "R": 600, "S": 600, "T": 600, "U": 600, "V": 600, "W": 600, "X": 600, "Y": 600, "Z": 600, "bracketleft": 600, "backslash": 600, "bracketright": 600, "asciicircum": 600, "underscore": 600, "quoteleft": 600, "a": 600, "b": 600, "c": 600, "d": 600, "e": 600, "f": 600, "g": 600, "h": 600, "i": 600, "j": 600, "k": 600, "l": 600, "m": 600, "n": 600, "o": 600, "p": 600, "q": 600, "r": 600, "s": 600, "t": 600, "u": 600, "v": 600, "w": 600, "x": 600, "y": 600, "z": 600, "braceleft": 600, "bar": 600, "braceright": 600, "asciitilde": 600, "exclamdown": 600, "cent": 600, "sterling": 600, "fraction": 600, "yen": 600, "florin": 600, "section": 600, "currency": 600, "quotesingle": 600, "quotedblleft": 600, "guillemotleft": 600, "guilsinglleft": 600, "guilsinglright": 600, "fi": 600, "fl": 600, "endash": 600, "dagger": 600, "daggerdbl": 600, "periodcentered": 600, "paragraph": 600, "bullet": 600, "quotesinglbase": 600, "quotedblbase": 600, "quotedblright": 600, "guillemotright": 600, "ellipsis": 600, "perthousand": 600, "questiondown": 600, "grave": 600, "acute": 600, "circumflex": 600, "tilde": 600, "macron": 600, "breve": 600, "dotaccent": 600, "dieresis": 600, "ring": 600, "cedilla": 600, "hungarumlaut": 600, "ogonek": 600, "caron": 600, "emdash": 600, "AE": 600, "ordfeminine": 600, "Lslash": 600, "Oslash": 600, "OE": 600, "ordmasculine": 600, "ae": 600, "dotlessi": 600, "lslash": 600, "oslash": 600, "oe": 600, "germandbls": 600, "Idieresis": 600, "eacute": 600, "abreve": 600, "uhungarumlaut": 600, "ecaron": 600, "Ydieresis": 600, "divide": 600, "Yacute": 600, "Acircumflex": 600, "aacute": 600, "Ucircumflex": 600, "yacute": 600, "scommaaccent": 600, "ecircumflex": 600, "Uring": 600, "Udieresis": 600, "aogonek": 600, "Uacute": 600, "uogonek": 600, "Edieresis": 600, "Dcroat": 600, "commaaccent": 600, "copyright": 600, "Emacron": 600, "ccaron": 600, "aring": 600, "Ncommaaccent": 600, "lacute": 600, "agrave": 600, "Tcommaaccent": 600, "Cacute": 600, "atilde": 600, "Edotaccent": 600, "scaron": 600, "scedilla": 600, "iacute": 600, "lozenge": 600, "Rcaron": 600, "Gcommaaccent": 600, "ucircumflex": 600, "acircumflex": 600, "Amacron": 600, "rcaron": 600, "ccedilla": 600, "Zdotaccent": 600, "Thorn": 600, "Omacron": 600, "Racute": 600, "Sacute": 600, "dcaron": 600, "Umacron": 600, "uring": 600, "threesuperior": 600, "Ograve": 600, "Agrave": 600, "Abreve": 600, "multiply": 600, "uacute": 600, "Tcaron": 600, "partialdiff": 600, "ydieresis": 600, "Nacute": 600, "icircumflex": 600, "Ecircumflex": 600, "adieresis": 600, "edieresis": 600, "cacute": 600, "nacute": 600, "umacron": 600, "Ncaron": 600, "Iacute": 600, "plusminus": 600, "brokenbar": 600, "registered": 600, "Gbreve": 600, "Idotaccent": 600, "summation": 600, "Egrave": 600, "racute": 600, "omacron": 600, "Zacute": 600, "Zcaron": 600, "greaterequal": 600, "Eth": 600, "Ccedilla": 600, "lcommaaccent": 600, "tcaron": 600, "eogonek": 600, "Uogonek": 600, "Aacute": 600, "Adieresis": 600, "egrave": 600, "zacute": 600, "iogonek": 600, "Oacute": 600, "oacute": 600, "amacron": 600, "sacute": 600, "idieresis": 600, "Ocircumflex": 600, "Ugrave": 600, "Delta": 600, "thorn": 600, "twosuperior": 600, "Odieresis": 600, "mu": 600, "igrave": 600, "ohungarumlaut": 600, "Eogonek": 600, "dcroat": 600, "threequarters": 600, "Scedilla": 600, "lcaron": 600, "Kcommaaccent": 600, "Lacute": 600, "trademark": 600, "edotaccent": 600, "Igrave": 600, "Imacron": 600, "Lcaron": 600, "onehalf": 600, "lessequal": 600, "ocircumflex": 600, "ntilde": 600, "Uhungarumlaut": 600, "Eacute": 600, "emacron": 600, "gbreve": 600, "onequarter": 600, "Scaron": 600, "Scommaaccent": 600, "Ohungarumlaut": 600, "degree": 600, "ograve": 600, "Ccaron": 600, "ugrave": 600, "radical": 600, "Dcaron": 600, "rcommaaccent": 600, "Ntilde": 600, "otilde": 600, "Rcommaaccent": 600, "Lcommaaccent": 600, "Atilde": 600, "Aogonek": 600, "Aring": 600, "Otilde": 600, "zdotaccent": 600, "Ecaron": 600, "Iogonek": 600, "kcommaaccent": 600, "minus": 600, "Icircumflex": 600, "ncaron": 600, "tcommaaccent": 600, "logicalnot": 600, "odieresis": 600, "udieresis": 600, "notequal": 600, "gcommaaccent": 600, "eth": 600, "zcaron": 600, "ncommaaccent": 600, "onesuperior": 600, "imacron": 600, "Euro": 600}, + }, + "Courier": { + types.NewRectangle(-23.0, -250.0, 715.0, 805.0), + map[string]int{"space": 600, "exclam": 600, "quotedbl": 600, "numbersign": 600, "dollar": 600, "percent": 600, "ampersand": 600, "quoteright": 600, "parenleft": 600, "parenright": 600, "asterisk": 600, "plus": 600, "comma": 600, "hyphen": 600, "period": 600, "slash": 600, "zero": 600, "one": 600, "two": 600, "three": 600, "four": 600, "five": 600, "six": 600, "seven": 600, "eight": 600, "nine": 600, "colon": 600, "semicolon": 600, "less": 600, "equal": 600, "greater": 600, "question": 600, "at": 600, "A": 600, "B": 600, "C": 600, "D": 600, "E": 600, "F": 600, "G": 600, "H": 600, "I": 600, "J": 600, "K": 600, "L": 600, "M": 600, "N": 600, "O": 600, "P": 600, "Q": 600, "R": 600, "S": 600, "T": 600, "U": 600, "V": 600, "W": 600, "X": 600, "Y": 600, "Z": 600, "bracketleft": 600, "backslash": 600, "bracketright": 600, "asciicircum": 600, "underscore": 600, "quoteleft": 600, "a": 600, "b": 600, "c": 600, "d": 600, "e": 600, "f": 600, "g": 600, "h": 600, "i": 600, "j": 600, "k": 600, "l": 600, "m": 600, "n": 600, "o": 600, "p": 600, "q": 600, "r": 600, "s": 600, "t": 600, "u": 600, "v": 600, "w": 600, "x": 600, "y": 600, "z": 600, "braceleft": 600, "bar": 600, "braceright": 600, "asciitilde": 600, "exclamdown": 600, "cent": 600, "sterling": 600, "fraction": 600, "yen": 600, "florin": 600, "section": 600, "currency": 600, "quotesingle": 600, "quotedblleft": 600, "guillemotleft": 600, "guilsinglleft": 600, "guilsinglright": 600, "fi": 600, "fl": 600, "endash": 600, "dagger": 600, "daggerdbl": 600, "periodcentered": 600, "paragraph": 600, "bullet": 600, "quotesinglbase": 600, "quotedblbase": 600, "quotedblright": 600, "guillemotright": 600, "ellipsis": 600, "perthousand": 600, "questiondown": 600, "grave": 600, "acute": 600, "circumflex": 600, "tilde": 600, "macron": 600, "breve": 600, "dotaccent": 600, "dieresis": 600, "ring": 600, "cedilla": 600, "hungarumlaut": 600, "ogonek": 600, "caron": 600, "emdash": 600, "AE": 600, "ordfeminine": 600, "Lslash": 600, "Oslash": 600, "OE": 600, "ordmasculine": 600, "ae": 600, "dotlessi": 600, "lslash": 600, "oslash": 600, "oe": 600, "germandbls": 600, "Idieresis": 600, "eacute": 600, "abreve": 600, "uhungarumlaut": 600, "ecaron": 600, "Ydieresis": 600, "divide": 600, "Yacute": 600, "Acircumflex": 600, "aacute": 600, "Ucircumflex": 600, "yacute": 600, "scommaaccent": 600, "ecircumflex": 600, "Uring": 600, "Udieresis": 600, "aogonek": 600, "Uacute": 600, "uogonek": 600, "Edieresis": 600, "Dcroat": 600, "commaaccent": 600, "copyright": 600, "Emacron": 600, "ccaron": 600, "aring": 600, "Ncommaaccent": 600, "lacute": 600, "agrave": 600, "Tcommaaccent": 600, "Cacute": 600, "atilde": 600, "Edotaccent": 600, "scaron": 600, "scedilla": 600, "iacute": 600, "lozenge": 600, "Rcaron": 600, "Gcommaaccent": 600, "ucircumflex": 600, "acircumflex": 600, "Amacron": 600, "rcaron": 600, "ccedilla": 600, "Zdotaccent": 600, "Thorn": 600, "Omacron": 600, "Racute": 600, "Sacute": 600, "dcaron": 600, "Umacron": 600, "uring": 600, "threesuperior": 600, "Ograve": 600, "Agrave": 600, "Abreve": 600, "multiply": 600, "uacute": 600, "Tcaron": 600, "partialdiff": 600, "ydieresis": 600, "Nacute": 600, "icircumflex": 600, "Ecircumflex": 600, "adieresis": 600, "edieresis": 600, "cacute": 600, "nacute": 600, "umacron": 600, "Ncaron": 600, "Iacute": 600, "plusminus": 600, "brokenbar": 600, "registered": 600, "Gbreve": 600, "Idotaccent": 600, "summation": 600, "Egrave": 600, "racute": 600, "omacron": 600, "Zacute": 600, "Zcaron": 600, "greaterequal": 600, "Eth": 600, "Ccedilla": 600, "lcommaaccent": 600, "tcaron": 600, "eogonek": 600, "Uogonek": 600, "Aacute": 600, "Adieresis": 600, "egrave": 600, "zacute": 600, "iogonek": 600, "Oacute": 600, "oacute": 600, "amacron": 600, "sacute": 600, "idieresis": 600, "Ocircumflex": 600, "Ugrave": 600, "Delta": 600, "thorn": 600, "twosuperior": 600, "Odieresis": 600, "mu": 600, "igrave": 600, "ohungarumlaut": 600, "Eogonek": 600, "dcroat": 600, "threequarters": 600, "Scedilla": 600, "lcaron": 600, "Kcommaaccent": 600, "Lacute": 600, "trademark": 600, "edotaccent": 600, "Igrave": 600, "Imacron": 600, "Lcaron": 600, "onehalf": 600, "lessequal": 600, "ocircumflex": 600, "ntilde": 600, "Uhungarumlaut": 600, "Eacute": 600, "emacron": 600, "gbreve": 600, "onequarter": 600, "Scaron": 600, "Scommaaccent": 600, "Ohungarumlaut": 600, "degree": 600, "ograve": 600, "Ccaron": 600, "ugrave": 600, "radical": 600, "Dcaron": 600, "rcommaaccent": 600, "Ntilde": 600, "otilde": 600, "Rcommaaccent": 600, "Lcommaaccent": 600, "Atilde": 600, "Aogonek": 600, "Aring": 600, "Otilde": 600, "zdotaccent": 600, "Ecaron": 600, "Iogonek": 600, "kcommaaccent": 600, "minus": 600, "Icircumflex": 600, "ncaron": 600, "tcommaaccent": 600, "logicalnot": 600, "odieresis": 600, "udieresis": 600, "notequal": 600, "gcommaaccent": 600, "eth": 600, "zcaron": 600, "ncommaaccent": 600, "onesuperior": 600, "imacron": 600, "Euro": 600}, + }, + "Helvetica-Bold": { + types.NewRectangle(-170.0, -228.0, 1003.0, 962.0), + map[string]int{"space": 278, "exclam": 333, "quotedbl": 474, "numbersign": 556, "dollar": 556, "percent": 889, "ampersand": 722, "quoteright": 278, "parenleft": 333, "parenright": 333, "asterisk": 389, "plus": 584, "comma": 278, "hyphen": 333, "period": 278, "slash": 278, "zero": 556, "one": 556, "two": 556, "three": 556, "four": 556, "five": 556, "six": 556, "seven": 556, "eight": 556, "nine": 556, "colon": 333, "semicolon": 333, "less": 584, "equal": 584, "greater": 584, "question": 611, "at": 975, "A": 722, "B": 722, "C": 722, "D": 722, "E": 667, "F": 611, "G": 778, "H": 722, "I": 278, "J": 556, "K": 722, "L": 611, "M": 833, "N": 722, "O": 778, "P": 667, "Q": 778, "R": 722, "S": 667, "T": 611, "U": 722, "V": 667, "W": 944, "X": 667, "Y": 667, "Z": 611, "bracketleft": 333, "backslash": 278, "bracketright": 333, "asciicircum": 584, "underscore": 556, "quoteleft": 278, "a": 556, "b": 611, "c": 556, "d": 611, "e": 556, "f": 333, "g": 611, "h": 611, "i": 278, "j": 278, "k": 556, "l": 278, "m": 889, "n": 611, "o": 611, "p": 611, "q": 611, "r": 389, "s": 556, "t": 333, "u": 611, "v": 556, "w": 778, "x": 556, "y": 556, "z": 500, "braceleft": 389, "bar": 280, "braceright": 389, "asciitilde": 584, "exclamdown": 333, "cent": 556, "sterling": 556, "fraction": 167, "yen": 556, "florin": 556, "section": 556, "currency": 556, "quotesingle": 238, "quotedblleft": 500, "guillemotleft": 556, "guilsinglleft": 333, "guilsinglright": 333, "fi": 611, "fl": 611, "endash": 556, "dagger": 556, "daggerdbl": 556, "periodcentered": 278, "paragraph": 556, "bullet": 350, "quotesinglbase": 278, "quotedblbase": 500, "quotedblright": 500, "guillemotright": 556, "ellipsis": 1000, "perthousand": 1000, "questiondown": 611, "grave": 333, "acute": 333, "circumflex": 333, "tilde": 333, "macron": 333, "breve": 333, "dotaccent": 333, "dieresis": 333, "ring": 333, "cedilla": 333, "hungarumlaut": 333, "ogonek": 333, "caron": 333, "emdash": 1000, "AE": 1000, "ordfeminine": 370, "Lslash": 611, "Oslash": 778, "OE": 1000, "ordmasculine": 365, "ae": 889, "dotlessi": 278, "lslash": 278, "oslash": 611, "oe": 944, "germandbls": 611, "Idieresis": 278, "eacute": 556, "abreve": 556, "uhungarumlaut": 611, "ecaron": 556, "Ydieresis": 667, "divide": 584, "Yacute": 667, "Acircumflex": 722, "aacute": 556, "Ucircumflex": 722, "yacute": 556, "scommaaccent": 556, "ecircumflex": 556, "Uring": 722, "Udieresis": 722, "aogonek": 556, "Uacute": 722, "uogonek": 611, "Edieresis": 667, "Dcroat": 722, "commaaccent": 250, "copyright": 737, "Emacron": 667, "ccaron": 556, "aring": 556, "Ncommaaccent": 722, "lacute": 278, "agrave": 556, "Tcommaaccent": 611, "Cacute": 722, "atilde": 556, "Edotaccent": 667, "scaron": 556, "scedilla": 556, "iacute": 278, "lozenge": 494, "Rcaron": 722, "Gcommaaccent": 778, "ucircumflex": 611, "acircumflex": 556, "Amacron": 722, "rcaron": 389, "ccedilla": 556, "Zdotaccent": 611, "Thorn": 667, "Omacron": 778, "Racute": 722, "Sacute": 667, "dcaron": 743, "Umacron": 722, "uring": 611, "threesuperior": 333, "Ograve": 778, "Agrave": 722, "Abreve": 722, "multiply": 584, "uacute": 611, "Tcaron": 611, "partialdiff": 494, "ydieresis": 556, "Nacute": 722, "icircumflex": 278, "Ecircumflex": 667, "adieresis": 556, "edieresis": 556, "cacute": 556, "nacute": 611, "umacron": 611, "Ncaron": 722, "Iacute": 278, "plusminus": 584, "brokenbar": 280, "registered": 737, "Gbreve": 778, "Idotaccent": 278, "summation": 600, "Egrave": 667, "racute": 389, "omacron": 611, "Zacute": 611, "Zcaron": 611, "greaterequal": 549, "Eth": 722, "Ccedilla": 722, "lcommaaccent": 278, "tcaron": 389, "eogonek": 556, "Uogonek": 722, "Aacute": 722, "Adieresis": 722, "egrave": 556, "zacute": 500, "iogonek": 278, "Oacute": 778, "oacute": 611, "amacron": 556, "sacute": 556, "idieresis": 278, "Ocircumflex": 778, "Ugrave": 722, "Delta": 612, "thorn": 611, "twosuperior": 333, "Odieresis": 778, "mu": 611, "igrave": 278, "ohungarumlaut": 611, "Eogonek": 667, "dcroat": 611, "threequarters": 834, "Scedilla": 667, "lcaron": 400, "Kcommaaccent": 722, "Lacute": 611, "trademark": 1000, "edotaccent": 556, "Igrave": 278, "Imacron": 278, "Lcaron": 611, "onehalf": 834, "lessequal": 549, "ocircumflex": 611, "ntilde": 611, "Uhungarumlaut": 722, "Eacute": 667, "emacron": 556, "gbreve": 611, "onequarter": 834, "Scaron": 667, "Scommaaccent": 667, "Ohungarumlaut": 778, "degree": 400, "ograve": 611, "Ccaron": 722, "ugrave": 611, "radical": 549, "Dcaron": 722, "rcommaaccent": 389, "Ntilde": 722, "otilde": 611, "Rcommaaccent": 722, "Lcommaaccent": 611, "Atilde": 722, "Aogonek": 722, "Aring": 722, "Otilde": 778, "zdotaccent": 500, "Ecaron": 667, "Iogonek": 278, "kcommaaccent": 556, "minus": 584, "Icircumflex": 278, "ncaron": 611, "tcommaaccent": 333, "logicalnot": 584, "odieresis": 611, "udieresis": 611, "notequal": 549, "gcommaaccent": 611, "eth": 611, "zcaron": 500, "ncommaaccent": 611, "onesuperior": 333, "imacron": 278, "Euro": 556}, + }, + "Helvetica-BoldOblique": { + types.NewRectangle(-174.0, -228.0, 1114.0, 962.0), + map[string]int{"space": 278, "exclam": 333, "quotedbl": 474, "numbersign": 556, "dollar": 556, "percent": 889, "ampersand": 722, "quoteright": 278, "parenleft": 333, "parenright": 333, "asterisk": 389, "plus": 584, "comma": 278, "hyphen": 333, "period": 278, "slash": 278, "zero": 556, "one": 556, "two": 556, "three": 556, "four": 556, "five": 556, "six": 556, "seven": 556, "eight": 556, "nine": 556, "colon": 333, "semicolon": 333, "less": 584, "equal": 584, "greater": 584, "question": 611, "at": 975, "A": 722, "B": 722, "C": 722, "D": 722, "E": 667, "F": 611, "G": 778, "H": 722, "I": 278, "J": 556, "K": 722, "L": 611, "M": 833, "N": 722, "O": 778, "P": 667, "Q": 778, "R": 722, "S": 667, "T": 611, "U": 722, "V": 667, "W": 944, "X": 667, "Y": 667, "Z": 611, "bracketleft": 333, "backslash": 278, "bracketright": 333, "asciicircum": 584, "underscore": 556, "quoteleft": 278, "a": 556, "b": 611, "c": 556, "d": 611, "e": 556, "f": 333, "g": 611, "h": 611, "i": 278, "j": 278, "k": 556, "l": 278, "m": 889, "n": 611, "o": 611, "p": 611, "q": 611, "r": 389, "s": 556, "t": 333, "u": 611, "v": 556, "w": 778, "x": 556, "y": 556, "z": 500, "braceleft": 389, "bar": 280, "braceright": 389, "asciitilde": 584, "exclamdown": 333, "cent": 556, "sterling": 556, "fraction": 167, "yen": 556, "florin": 556, "section": 556, "currency": 556, "quotesingle": 238, "quotedblleft": 500, "guillemotleft": 556, "guilsinglleft": 333, "guilsinglright": 333, "fi": 611, "fl": 611, "endash": 556, "dagger": 556, "daggerdbl": 556, "periodcentered": 278, "paragraph": 556, "bullet": 350, "quotesinglbase": 278, "quotedblbase": 500, "quotedblright": 500, "guillemotright": 556, "ellipsis": 1000, "perthousand": 1000, "questiondown": 611, "grave": 333, "acute": 333, "circumflex": 333, "tilde": 333, "macron": 333, "breve": 333, "dotaccent": 333, "dieresis": 333, "ring": 333, "cedilla": 333, "hungarumlaut": 333, "ogonek": 333, "caron": 333, "emdash": 1000, "AE": 1000, "ordfeminine": 370, "Lslash": 611, "Oslash": 778, "OE": 1000, "ordmasculine": 365, "ae": 889, "dotlessi": 278, "lslash": 278, "oslash": 611, "oe": 944, "germandbls": 611, "Idieresis": 278, "eacute": 556, "abreve": 556, "uhungarumlaut": 611, "ecaron": 556, "Ydieresis": 667, "divide": 584, "Yacute": 667, "Acircumflex": 722, "aacute": 556, "Ucircumflex": 722, "yacute": 556, "scommaaccent": 556, "ecircumflex": 556, "Uring": 722, "Udieresis": 722, "aogonek": 556, "Uacute": 722, "uogonek": 611, "Edieresis": 667, "Dcroat": 722, "commaaccent": 250, "copyright": 737, "Emacron": 667, "ccaron": 556, "aring": 556, "Ncommaaccent": 722, "lacute": 278, "agrave": 556, "Tcommaaccent": 611, "Cacute": 722, "atilde": 556, "Edotaccent": 667, "scaron": 556, "scedilla": 556, "iacute": 278, "lozenge": 494, "Rcaron": 722, "Gcommaaccent": 778, "ucircumflex": 611, "acircumflex": 556, "Amacron": 722, "rcaron": 389, "ccedilla": 556, "Zdotaccent": 611, "Thorn": 667, "Omacron": 778, "Racute": 722, "Sacute": 667, "dcaron": 743, "Umacron": 722, "uring": 611, "threesuperior": 333, "Ograve": 778, "Agrave": 722, "Abreve": 722, "multiply": 584, "uacute": 611, "Tcaron": 611, "partialdiff": 494, "ydieresis": 556, "Nacute": 722, "icircumflex": 278, "Ecircumflex": 667, "adieresis": 556, "edieresis": 556, "cacute": 556, "nacute": 611, "umacron": 611, "Ncaron": 722, "Iacute": 278, "plusminus": 584, "brokenbar": 280, "registered": 737, "Gbreve": 778, "Idotaccent": 278, "summation": 600, "Egrave": 667, "racute": 389, "omacron": 611, "Zacute": 611, "Zcaron": 611, "greaterequal": 549, "Eth": 722, "Ccedilla": 722, "lcommaaccent": 278, "tcaron": 389, "eogonek": 556, "Uogonek": 722, "Aacute": 722, "Adieresis": 722, "egrave": 556, "zacute": 500, "iogonek": 278, "Oacute": 778, "oacute": 611, "amacron": 556, "sacute": 556, "idieresis": 278, "Ocircumflex": 778, "Ugrave": 722, "Delta": 612, "thorn": 611, "twosuperior": 333, "Odieresis": 778, "mu": 611, "igrave": 278, "ohungarumlaut": 611, "Eogonek": 667, "dcroat": 611, "threequarters": 834, "Scedilla": 667, "lcaron": 400, "Kcommaaccent": 722, "Lacute": 611, "trademark": 1000, "edotaccent": 556, "Igrave": 278, "Imacron": 278, "Lcaron": 611, "onehalf": 834, "lessequal": 549, "ocircumflex": 611, "ntilde": 611, "Uhungarumlaut": 722, "Eacute": 667, "emacron": 556, "gbreve": 611, "onequarter": 834, "Scaron": 667, "Scommaaccent": 667, "Ohungarumlaut": 778, "degree": 400, "ograve": 611, "Ccaron": 722, "ugrave": 611, "radical": 549, "Dcaron": 722, "rcommaaccent": 389, "Ntilde": 722, "otilde": 611, "Rcommaaccent": 722, "Lcommaaccent": 611, "Atilde": 722, "Aogonek": 722, "Aring": 722, "Otilde": 778, "zdotaccent": 500, "Ecaron": 667, "Iogonek": 278, "kcommaaccent": 556, "minus": 584, "Icircumflex": 278, "ncaron": 611, "tcommaaccent": 333, "logicalnot": 584, "odieresis": 611, "udieresis": 611, "notequal": 549, "gcommaaccent": 611, "eth": 611, "zcaron": 500, "ncommaaccent": 611, "onesuperior": 333, "imacron": 278, "Euro": 556}, + }, + "Helvetica-Oblique": { + types.NewRectangle(-170.0, -225.0, 1116.0, 931.0), + map[string]int{"space": 278, "exclam": 278, "quotedbl": 355, "numbersign": 556, "dollar": 556, "percent": 889, "ampersand": 667, "quoteright": 222, "parenleft": 333, "parenright": 333, "asterisk": 389, "plus": 584, "comma": 278, "hyphen": 333, "period": 278, "slash": 278, "zero": 556, "one": 556, "two": 556, "three": 556, "four": 556, "five": 556, "six": 556, "seven": 556, "eight": 556, "nine": 556, "colon": 278, "semicolon": 278, "less": 584, "equal": 584, "greater": 584, "question": 556, "at": 1015, "A": 667, "B": 667, "C": 722, "D": 722, "E": 667, "F": 611, "G": 778, "H": 722, "I": 278, "J": 500, "K": 667, "L": 556, "M": 833, "N": 722, "O": 778, "P": 667, "Q": 778, "R": 722, "S": 667, "T": 611, "U": 722, "V": 667, "W": 944, "X": 667, "Y": 667, "Z": 611, "bracketleft": 278, "backslash": 278, "bracketright": 278, "asciicircum": 469, "underscore": 556, "quoteleft": 222, "a": 556, "b": 556, "c": 500, "d": 556, "e": 556, "f": 278, "g": 556, "h": 556, "i": 222, "j": 222, "k": 500, "l": 222, "m": 833, "n": 556, "o": 556, "p": 556, "q": 556, "r": 333, "s": 500, "t": 278, "u": 556, "v": 500, "w": 722, "x": 500, "y": 500, "z": 500, "braceleft": 334, "bar": 260, "braceright": 334, "asciitilde": 584, "exclamdown": 333, "cent": 556, "sterling": 556, "fraction": 167, "yen": 556, "florin": 556, "section": 556, "currency": 556, "quotesingle": 191, "quotedblleft": 333, "guillemotleft": 556, "guilsinglleft": 333, "guilsinglright": 333, "fi": 500, "fl": 500, "endash": 556, "dagger": 556, "daggerdbl": 556, "periodcentered": 278, "paragraph": 537, "bullet": 350, "quotesinglbase": 222, "quotedblbase": 333, "quotedblright": 333, "guillemotright": 556, "ellipsis": 1000, "perthousand": 1000, "questiondown": 611, "grave": 333, "acute": 333, "circumflex": 333, "tilde": 333, "macron": 333, "breve": 333, "dotaccent": 333, "dieresis": 333, "ring": 333, "cedilla": 333, "hungarumlaut": 333, "ogonek": 333, "caron": 333, "emdash": 1000, "AE": 1000, "ordfeminine": 370, "Lslash": 556, "Oslash": 778, "OE": 1000, "ordmasculine": 365, "ae": 889, "dotlessi": 278, "lslash": 222, "oslash": 611, "oe": 944, "germandbls": 611, "Idieresis": 278, "eacute": 556, "abreve": 556, "uhungarumlaut": 556, "ecaron": 556, "Ydieresis": 667, "divide": 584, "Yacute": 667, "Acircumflex": 667, "aacute": 556, "Ucircumflex": 722, "yacute": 500, "scommaaccent": 500, "ecircumflex": 556, "Uring": 722, "Udieresis": 722, "aogonek": 556, "Uacute": 722, "uogonek": 556, "Edieresis": 667, "Dcroat": 722, "commaaccent": 250, "copyright": 737, "Emacron": 667, "ccaron": 500, "aring": 556, "Ncommaaccent": 722, "lacute": 222, "agrave": 556, "Tcommaaccent": 611, "Cacute": 722, "atilde": 556, "Edotaccent": 667, "scaron": 500, "scedilla": 500, "iacute": 278, "lozenge": 471, "Rcaron": 722, "Gcommaaccent": 778, "ucircumflex": 556, "acircumflex": 556, "Amacron": 667, "rcaron": 333, "ccedilla": 500, "Zdotaccent": 611, "Thorn": 667, "Omacron": 778, "Racute": 722, "Sacute": 667, "dcaron": 643, "Umacron": 722, "uring": 556, "threesuperior": 333, "Ograve": 778, "Agrave": 667, "Abreve": 667, "multiply": 584, "uacute": 556, "Tcaron": 611, "partialdiff": 476, "ydieresis": 500, "Nacute": 722, "icircumflex": 278, "Ecircumflex": 667, "adieresis": 556, "edieresis": 556, "cacute": 500, "nacute": 556, "umacron": 556, "Ncaron": 722, "Iacute": 278, "plusminus": 584, "brokenbar": 260, "registered": 737, "Gbreve": 778, "Idotaccent": 278, "summation": 600, "Egrave": 667, "racute": 333, "omacron": 556, "Zacute": 611, "Zcaron": 611, "greaterequal": 549, "Eth": 722, "Ccedilla": 722, "lcommaaccent": 222, "tcaron": 317, "eogonek": 556, "Uogonek": 722, "Aacute": 667, "Adieresis": 667, "egrave": 556, "zacute": 500, "iogonek": 222, "Oacute": 778, "oacute": 556, "amacron": 556, "sacute": 500, "idieresis": 278, "Ocircumflex": 778, "Ugrave": 722, "Delta": 612, "thorn": 556, "twosuperior": 333, "Odieresis": 778, "mu": 556, "igrave": 278, "ohungarumlaut": 556, "Eogonek": 667, "dcroat": 556, "threequarters": 834, "Scedilla": 667, "lcaron": 299, "Kcommaaccent": 667, "Lacute": 556, "trademark": 1000, "edotaccent": 556, "Igrave": 278, "Imacron": 278, "Lcaron": 556, "onehalf": 834, "lessequal": 549, "ocircumflex": 556, "ntilde": 556, "Uhungarumlaut": 722, "Eacute": 667, "emacron": 556, "gbreve": 556, "onequarter": 834, "Scaron": 667, "Scommaaccent": 667, "Ohungarumlaut": 778, "degree": 400, "ograve": 556, "Ccaron": 722, "ugrave": 556, "radical": 453, "Dcaron": 722, "rcommaaccent": 333, "Ntilde": 722, "otilde": 556, "Rcommaaccent": 722, "Lcommaaccent": 556, "Atilde": 667, "Aogonek": 667, "Aring": 667, "Otilde": 778, "zdotaccent": 500, "Ecaron": 667, "Iogonek": 278, "kcommaaccent": 500, "minus": 584, "Icircumflex": 278, "ncaron": 556, "tcommaaccent": 278, "logicalnot": 584, "odieresis": 556, "udieresis": 556, "notequal": 549, "gcommaaccent": 556, "eth": 556, "zcaron": 500, "ncommaaccent": 556, "onesuperior": 333, "imacron": 278, "Euro": 556}, + }, + "Helvetica": { + types.NewRectangle(-166.0, -225.0, 1000.0, 931.0), + map[string]int{"space": 278, "exclam": 278, "quotedbl": 355, "numbersign": 556, "dollar": 556, "percent": 889, "ampersand": 667, "quoteright": 222, "parenleft": 333, "parenright": 333, "asterisk": 389, "plus": 584, "comma": 278, "hyphen": 333, "period": 278, "slash": 278, "zero": 556, "one": 556, "two": 556, "three": 556, "four": 556, "five": 556, "six": 556, "seven": 556, "eight": 556, "nine": 556, "colon": 278, "semicolon": 278, "less": 584, "equal": 584, "greater": 584, "question": 556, "at": 1015, "A": 667, "B": 667, "C": 722, "D": 722, "E": 667, "F": 611, "G": 778, "H": 722, "I": 278, "J": 500, "K": 667, "L": 556, "M": 833, "N": 722, "O": 778, "P": 667, "Q": 778, "R": 722, "S": 667, "T": 611, "U": 722, "V": 667, "W": 944, "X": 667, "Y": 667, "Z": 611, "bracketleft": 278, "backslash": 278, "bracketright": 278, "asciicircum": 469, "underscore": 556, "quoteleft": 222, "a": 556, "b": 556, "c": 500, "d": 556, "e": 556, "f": 278, "g": 556, "h": 556, "i": 222, "j": 222, "k": 500, "l": 222, "m": 833, "n": 556, "o": 556, "p": 556, "q": 556, "r": 333, "s": 500, "t": 278, "u": 556, "v": 500, "w": 722, "x": 500, "y": 500, "z": 500, "braceleft": 334, "bar": 260, "braceright": 334, "asciitilde": 584, "exclamdown": 333, "cent": 556, "sterling": 556, "fraction": 167, "yen": 556, "florin": 556, "section": 556, "currency": 556, "quotesingle": 191, "quotedblleft": 333, "guillemotleft": 556, "guilsinglleft": 333, "guilsinglright": 333, "fi": 500, "fl": 500, "endash": 556, "dagger": 556, "daggerdbl": 556, "periodcentered": 278, "paragraph": 537, "bullet": 350, "quotesinglbase": 222, "quotedblbase": 333, "quotedblright": 333, "guillemotright": 556, "ellipsis": 1000, "perthousand": 1000, "questiondown": 611, "grave": 333, "acute": 333, "circumflex": 333, "tilde": 333, "macron": 333, "breve": 333, "dotaccent": 333, "dieresis": 333, "ring": 333, "cedilla": 333, "hungarumlaut": 333, "ogonek": 333, "caron": 333, "emdash": 1000, "AE": 1000, "ordfeminine": 370, "Lslash": 556, "Oslash": 778, "OE": 1000, "ordmasculine": 365, "ae": 889, "dotlessi": 278, "lslash": 222, "oslash": 611, "oe": 944, "germandbls": 611, "Idieresis": 278, "eacute": 556, "abreve": 556, "uhungarumlaut": 556, "ecaron": 556, "Ydieresis": 667, "divide": 584, "Yacute": 667, "Acircumflex": 667, "aacute": 556, "Ucircumflex": 722, "yacute": 500, "scommaaccent": 500, "ecircumflex": 556, "Uring": 722, "Udieresis": 722, "aogonek": 556, "Uacute": 722, "uogonek": 556, "Edieresis": 667, "Dcroat": 722, "commaaccent": 250, "copyright": 737, "Emacron": 667, "ccaron": 500, "aring": 556, "Ncommaaccent": 722, "lacute": 222, "agrave": 556, "Tcommaaccent": 611, "Cacute": 722, "atilde": 556, "Edotaccent": 667, "scaron": 500, "scedilla": 500, "iacute": 278, "lozenge": 471, "Rcaron": 722, "Gcommaaccent": 778, "ucircumflex": 556, "acircumflex": 556, "Amacron": 667, "rcaron": 333, "ccedilla": 500, "Zdotaccent": 611, "Thorn": 667, "Omacron": 778, "Racute": 722, "Sacute": 667, "dcaron": 643, "Umacron": 722, "uring": 556, "threesuperior": 333, "Ograve": 778, "Agrave": 667, "Abreve": 667, "multiply": 584, "uacute": 556, "Tcaron": 611, "partialdiff": 476, "ydieresis": 500, "Nacute": 722, "icircumflex": 278, "Ecircumflex": 667, "adieresis": 556, "edieresis": 556, "cacute": 500, "nacute": 556, "umacron": 556, "Ncaron": 722, "Iacute": 278, "plusminus": 584, "brokenbar": 260, "registered": 737, "Gbreve": 778, "Idotaccent": 278, "summation": 600, "Egrave": 667, "racute": 333, "omacron": 556, "Zacute": 611, "Zcaron": 611, "greaterequal": 549, "Eth": 722, "Ccedilla": 722, "lcommaaccent": 222, "tcaron": 317, "eogonek": 556, "Uogonek": 722, "Aacute": 667, "Adieresis": 667, "egrave": 556, "zacute": 500, "iogonek": 222, "Oacute": 778, "oacute": 556, "amacron": 556, "sacute": 500, "idieresis": 278, "Ocircumflex": 778, "Ugrave": 722, "Delta": 612, "thorn": 556, "twosuperior": 333, "Odieresis": 778, "mu": 556, "igrave": 278, "ohungarumlaut": 556, "Eogonek": 667, "dcroat": 556, "threequarters": 834, "Scedilla": 667, "lcaron": 299, "Kcommaaccent": 667, "Lacute": 556, "trademark": 1000, "edotaccent": 556, "Igrave": 278, "Imacron": 278, "Lcaron": 556, "onehalf": 834, "lessequal": 549, "ocircumflex": 556, "ntilde": 556, "Uhungarumlaut": 722, "Eacute": 667, "emacron": 556, "gbreve": 556, "onequarter": 834, "Scaron": 667, "Scommaaccent": 667, "Ohungarumlaut": 778, "degree": 400, "ograve": 556, "Ccaron": 722, "ugrave": 556, "radical": 453, "Dcaron": 722, "rcommaaccent": 333, "Ntilde": 722, "otilde": 556, "Rcommaaccent": 722, "Lcommaaccent": 556, "Atilde": 667, "Aogonek": 667, "Aring": 667, "Otilde": 778, "zdotaccent": 500, "Ecaron": 667, "Iogonek": 278, "kcommaaccent": 500, "minus": 584, "Icircumflex": 278, "ncaron": 556, "tcommaaccent": 278, "logicalnot": 584, "odieresis": 556, "udieresis": 556, "notequal": 549, "gcommaaccent": 556, "eth": 556, "zcaron": 500, "ncommaaccent": 556, "onesuperior": 333, "imacron": 278, "Euro": 556}, + }, + "Symbol": { + types.NewRectangle(-180.0, -293.0, 1090.0, 1010.0), + map[string]int{"space": 250, "exclam": 333, "universal": 713, "numbersign": 500, "existential": 549, "percent": 833, "ampersand": 778, "suchthat": 439, "parenleft": 333, "parenright": 333, "asteriskmath": 500, "plus": 549, "comma": 250, "minus": 549, "period": 250, "slash": 278, "zero": 500, "one": 500, "two": 500, "three": 500, "four": 500, "five": 500, "six": 500, "seven": 500, "eight": 500, "nine": 500, "colon": 278, "semicolon": 278, "less": 549, "equal": 549, "greater": 549, "question": 444, "congruent": 549, "Alpha": 722, "Beta": 667, "Chi": 722, "Delta": 612, "Epsilon": 611, "Phi": 763, "Gamma": 603, "Eta": 722, "Iota": 333, "theta1": 631, "Kappa": 722, "Lambda": 686, "Mu": 889, "Nu": 722, "Omicron": 722, "Pi": 768, "Theta": 741, "Rho": 556, "Sigma": 592, "Tau": 611, "Upsilon": 690, "sigma1": 439, "Omega": 768, "Xi": 645, "Psi": 795, "Zeta": 611, "bracketleft": 333, "therefore": 863, "bracketright": 333, "perpendicular": 658, "underscore": 500, "radicalex": 500, "alpha": 631, "beta": 549, "chi": 549, "delta": 494, "epsilon": 439, "phi": 521, "gamma": 411, "eta": 603, "iota": 329, "phi1": 603, "kappa": 549, "lambda": 549, "mu": 576, "nu": 521, "omicron": 549, "pi": 549, "theta": 521, "rho": 549, "sigma": 603, "tau": 439, "upsilon": 576, "omega1": 713, "omega": 686, "xi": 493, "psi": 686, "zeta": 494, "braceleft": 480, "bar": 200, "braceright": 480, "similar": 549, "Euro": 750, "Upsilon1": 620, "minute": 247, "lessequal": 549, "fraction": 167, "infinity": 713, "florin": 500, "club": 753, "diamond": 753, "heart": 753, "spade": 753, "arrowboth": 1042, "arrowleft": 987, "arrowup": 603, "arrowright": 987, "arrowdown": 603, "degree": 400, "plusminus": 549, "second": 411, "greaterequal": 549, "multiply": 549, "proportional": 713, "partialdiff": 494, "bullet": 460, "divide": 549, "notequal": 549, "equivalence": 549, "approxequal": 549, "ellipsis": 1000, "arrowvertex": 603, "arrowhorizex": 1000, "carriagereturn": 658, "aleph": 823, "Ifraktur": 686, "Rfraktur": 795, "weierstrass": 987, "circlemultiply": 768, "circleplus": 768, "emptyset": 823, "intersection": 768, "union": 768, "propersuperset": 713, "reflexsuperset": 713, "notsubset": 713, "propersubset": 713, "reflexsubset": 713, "element": 713, "notelement": 713, "angle": 768, "gradient": 713, "registerserif": 790, "copyrightserif": 790, "trademarkserif": 890, "product": 823, "radical": 549, "dotmath": 250, "logicalnot": 713, "logicaland": 603, "logicalor": 603, "arrowdblboth": 1042, "arrowdblleft": 987, "arrowdblup": 603, "arrowdblright": 987, "arrowdbldown": 603, "lozenge": 494, "angleleft": 329, "registersans": 790, "copyrightsans": 790, "trademarksans": 786, "summation": 713, "parenlefttp": 384, "parenleftex": 384, "parenleftbt": 384, "bracketlefttp": 384, "bracketleftex": 384, "bracketleftbt": 384, "bracelefttp": 494, "braceleftmid": 494, "braceleftbt": 494, "braceex": 494, "angleright": 329, "integral": 274, "integraltp": 686, "integralex": 686, "integralbt": 686, "parenrighttp": 384, "parenrightex": 384, "parenrightbt": 384, "bracketrighttp": 384, "bracketrightex": 384, "bracketrightbt": 384, "bracerighttp": 494, "bracerightmid": 494, "bracerightbt": 494, "apple": 790}, + }, + "Times-Bold": { + types.NewRectangle(-168.0, -218.0, 1000.0, 935.0), + map[string]int{"space": 250, "exclam": 333, "quotedbl": 555, "numbersign": 500, "dollar": 500, "percent": 1000, "ampersand": 833, "quoteright": 333, "parenleft": 333, "parenright": 333, "asterisk": 500, "plus": 570, "comma": 250, "hyphen": 333, "period": 250, "slash": 278, "zero": 500, "one": 500, "two": 500, "three": 500, "four": 500, "five": 500, "six": 500, "seven": 500, "eight": 500, "nine": 500, "colon": 333, "semicolon": 333, "less": 570, "equal": 570, "greater": 570, "question": 500, "at": 930, "A": 722, "B": 667, "C": 722, "D": 722, "E": 667, "F": 611, "G": 778, "H": 778, "I": 389, "J": 500, "K": 778, "L": 667, "M": 944, "N": 722, "O": 778, "P": 611, "Q": 778, "R": 722, "S": 556, "T": 667, "U": 722, "V": 722, "W": 1000, "X": 722, "Y": 722, "Z": 667, "bracketleft": 333, "backslash": 278, "bracketright": 333, "asciicircum": 581, "underscore": 500, "quoteleft": 333, "a": 500, "b": 556, "c": 444, "d": 556, "e": 444, "f": 333, "g": 500, "h": 556, "i": 278, "j": 333, "k": 556, "l": 278, "m": 833, "n": 556, "o": 500, "p": 556, "q": 556, "r": 444, "s": 389, "t": 333, "u": 556, "v": 500, "w": 722, "x": 500, "y": 500, "z": 444, "braceleft": 394, "bar": 220, "braceright": 394, "asciitilde": 520, "exclamdown": 333, "cent": 500, "sterling": 500, "fraction": 167, "yen": 500, "florin": 500, "section": 500, "currency": 500, "quotesingle": 278, "quotedblleft": 500, "guillemotleft": 500, "guilsinglleft": 333, "guilsinglright": 333, "fi": 556, "fl": 556, "endash": 500, "dagger": 500, "daggerdbl": 500, "periodcentered": 250, "paragraph": 540, "bullet": 350, "quotesinglbase": 333, "quotedblbase": 500, "quotedblright": 500, "guillemotright": 500, "ellipsis": 1000, "perthousand": 1000, "questiondown": 500, "grave": 333, "acute": 333, "circumflex": 333, "tilde": 333, "macron": 333, "breve": 333, "dotaccent": 333, "dieresis": 333, "ring": 333, "cedilla": 333, "hungarumlaut": 333, "ogonek": 333, "caron": 333, "emdash": 1000, "AE": 1000, "ordfeminine": 300, "Lslash": 667, "Oslash": 778, "OE": 1000, "ordmasculine": 330, "ae": 722, "dotlessi": 278, "lslash": 278, "oslash": 500, "oe": 722, "germandbls": 556, "Idieresis": 389, "eacute": 444, "abreve": 500, "uhungarumlaut": 556, "ecaron": 444, "Ydieresis": 722, "divide": 570, "Yacute": 722, "Acircumflex": 722, "aacute": 500, "Ucircumflex": 722, "yacute": 500, "scommaaccent": 389, "ecircumflex": 444, "Uring": 722, "Udieresis": 722, "aogonek": 500, "Uacute": 722, "uogonek": 556, "Edieresis": 667, "Dcroat": 722, "commaaccent": 250, "copyright": 747, "Emacron": 667, "ccaron": 444, "aring": 500, "Ncommaaccent": 722, "lacute": 278, "agrave": 500, "Tcommaaccent": 667, "Cacute": 722, "atilde": 500, "Edotaccent": 667, "scaron": 389, "scedilla": 389, "iacute": 278, "lozenge": 494, "Rcaron": 722, "Gcommaaccent": 778, "ucircumflex": 556, "acircumflex": 500, "Amacron": 722, "rcaron": 444, "ccedilla": 444, "Zdotaccent": 667, "Thorn": 611, "Omacron": 778, "Racute": 722, "Sacute": 556, "dcaron": 672, "Umacron": 722, "uring": 556, "threesuperior": 300, "Ograve": 778, "Agrave": 722, "Abreve": 722, "multiply": 570, "uacute": 556, "Tcaron": 667, "partialdiff": 494, "ydieresis": 500, "Nacute": 722, "icircumflex": 278, "Ecircumflex": 667, "adieresis": 500, "edieresis": 444, "cacute": 444, "nacute": 556, "umacron": 556, "Ncaron": 722, "Iacute": 389, "plusminus": 570, "brokenbar": 220, "registered": 747, "Gbreve": 778, "Idotaccent": 389, "summation": 600, "Egrave": 667, "racute": 444, "omacron": 500, "Zacute": 667, "Zcaron": 667, "greaterequal": 549, "Eth": 722, "Ccedilla": 722, "lcommaaccent": 278, "tcaron": 416, "eogonek": 444, "Uogonek": 722, "Aacute": 722, "Adieresis": 722, "egrave": 444, "zacute": 444, "iogonek": 278, "Oacute": 778, "oacute": 500, "amacron": 500, "sacute": 389, "idieresis": 278, "Ocircumflex": 778, "Ugrave": 722, "Delta": 612, "thorn": 556, "twosuperior": 300, "Odieresis": 778, "mu": 556, "igrave": 278, "ohungarumlaut": 500, "Eogonek": 667, "dcroat": 556, "threequarters": 750, "Scedilla": 556, "lcaron": 394, "Kcommaaccent": 778, "Lacute": 667, "trademark": 1000, "edotaccent": 444, "Igrave": 389, "Imacron": 389, "Lcaron": 667, "onehalf": 750, "lessequal": 549, "ocircumflex": 500, "ntilde": 556, "Uhungarumlaut": 722, "Eacute": 667, "emacron": 444, "gbreve": 500, "onequarter": 750, "Scaron": 556, "Scommaaccent": 556, "Ohungarumlaut": 778, "degree": 400, "ograve": 500, "Ccaron": 722, "ugrave": 556, "radical": 549, "Dcaron": 722, "rcommaaccent": 444, "Ntilde": 722, "otilde": 500, "Rcommaaccent": 722, "Lcommaaccent": 667, "Atilde": 722, "Aogonek": 722, "Aring": 722, "Otilde": 778, "zdotaccent": 444, "Ecaron": 667, "Iogonek": 389, "kcommaaccent": 556, "minus": 570, "Icircumflex": 389, "ncaron": 556, "tcommaaccent": 333, "logicalnot": 570, "odieresis": 500, "udieresis": 556, "notequal": 549, "gcommaaccent": 500, "eth": 500, "zcaron": 444, "ncommaaccent": 556, "onesuperior": 300, "imacron": 278, "Euro": 500}, + }, + "Times-BoldItalic": { + types.NewRectangle(-200.0, -218.0, 996.0, 921.0), + map[string]int{"space": 250, "exclam": 389, "quotedbl": 555, "numbersign": 500, "dollar": 500, "percent": 833, "ampersand": 778, "quoteright": 333, "parenleft": 333, "parenright": 333, "asterisk": 500, "plus": 570, "comma": 250, "hyphen": 333, "period": 250, "slash": 278, "zero": 500, "one": 500, "two": 500, "three": 500, "four": 500, "five": 500, "six": 500, "seven": 500, "eight": 500, "nine": 500, "colon": 333, "semicolon": 333, "less": 570, "equal": 570, "greater": 570, "question": 500, "at": 832, "A": 667, "B": 667, "C": 667, "D": 722, "E": 667, "F": 667, "G": 722, "H": 778, "I": 389, "J": 500, "K": 667, "L": 611, "M": 889, "N": 722, "O": 722, "P": 611, "Q": 722, "R": 667, "S": 556, "T": 611, "U": 722, "V": 667, "W": 889, "X": 667, "Y": 611, "Z": 611, "bracketleft": 333, "backslash": 278, "bracketright": 333, "asciicircum": 570, "underscore": 500, "quoteleft": 333, "a": 500, "b": 500, "c": 444, "d": 500, "e": 444, "f": 333, "g": 500, "h": 556, "i": 278, "j": 278, "k": 500, "l": 278, "m": 778, "n": 556, "o": 500, "p": 500, "q": 500, "r": 389, "s": 389, "t": 278, "u": 556, "v": 444, "w": 667, "x": 500, "y": 444, "z": 389, "braceleft": 348, "bar": 220, "braceright": 348, "asciitilde": 570, "exclamdown": 389, "cent": 500, "sterling": 500, "fraction": 167, "yen": 500, "florin": 500, "section": 500, "currency": 500, "quotesingle": 278, "quotedblleft": 500, "guillemotleft": 500, "guilsinglleft": 333, "guilsinglright": 333, "fi": 556, "fl": 556, "endash": 500, "dagger": 500, "daggerdbl": 500, "periodcentered": 250, "paragraph": 500, "bullet": 350, "quotesinglbase": 333, "quotedblbase": 500, "quotedblright": 500, "guillemotright": 500, "ellipsis": 1000, "perthousand": 1000, "questiondown": 500, "grave": 333, "acute": 333, "circumflex": 333, "tilde": 333, "macron": 333, "breve": 333, "dotaccent": 333, "dieresis": 333, "ring": 333, "cedilla": 333, "hungarumlaut": 333, "ogonek": 333, "caron": 333, "emdash": 1000, "AE": 944, "ordfeminine": 266, "Lslash": 611, "Oslash": 722, "OE": 944, "ordmasculine": 300, "ae": 722, "dotlessi": 278, "lslash": 278, "oslash": 500, "oe": 722, "germandbls": 500, "Idieresis": 389, "eacute": 444, "abreve": 500, "uhungarumlaut": 556, "ecaron": 444, "Ydieresis": 611, "divide": 570, "Yacute": 611, "Acircumflex": 667, "aacute": 500, "Ucircumflex": 722, "yacute": 444, "scommaaccent": 389, "ecircumflex": 444, "Uring": 722, "Udieresis": 722, "aogonek": 500, "Uacute": 722, "uogonek": 556, "Edieresis": 667, "Dcroat": 722, "commaaccent": 250, "copyright": 747, "Emacron": 667, "ccaron": 444, "aring": 500, "Ncommaaccent": 722, "lacute": 278, "agrave": 500, "Tcommaaccent": 611, "Cacute": 667, "atilde": 500, "Edotaccent": 667, "scaron": 389, "scedilla": 389, "iacute": 278, "lozenge": 494, "Rcaron": 667, "Gcommaaccent": 722, "ucircumflex": 556, "acircumflex": 500, "Amacron": 667, "rcaron": 389, "ccedilla": 444, "Zdotaccent": 611, "Thorn": 611, "Omacron": 722, "Racute": 667, "Sacute": 556, "dcaron": 608, "Umacron": 722, "uring": 556, "threesuperior": 300, "Ograve": 722, "Agrave": 667, "Abreve": 667, "multiply": 570, "uacute": 556, "Tcaron": 611, "partialdiff": 494, "ydieresis": 444, "Nacute": 722, "icircumflex": 278, "Ecircumflex": 667, "adieresis": 500, "edieresis": 444, "cacute": 444, "nacute": 556, "umacron": 556, "Ncaron": 722, "Iacute": 389, "plusminus": 570, "brokenbar": 220, "registered": 747, "Gbreve": 722, "Idotaccent": 389, "summation": 600, "Egrave": 667, "racute": 389, "omacron": 500, "Zacute": 611, "Zcaron": 611, "greaterequal": 549, "Eth": 722, "Ccedilla": 667, "lcommaaccent": 278, "tcaron": 366, "eogonek": 444, "Uogonek": 722, "Aacute": 667, "Adieresis": 667, "egrave": 444, "zacute": 389, "iogonek": 278, "Oacute": 722, "oacute": 500, "amacron": 500, "sacute": 389, "idieresis": 278, "Ocircumflex": 722, "Ugrave": 722, "Delta": 612, "thorn": 500, "twosuperior": 300, "Odieresis": 722, "mu": 576, "igrave": 278, "ohungarumlaut": 500, "Eogonek": 667, "dcroat": 500, "threequarters": 750, "Scedilla": 556, "lcaron": 382, "Kcommaaccent": 667, "Lacute": 611, "trademark": 1000, "edotaccent": 444, "Igrave": 389, "Imacron": 389, "Lcaron": 611, "onehalf": 750, "lessequal": 549, "ocircumflex": 500, "ntilde": 556, "Uhungarumlaut": 722, "Eacute": 667, "emacron": 444, "gbreve": 500, "onequarter": 750, "Scaron": 556, "Scommaaccent": 556, "Ohungarumlaut": 722, "degree": 400, "ograve": 500, "Ccaron": 667, "ugrave": 556, "radical": 549, "Dcaron": 722, "rcommaaccent": 389, "Ntilde": 722, "otilde": 500, "Rcommaaccent": 667, "Lcommaaccent": 611, "Atilde": 667, "Aogonek": 667, "Aring": 667, "Otilde": 722, "zdotaccent": 389, "Ecaron": 667, "Iogonek": 389, "kcommaaccent": 500, "minus": 606, "Icircumflex": 389, "ncaron": 556, "tcommaaccent": 278, "logicalnot": 606, "odieresis": 500, "udieresis": 556, "notequal": 549, "gcommaaccent": 500, "eth": 500, "zcaron": 389, "ncommaaccent": 556, "onesuperior": 300, "imacron": 278, "Euro": 500}, + }, + "Times-Italic": { + types.NewRectangle(-169.0, -217.0, 1010.0, 883.0), + map[string]int{"space": 250, "exclam": 333, "quotedbl": 420, "numbersign": 500, "dollar": 500, "percent": 833, "ampersand": 778, "quoteright": 333, "parenleft": 333, "parenright": 333, "asterisk": 500, "plus": 675, "comma": 250, "hyphen": 333, "period": 250, "slash": 278, "zero": 500, "one": 500, "two": 500, "three": 500, "four": 500, "five": 500, "six": 500, "seven": 500, "eight": 500, "nine": 500, "colon": 333, "semicolon": 333, "less": 675, "equal": 675, "greater": 675, "question": 500, "at": 920, "A": 611, "B": 611, "C": 667, "D": 722, "E": 611, "F": 611, "G": 722, "H": 722, "I": 333, "J": 444, "K": 667, "L": 556, "M": 833, "N": 667, "O": 722, "P": 611, "Q": 722, "R": 611, "S": 500, "T": 556, "U": 722, "V": 611, "W": 833, "X": 611, "Y": 556, "Z": 556, "bracketleft": 389, "backslash": 278, "bracketright": 389, "asciicircum": 422, "underscore": 500, "quoteleft": 333, "a": 500, "b": 500, "c": 444, "d": 500, "e": 444, "f": 278, "g": 500, "h": 500, "i": 278, "j": 278, "k": 444, "l": 278, "m": 722, "n": 500, "o": 500, "p": 500, "q": 500, "r": 389, "s": 389, "t": 278, "u": 500, "v": 444, "w": 667, "x": 444, "y": 444, "z": 389, "braceleft": 400, "bar": 275, "braceright": 400, "asciitilde": 541, "exclamdown": 389, "cent": 500, "sterling": 500, "fraction": 167, "yen": 500, "florin": 500, "section": 500, "currency": 500, "quotesingle": 214, "quotedblleft": 556, "guillemotleft": 500, "guilsinglleft": 333, "guilsinglright": 333, "fi": 500, "fl": 500, "endash": 500, "dagger": 500, "daggerdbl": 500, "periodcentered": 250, "paragraph": 523, "bullet": 350, "quotesinglbase": 333, "quotedblbase": 556, "quotedblright": 556, "guillemotright": 500, "ellipsis": 889, "perthousand": 1000, "questiondown": 500, "grave": 333, "acute": 333, "circumflex": 333, "tilde": 333, "macron": 333, "breve": 333, "dotaccent": 333, "dieresis": 333, "ring": 333, "cedilla": 333, "hungarumlaut": 333, "ogonek": 333, "caron": 333, "emdash": 889, "AE": 889, "ordfeminine": 276, "Lslash": 556, "Oslash": 722, "OE": 944, "ordmasculine": 310, "ae": 667, "dotlessi": 278, "lslash": 278, "oslash": 500, "oe": 667, "germandbls": 500, "Idieresis": 333, "eacute": 444, "abreve": 500, "uhungarumlaut": 500, "ecaron": 444, "Ydieresis": 556, "divide": 675, "Yacute": 556, "Acircumflex": 611, "aacute": 500, "Ucircumflex": 722, "yacute": 444, "scommaaccent": 389, "ecircumflex": 444, "Uring": 722, "Udieresis": 722, "aogonek": 500, "Uacute": 722, "uogonek": 500, "Edieresis": 611, "Dcroat": 722, "commaaccent": 250, "copyright": 760, "Emacron": 611, "ccaron": 444, "aring": 500, "Ncommaaccent": 667, "lacute": 278, "agrave": 500, "Tcommaaccent": 556, "Cacute": 667, "atilde": 500, "Edotaccent": 611, "scaron": 389, "scedilla": 389, "iacute": 278, "lozenge": 471, "Rcaron": 611, "Gcommaaccent": 722, "ucircumflex": 500, "acircumflex": 500, "Amacron": 611, "rcaron": 389, "ccedilla": 444, "Zdotaccent": 556, "Thorn": 611, "Omacron": 722, "Racute": 611, "Sacute": 500, "dcaron": 544, "Umacron": 722, "uring": 500, "threesuperior": 300, "Ograve": 722, "Agrave": 611, "Abreve": 611, "multiply": 675, "uacute": 500, "Tcaron": 556, "partialdiff": 476, "ydieresis": 444, "Nacute": 667, "icircumflex": 278, "Ecircumflex": 611, "adieresis": 500, "edieresis": 444, "cacute": 444, "nacute": 500, "umacron": 500, "Ncaron": 667, "Iacute": 333, "plusminus": 675, "brokenbar": 275, "registered": 760, "Gbreve": 722, "Idotaccent": 333, "summation": 600, "Egrave": 611, "racute": 389, "omacron": 500, "Zacute": 556, "Zcaron": 556, "greaterequal": 549, "Eth": 722, "Ccedilla": 667, "lcommaaccent": 278, "tcaron": 300, "eogonek": 444, "Uogonek": 722, "Aacute": 611, "Adieresis": 611, "egrave": 444, "zacute": 389, "iogonek": 278, "Oacute": 722, "oacute": 500, "amacron": 500, "sacute": 389, "idieresis": 278, "Ocircumflex": 722, "Ugrave": 722, "Delta": 612, "thorn": 500, "twosuperior": 300, "Odieresis": 722, "mu": 500, "igrave": 278, "ohungarumlaut": 500, "Eogonek": 611, "dcroat": 500, "threequarters": 750, "Scedilla": 500, "lcaron": 300, "Kcommaaccent": 667, "Lacute": 556, "trademark": 980, "edotaccent": 444, "Igrave": 333, "Imacron": 333, "Lcaron": 611, "onehalf": 750, "lessequal": 549, "ocircumflex": 500, "ntilde": 500, "Uhungarumlaut": 722, "Eacute": 611, "emacron": 444, "gbreve": 500, "onequarter": 750, "Scaron": 500, "Scommaaccent": 500, "Ohungarumlaut": 722, "degree": 400, "ograve": 500, "Ccaron": 667, "ugrave": 500, "radical": 453, "Dcaron": 722, "rcommaaccent": 389, "Ntilde": 667, "otilde": 500, "Rcommaaccent": 611, "Lcommaaccent": 556, "Atilde": 611, "Aogonek": 611, "Aring": 611, "Otilde": 722, "zdotaccent": 389, "Ecaron": 611, "Iogonek": 333, "kcommaaccent": 444, "minus": 675, "Icircumflex": 333, "ncaron": 500, "tcommaaccent": 278, "logicalnot": 675, "odieresis": 500, "udieresis": 500, "notequal": 549, "gcommaaccent": 500, "eth": 500, "zcaron": 389, "ncommaaccent": 500, "onesuperior": 300, "imacron": 278, "Euro": 500}, + }, + "Times-Roman": { + types.NewRectangle(-168.0, -218.0, 1000.0, 898.0), + map[string]int{"space": 250, "exclam": 333, "quotedbl": 408, "numbersign": 500, "dollar": 500, "percent": 833, "ampersand": 778, "quoteright": 333, "parenleft": 333, "parenright": 333, "asterisk": 500, "plus": 564, "comma": 250, "hyphen": 333, "period": 250, "slash": 278, "zero": 500, "one": 500, "two": 500, "three": 500, "four": 500, "five": 500, "six": 500, "seven": 500, "eight": 500, "nine": 500, "colon": 278, "semicolon": 278, "less": 564, "equal": 564, "greater": 564, "question": 444, "at": 921, "A": 722, "B": 667, "C": 667, "D": 722, "E": 611, "F": 556, "G": 722, "H": 722, "I": 333, "J": 389, "K": 722, "L": 611, "M": 889, "N": 722, "O": 722, "P": 556, "Q": 722, "R": 667, "S": 556, "T": 611, "U": 722, "V": 722, "W": 944, "X": 722, "Y": 722, "Z": 611, "bracketleft": 333, "backslash": 278, "bracketright": 333, "asciicircum": 469, "underscore": 500, "quoteleft": 333, "a": 444, "b": 500, "c": 444, "d": 500, "e": 444, "f": 333, "g": 500, "h": 500, "i": 278, "j": 278, "k": 500, "l": 278, "m": 778, "n": 500, "o": 500, "p": 500, "q": 500, "r": 333, "s": 389, "t": 278, "u": 500, "v": 500, "w": 722, "x": 500, "y": 500, "z": 444, "braceleft": 480, "bar": 200, "braceright": 480, "asciitilde": 541, "exclamdown": 333, "cent": 500, "sterling": 500, "fraction": 167, "yen": 500, "florin": 500, "section": 500, "currency": 500, "quotesingle": 180, "quotedblleft": 444, "guillemotleft": 500, "guilsinglleft": 333, "guilsinglright": 333, "fi": 556, "fl": 556, "endash": 500, "dagger": 500, "daggerdbl": 500, "periodcentered": 250, "paragraph": 453, "bullet": 350, "quotesinglbase": 333, "quotedblbase": 444, "quotedblright": 444, "guillemotright": 500, "ellipsis": 1000, "perthousand": 1000, "questiondown": 444, "grave": 333, "acute": 333, "circumflex": 333, "tilde": 333, "macron": 333, "breve": 333, "dotaccent": 333, "dieresis": 333, "ring": 333, "cedilla": 333, "hungarumlaut": 333, "ogonek": 333, "caron": 333, "emdash": 1000, "AE": 889, "ordfeminine": 276, "Lslash": 611, "Oslash": 722, "OE": 889, "ordmasculine": 310, "ae": 667, "dotlessi": 278, "lslash": 278, "oslash": 500, "oe": 722, "germandbls": 500, "Idieresis": 333, "eacute": 444, "abreve": 444, "uhungarumlaut": 500, "ecaron": 444, "Ydieresis": 722, "divide": 564, "Yacute": 722, "Acircumflex": 722, "aacute": 444, "Ucircumflex": 722, "yacute": 500, "scommaaccent": 389, "ecircumflex": 444, "Uring": 722, "Udieresis": 722, "aogonek": 444, "Uacute": 722, "uogonek": 500, "Edieresis": 611, "Dcroat": 722, "commaaccent": 250, "copyright": 760, "Emacron": 611, "ccaron": 444, "aring": 444, "Ncommaaccent": 722, "lacute": 278, "agrave": 444, "Tcommaaccent": 611, "Cacute": 667, "atilde": 444, "Edotaccent": 611, "scaron": 389, "scedilla": 389, "iacute": 278, "lozenge": 471, "Rcaron": 667, "Gcommaaccent": 722, "ucircumflex": 500, "acircumflex": 444, "Amacron": 722, "rcaron": 333, "ccedilla": 444, "Zdotaccent": 611, "Thorn": 556, "Omacron": 722, "Racute": 667, "Sacute": 556, "dcaron": 588, "Umacron": 722, "uring": 500, "threesuperior": 300, "Ograve": 722, "Agrave": 722, "Abreve": 722, "multiply": 564, "uacute": 500, "Tcaron": 611, "partialdiff": 476, "ydieresis": 500, "Nacute": 722, "icircumflex": 278, "Ecircumflex": 611, "adieresis": 444, "edieresis": 444, "cacute": 444, "nacute": 500, "umacron": 500, "Ncaron": 722, "Iacute": 333, "plusminus": 564, "brokenbar": 200, "registered": 760, "Gbreve": 722, "Idotaccent": 333, "summation": 600, "Egrave": 611, "racute": 333, "omacron": 500, "Zacute": 611, "Zcaron": 611, "greaterequal": 549, "Eth": 722, "Ccedilla": 667, "lcommaaccent": 278, "tcaron": 326, "eogonek": 444, "Uogonek": 722, "Aacute": 722, "Adieresis": 722, "egrave": 444, "zacute": 444, "iogonek": 278, "Oacute": 722, "oacute": 500, "amacron": 444, "sacute": 389, "idieresis": 278, "Ocircumflex": 722, "Ugrave": 722, "Delta": 612, "thorn": 500, "twosuperior": 300, "Odieresis": 722, "mu": 500, "igrave": 278, "ohungarumlaut": 500, "Eogonek": 611, "dcroat": 500, "threequarters": 750, "Scedilla": 556, "lcaron": 344, "Kcommaaccent": 722, "Lacute": 611, "trademark": 980, "edotaccent": 444, "Igrave": 333, "Imacron": 333, "Lcaron": 611, "onehalf": 750, "lessequal": 549, "ocircumflex": 500, "ntilde": 500, "Uhungarumlaut": 722, "Eacute": 611, "emacron": 444, "gbreve": 500, "onequarter": 750, "Scaron": 556, "Scommaaccent": 556, "Ohungarumlaut": 722, "degree": 400, "ograve": 500, "Ccaron": 667, "ugrave": 500, "radical": 453, "Dcaron": 722, "rcommaaccent": 333, "Ntilde": 722, "otilde": 500, "Rcommaaccent": 667, "Lcommaaccent": 611, "Atilde": 722, "Aogonek": 722, "Aring": 722, "Otilde": 722, "zdotaccent": 444, "Ecaron": 611, "Iogonek": 333, "kcommaaccent": 500, "minus": 564, "Icircumflex": 333, "ncaron": 500, "tcommaaccent": 278, "logicalnot": 564, "odieresis": 500, "udieresis": 500, "notequal": 549, "gcommaaccent": 500, "eth": 500, "zcaron": 444, "ncommaaccent": 500, "onesuperior": 300, "imacron": 278, "Euro": 500}, + }, + "ZapfDingbats": { + types.NewRectangle(-1.0, -143.0, 981.0, 820.0), + map[string]int{"space": 278, "a1": 974, "a2": 961, "a202": 974, "a3": 980, "a4": 719, "a5": 789, "a119": 790, "a118": 791, "a117": 690, "a11": 960, "a12": 939, "a13": 549, "a14": 855, "a15": 911, "a16": 933, "a105": 911, "a17": 945, "a18": 974, "a19": 755, "a20": 846, "a21": 762, "a22": 761, "a23": 571, "a24": 677, "a25": 763, "a26": 760, "a27": 759, "a28": 754, "a6": 494, "a7": 552, "a8": 537, "a9": 577, "a10": 692, "a29": 786, "a30": 788, "a31": 788, "a32": 790, "a33": 793, "a34": 794, "a35": 816, "a36": 823, "a37": 789, "a38": 841, "a39": 823, "a40": 833, "a41": 816, "a42": 831, "a43": 923, "a44": 744, "a45": 723, "a46": 749, "a47": 790, "a48": 792, "a49": 695, "a50": 776, "a51": 768, "a52": 792, "a53": 759, "a54": 707, "a55": 708, "a56": 682, "a57": 701, "a58": 826, "a59": 815, "a60": 789, "a61": 789, "a62": 707, "a63": 687, "a64": 696, "a65": 689, "a66": 786, "a67": 787, "a68": 713, "a69": 791, "a70": 785, "a71": 791, "a72": 873, "a73": 761, "a74": 762, "a203": 762, "a75": 759, "a204": 759, "a76": 892, "a77": 892, "a78": 788, "a79": 784, "a81": 438, "a82": 138, "a83": 277, "a84": 415, "a97": 392, "a98": 392, "a99": 668, "a100": 668, "a89": 390, "a90": 390, "a93": 317, "a94": 317, "a91": 276, "a92": 276, "a205": 509, "a85": 509, "a206": 410, "a86": 410, "a87": 234, "a88": 234, "a95": 334, "a96": 334, "a101": 732, "a102": 544, "a103": 544, "a104": 910, "a106": 667, "a107": 760, "a108": 760, "a112": 776, "a111": 595, "a110": 694, "a109": 626, "a120": 788, "a121": 788, "a122": 788, "a123": 788, "a124": 788, "a125": 788, "a126": 788, "a127": 788, "a128": 788, "a129": 788, "a130": 788, "a131": 788, "a132": 788, "a133": 788, "a134": 788, "a135": 788, "a136": 788, "a137": 788, "a138": 788, "a139": 788, "a140": 788, "a141": 788, "a142": 788, "a143": 788, "a144": 788, "a145": 788, "a146": 788, "a147": 788, "a148": 788, "a149": 788, "a150": 788, "a151": 788, "a152": 788, "a153": 788, "a154": 788, "a155": 788, "a156": 788, "a157": 788, "a158": 788, "a159": 788, "a160": 894, "a161": 838, "a163": 1016, "a164": 458, "a196": 748, "a165": 924, "a192": 748, "a166": 918, "a167": 927, "a168": 928, "a169": 928, "a170": 834, "a171": 873, "a172": 828, "a173": 924, "a162": 924, "a174": 917, "a175": 930, "a176": 931, "a177": 463, "a178": 883, "a179": 836, "a193": 836, "a180": 867, "a199": 867, "a181": 696, "a200": 696, "a182": 874, "a201": 874, "a183": 760, "a184": 946, "a197": 771, "a185": 865, "a194": 771, "a198": 888, "a186": 967, "a195": 888, "a187": 831, "a188": 873, "a189": 927, "a190": 970, "a191": 918}, + }, +} diff --git a/pkg/api/annotation.go b/pkg/api/annotation.go new file mode 100644 index 0000000000000000000000000000000000000000..c3cfc27d54a85aae45b00c76a078a4ddb5e44d37 --- /dev/null +++ b/pkg/api/annotation.go @@ -0,0 +1,402 @@ +/* +Copyright 2021 The pdfcpu Authors. + +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 + + 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 api + +import ( + "io" + "os" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pkg/errors" +) + +// Annotations returns page annotations of rs for selected pages. +func Annotations(rs io.ReadSeeker, selectedPages []string, conf *model.Configuration) (map[int]model.PgAnnots, error) { + if rs == nil { + return nil, errors.New("pdfcpu: Annotations: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTANNOTATIONS + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return nil, err + } + + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) + if err != nil { + return nil, err + } + + return pdfcpu.AnnotationsForSelectedPages(ctx, pages), nil +} + +// AddAnnotations adds annotations for selected pages in rs and writes the result to w. +func AddAnnotations(rs io.ReadSeeker, w io.Writer, selectedPages []string, ann model.AnnotationRenderer, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: AddAnnotations: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.ADDANNOTATIONS + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) + if err != nil { + return err + } + + ok, err := pdfcpu.AddAnnotations(ctx, pages, ann, false) + if err != nil { + return err + } + if !ok { + return errors.New("pdfcpu: AddAnnotations: No annotations added") + } + + return Write(ctx, w, conf) +} + +// AddAnnotationsAsIncrement adds annotations for selected pages in rws and writes out a PDF increment. +func AddAnnotationsAsIncrement(rws io.ReadWriteSeeker, selectedPages []string, ar model.AnnotationRenderer, conf *model.Configuration) error { + if rws == nil { + return errors.New("pdfcpu: AddAnnotationsAsIncrement: missing rws") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.ADDANNOTATIONS + + ctx, err := ReadAndValidate(rws, conf) + if err != nil { + return err + } + + if *ctx.HeaderVersion < model.V14 { + return errors.New("Incremental writing not supported for PDF version < V1.4 (Hint: Use pdfcpu optimize then try again)") + } + + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) + if err != nil { + return err + } + + ok, err := pdfcpu.AddAnnotations(ctx, pages, ar, true) + if err != nil { + return err + } + if !ok { + return errors.New("pdfcpu: AddAnnotationsAsIncrement: No annotations added") + } + + return WriteIncr(ctx, rws, conf) +} + +// AddAnnotationsFile adds annotations for selected pages to a PDF context read from inFile and writes the result to outFile. +func AddAnnotationsFile(inFile, outFile string, selectedPages []string, ar model.AnnotationRenderer, conf *model.Configuration, incr bool) (err error) { + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + logWritingTo(outFile) + } else { + logWritingTo(inFile) + if incr { + f, err := os.OpenFile(inFile, os.O_RDWR, 0644) + if err != nil { + return err + } + defer f.Close() + return AddAnnotationsAsIncrement(f, selectedPages, ar, conf) + } + } + + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return AddAnnotations(f1, f2, selectedPages, ar, conf) +} + +// AddAnnotationsMap adds annotations in m to corresponding pages of rs and writes the result to w. +func AddAnnotationsMap(rs io.ReadSeeker, w io.Writer, m map[int][]model.AnnotationRenderer, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: AddAnnotationsMap: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.ADDANNOTATIONS + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + ok, err := pdfcpu.AddAnnotationsMap(ctx, m, false) + if err != nil { + return err + } + if !ok { + return errors.New("pdfcpu: AddAnnotationsMap: No annotations added") + } + + return Write(ctx, w, conf) +} + +// AddAnnotationsMapAsIncrement adds annotations in m to corresponding pages of rws and writes out a PDF increment. +func AddAnnotationsMapAsIncrement(rws io.ReadWriteSeeker, m map[int][]model.AnnotationRenderer, conf *model.Configuration) error { + if rws == nil { + return errors.New("pdfcpu: AddAnnotationsMapAsIncrement: missing rws") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.ADDANNOTATIONS + + ctx, err := ReadAndValidate(rws, conf) + if err != nil { + return err + } + + if *ctx.HeaderVersion < model.V14 { + return errors.New("Increment writing not supported for PDF version < V1.4 (Hint: Use pdfcpu optimize then try again)") + } + + ok, err := pdfcpu.AddAnnotationsMap(ctx, m, true) + if err != nil { + return err + } + if !ok { + return errors.New("pdfcpu: AddAnnotationsMapAsIncrement: No annotations added") + } + + return WriteIncr(ctx, rws, conf) +} + +// AddAnnotationsMapFile adds annotations in m to corresponding pages of inFile and writes the result to outFile. +func AddAnnotationsMapFile(inFile, outFile string, m map[int][]model.AnnotationRenderer, conf *model.Configuration, incr bool) (err error) { + tmpFile := inFile + ".tmp" + + if outFile != "" && inFile != outFile { + tmpFile = outFile + logWritingTo(outFile) + } else { + logWritingTo(inFile) + if incr { + f, err := os.OpenFile(inFile, os.O_RDWR, 0644) + if err != nil { + return err + } + defer f.Close() + return AddAnnotationsMapAsIncrement(f, m, conf) + } + } + + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return AddAnnotationsMap(f1, f2, m, conf) +} + +// RemoveAnnotations removes annotations for selected pages by id and object number +// from a PDF context read from rs and writes the result to w. +func RemoveAnnotations(rs io.ReadSeeker, w io.Writer, selectedPages, idsAndTypes []string, objNrs []int, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: RemoveAnnotations: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.REMOVEANNOTATIONS + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) + if err != nil { + return err + } + + ok, err := pdfcpu.RemoveAnnotations(ctx, pages, idsAndTypes, objNrs, false) + if err != nil { + return err + } + if !ok { + return errors.New("pdfcpu: RemoveAnnotations: No annotation removed") + } + + return Write(ctx, w, conf) +} + +// RemoveAnnotationsAsIncrement removes annotations for selected pages by ids and object number +// from a PDF context read from rs and writes out a PDF increment. +func RemoveAnnotationsAsIncrement(rws io.ReadWriteSeeker, selectedPages, idsAndTypes []string, objNrs []int, conf *model.Configuration) error { + if rws == nil { + return errors.New("pdfcpu: RemoveAnnotationsAsIncrement: missing rws") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.REMOVEANNOTATIONS + + ctx, err := ReadAndValidate(rws, conf) + if err != nil { + return err + } + + if *ctx.HeaderVersion < model.V14 { + return errors.New("pdfcpu: Incremental writing unsupported for PDF version < V1.4 (Hint: Use pdfcpu optimize then try again)") + } + + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) + if err != nil { + return err + } + + ok, err := pdfcpu.RemoveAnnotations(ctx, pages, idsAndTypes, objNrs, true) + if err != nil { + return err + } + if !ok { + return errors.New("pdfcpu: RemoveAnnotationsAsIncrement: No annotation removed") + } + + return WriteIncr(ctx, rws, conf) +} + +// RemoveAnnotationsFile removes annotations for selected pages by id and object number +// from a PDF context read from inFile and writes the result to outFile. +func RemoveAnnotationsFile(inFile, outFile string, selectedPages, idsAndTypes []string, objNrs []int, conf *model.Configuration, incr bool) (err error) { + var f1, f2 *os.File + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + logWritingTo(outFile) + } else { + logWritingTo(inFile) + if incr { + if f1, err = os.OpenFile(inFile, os.O_RDWR, 0644); err != nil { + return err + } + defer func() { + cerr := f1.Close() + if err == nil { + err = cerr + } + }() + return RemoveAnnotationsAsIncrement(f1, selectedPages, idsAndTypes, objNrs, conf) + } + } + + if f1, err = os.Open(inFile); err != nil { + return err + } + + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return RemoveAnnotations(f1, f2, selectedPages, idsAndTypes, objNrs, conf) +} diff --git a/pkg/api/api.go b/pkg/api/api.go new file mode 100644 index 0000000000000000000000000000000000000000..535be79949010f09c718631a87ec32270f238d75 --- /dev/null +++ b/pkg/api/api.go @@ -0,0 +1,266 @@ +/* + Copyright 2018 The pdfcpu Authors. + + 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 + + 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 api lets you integrate pdfcpu's operations into your Go backend. +// +// There are two api layers supporting all pdfcpu operations: +// 1. The file based layer (used by pdfcpu's cli) +// 2. The io.ReadSeeker/io.Writer based layer for backend integration. +// +// For any pdfcpu command there are two functions. +// +// The file based function always calls the io.ReadSeeker/io.Writer based function: +// +// func CommandFile(inFile, outFile string, conf *pdf.Configuration) error +// func Command(rs io.ReadSeeker, w io.Writer, conf *pdf.Configuration) error +// +// eg. for optimization: +// +// func OptimizeFile(inFile, outFile string, conf *pdf.Configuration) error +// func Optimize(rs io.ReadSeeker, w io.Writer, conf *pdf.Configuration) error +package api + +import ( + "bufio" + "io" + "os" + "sync" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/validate" + "github.com/pkg/errors" +) + +func logDisclaimerPDF20() { + disclaimer := ` +***************************** Disclaimer **************************** +* PDF 2.0 features are supported on a need basis. * +* (See ISO 32000:2 6.3.2 Conformance of PDF processors) * +* At the moment pdfcpu comes with basic PDF 2.0 support. * +* Please let us know which feature you would like to see supported, * +* provide a sample PDF file and create an issue: * +* https://github.com/pdfcpu/pdfcpu/issues/new/choose * +* Thank you for using pdfcpu <3 * +*********************************************************************` + + if log.ValidateEnabled() { + log.Validate.Println(disclaimer) + } + if log.CLIEnabled() { + log.CLI.Println(disclaimer) + } +} + +// ReadContext uses an io.ReadSeeker to build an internal structure holding its cross reference table aka the Context. +func ReadContext(rs io.ReadSeeker, conf *model.Configuration) (*model.Context, error) { + if rs == nil { + return nil, errors.New("pdfcpu: ReadContext: missing rs") + } + return pdfcpu.Read(rs, conf) +} + +// ReadContextFile returns inFile's validated context. +func ReadContextFile(inFile string) (*model.Context, error) { + f, err := os.Open(inFile) + if err != nil { + return nil, err + } + defer f.Close() + + ctx, err := ReadContext(f, model.NewDefaultConfiguration()) + if err != nil { + return nil, err + } + + if ctx.Version() == model.V20 { + logDisclaimerPDF20() + } + + if err = validate.XRefTable(ctx.XRefTable); err != nil { + return nil, err + } + + return ctx, err +} + +// ValidateContext validates ctx. +func ValidateContext(ctx *model.Context) error { + if ctx.Version() == model.V20 { + logDisclaimerPDF20() + } + return validate.XRefTable(ctx.XRefTable) +} + +// OptimizeContext optimizes ctx. +func OptimizeContext(ctx *model.Context) error { + if log.CLIEnabled() { + log.CLI.Println("optimizing...") + } + return pdfcpu.OptimizeXRefTable(ctx) +} + +// WriteContext writes ctx to w. +func WriteContext(ctx *model.Context, w io.Writer) error { + if f, ok := w.(*os.File); ok { + // In order to retrieve the written file size. + ctx.Write.Fp = f + } + ctx.Write.Writer = bufio.NewWriter(w) + defer ctx.Write.Flush() + return pdfcpu.Write(ctx) +} + +// WriteIncrement writes a PDF increment for ctx to w. +func WriteIncrement(ctx *model.Context, w io.Writer) error { + ctx.Write.Writer = bufio.NewWriter(w) + defer ctx.Write.Flush() + return pdfcpu.WriteIncrement(ctx) +} + +// WriteContextFile writes ctx to outFile. +func WriteContextFile(ctx *model.Context, outFile string) error { + f, err := os.Create(outFile) + if err != nil { + return err + } + defer f.Close() + return WriteContext(ctx, f) +} + +// ReadAndValidate returns a model.Context of rs ready for processing. +func ReadAndValidate(rs io.ReadSeeker, conf *model.Configuration) (ctx *model.Context, err error) { + if ctx, err = ReadContext(rs, conf); err != nil { + return nil, err + } + + if err := ValidateContext(ctx); err != nil { + return nil, err + } + + return ctx, nil +} + +func cmdAssumingOptimization(cmd model.CommandMode) bool { + return cmd == model.OPTIMIZE || + cmd == model.FILLFORMFIELDS || + cmd == model.RESETFORMFIELDS || + cmd == model.LISTIMAGES || + cmd == model.EXTRACTIMAGES || + cmd == model.EXTRACTFONTS +} + +// ReadValidateAndOptimize returns an optimized model.Context of rs ready for processing a specific command. +// conf.Cmd is expected to be configured properly. +func ReadValidateAndOptimize(rs io.ReadSeeker, conf *model.Configuration) (ctx *model.Context, err error) { + if conf == nil { + return nil, errors.New("pdfcpu: ReadValidateAndOptimize: missing conf") + } + + ctx, err = ReadAndValidate(rs, conf) + if err != nil { + return nil, err + } + + // With the exception of commands utilizing structs provided the Optimize step + // command optimization of the cross reference table is optional but usually recommended. + // For large or complex files it may make sense to skip optimization and set conf.Optimize = false. + if cmdAssumingOptimization(conf.Cmd) || conf.Optimize { + if err = OptimizeContext(ctx); err != nil { + return nil, err + } + } + + // TODO move to form related commands. + if err := pdfcpu.CacheFormFonts(ctx); err != nil { + return nil, err + } + + return ctx, nil +} + +func logWritingTo(s string) { + if log.CLIEnabled() { + log.CLI.Printf("writing %s...\n", s) + } +} + +func Write(ctx *model.Context, w io.Writer, conf *model.Configuration) error { + if log.StatsEnabled() { + log.Stats.Printf("XRefTable:\n%s\n", ctx) + } + + // Note side effects of validation before writing! + // if conf.PostProcessValidate { + // if err := ValidateContext(ctx); err != nil { + // return err + // } + // } + + return WriteContext(ctx, w) +} + +func WriteIncr(ctx *model.Context, rws io.ReadWriteSeeker, conf *model.Configuration) error { + if log.StatsEnabled() { + log.Stats.Printf("XRefTable:\n%s\n", ctx) + } + + if conf.PostProcessValidate { + if err := ValidateContext(ctx); err != nil { + return err + } + } + + if _, err := rws.Seek(0, io.SeekEnd); err != nil { + return err + } + + return WriteIncrement(ctx, rws) +} + +// EnsureDefaultConfigAt switches to the pdfcpu config dir located at path. +// If path/pdfcpu is not existent, it will be created including config.yml +func EnsureDefaultConfigAt(path string) error { + // Call if you have specific requirements regarding the location of the pdfcpu config dir. + return model.EnsureDefaultConfigAt(path) +} + +var ( + // mutexDisableConfigDir protects DisableConfigDir from concurrent access. + // NOTE Not a guard for model.ConfigPath! + mutexDisableConfigDir sync.Mutex +) + +// DisableConfigDir disables the configuration directory. +// Any needed default configuration will be loaded from configuration.go +// Since the config dir also contains the user font dir, this also limits font usage to the default core font set +// No user fonts will be available. +func DisableConfigDir() { + mutexDisableConfigDir.Lock() + defer mutexDisableConfigDir.Unlock() + // Call if you don't want to use a specific configuration + // and also do not need to use user fonts. + model.ConfigPath = "disable" +} + +// LoadConfiguration locates and loads the default configuration +// and also loads installed user fonts. +func LoadConfiguration() *model.Configuration { + // Call if you don't have a specific config dir location + // and need to use user fonts for stamping or watermarking. + return model.NewDefaultConfiguration() +} diff --git a/pkg/api/attach.go b/pkg/api/attach.go new file mode 100644 index 0000000000000000000000000000000000000000..f6df1108cf182d6596de0a69383fda9bf3469d60 --- /dev/null +++ b/pkg/api/attach.go @@ -0,0 +1,273 @@ +/* + Copyright 2019 The pdfcpu Authors. + + 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 + + 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 api + +import ( + "io" + "os" + "path/filepath" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pkg/errors" +) + +// Attachments returns rs's attachments. +func Attachments(rs io.ReadSeeker, conf *model.Configuration) ([]model.Attachment, error) { + if rs == nil { + return nil, errors.New("pdfcpu: Attachments: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTATTACHMENTS + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return nil, err + } + + return ctx.ListAttachments() +} + +// AddAttachments embeds files into a PDF context read from rs and writes the result to w. +// file is either a file name or a file name and a description separated by a comma. +func AddAttachments(rs io.ReadSeeker, w io.Writer, files []string, coll bool, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: AddAttachments: missing rs") + } + + if w == nil { + return errors.New("pdfcpu: AddAttachments: missing w") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.ADDATTACHMENTS + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + var ok bool + + for _, fn := range files { + s := strings.Split(fn, ",") + if len(s) == 0 || len(s) > 2 { + continue + } + + fileName := s[0] + desc := "" + if len(s) == 2 { + desc = s[1] + } + + if log.CLIEnabled() { + log.CLI.Printf("adding %s\n", fileName) + } + f, err := os.Open(fileName) + if err != nil { + return err + } + defer f.Close() + + fi, err := f.Stat() + if err != nil { + return err + } + mt := fi.ModTime() + + a := model.Attachment{Reader: f, ID: filepath.Base(fileName), Desc: desc, ModTime: &mt} + if err = ctx.AddAttachment(a, coll); err != nil { + return err + } + ok = true + } + + if !ok { + return errors.New("pdfcpu: AddAttachments: No attachment added") + } + + return Write(ctx, w, conf) +} + +// AddAttachmentsFile embeds files into a PDF context read from inFile and writes the result to outFile. +func AddAttachmentsFile(inFile, outFile string, files []string, coll bool, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + } + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return AddAttachments(f1, f2, files, coll, conf) +} + +// RemoveAttachments deletes embedded files from a PDF context read from rs and writes the result to w. +func RemoveAttachments(rs io.ReadSeeker, w io.Writer, files []string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: RemoveAttachments: missing rs") + } + + if w == nil { + return errors.New("pdfcpu: RemoveAttachments: missing w") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.ADDATTACHMENTS + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + var ok bool + if ok, err = ctx.RemoveAttachments(files); err != nil { + return err + } + if !ok { + return errors.New("pdfcpu: RemoveAttachments: No attachment removed") + } + + return Write(ctx, w, conf) +} + +// RemoveAttachmentsFile deletes embedded files from a PDF context read from inFile and writes the result to outFile. +func RemoveAttachmentsFile(inFile, outFile string, files []string, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + } + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return RemoveAttachments(f1, f2, files, conf) +} + +// ExtractAttachmentsRaw extracts embedded files from a PDF context read from rs. +func ExtractAttachmentsRaw(rs io.ReadSeeker, outDir string, fileNames []string, conf *model.Configuration) ([]model.Attachment, error) { + if rs == nil { + return nil, errors.New("pdfcpu: ExtractAttachmentsRaw: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.EXTRACTATTACHMENTS + + ctx, err := ReadAndValidate(rs, conf) + if err != nil { + return nil, err + } + + return ctx.ExtractAttachments(fileNames) +} + +// ExtractAttachments extracts embedded files from a PDF context read from rs into outDir. +func ExtractAttachments(rs io.ReadSeeker, outDir string, fileNames []string, conf *model.Configuration) error { + aa, err := ExtractAttachmentsRaw(rs, outDir, fileNames, conf) + if err != nil { + return err + } + + for _, a := range aa { + fileName := filepath.Join(outDir, a.FileName) + logWritingTo(fileName) + f, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.ModePerm) + if err != nil { + return err + } + if _, err = io.Copy(f, a); err != nil { + return err + } + if err := f.Close(); err != nil { + return err + } + } + + return nil +} + +// ExtractAttachmentsFile extracts embedded files from a PDF context read from inFile into outDir. +func ExtractAttachmentsFile(inFile, outDir string, files []string, conf *model.Configuration) error { + f, err := os.Open(inFile) + if err != nil { + return err + } + defer f.Close() + + return ExtractAttachments(f, outDir, files, conf) +} diff --git a/pkg/api/booklet.go b/pkg/api/booklet.go new file mode 100644 index 0000000000000000000000000000000000000000..152da0316df639237ce86e07f5a5310088a32df5 --- /dev/null +++ b/pkg/api/booklet.go @@ -0,0 +1,131 @@ +/* + Copyright 2021 The pdfcpu Authors. + + 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 + + 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 api + +import ( + "io" + "os" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// BookletFromImages creates a booklet from images. +func BookletFromImages(conf *model.Configuration, imageFileNames []string, nup *model.NUp) (*model.Context, error) { + if nup.PageDim == nil { + // Set default paper size. + nup.PageDim = types.PaperSize[nup.PageSize] + } + + ctx, err := pdfcpu.CreateContextWithXRefTable(conf, nup.PageDim) + if err != nil { + return nil, err + } + + pagesIndRef, err := ctx.Pages() + if err != nil { + return nil, err + } + + // This is the page tree root. + pagesDict, err := ctx.DereferenceDict(*pagesIndRef) + if err != nil { + return nil, err + } + + err = pdfcpu.BookletFromImages(ctx, imageFileNames, nup, pagesDict, pagesIndRef) + + return ctx, err +} + +// Booklet arranges PDF pages on larger sheets of paper and writes the result to w. +func Booklet(rs io.ReadSeeker, w io.Writer, imgFiles, selectedPages []string, nup *model.NUp, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: Booklet: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.BOOKLET + + if log.InfoEnabled() { + log.Info.Printf("%s", nup) + } + + var ( + ctx *model.Context + err error + ) + + if nup.ImgInputFile { + + if ctx, err = BookletFromImages(conf, imgFiles, nup); err != nil { + return err + } + + } else { + + if ctx, err = ReadAndValidate(rs, conf); err != nil { + return err + } + + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) + if err != nil { + return err + } + + if err = pdfcpu.BookletFromPDF(ctx, pages, nup); err != nil { + return err + } + } + + return Write(ctx, w, conf) +} + +// BookletFile rearranges PDF pages or images into a booklet layout and writes the result to outFile. +func BookletFile(inFiles []string, outFile string, selectedPages []string, nup *model.NUp, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + // booklet from a PDF + if f1, err = os.Open(inFiles[0]); err != nil { + return err + } + + if f2, err = os.Create(outFile); err != nil { + f1.Close() + return err + } + logWritingTo(outFile) + + defer func() { + if err != nil { + f2.Close() + f1.Close() + return + } + if err = f2.Close(); err != nil { + return + } + err = f1.Close() + }() + + return Booklet(f1, f2, inFiles, selectedPages, nup, conf) +} diff --git a/pkg/api/bookmark.go b/pkg/api/bookmark.go new file mode 100644 index 0000000000000000000000000000000000000000..56072bb39cb1e75b4d6f22e0819ff9db1e17e5cb --- /dev/null +++ b/pkg/api/bookmark.go @@ -0,0 +1,322 @@ +/* + Copyright 2021 The pdfcpu Authors. + + 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 + + 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 api + +import ( + "io" + "os" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pkg/errors" +) + +var ( + ErrNoOutlines = errors.New("pdfcpu: no outlines available") + ErrOutlines = errors.New("pdfcpu: existing outlines") +) + +// Bookmarks returns rs's bookmark hierarchy. +func Bookmarks(rs io.ReadSeeker, conf *model.Configuration) ([]pdfcpu.Bookmark, error) { + if rs == nil { + return nil, errors.New("pdfcpu: Bookmarks: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.LISTBOOKMARKS + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return nil, err + } + return pdfcpu.Bookmarks(ctx) +} + +// ExportBookmarksJSON extracts outline data from rs (originating from source) and writes the result to w. +func ExportBookmarksJSON(rs io.ReadSeeker, w io.Writer, source string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: ExportBookmarksJSON: missing rs") + } + + if w == nil { + return errors.New("pdfcpu: ExportBookmarksJSON: missing w") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.EXPORTBOOKMARKS + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + ok, err := pdfcpu.ExportBookmarksJSON(ctx, source, w) + if err != nil { + return err + } + if !ok { + return ErrNoOutlines + } + + return nil +} + +// ExportBookmarksFile extracts outline data from inFilePDF and writes the result to outFileJSON. +func ExportBookmarksFile(inFilePDF, outFileJSON string, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if f1, err = os.Open(inFilePDF); err != nil { + return err + } + + if f2, err = os.Create(outFileJSON); err != nil { + f1.Close() + return err + } + logWritingTo(outFileJSON) + + defer func() { + if err != nil { + f2.Close() + f1.Close() + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + }() + + return ExportBookmarksJSON(f1, f2, inFilePDF, conf) +} + +// ImportBookmarks creates/replaces outlines in rs and writes the result to w. +func ImportBookmarks(rs io.ReadSeeker, rd io.Reader, w io.Writer, replace bool, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: ImportBookmarks: missing rs") + } + + if rd == nil { + return errors.New("pdfcpu: ImportBookmarks: missing rd") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.IMPORTBOOKMARKS + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + ok, err := pdfcpu.ImportBookmarks(ctx, rd, replace) + if err != nil { + return err + } + if !ok { + return ErrOutlines + } + + return WriteContext(ctx, w) +} + +// ImportBookmarks creates/replaces outlines in inFilePDF and writes the result to outFilePDF. +func ImportBookmarksFile(inFilePDF, inFileJSON, outFilePDF string, replace bool, conf *model.Configuration) (err error) { + var f0, f1, f2 *os.File + + if f0, err = os.Open(inFilePDF); err != nil { + return err + } + + if f1, err = os.Open(inFileJSON); err != nil { + return err + } + + tmpFile := inFilePDF + ".tmp" + if outFilePDF != "" && inFilePDF != outFilePDF { + tmpFile = outFilePDF + } + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFilePDF == "" || inFilePDF == outFilePDF { + err = os.Rename(tmpFile, inFilePDF) + } + }() + + return ImportBookmarks(f0, f1, f2, replace, conf) +} + +// AddBookmarks adds a single bookmark outline layer to the PDF context read from rs and writes the result to w. +func AddBookmarks(rs io.ReadSeeker, w io.Writer, bms []pdfcpu.Bookmark, replace bool, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: AddBookmarks: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.ADDBOOKMARKS + + if len(bms) == 0 { + return errors.New("pdfcpu: AddBookmarks: missing bms") + } + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + if err := pdfcpu.AddBookmarks(ctx, bms, replace); err != nil { + return err + } + + return WriteContext(ctx, w) +} + +// AddBookmarksFile adds outlines to the PDF context read from inFile and writes the result to outFile. +func AddBookmarksFile(inFile, outFile string, bms []pdfcpu.Bookmark, replace bool, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + } + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return AddBookmarks(f1, f2, bms, replace, conf) +} + +// RemoveBookmarks deletes outlines from rs and writes the result to w. +func RemoveBookmarks(rs io.ReadSeeker, w io.Writer, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: AddBookmarks: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.REMOVEBOOKMARKS + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + ok, err := pdfcpu.RemoveBookmarks(ctx) + if err != nil { + return err + } + if !ok { + return ErrNoOutlines + } + + return WriteContext(ctx, w) +} + +// RemoveBookmarksFile deletes outlines from inFile and writes the result to outFile. +func RemoveBookmarksFile(inFile, outFile string, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + } + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return RemoveBookmarks(f1, f2, conf) +} diff --git a/pkg/api/box.go b/pkg/api/box.go new file mode 100644 index 0000000000000000000000000000000000000000..615f0087f0ebc077d9b45fb7f723444c45cad889 --- /dev/null +++ b/pkg/api/box.go @@ -0,0 +1,292 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 api + +import ( + "io" + "os" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// PageBoundariesFromBoxList parses a list of box types. +func PageBoundariesFromBoxList(s string) (*model.PageBoundaries, error) { + return model.ParseBoxList(s) +} + +// PageBoundaries parses a list of box definitions and assignments. +func PageBoundaries(s string, unit types.DisplayUnit) (*model.PageBoundaries, error) { + return model.ParsePageBoundaries(s, unit) +} + +// Box parses a box definition. +func Box(s string, u types.DisplayUnit) (*model.Box, error) { + return model.ParseBox(s, u) +} + +// Boxes returns rs's page boundaries for selected pages of rs. +func Boxes(rs io.ReadSeeker, selectedPages []string, conf *model.Configuration) ([]model.PageBoundaries, error) { + if rs == nil { + return nil, errors.New("pdfcpu: Boxes: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTBOXES + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return nil, err + } + + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) + if err != nil { + return nil, err + } + + return ctx.PageBoundaries(pages) +} + +// AddBoxes adds page boundaries for selected pages of rs and writes result to w. +func AddBoxes(rs io.ReadSeeker, w io.Writer, selectedPages []string, pb *model.PageBoundaries, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: AddBoxes: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.ADDBOXES + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) + if err != nil { + return err + } + + if err = ctx.AddPageBoundaries(pages, pb); err != nil { + return err + } + + return Write(ctx, w, conf) +} + +// AddBoxesFile adds page boundaries for selected pages of inFile and writes result to outFile. +func AddBoxesFile(inFile, outFile string, selectedPages []string, pb *model.PageBoundaries, conf *model.Configuration) (err error) { + var f1, f2 *os.File + if log.CLIEnabled() { + log.CLI.Printf("adding %s for %s\n", pb, inFile) + } + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + logWritingTo(outFile) + } else { + logWritingTo(inFile) + } + + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return AddBoxes(f1, f2, selectedPages, pb, conf) +} + +// RemoveBoxes removes page boundaries as specified in pb for selected pages of rs and writes result to w. +func RemoveBoxes(rs io.ReadSeeker, w io.Writer, selectedPages []string, pb *model.PageBoundaries, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: RemoveBoxes: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.REMOVEBOXES + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) + if err != nil { + return err + } + + if err = ctx.RemovePageBoundaries(pages, pb); err != nil { + return err + } + + return Write(ctx, w, conf) +} + +// RemoveBoxesFile removes page boundaries as specified in pb for selected pages of inFile and writes result to outFile. +func RemoveBoxesFile(inFile, outFile string, selectedPages []string, pb *model.PageBoundaries, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if log.CLIEnabled() { + log.CLI.Printf("removing %s for %s\n", pb, inFile) + } + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + logWritingTo(outFile) + } else { + logWritingTo(inFile) + } + + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return RemoveBoxes(f1, f2, selectedPages, pb, conf) +} + +// Crop adds crop boxes for selected pages of rs and writes result to w. +func Crop(rs io.ReadSeeker, w io.Writer, selectedPages []string, b *model.Box, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: Crop: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.CROP + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) + if err != nil { + return err + } + + if err = ctx.Crop(pages, b); err != nil { + return err + } + + return Write(ctx, w, conf) +} + +// CropFile adds crop boxes for selected pages of inFile and writes result to outFile. +func CropFile(inFile, outFile string, selectedPages []string, b *model.Box, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if log.CLIEnabled() { + log.CLI.Printf("cropping %s\n", inFile) + } + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + logWritingTo(outFile) + } else { + logWritingTo(inFile) + } + + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.CROP + + return Crop(f1, f2, selectedPages, b, conf) +} diff --git a/pkg/api/collect.go b/pkg/api/collect.go new file mode 100644 index 0000000000000000000000000000000000000000..7e174dd88782ebe86cbcfe5af19d5fbed5050b96 --- /dev/null +++ b/pkg/api/collect.go @@ -0,0 +1,97 @@ +/* + Copyright 2020 The pdfcpu Authors. + + 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 + + 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 api + +import ( + "io" + "os" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pkg/errors" +) + +// Collect creates a custom PDF page sequence for selected pages of rs and writes the result to w. +func Collect(rs io.ReadSeeker, w io.Writer, selectedPages []string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: Collect: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.COLLECT + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + pages, err := PagesForPageCollection(ctx.PageCount, selectedPages) + if err != nil { + return err + } + + ctxDest, err := pdfcpu.ExtractPages(ctx, pages, false) + if err != nil { + return err + } + + return Write(ctxDest, w, conf) +} + +// CollectFile creates a custom PDF page sequence for inFile and writes the result to outFile. +func CollectFile(inFile, outFile string, selectedPages []string, conf *model.Configuration) (err error) { + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + logWritingTo(outFile) + } else { + logWritingTo(inFile) + } + + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return Collect(f1, f2, selectedPages, conf) +} diff --git a/pkg/api/create.go b/pkg/api/create.go new file mode 100644 index 0000000000000000000000000000000000000000..21fd6deafebddc308f7786b4b69eb8235656cb78 --- /dev/null +++ b/pkg/api/create.go @@ -0,0 +1,145 @@ +/* + Copyright 2019 The pdfcpu Authors. + + 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 + + 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 api + +import ( + "io" + "os" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/create" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// CreatePDFFile creates a PDF file for an xRefTable and writes it to outFile. +func CreatePDFFile(xRefTable *model.XRefTable, outFile string, conf *model.Configuration) error { + f, err := os.Create(outFile) + if err != nil { + return err + } + defer f.Close() + ctx := pdfcpu.CreateContext(xRefTable, conf) + return WriteContext(ctx, f) +} + +// Create renders the PDF structure represented by rs into w. +// If rs is present, new PDF content will be appended including any empty pages needed. +// rd is a JSON representation of PDF page content which may include form data. +func Create(rs io.ReadSeeker, rd io.Reader, w io.Writer, conf *model.Configuration) error { + if rd == nil { + return errors.New("pdfcpu: Create: missing rd") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.CREATE + + var ( + ctx *model.Context + err error + ) + + if rs != nil { + ctx, err = ReadValidateAndOptimize(rs, conf) + } else { + ctx, err = pdfcpu.CreateContextWithXRefTable(conf, types.PaperSize["A4"]) + } + if err != nil { + return err + } + + if err := create.FromJSON(ctx, rd); err != nil { + return err + } + + if conf.PostProcessValidate { + if err = ValidateContext(ctx); err != nil { + return err + } + } + + return WriteContext(ctx, w) +} + +func handleOutFilePDF(inFilePDF, outFilePDF string, tmpFile *string) { + if outFilePDF != "" && inFilePDF != outFilePDF { + *tmpFile = outFilePDF + logWritingTo(outFilePDF) + } else { + logWritingTo(inFilePDF) + } +} + +// CreateFile renders the PDF structure represented by inFileJSON into outFilePDF. +// If inFilePDF is present, new PDF content will be appended including any empty pages needed. +// inFileJSON represents PDF page content which may include form data. +func CreateFile(inFilePDF, inFileJSON, outFilePDF string, conf *model.Configuration) (err error) { + var f0, f1, f2 *os.File + + if f0, err = os.Open(inFileJSON); err != nil { + return err + } + + rs := io.ReadSeeker(nil) + f1 = nil + if fileExists(inFilePDF) { + if f1, err = os.Open(inFilePDF); err != nil { + return err + } + log.CLI.Printf("reading %s...\n", inFilePDF) + rs = f1 + } + + tmpFile := inFilePDF + ".tmp" + handleOutFilePDF(inFilePDF, outFilePDF, &tmpFile) + + if f2, err = os.Create(tmpFile); err != nil { + return err + } + + defer func() { + if err != nil { + f2.Close() + if f1 != nil { + f1.Close() + } + f0.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if f1 != nil { + if err = f1.Close(); err != nil { + return + } + } + if err = f0.Close(); err != nil { + return + } + if outFilePDF == "" || inFilePDF == outFilePDF { + err = os.Rename(tmpFile, inFilePDF) + } + }() + + return Create(rs, f0, f2, conf) +} diff --git a/pkg/api/crypto.go b/pkg/api/crypto.go new file mode 100644 index 0000000000000000000000000000000000000000..8561a9fbeeb5816c936305022ba74d434df18601 --- /dev/null +++ b/pkg/api/crypto.go @@ -0,0 +1,287 @@ +/* + Copyright 2020 The pdfcpu Authors. + + 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 + + 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 api + +import ( + "io" + "os" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pkg/errors" +) + +// Encrypt reads a PDF stream from rs and writes the encrypted PDF stream to w. +// A configuration containing at least the current passwords is required. +func Encrypt(rs io.ReadSeeker, w io.Writer, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: Encrypt: missing rs") + } + + if conf == nil { + return errors.New("pdfcpu: missing configuration for encryption") + } + conf.Cmd = model.ENCRYPT + + return Optimize(rs, w, conf) +} + +// EncryptFile encrypts inFile and writes the result to outFile. +// A configuration containing at least the current passwords is required. +func EncryptFile(inFile, outFile string, conf *model.Configuration) (err error) { + if conf == nil { + return errors.New("pdfcpu: missing configuration for encryption") + } + conf.Cmd = model.ENCRYPT + + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + logWritingTo(outFile) + } else { + logWritingTo(inFile) + } + + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return Encrypt(f1, f2, conf) +} + +// Decrypt reads a PDF stream from rs and writes the encrypted PDF stream to w. +// A configuration containing at least the current passwords is required. +func Decrypt(rs io.ReadSeeker, w io.Writer, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: Decrypt: missing rs") + } + + if conf == nil { + return errors.New("pdfcpu: missing configuration for decryption") + } + conf.Cmd = model.DECRYPT + + return Optimize(rs, w, conf) +} + +// DecryptFile decrypts inFile and writes the result to outFile. +// A configuration containing at least the current passwords is required. +func DecryptFile(inFile, outFile string, conf *model.Configuration) (err error) { + if conf == nil { + return errors.New("pdfcpu: missing configuration for decryption") + } + conf.Cmd = model.DECRYPT + + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + logWritingTo(outFile) + } else { + logWritingTo(inFile) + } + + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return Decrypt(f1, f2, conf) +} + +// ChangeUserPassword reads a PDF stream from rs, changes the user password and writes the encrypted PDF stream to w. +// A configuration containing the current passwords is required. +func ChangeUserPassword(rs io.ReadSeeker, w io.Writer, pwOld, pwNew string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: ChangeUserPassword: missing rs") + } + + if conf == nil { + return errors.New("pdfcpu: missing configuration for change user password") + } + + conf.Cmd = model.CHANGEUPW + conf.UserPW = pwOld + conf.UserPWNew = &pwNew + + return Optimize(rs, w, conf) +} + +// ChangeUserPasswordFile reads inFile, changes the user password and writes the result to outFile. +// A configuration containing the current passwords is required. +func ChangeUserPasswordFile(inFile, outFile string, pwOld, pwNew string, conf *model.Configuration) (err error) { + if conf == nil { + return errors.New("pdfcpu: missing configuration for change user password") + } + + conf.Cmd = model.CHANGEUPW + conf.UserPW = pwOld + conf.UserPWNew = &pwNew + + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + logWritingTo(outFile) + } else { + logWritingTo(inFile) + } + + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return ChangeUserPassword(f1, f2, pwOld, pwNew, conf) +} + +// ChangeOwnerPassword reads a PDF stream from rs, changes the owner password and writes the encrypted PDF stream to w. +// A configuration containing the current passwords is required. +func ChangeOwnerPassword(rs io.ReadSeeker, w io.Writer, pwOld, pwNew string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: ChangeOwnerPassword: missing rs") + } + + if conf == nil { + return errors.New("pdfcpu: missing configuration for change owner password") + } + + conf.Cmd = model.CHANGEOPW + conf.OwnerPW = pwOld + conf.OwnerPWNew = &pwNew + + return Optimize(rs, w, conf) +} + +// ChangeOwnerPasswordFile reads inFile, changes the owner password and writes the result to outFile. +// A configuration containing the current passwords is required. +func ChangeOwnerPasswordFile(inFile, outFile string, pwOld, pwNew string, conf *model.Configuration) (err error) { + if conf == nil { + return errors.New("pdfcpu: missing configuration for change owner password") + } + conf.Cmd = model.CHANGEOPW + conf.OwnerPW = pwOld + conf.OwnerPWNew = &pwNew + + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + logWritingTo(outFile) + } else { + logWritingTo(inFile) + } + + if f2, err = os.Create(tmpFile); err != nil { + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return ChangeOwnerPassword(f1, f2, pwOld, pwNew, conf) +} diff --git a/pkg/api/cut.go b/pkg/api/cut.go new file mode 100644 index 0000000000000000000000000000000000000000..a6d81f7b51659382be24364804843f0bfe59679b --- /dev/null +++ b/pkg/api/cut.go @@ -0,0 +1,294 @@ +/* +Copyright 2023 The pdfcpu Authors. + +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 + + 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 api + +import ( + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +func prepareForCut(rs io.ReadSeeker, selectedPages []string, conf *model.Configuration) (*model.Context, types.IntSet, error) { + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return nil, nil, err + } + + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) + if err != nil { + return nil, nil, err + } + + return ctx, pages, nil +} + +// Poster applies cut for selected pages of rs and generates corresponding poster tiles in outDir. +func Poster(rs io.ReadSeeker, outDir, fileName string, selectedPages []string, cut *model.Cut, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: Poster: missing rs") + } + + if cut.PageSize == "" && !cut.UserDim { + return errors.New("pdfcpu: poster - please supply either dimensions or form size ") + } + + if cut.Scale < 1 { + return errors.Errorf("pdfcpu: invalid scale factor %.2f: i >= 1.0\n", cut.Scale) + } + + if rs == nil { + return errors.New("pdfcpu poster: Please provide rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.POSTER + + ctxSrc, pages, err := prepareForCut(rs, selectedPages, conf) + if err != nil { + return err + } + + if len(pages) == 0 { + log.CLI.Println("aborted: nothing to cut!") + return nil + } + + for i, v := range pages { + if !v { + continue + } + ctxDest, err := pdfcpu.PosterPage(ctxSrc, i, cut) + if err != nil { + return err + } + + outFile := filepath.Join(outDir, fmt.Sprintf("%s_page_%d.pdf", fileName, i)) + logWritingTo(outFile) + + if conf.PostProcessValidate { + if err = ValidateContext(ctxDest); err != nil { + return err + } + } + + if err := WriteContextFile(ctxDest, outFile); err != nil { + return err + } + } + + return nil +} + +// PosterFile applies cut for selected pages of inFile and generates corresponding poster tiles in outDir. +func PosterFile(inFile, outDir, outFile string, selectedPages []string, cut *model.Cut, conf *model.Configuration) error { + f, err := os.Open(inFile) + if err != nil { + return err + } + defer f.Close() + + log.CLI.Printf("ndown %s into %s/ ...\n", inFile, outDir) + + if outFile == "" { + outFile = strings.TrimSuffix(filepath.Base(inFile), ".pdf") + } + + return Poster(f, outDir, outFile, selectedPages, cut, conf) +} + +// NDown applies n & cutConf for selected pages of rs and writes results to outDir. +func NDown(rs io.ReadSeeker, outDir, fileName string, selectedPages []string, n int, cut *model.Cut, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu NDown: Please provide rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.NDOWN + + ctxSrc, pages, err := prepareForCut(rs, selectedPages, conf) + if err != nil { + return err + } + + if len(pages) == 0 { + if log.CLIEnabled() { + log.CLI.Println("aborted: nothing to cut!") + } + return nil + } + + for i, v := range pages { + if !v { + continue + } + ctxDest, err := pdfcpu.NDownPage(ctxSrc, i, n, cut) + if err != nil { + return err + } + + if conf.PostProcessValidate { + if err = ValidateContext(ctxDest); err != nil { + return err + } + } + + outFile := filepath.Join(outDir, fmt.Sprintf("%s_page_%d.pdf", fileName, i)) + if log.CLIEnabled() { + log.CLI.Printf("writing %s\n", outFile) + } + if err := WriteContextFile(ctxDest, outFile); err != nil { + return err + } + } + + return nil +} + +// NDownFile applies n & cutConf for selected pages of inFile and writes results to outDir. +func NDownFile(inFile, outDir, outFile string, selectedPages []string, n int, cut *model.Cut, conf *model.Configuration) error { + f, err := os.Open(inFile) + if err != nil { + return err + } + defer f.Close() + + if log.CLIEnabled() { + log.CLI.Printf("ndown %s into %s/ ...\n", inFile, outDir) + } + + if outFile == "" { + outFile = strings.TrimSuffix(filepath.Base(inFile), ".pdf") + } + + return NDown(f, outDir, outFile, selectedPages, n, cut, conf) +} + +func validateCut(cut *model.Cut) error { + sort.Float64s(cut.Hor) + + for _, f := range cut.Hor { + if f < 0 || f >= 1 { + return errors.New("pdfcpu: Invalid cut points. Please consult pdfcpu help cut") + } + } + if len(cut.Hor) == 0 || cut.Hor[0] > 0 { + cut.Hor = append([]float64{0}, cut.Hor...) + } + + sort.Float64s(cut.Vert) + for _, f := range cut.Vert { + if f < 0 || f >= 1 { + return errors.New("pdfcpu: Invalid cut points. Please consult pdfcpu help cut") + } + } + if len(cut.Vert) == 0 || cut.Vert[0] > 0 { + cut.Vert = append([]float64{0}, cut.Vert...) + } + + return nil +} + +// Cut applies cutConf for selected pages of rs and writes results to outDir. +func Cut(rs io.ReadSeeker, outDir, fileName string, selectedPages []string, cut *model.Cut, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: Cut: missing rs") + } + + if len(cut.Hor) == 0 && len(cut.Vert) == 0 { + return errors.New("pdfcpu: Invalid cut configuration string: missing hor/ver cutpoints. Please consult pdfcpu help cut") + } + + if err := validateCut(cut); err != nil { + return err + } + + if rs == nil { + return errors.New("pdfcpu cut: Please provide rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.CUT + + ctxSrc, pages, err := prepareForCut(rs, selectedPages, conf) + if err != nil { + return err + } + + if len(pages) == 0 { + log.CLI.Println("aborted: nothing to cut!") + return nil + } + + for i, v := range pages { + if !v { + continue + } + ctxDest, err := pdfcpu.CutPage(ctxSrc, i, cut) + if err != nil { + return err + } + + if conf.PostProcessValidate { + if err = ValidateContext(ctxDest); err != nil { + return err + } + } + + outFile := filepath.Join(outDir, fmt.Sprintf("%s_page_%d.pdf", fileName, i)) + logWritingTo(outFile) + + if err := WriteContextFile(ctxDest, outFile); err != nil { + return err + } + } + + return nil +} + +// CutFile applies cutConf for selected pages of inFile and writes results to outDir. +func CutFile(inFile, outDir, outFile string, selectedPages []string, cut *model.Cut, conf *model.Configuration) error { + f, err := os.Open(inFile) + if err != nil { + return err + } + defer f.Close() + + if log.CLIEnabled() { + log.CLI.Printf("cutting %s into %s/ ...\n", inFile, outDir) + } + + if outFile == "" { + outFile = strings.TrimSuffix(filepath.Base(inFile), ".pdf") + } + + return Cut(f, outDir, outFile, selectedPages, cut, conf) +} diff --git a/pkg/api/example_test.go b/pkg/api/example_test.go new file mode 100644 index 0000000000000000000000000000000000000000..8820b18aaa9be36d87ee6b322443145d30824fcc --- /dev/null +++ b/pkg/api/example_test.go @@ -0,0 +1,301 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 api + +import ( + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +func ExampleValidateFile() { + + // Use the default configuration to validate in.pdf. + ValidateFile("in.pdf", nil) +} + +func ExampleOptimizeFile() { + + conf := model.NewDefaultConfiguration() + + // Set passwords for encrypted files. + conf.UserPW = "upw" + conf.OwnerPW = "opw" + + // Configure end of line sequence for writing. + conf.Eol = types.EolLF + + // Create an optimized version of in.pdf and write it to out.pdf. + OptimizeFile("in.pdf", "out.pdf", conf) + + // Create an optimized version of inFile. + // If you want to modify the original file, pass an empty string for outFile. + // Use nil for a default configuration. + OptimizeFile("in.pdf", "", nil) +} + +func ExampleTrimFile() { + + // Create a trimmed version of in.pdf containing odd page numbers only. + TrimFile("in.pdf", "outFile", []string{"odd"}, nil) + + // Create a trimmed version of in.pdf containing the first two pages only. + // If you want to modify the original file, pass an empty string for outFile. + TrimFile("in.pdf", "", []string{"1-2"}, nil) +} + +func ExampleSplitFile() { + + // Create single page PDFs for in.pdf in outDir using the default configuration. + SplitFile("in.pdf", "outDir", 1, nil) + + // Create dual page PDFs for in.pdf in outDir using the default configuration. + SplitFile("in.pdf", "outDir", 2, nil) + + // Create a sequence of PDFs representing bookmark secions. + SplitFile("in.pdf", "outDir", 0, nil) +} + +func ExampleRotateFile() { + + // Rotate all pages of in.pdf, clockwise by 90 degrees and write the result to out.pdf. + RotateFile("in.pdf", "out.pdf", 90, nil, nil) + + // Rotate the first page of in.pdf by 180 degrees. + // If you want to modify the original file, pass an empty string as outFile. + RotateFile("in.pdf", "", 180, []string{"1"}, nil) +} + +func ExampleMergeCreateFile() { + + // Merge inFiles by concatenation in the order specified and write the result to out.pdf. + // out.pdf will be overwritten. + inFiles := []string{"in1.pdf", "in2.pdf"} + MergeCreateFile(inFiles, "out.pdf", false, nil) +} + +func ExampleMergeAppendFile() { + + // Merge inFiles by concatenation in the order specified and write the result to out.pdf. + // If out.pdf already exists it will be preserved and serves as the beginning of the merge result. + inFiles := []string{"in1.pdf", "in2.pdf"} + MergeAppendFile(inFiles, "out.pdf", false, nil) +} + +func ExampleInsertPagesFile() { + + // Insert a blank page into in.pdf before page #3. + InsertPagesFile("in.pdf", "", []string{"3"}, true, nil, nil) + + // Insert a blank page into in.pdf after every page. + InsertPagesFile("in.pdf", "", nil, false, nil, nil) +} + +func ExampleRemovePagesFile() { + + // Remove pages 2 and 8 of in.pdf. + RemovePagesFile("in.pdf", "", []string{"2", "8"}, nil) + + // Remove first 2 pages of in.pdf. + RemovePagesFile("in.pdf", "", []string{"-2"}, nil) + + // Remove all pages >= 10 of in.pdf. + RemovePagesFile("in.pdf", "", []string{"10-"}, nil) +} + +func ExampleAddWatermarksFile() { + + // Unique abbreviations are accepted for all watermark descriptor parameters. + // eg. sc = scalefactor or rot = rotation + + // Add a "Demo" watermark to all pages of in.pdf along the diagonal running from lower left to upper right. + onTop := false + update := false + wm, _ := TextWatermark("Demo", "", onTop, update, types.POINTS) + AddWatermarksFile("in.pdf", "", nil, wm, nil) + + // Stamp all odd pages of in.pdf in red "Confidential" in 48 point Courier + // using a rotation angle of 45 degrees and an absolute scalefactor of 1.0. + onTop = true + wm, _ = TextWatermark("Confidential", "font:Courier, points:48, col: 1 0 0, rot:45, scale:1 abs, ", onTop, update, types.POINTS) + AddWatermarksFile("in.pdf", "", []string{"odd"}, wm, nil) + + // Add image stamps to in.pdf using absolute scaling and a negative rotation of 90 degrees. + wm, _ = ImageWatermark("image.png", "scalefactor:.5 a, rot:-90", onTop, update, types.POINTS) + AddWatermarksFile("in.pdf", "", nil, wm, nil) + + // Add a PDF stamp to all pages of in.pdf using the 2nd page of stamp.pdf, use absolute scaling of 0.5 + // and rotate along the 2nd diagonal running from upper left to lower right corner. + wm, _ = PDFWatermark("stamp.pdf:2", "scale:.5 abs, diagonal:2", onTop, update, types.POINTS) + AddWatermarksFile("in.pdf", "", nil, wm, nil) +} + +func ExampleRemoveWatermarksFile() { + + // Add a "Demo" stamp to all pages of in.pdf along the diagonal running from lower left to upper right. + onTop := true + update := false + wm, _ := TextWatermark("Demo", "", onTop, update, types.POINTS) + AddWatermarksFile("in.pdf", "", nil, wm, nil) + + // Update stamp for correction: + update = true + wm, _ = TextWatermark("Confidential", "", onTop, update, types.POINTS) + AddWatermarksFile("in.pdf", "", nil, wm, nil) + + // Add another watermark on top of page 1 + wm, _ = TextWatermark("Footer stamp", "c:.5 1 1, pos:bc", onTop, update, types.POINTS) + AddWatermarksFile("in.pdf", "", nil, wm, nil) + + // Remove watermark on page 1 + RemoveWatermarksFile("in.pdf", "", []string{"1"}, nil) + + // Remove all watermarks + RemoveWatermarksFile("in.pdf", "", nil, nil) +} + +func ExampleImportImagesFile() { + + // Convert an image into a single page of out.pdf which will be created if necessary. + // The page dimensions will match the image dimensions. + // If out.pdf already exists, append a new page. + // Use the default import configuration. + ImportImagesFile([]string{"image.png"}, "out.pdf", nil, nil) + + // Import images by creating an A3 page for each image. + // Images are page centered with 1.0 relative scaling. + // Import an image as a new page of the existing out.pdf. + imp, _ := Import("form:A3, pos:c, s:1.0", types.POINTS) + ImportImagesFile([]string{"a1.png", "a2.jpg", "a3.tiff"}, "out.pdf", imp, nil) +} + +func ExampleNUpFile() { + + // 4-Up in.pdf and write result to out.pdf. + nup, _ := PDFNUpConfig(4, "", nil) + inFiles := []string{"in.pdf"} + NUpFile(inFiles, "out.pdf", nil, nup, nil) + + // 9-Up a sequence of images using format Tabloid w/o borders and no margins. + nup, _ = ImageNUpConfig(9, "f:Tabloid, b:off, m:0", nil) + inFiles = []string{"in1.png", "in2.jpg", "in3.tiff"} + NUpFile(inFiles, "out.pdf", nil, nup, nil) + + // TestGridFromPDF + nup, _ = PDFGridConfig(1, 3, "f:LegalL", nil) + inFiles = []string{"in.pdf"} + NUpFile(inFiles, "out.pdf", nil, nup, nil) + + // TestGridFromImages + nup, _ = ImageGridConfig(4, 2, "d:500 500, m:20, b:off", nil) + inFiles = []string{"in1.png", "in2.jpg", "in3.tiff"} + NUpFile(inFiles, "out.pdf", nil, nup, nil) +} + +func ExampleSetPermissionsFile() { + + // Setting all permissions for the AES-256 encrypted in.pdf. + conf := model.NewAESConfiguration("upw", "opw", 256) + conf.Permissions = model.PermissionsAll + SetPermissionsFile("in.pdf", "", conf) + + // Restricting permissions for the AES-256 encrypted in.pdf. + conf = model.NewAESConfiguration("upw", "opw", 256) + conf.Permissions = model.PermissionsNone + SetPermissionsFile("in.pdf", "", conf) +} + +func ExampleEncryptFile() { + + // Encrypting a file using AES-256. + conf := model.NewAESConfiguration("upw", "opw", 256) + EncryptFile("in.pdf", "", conf) +} + +func ExampleDecryptFile() { + + // Decrypting an AES-256 encrypted file. + conf := model.NewAESConfiguration("upw", "opw", 256) + DecryptFile("in.pdf", "", conf) +} + +func ExampleChangeUserPasswordFile() { + + // Changing the user password for an AES-256 encrypted file. + conf := model.NewAESConfiguration("upw", "opw", 256) + ChangeUserPasswordFile("in.pdf", "", "upw", "upwNew", conf) +} + +func ExampleChangeOwnerPasswordFile() { + + // Changing the owner password for an AES-256 encrypted file. + conf := model.NewAESConfiguration("upw", "opw", 256) + ChangeOwnerPasswordFile("in.pdf", "", "opw", "opwNew", conf) +} + +func ExampleAddAttachmentsFile() { + + // Attach 3 files to in.pdf. + AddAttachmentsFile("in.pdf", "", []string{"img.jpg", "attach.pdf", "test.zip"}, false, nil) +} + +func ExampleRemoveAttachmentsFile() { + + // Remove 1 attachment from in.pdf. + RemoveAttachmentsFile("in.pdf", "", []string{"img.jpg"}, nil) + + // Remove all attachments from in.pdf + RemoveAttachmentsFile("in.pdf", "", nil, nil) +} + +func ExampleExtractAttachmentsFile() { + + // Extract 1 attachment from in.pdf into outDir. + ExtractAttachmentsFile("in.pdf", "outDir", []string{"img.jpg"}, nil) + + // Extract all attachments from in.pdf into outDir + ExtractAttachmentsFile("in.pdf", "outDir", nil, nil) +} + +func ExampleExtractImagesFile() { + + // Extract embedded images from in.pdf into outDir. + ExtractImagesFile("in.pdf", "outDir", nil, nil) +} + +func ExampleExtractFontsFile() { + + // Extract embedded fonts for pages 1-3 from in.pdf into outDir. + ExtractFontsFile("in.pdf", "outDir", []string{"1-3"}, nil) +} + +func ExampleExtractContentFile() { + + // Extract content for all pages in PDF syntax from in.pdf into outDir. + ExtractContentFile("in.pdf", "outDir", nil, nil) +} + +func ExampleExtractPagesFile() { + + // Extract all even numbered pages from in.pdf into outDir. + ExtractPagesFile("in.pdf", "outDir", []string{"even"}, nil) +} + +func ExampleExtractMetadataFile() { + + // Extract all metadata from in.pdf into outDir. + ExtractMetadataFile("in.pdf", "outDir", nil) +} diff --git a/pkg/api/extract.go b/pkg/api/extract.go new file mode 100644 index 0000000000000000000000000000000000000000..96a24a0d4396563c281ecd7cad90b43f41d7532b --- /dev/null +++ b/pkg/api/extract.go @@ -0,0 +1,426 @@ +/* + Copyright 2019 The pdfcpu Authors. + + 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 + + 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 api + +import ( + "bytes" + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pkg/errors" +) + +// ExtractImagesRaw returns []pdfcpu.Image containing io.Readers for images contained in selectedPages. +// Beware of memory intensive returned slice. +func ExtractImagesRaw(rs io.ReadSeeker, selectedPages []string, conf *model.Configuration) ([]map[int]model.Image, error) { + if rs == nil { + return nil, errors.New("pdfcpu: ExtractImages: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.EXTRACTIMAGES + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return nil, err + } + + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) + if err != nil { + return nil, err + } + + var images []map[int]model.Image + for i, v := range pages { + if !v { + continue + } + mm, err := pdfcpu.ExtractPageImages(ctx, i, false) + if err != nil { + return nil, err + } + images = append(images, mm) + } + + return images, nil +} + +// ExtractImages extracts and digests embedded image resources from rs for selected pages. +func ExtractImages(rs io.ReadSeeker, selectedPages []string, digestImage func(model.Image, bool, int) error, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: ExtractImages: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.EXTRACTIMAGES + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) + if err != nil { + return err + } + + pageNrs := []int{} + for k, v := range pages { + if !v { + continue + } + pageNrs = append(pageNrs, k) + } + + sort.Ints(pageNrs) + maxPageDigits := len(strconv.Itoa(pageNrs[len(pageNrs)-1])) + + for _, i := range pageNrs { + mm, err := pdfcpu.ExtractPageImages(ctx, i, false) + if err != nil { + return err + } + singleImgPerPage := len(mm) == 1 + for _, img := range mm { + if err := digestImage(img, singleImgPerPage, maxPageDigits); err != nil { + return err + } + } + } + + return nil +} + +// ExtractImagesFile dumps embedded image resources from inFile into outDir for selected pages. +func ExtractImagesFile(inFile, outDir string, selectedPages []string, conf *model.Configuration) error { + f, err := os.Open(inFile) + if err != nil { + return err + } + defer f.Close() + + if log.CLIEnabled() { + log.CLI.Printf("extracting images from %s into %s/ ...\n", inFile, outDir) + } + fileName := strings.TrimSuffix(filepath.Base(inFile), ".pdf") + + return ExtractImages(f, selectedPages, pdfcpu.WriteImageToDisk(outDir, fileName), conf) +} + +func writeFonts(ff []pdfcpu.Font, outDir, fileName string) error { + for _, f := range ff { + outFile := filepath.Join(outDir, fmt.Sprintf("%s_%s.%s", fileName, f.Name, f.Type)) + logWritingTo(outFile) + w, err := os.Create(outFile) + if err != nil { + return err + } + if _, err = io.Copy(w, f); err != nil { + return err + } + if err := w.Close(); err != nil { + return err + } + } + + return nil +} + +// ExtractFonts dumps embedded fontfiles from rs into outDir for selected pages. +func ExtractFonts(rs io.ReadSeeker, outDir, fileName string, selectedPages []string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: ExtractFonts: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.EXTRACTFONTS + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) + if err != nil { + return err + } + + fileName = strings.TrimSuffix(filepath.Base(fileName), ".pdf") + + for i, v := range pages { + if !v { + continue + } + ff, err := pdfcpu.ExtractPageFonts(ctx, i) + if err != nil { + return err + } + if err := writeFonts(ff, outDir, fileName); err != nil { + return err + } + } + + ff, err := pdfcpu.ExtractFormFonts(ctx) + if err != nil { + return err + } + + return writeFonts(ff, outDir, fileName) +} + +// ExtractFontsFile dumps embedded fontfiles from inFile into outDir for selected pages. +func ExtractFontsFile(inFile, outDir string, selectedPages []string, conf *model.Configuration) error { + f, err := os.Open(inFile) + if err != nil { + return err + } + defer f.Close() + + if log.CLIEnabled() { + log.CLI.Printf("extracting fonts from %s into %s/ ...\n", inFile, outDir) + } + + return ExtractFonts(f, outDir, filepath.Base(inFile), selectedPages, conf) +} + +// WritePage consumes an io.Reader containing some PDF bytes and writes to outDir/fileName. +func WritePage(r io.Reader, outDir, fileName string, pageNr int) error { + outFile := filepath.Join(outDir, fmt.Sprintf("%s_page_%d.pdf", fileName, pageNr)) + logWritingTo(outFile) + w, err := os.Create(outFile) + if err != nil { + return err + } + if _, err = io.Copy(w, r); err != nil { + return err + } + return w.Close() +} + +// ExtractPage extracts the page with pageNr out of ctx into an io.Reader. +func ExtractPage(ctx *model.Context, pageNr int) (io.Reader, error) { + ctxNew, err := pdfcpu.ExtractPages(ctx, []int{pageNr}, false) + if err != nil { + return nil, err + } + + var b bytes.Buffer + if err := WriteContext(ctxNew, &b); err != nil { + return nil, err + } + + return &b, nil +} + +// ExtractPages generates single page PDF files from rs in outDir for selected pages. +func ExtractPages(rs io.ReadSeeker, outDir, fileName string, selectedPages []string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: ExtractPages: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.EXTRACTPAGES + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) + if err != nil { + return err + } + + if len(pages) == 0 { + if log.CLIEnabled() { + log.CLI.Println("aborted: missing page numbers!") + } + return nil + } + + fileName = strings.TrimSuffix(filepath.Base(fileName), ".pdf") + + for _, i := range sortedPages(pages) { + r, err := ExtractPage(ctx, i) + if err != nil { + return err + } + if err := WritePage(r, outDir, fileName, i); err != nil { + return err + } + } + + return nil +} + +// ExtractPagesFile generates single page PDF files from inFile in outDir for selected pages. +func ExtractPagesFile(inFile, outDir string, selectedPages []string, conf *model.Configuration) error { + f, err := os.Open(inFile) + if err != nil { + return err + } + defer f.Close() + + if log.CLIEnabled() { + log.CLI.Printf("extracting pages from %s into %s/ ...\n", inFile, outDir) + } + + return ExtractPages(f, outDir, filepath.Base(inFile), selectedPages, conf) +} + +// ExtractContent dumps "PDF source" files from rs into outDir for selected pages. +func ExtractContent(rs io.ReadSeeker, outDir, fileName string, selectedPages []string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: ExtractContent: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.EXTRACTCONTENT + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) + if err != nil { + return err + } + + fileName = strings.TrimSuffix(filepath.Base(fileName), ".pdf") + + for p, v := range pages { + if !v { + continue + } + + r, err := pdfcpu.ExtractPageContent(ctx, p) + if err != nil { + return err + } + if r == nil { + continue + } + + outFile := filepath.Join(outDir, fmt.Sprintf("%s_Content_page_%d.txt", fileName, p)) + logWritingTo(outFile) + f, err := os.Create(outFile) + if err != nil { + return err + } + + if _, err = io.Copy(f, r); err != nil { + return err + } + + if err := f.Close(); err != nil { + return err + } + } + + return nil +} + +// ExtractContentFile dumps "PDF source" files from inFile into outDir for selected pages. +func ExtractContentFile(inFile, outDir string, selectedPages []string, conf *model.Configuration) error { + f, err := os.Open(inFile) + if err != nil { + return err + } + defer f.Close() + + if log.CLIEnabled() { + log.CLI.Printf("extracting content from %s into %s/ ...\n", inFile, outDir) + } + + return ExtractContent(f, outDir, inFile, selectedPages, conf) +} + +// ExtractMetadata dumps all metadata dict entries for rs into outDir. +func ExtractMetadata(rs io.ReadSeeker, outDir, fileName string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: ExtractMetadata: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.EXTRACTMETADATA + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + mm, err := pdfcpu.ExtractMetadata(ctx) + if err != nil { + return err + } + + if len(mm) > 0 { + fileName = strings.TrimSuffix(filepath.Base(fileName), ".pdf") + for _, m := range mm { + outFile := filepath.Join(outDir, fmt.Sprintf("%s_Metadata_%s_%d_%d.txt", fileName, m.ParentType, m.ParentObjNr, m.ObjNr)) + logWritingTo(outFile) + f, err := os.Create(outFile) + if err != nil { + return err + } + if _, err = io.Copy(f, m); err != nil { + return err + } + if err := f.Close(); err != nil { + return err + } + } + } + + return nil +} + +// ExtractMetadataFile dumps all metadata dict entries for inFile into outDir. +func ExtractMetadataFile(inFile, outDir string, conf *model.Configuration) error { + f, err := os.Open(inFile) + if err != nil { + return err + } + defer f.Close() + + if log.CLIEnabled() { + log.CLI.Printf("extracting metadata from %s into %s/ ...\n", inFile, outDir) + } + + return ExtractMetadata(f, outDir, filepath.Base(inFile), conf) +} diff --git a/pkg/api/font.go b/pkg/api/font.go new file mode 100644 index 0000000000000000000000000000000000000000..19c48e82a35842fc68eec109f8847976f912acc2 --- /dev/null +++ b/pkg/api/font.go @@ -0,0 +1,260 @@ +/* + Copyright 2020 The pdfcpu Authors. + + 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 + + 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 api + +import ( + "bytes" + "fmt" + + "path/filepath" + "sort" + "unicode/utf8" + + "github.com/pdfcpu/pdfcpu/pkg/font" + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// ListFonts returns a list of supported fonts. +func ListFonts() ([]string, error) { + // Get list of PDF core fonts. + coreFonts := font.CoreFontNames() + for i, s := range coreFonts { + coreFonts[i] = " " + s + } + sort.Strings(coreFonts) + + sscf := []string{"Corefonts:"} + sscf = append(sscf, coreFonts...) + + // Get installed fonts from pdfcpu config dir in users home dir + userFonts := font.UserFontNamesVerbose() + for i, s := range userFonts { + userFonts[i] = " " + s + } + sort.Strings(userFonts) + + ssuf := []string{fmt.Sprintf("Userfonts(%s):", font.UserFontDir)} + ssuf = append(ssuf, userFonts...) + + sscf = append(sscf, "") + return append(sscf, ssuf...), nil +} + +// InstallFonts installs true type fonts for embedding. +func InstallFonts(fileNames []string) error { + if log.CLIEnabled() { + log.CLI.Printf("installing to %s...", font.UserFontDir) + } + + for _, fn := range fileNames { + switch filepath.Ext(fn) { + case ".ttf": + //log.CLI.Println(filepath.Base(fn)) + if err := font.InstallTrueTypeFont(font.UserFontDir, fn); err != nil { + if log.CLIEnabled() { + log.CLI.Printf("%v", err) + } + } + case ".ttc": + //log.CLI.Println(filepath.Base(fn)) + if err := font.InstallTrueTypeCollection(font.UserFontDir, fn); err != nil { + if log.CLIEnabled() { + log.CLI.Printf("%v", err) + } + } + } + } + + return font.LoadUserFonts() +} + +func rowLabel(xRefTable *model.XRefTable, i int, td model.TextDescriptor, baseFontName, baseFontKey string, buf *bytes.Buffer, mb *types.Rectangle, left bool) { + x := 39. + if !left { + x = 7750 + } + s := fmt.Sprintf("#%02X", i) + td.X, td.Y, td.Text = x, float64(7677-i*30), s + td.StrokeCol, td.FillCol = color.Black, color.SimpleColor{B: .8} + td.FontName, td.FontKey, td.FontSize = baseFontName, baseFontKey, 14 + + model.WriteMultiLine(xRefTable, buf, mb, nil, td) +} + +func columnsLabel(xRefTable *model.XRefTable, td model.TextDescriptor, baseFontName, baseFontKey string, buf *bytes.Buffer, mb *types.Rectangle, top bool) { + y := 7700. + if !top { + y = 0 + } + + td.FontName, td.FontKey = baseFontName, baseFontKey + + for i := 0; i < 256; i++ { + s := fmt.Sprintf("#%02X", i) + td.X, td.Y, td.Text, td.FontSize = float64(70+i*30), y, s, 14 + td.StrokeCol, td.FillCol = color.Black, color.SimpleColor{B: .8} + model.WriteMultiLine(xRefTable, buf, mb, nil, td) + } +} + +func surrogate(r rune) bool { + return r >= 0xD800 && r <= 0xDFFF +} + +func writeUserFontDemoContent(xRefTable *model.XRefTable, p model.Page, fontName string, plane int) { + baseFontName := "Helvetica" + baseFontSize := 24 + baseFontKey := p.Fm.EnsureKey(baseFontName) + + fontKey := p.Fm.EnsureKey(fontName) + fontSize := 24 + + fillCol := color.NewSimpleColor(0xf7e6c7) + draw.DrawGrid(p.Buf, 16*16, 16*16, types.RectForWidthAndHeight(55, 16, 16*480, 16*480), color.Black, &fillCol) + + td := model.TextDescriptor{ + FontName: fontName, + Embed: true, + FontKey: fontKey, + FontSize: baseFontSize, + HAlign: types.AlignCenter, + VAlign: types.AlignBaseline, + Scale: 1.0, + ScaleAbs: true, + RMode: draw.RMFill, + StrokeCol: color.Black, + FillCol: color.NewSimpleColor(0xab6f30), + ShowBackground: true, + BackgroundCol: color.SimpleColor{R: 1., G: .98, B: .77}, + } + + from := plane * 0x10000 + to := (plane+1)*0x10000 - 1 + s := fmt.Sprintf("%s %d points (%04X - %04X)", fontName, fontSize, from, to) + + td.X, td.Y, td.Text = p.MediaBox.Width()/2, 7750, s + td.FontName, td.FontKey = baseFontName, baseFontKey + td.StrokeCol, td.FillCol = color.NewSimpleColor(0x77bdbd), color.NewSimpleColor(0xab6f30) + model.WriteMultiLine(xRefTable, p.Buf, p.MediaBox, nil, td) + + columnsLabel(xRefTable, td, baseFontName, baseFontKey, p.Buf, p.MediaBox, true) + base := rune(plane * 0x10000) + for j := 0; j < 256; j++ { + rowLabel(xRefTable, j, td, baseFontName, baseFontKey, p.Buf, p.MediaBox, true) + buf := make([]byte, 4) + td.StrokeCol, td.FillCol = color.Black, color.Black + td.FontName, td.FontKey, td.FontSize = fontName, fontKey, fontSize-2 + for i := 0; i < 256; i++ { + r := base + rune(j*256+i) + s = " " + if !surrogate(r) { + n := utf8.EncodeRune(buf, r) + s = string(buf[:n]) + } + td.X, td.Y, td.Text = float64(70+i*30), float64(7672-j*30), s + model.WriteMultiLine(xRefTable, p.Buf, p.MediaBox, nil, td) + } + rowLabel(xRefTable, j, td, baseFontName, baseFontKey, p.Buf, p.MediaBox, false) + } + columnsLabel(xRefTable, td, baseFontName, baseFontKey, p.Buf, p.MediaBox, false) +} + +func createUserFontDemoPage(xRefTable *model.XRefTable, w, h, plane int, fontName string) model.Page { + mediaBox := types.RectForDim(float64(w), float64(h)) + p := model.NewPageWithBg(mediaBox, color.NewSimpleColor(0xbeded9)) + writeUserFontDemoContent(xRefTable, p, fontName, plane) + return p +} + +func planeString(i int) string { + switch i { + case 0: + return "BMP" // Basic Multilingual Plane + case 1: + return "SMP" // Supplementary Multilingual Plane + case 2: + return "SIP" // Supplementary Ideographic Plane + case 3: + return "TIP" // Tertiary Ideographic Plane + case 14: + return "SSP" // Supplementary Special-purpose Plane + case 15: + return "SPUA" // Supplementary Private Use Area Plane + } + return "" +} + +// CreateUserFontDemoFiles creates single page PDF for each Unicode plane covered. +func CreateUserFontDemoFiles(dir, fn string) error { + w, h := 7800, 7800 + font.UserFontMetricsLock.RLock() + ttf, ok := font.UserFontMetrics[fn] + font.UserFontMetricsLock.RUnlock() + if !ok { + return errors.Errorf("pdfcpu: font %s not available\n", fn) + } + // Create a single page PDF for each Unicode plane with existing glyphs. + for i := range ttf.Planes { + xRefTable, err := pdfcpu.CreateDemoXRef() + if err != nil { + return err + } + p := createUserFontDemoPage(xRefTable, w, h, i, fn) + + rootDict, err := xRefTable.Catalog() + if err != nil { + return err + } + if err = pdfcpu.AddPageTreeWithSamplePage(xRefTable, rootDict, p); err != nil { + return err + } + fileName := filepath.Join(dir, fn+"_"+planeString(i)+".pdf") + if err := CreatePDFFile(xRefTable, fileName, nil); err != nil { + return err + } + } + return nil +} + +// CreateCheatSheetsUserFonts creates single page PDF cheat sheets for installed user fonts. +func CreateCheatSheetsUserFonts(fontNames []string) error { + if len(fontNames) == 0 { + fontNames = font.UserFontNames() + } + sort.Strings(fontNames) + for _, fn := range fontNames { + if !font.IsUserFont(fn) { + if log.CLIEnabled() { + log.CLI.Printf("unknown user font: %s\n", fn) + } + continue + } + if log.CLIEnabled() { + log.CLI.Println("creating cheatsheets for: " + fn) + } + if err := CreateUserFontDemoFiles(".", fn); err != nil { + return err + } + } + return nil +} diff --git a/pkg/api/form.go b/pkg/api/form.go new file mode 100644 index 0000000000000000000000000000000000000000..711bfb080fba18a666f2144d5e57e52e594b31e8 --- /dev/null +++ b/pkg/api/form.go @@ -0,0 +1,849 @@ +/* + Copyright 2023 The pdfcpu Authors. + + 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 + + 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 api + +import ( + "bytes" + "encoding/csv" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/create" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/form" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +var ( + ErrNoFormData = errors.New("pdfcpu: missing form data") + ErrNoFormFieldsAffected = errors.New("pdfcpu: no form fields affected") + ErrInvalidCSV = errors.New("pdfcpu: invalid csv input file") + ErrInvalidJSON = errors.New("pdfcpu: invalid JSON encoding") +) + +// FormFields returns all form fields of rs. +func FormFields(rs io.ReadSeeker, conf *model.Configuration) ([]form.Field, error) { + if rs == nil { + return nil, errors.New("pdfcpu: FormFields: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTFORMFIELDS + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return nil, err + } + + fields, _, err := form.FormFields(ctx) + + return fields, err +} + +// RemoveFormFields deletes form fields in rs and writes the result to w. +func RemoveFormFields(rs io.ReadSeeker, w io.Writer, fieldIDsOrNames []string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: RemoveFormFields: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.REMOVEFORMFIELDS + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + ok, err := form.RemoveFormFields(ctx, fieldIDsOrNames) + if err != nil { + return err + } + if !ok { + return ErrNoFormFieldsAffected + } + + return Write(ctx, w, conf) +} + +// RemoveFormFieldsFile deletes form fields in inFile and writes the result to outFile. +func RemoveFormFieldsFile(inFile, outFile string, fieldIDsOrNames []string, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + } + logWritingTo(outFile) + + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return RemoveFormFields(f1, f2, fieldIDsOrNames, conf) +} + +// LockFormFields turns form fields in rs into read-only and writes the result to w. +func LockFormFields(rs io.ReadSeeker, w io.Writer, fieldIDsOrNames []string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: LockFormFields: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LOCKFORMFIELDS + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + ok, err := form.LockFormFields(ctx, fieldIDsOrNames) + if err != nil { + return err + } + if !ok { + return ErrNoFormFieldsAffected + } + + return Write(ctx, w, conf) +} + +// LockFormFieldsFile turns form fields of inFile into read-only and writes the result to outFile. +func LockFormFieldsFile(inFile, outFile string, fieldIDsOrNames []string, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + } + logWritingTo(outFile) + + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return LockFormFields(f1, f2, fieldIDsOrNames, conf) +} + +// UnlockFormFields makess form fields in rs writeable and writes the result to w. +func UnlockFormFields(rs io.ReadSeeker, w io.Writer, fieldIDsOrNames []string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: UnlockFormFields: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.UNLOCKFORMFIELDS + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + ok, err := form.UnlockFormFields(ctx, fieldIDsOrNames) + if err != nil { + return err + } + if !ok { + return ErrNoFormFieldsAffected + } + + return Write(ctx, w, conf) +} + +// UnlockFormFieldsFile makes form fields of inFile writeable and writes the result to outFile. +func UnlockFormFieldsFile(inFile, outFile string, fieldIDsOrNames []string, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + } + logWritingTo(outFile) + + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return UnlockFormFields(f1, f2, fieldIDsOrNames, conf) +} + +// ResetFormFields resets form fields of rs and writes the result to w. +func ResetFormFields(rs io.ReadSeeker, w io.Writer, fieldIDsOrNames []string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: ResetFormFields: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.RESETFORMFIELDS + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + ok, err := form.ResetFormFields(ctx, fieldIDsOrNames) + if err != nil { + return err + } + if !ok { + return ErrNoFormFieldsAffected + } + + return Write(ctx, w, conf) +} + +// ResetFormFieldsFile resets form fields of inFile and writes the result to outFile. +func ResetFormFieldsFile(inFile, outFile string, fieldIDsOrNames []string, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + } + logWritingTo(outFile) + + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return ResetFormFields(f1, f2, fieldIDsOrNames, conf) +} + +// ExportForm extracts form data originating from source from rs. +func ExportForm(rs io.ReadSeeker, source string, conf *model.Configuration) (*form.FormGroup, error) { + if rs == nil { + return nil, errors.New("pdfcpu: ExportForm: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.EXPORTFORMFIELDS + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return nil, err + } + + formGroup, ok, err := form.ExportForm(ctx.XRefTable, source) + if err != nil { + return nil, err + } + if !ok { + return nil, ErrNoFormFieldsAffected + } + + return formGroup, nil +} + +// ExportFormJSON extracts form data originating from source from rs and writes the result to w. +func ExportFormJSON(rs io.ReadSeeker, w io.Writer, source string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: ExportFormJSON: missing rs") + } + + if w == nil { + return errors.New("pdfcpu: ExportFormJSON: missing w") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.EXPORTFORMFIELDS + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + ok, err := form.ExportFormJSON(ctx.XRefTable, source, w) + if err != nil { + return err + } + if !ok { + return ErrNoFormFieldsAffected + } + + return nil +} + +// ExportFormFile extracts form data from inFilePDF and writes the result to outFileJSON. +func ExportFormFile(inFilePDF, outFileJSON string, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if f1, err = os.Open(inFilePDF); err != nil { + return err + } + + if f2, err = os.Create(outFileJSON); err != nil { + f1.Close() + return err + } + logWritingTo(outFileJSON) + + defer func() { + if err != nil { + f2.Close() + f1.Close() + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + }() + + return ExportFormJSON(f1, f2, inFilePDF, conf) +} + +func validateComboBoxValues(f form.Form) error { + for _, cb := range f.ComboBoxes { + if cb.Value == "" || cb.Editable { + continue + } + if len(cb.Options) > 0 { + if !types.MemberOf(cb.Value, cb.Options) { + return errors.Errorf("pdfcpu: fill field name: \"%s\" unknown value: \"%s\" - options: %v\n", cb.Name, cb.Value, cb.Options) + } + } + } + return nil +} + +func validateListBoxValues(f form.Form) error { + for _, lb := range f.ListBoxes { + if len(lb.Values) == 0 { + continue + } + if len(lb.Options) > 0 { + for _, v := range lb.Values { + if !types.MemberOf(v, lb.Options) { + return errors.Errorf("pdfcpu: fill field name: \"%s\" unknown value: \"%s\" - options: %v\n", lb.Name, v, lb.Options) + } + } + } + } + return nil +} + +func validateRadioButtonGroupValues(f form.Form) error { + for _, rbg := range f.RadioButtonGroups { + if rbg.Value == "" { + continue + } + if len(rbg.Options) > 0 { + if !types.MemberOf(rbg.Value, rbg.Options) { + return errors.Errorf("pdfcpu: fill field name: \"%s\" unknown value: \"%s\" - options: %v\n", rbg.Name, rbg.Value, rbg.Options) + } + } + } + return nil +} + +func validateOptionValues(f form.Form) error { + if err := validateRadioButtonGroupValues(f); err != nil { + return err + } + + if err := validateComboBoxValues(f); err != nil { + return err + } + + if err := validateListBoxValues(f); err != nil { + return err + } + + return nil +} + +func fillPostProc(ctx *model.Context, pp []*model.Page) error { + if _, _, err := create.UpdatePageTree(ctx, pp, nil); err != nil { + return err + } + + return ValidateContext(ctx) +} + +// FillForm populates the form rs with data from rd and writes the result to w. +func FillForm(rs io.ReadSeeker, rd io.Reader, w io.Writer, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: FillForm: missing rs") + } + + if rd == nil { + return errors.New("pdfcpu: FillForm: missing rd") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.FILLFORMFIELDS + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + ctx.RemoveSignature() + + var buf bytes.Buffer + if _, err := io.Copy(&buf, rd); err != nil { + return err + } + + bb := buf.Bytes() + + if !json.Valid(bb) { + return ErrInvalidJSON + } + + formGroup := form.FormGroup{} + + if err := json.Unmarshal(bb, &formGroup); err != nil { + return err + } + + if len(formGroup.Forms) == 0 { + return ErrNoFormData + } + + f := formGroup.Forms[0] + + if err := validateOptionValues(f); err != nil { + return err + } + + if log.CLIEnabled() { + log.CLI.Println("filling...") + } + + ok, pp, err := form.FillForm(ctx, form.FillDetails(&f, nil), f.Pages, form.JSON) + if err != nil { + return err + } + if !ok { + return ErrNoFormFieldsAffected + } + + if err := fillPostProc(ctx, pp); err != nil { + return err + } + + return Write(ctx, w, conf) +} + +// FillFormFile populates the form inFilePDF with data from inFileJSON and writes the result to outFilePDF. +func FillFormFile(inFilePDF, inFileJSON, outFilePDF string, conf *model.Configuration) (err error) { + var f0, f1, f2 *os.File + + if f0, err = os.Open(inFileJSON); err != nil { + return err + } + + if f1, err = os.Open(inFilePDF); err != nil { + f0.Close() + return err + } + rs := f1 + + tmpFile := inFilePDF + ".tmp" + if outFilePDF != "" && inFilePDF != outFilePDF { + tmpFile = outFilePDF + } + logWritingTo(outFilePDF) + + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + f0.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + f0.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if err = f0.Close(); err != nil { + return + } + if outFilePDF == "" || inFilePDF == outFilePDF { + err = os.Rename(tmpFile, inFilePDF) + } + }() + + return FillForm(rs, f0, f2, conf) +} + +func parseFormGroup(rd io.Reader) (*form.FormGroup, error) { + formGroup := &form.FormGroup{} + + var buf bytes.Buffer + if _, err := io.Copy(&buf, rd); err != nil { + return nil, err + } + + bb := buf.Bytes() + + if !json.Valid(bb) { + return nil, ErrInvalidJSON + } + + if err := json.Unmarshal(bb, formGroup); err != nil { + return nil, err + } + + if len(formGroup.Forms) == 0 { + return nil, ErrNoFormData + } + + return formGroup, nil +} + +func mergeForms(outDir, fileName string, outFiles []string, conf *model.Configuration) error { + outFile := filepath.Join(outDir, fileName+".pdf") + if err := MergeCreateFile(outFiles, outFile, false, conf); err != nil { + return err + } + if log.CLIEnabled() { + log.CLI.Println("cleaning up...") + } + for _, fn := range outFiles { + if err := os.Remove(fn); err != nil { + return err + } + } + return nil +} + +func multiFillFormJSON(inFilePDF string, rd io.Reader, outDir, fileName string, merge bool, conf *model.Configuration) error { + formGroup, err := parseFormGroup(rd) + if err != nil { + return err + } + + var outFiles []string + + for i, f := range formGroup.Forms { + + rs, err := os.Open(inFilePDF) + if err != nil { + return err + } + defer rs.Close() + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + ok, pp, err := form.FillForm(ctx, form.FillDetails(&f, nil), f.Pages, form.JSON) + if err != nil { + return err + } + if !ok { + return ErrNoFormFieldsAffected + } + + if _, _, err := create.UpdatePageTree(ctx, pp, nil); err != nil { + return err + } + + if conf.PostProcessValidate { + if err = ValidateContext(ctx); err != nil { + return err + } + } + + outFile := filepath.Join(outDir, fmt.Sprintf("%s_%02d.pdf", fileName, i+1)) + if log.CLIEnabled() { + log.CLI.Printf("writing %s\n", outFile) + } + + if err := WriteContextFile(ctx, outFile); err != nil { + return err + } + outFiles = append(outFiles, outFile) + } + + if merge { + if err := mergeForms(outDir, fileName, outFiles, conf); err != nil { + return err + } + } + + return nil +} + +func parseCSVLines(rd io.Reader) ([][]string, error) { + // Does NOT do any fieldtype checking! + // Don't use unless you know your form anatomy inside out! + + // The first row is expected to hold the fieldIDs/fieldNames of the fields to be filled - the only form metadata needed for this usecase. + // The remaining rows are the corresponding data tuples. + // Each row results in one separate PDF form written to outDir. + + // fieldName1 fieldName2 fieldName3 fieldName4 + // John Doe 1.1.2000 male + // Jane Doe 1.1.2000 female + // Jacky Doe 1.1.2000 non-binary + + csvLines, err := csv.NewReader(rd).ReadAll() + if err != nil { + return nil, err + } + + if len(csvLines) < 2 { + return nil, ErrInvalidCSV + } + + fieldNames := csvLines[0] + if len(fieldNames) == 0 { + return nil, ErrInvalidCSV + } + + return csvLines, nil +} + +func multiFillFormCSV(inFilePDF string, rd io.Reader, outDir, fileName string, merge bool, conf *model.Configuration) error { + csvLines, err := parseCSVLines(rd) + if err != nil { + return err + } + + fieldNames := csvLines[0] + var outFiles []string + + for i, formRecord := range csvLines[1:] { + + f, err := os.Open(inFilePDF) + if err != nil { + return err + } + defer f.Close() + + ctx, err := ReadValidateAndOptimize(f, conf) + if err != nil { + return err + } + + fieldMap, imgPageMap, err := form.FieldMap(fieldNames, formRecord) + if err != nil { + return err + } + + ok, pp, err := form.FillForm(ctx, form.FillDetails(nil, fieldMap), imgPageMap, form.CSV) + if err != nil { + return err + } + if !ok { + return ErrNoFormFieldsAffected + } + + if _, _, err := create.UpdatePageTree(ctx, pp, nil); err != nil { + return err + } + + if conf.PostProcessValidate { + if err = ValidateContext(ctx); err != nil { + return err + } + } + + outFile := filepath.Join(outDir, fmt.Sprintf("%s_%02d.pdf", fileName, i+1)) + logWritingTo(outFile) + if err := WriteContextFile(ctx, outFile); err != nil { + return err + } + outFiles = append(outFiles, outFile) + } + + if merge { + if err := mergeForms(outDir, fileName, outFiles, conf); err != nil { + return err + } + } + + return nil +} + +// MultiFillForm populates multiples instances of inFilePDF's form with data from rd and writes the result to outDir. +func MultiFillForm(inFilePDF string, rd io.Reader, outDir, fileName string, format form.DataFormat, merge bool, conf *model.Configuration) error { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.MULTIFILLFORMFIELDS + + fileName = strings.TrimSuffix(filepath.Base(fileName), ".pdf") + + if format == form.JSON { + return multiFillFormJSON(inFilePDF, rd, outDir, fileName, merge, conf) + } + + return multiFillFormCSV(inFilePDF, rd, outDir, fileName, merge, conf) +} + +// MultiFillFormFile populates multiples instances of inFilePDFs form with data from inFileData and writes the result to outDir. +func MultiFillFormFile(inFilePDF, inFileData, outDir, outFilePDF string, merge bool, conf *model.Configuration) (err error) { + format := form.JSON + if strings.HasSuffix(strings.ToLower(inFileData), ".csv") { + format = form.CSV + } + + var f *os.File + + if f, err = os.Open(inFileData); err != nil { + return err + } + + defer func() { + cerr := f.Close() + if err == nil { + err = cerr + } + }() + + s := "JSON" + if format == form.CSV { + s = "CSV" + } + + outFileBase := filepath.Base(outFilePDF) + + if log.CLIEnabled() { + log.CLI.Printf("filling multiple forms via %s based on %s data from %s into %s/%s ...\n", inFilePDF, s, inFileData, outDir, outFileBase) + } + + return MultiFillForm(inFilePDF, f, outDir, outFileBase, format, merge, conf) +} diff --git a/pkg/api/image.go b/pkg/api/image.go new file mode 100644 index 0000000000000000000000000000000000000000..c6386240fae4bb981751f81d0a6f2e42bc22f58d --- /dev/null +++ b/pkg/api/image.go @@ -0,0 +1,51 @@ +/* +Copyright 2021 The pdfcpu Authors. + +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 + + 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 api + +import ( + "io" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pkg/errors" +) + +// Images returns all embedded images of rs. +func Images(rs io.ReadSeeker, selectedPages []string, conf *model.Configuration) ([]map[int]model.Image, error) { + if rs == nil { + return nil, errors.New("pdfcpu: ListImages: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTIMAGES + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return nil, err + } + + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) + if err != nil { + return nil, err + } + + ii, _, err := pdfcpu.Images(ctx, pages) + + return ii, err +} diff --git a/pkg/api/importImage.go b/pkg/api/importImage.go new file mode 100644 index 0000000000000000000000000000000000000000..3932dbf48eb32bb5678cce0ec6ac5da1e3ed28c8 --- /dev/null +++ b/pkg/api/importImage.go @@ -0,0 +1,190 @@ +/* + Copyright 2020 The pdfcpu Authors. + + 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 + + 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 api + +import ( + "bufio" + "io" + "os" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +// Import parses an Import command string into an internal structure. +func Import(s string, u types.DisplayUnit) (*pdfcpu.Import, error) { + return pdfcpu.ParseImportDetails(s, u) +} + +// ImportImages appends PDF pages containing images to rs and writes the result to w. +// If rs == nil a new PDF file will be written to w. +func ImportImages(rs io.ReadSeeker, w io.Writer, imgs []io.Reader, imp *pdfcpu.Import, conf *model.Configuration) error { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.IMPORTIMAGES + + if imp == nil { + imp = pdfcpu.DefaultImportConfig() + } + + var ( + ctx *model.Context + err error + ) + + if rs != nil { + ctx, err = ReadAndValidate(rs, conf) + } else { + ctx, err = pdfcpu.CreateContextWithXRefTable(conf, imp.PageDim) + } + if err != nil { + return err + } + + pagesIndRef, err := ctx.Pages() + if err != nil { + return err + } + + // This is the page tree root. + pagesDict, err := ctx.DereferenceDict(*pagesIndRef) + if err != nil { + return err + } + + for _, r := range imgs { + + indRef, err := pdfcpu.NewPageForImage(ctx.XRefTable, r, pagesIndRef, imp) + if err != nil { + return err + } + + if err := ctx.SetValid(*indRef); err != nil { + return err + } + + if err = model.AppendPageTree(indRef, 1, pagesDict); err != nil { + return err + } + + ctx.PageCount++ + } + + return Write(ctx, w, conf) +} + +func fileExists(filename string) bool { + var ret bool + f, err := os.Open(filename) + if err == nil { + ret = true + } + defer f.Close() + return ret + +} + +func prepImgFiles(imgFiles []string, f1 *os.File) ([]io.ReadCloser, []io.Reader, error) { + rc := make([]io.ReadCloser, len(imgFiles)) + rr := make([]io.Reader, len(imgFiles)) + + for i, fn := range imgFiles { + f, err := os.Open(fn) + if err != nil { + if f1 != nil { + f1.Close() + } + return nil, nil, err + } + rc[i] = f + rr[i] = bufio.NewReader(f) + } + + return rc, rr, nil +} + +func logImportImages(s, outFile string) { + if log.CLIEnabled() { + log.CLI.Printf("%s to %s...\n", s, outFile) + } +} + +// ImportImagesFile appends PDF pages containing images to outFile which will be created if necessary. +func ImportImagesFile(imgFiles []string, outFile string, imp *pdfcpu.Import, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + rs := io.ReadSeeker(nil) + f1 = nil + tmpFile := outFile + if fileExists(outFile) { + if f1, err = os.Open(outFile); err != nil { + return err + } + rs = f1 + tmpFile += ".tmp" + logImportImages("appending", outFile) + } else { + logImportImages("writing", outFile) + } + + rc, rr, err := prepImgFiles(imgFiles, f1) + if err != nil { + return err + } + + if f2, err = os.Create(tmpFile); err != nil { + if f1 != nil { + f1.Close() + } + return err + } + + defer func() { + if err != nil { + f2.Close() + if f1 != nil { + f1.Close() + os.Remove(tmpFile) + } + for _, f := range rc { + f.Close() + } + return + } + if err = f2.Close(); err != nil { + return + } + if f1 != nil { + if err = f1.Close(); err != nil { + return + } + if err = os.Rename(tmpFile, outFile); err != nil { + return + } + } + for _, f := range rc { + if err := f.Close(); err != nil { + return + } + } + }() + + return ImportImages(rs, f2, rr, imp, conf) +} diff --git a/pkg/api/info.go b/pkg/api/info.go new file mode 100644 index 0000000000000000000000000000000000000000..b23862fb67d211958f97d4ec2db513a21a5f737a --- /dev/null +++ b/pkg/api/info.go @@ -0,0 +1,55 @@ +/* + Copyright 2020 The pdfcpu Authors. + + 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 + + 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 api + +import ( + "io" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pkg/errors" +) + +// PDFInfo returns information about rs. +func PDFInfo(rs io.ReadSeeker, fileName string, selectedPages []string, conf *model.Configuration) (*pdfcpu.PDFInfo, error) { + if rs == nil { + return nil, errors.New("pdfcpu: PDFInfo: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.LISTINFO + + ctx, err := ReadAndValidate(rs, conf) + if err != nil { + return nil, err + } + + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, false, true) + if err != nil { + return nil, err + } + + if err := pdfcpu.DetectWatermarks(ctx); err != nil { + return nil, err + } + + return pdfcpu.Info(ctx, fileName, pages) +} diff --git a/pkg/api/keyword.go b/pkg/api/keyword.go new file mode 100644 index 0000000000000000000000000000000000000000..8b76bc8bd61ac138f071dcaf3202770bf7607354 --- /dev/null +++ b/pkg/api/keyword.go @@ -0,0 +1,177 @@ +/* + Copyright 2020 The pdfcpu Authors. + + 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 + + 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 api + +import ( + "io" + "os" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pkg/errors" +) + +// Keywords returns the keywords of rs's info dict. +func Keywords(rs io.ReadSeeker, conf *model.Configuration) ([]string, error) { + if rs == nil { + return nil, errors.New("pdfcpu: ListKeywords: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.LISTKEYWORDS + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return nil, err + } + + return pdfcpu.KeywordsList(ctx) +} + +// AddKeywords adds keywords to rs's infodict and writes the result to w. +func AddKeywords(rs io.ReadSeeker, w io.Writer, files []string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: AddKeywords: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.ADDKEYWORDS + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + if err = pdfcpu.KeywordsAdd(ctx, files); err != nil { + return err + } + + return Write(ctx, w, conf) +} + +// AddKeywordsFile adds keywords to inFile's infodict and writes the result to outFile. +func AddKeywordsFile(inFile, outFile string, files []string, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + } + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return AddKeywords(f1, f2, files, conf) +} + +// RemoveKeywords deletes keywords from rs's infodict and writes the result to w. +func RemoveKeywords(rs io.ReadSeeker, w io.Writer, keywords []string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: RemoveKeywords: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.REMOVEKEYWORDS + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + var ok bool + if ok, err = pdfcpu.KeywordsRemove(ctx, keywords); err != nil { + return err + } + if !ok { + return errors.New("no keyword removed") + } + + return Write(ctx, w, conf) +} + +// RemoveKeywordsFile deletes keywords from inFile's infodict and writes the result to outFile. +func RemoveKeywordsFile(inFile, outFile string, keywords []string, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + } + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return RemoveKeywords(f1, f2, keywords, conf) +} diff --git a/pkg/api/merge.go b/pkg/api/merge.go new file mode 100644 index 0000000000000000000000000000000000000000..53bcdc40841bafe5b9bf41d5dd0473527e92a1f1 --- /dev/null +++ b/pkg/api/merge.go @@ -0,0 +1,313 @@ +/* + Copyright 2020 The pdfcpu Authors. + + 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 + + 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 api + +import ( + "io" + "os" + "path/filepath" + "strconv" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pkg/errors" +) + +// appendTo appends rs to ctxDest's page tree. +func appendTo(rs io.ReadSeeker, fName string, ctxDest *model.Context, dividerPage bool) error { + ctxSource, err := ReadAndValidate(rs, ctxDest.Configuration) + if err != nil { + return err + } + + if ctxDest.Version() < model.V20 && ctxSource.Version() == model.V20 { + return pdfcpu.ErrUnsupportedVersion + } + + // Merge source context into dest context. + return pdfcpu.MergeXRefTables(fName, ctxSource, ctxDest, false, dividerPage) +} + +// MergeRaw merges a sequence of PDF streams and writes the result to w. +func MergeRaw(rsc []io.ReadSeeker, w io.Writer, dividerPage bool, conf *model.Configuration) error { + if rsc == nil { + return errors.New("pdfcpu: MergeRaw: missing rsc") + } + + if w == nil { + return errors.New("pdfcpu: MergeRaw: missing w") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.MERGECREATE + conf.ValidationMode = model.ValidationRelaxed + conf.CreateBookmarks = false + + ctxDest, err := ReadAndValidate(rsc[0], conf) + if err != nil { + return err + } + + ctxDest.EnsureVersionForWriting() + + for i, f := range rsc[1:] { + if err = appendTo(f, strconv.Itoa(i), ctxDest, dividerPage); err != nil { + return err + } + } + + if err = OptimizeContext(ctxDest); err != nil { + return err + } + + return WriteContext(ctxDest, w) +} + +func prepDestContext(destFile string, rs io.ReadSeeker, conf *model.Configuration) (*model.Context, error) { + ctxDest, err := ReadAndValidate(rs, conf) + if err != nil { + return nil, err + } + + if conf.CreateBookmarks { + if err := pdfcpu.EnsureOutlines(ctxDest, filepath.Base(destFile), conf.Cmd == model.MERGEAPPEND); err != nil { + return nil, err + } + } + + if ctxDest.Version() < model.V20 { + ctxDest.EnsureVersionForWriting() + } + + return ctxDest, nil +} + +// Merge concatenates inFiles. +// if destFile is supplied it appends the result to destfile (=MERGEAPPEND) +// if no destFile supplied it writes the result to the first entry of inFiles (=MERGECREATE). +func Merge(destFile string, inFiles []string, w io.Writer, conf *model.Configuration, dividerPage bool) error { + if w == nil { + return errors.New("pdfcpu: Merge: Please provide w") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.MERGECREATE + conf.ValidationMode = model.ValidationRelaxed + + if destFile != "" { + conf.Cmd = model.MERGEAPPEND + } else { + destFile = inFiles[0] + inFiles = inFiles[1:] + } + + f, err := os.Open(destFile) + if err != nil { + return err + } + defer f.Close() + + if conf.Cmd == model.MERGECREATE { + if log.CLIEnabled() { + log.CLI.Println(destFile) + } + } + + ctxDest, err := prepDestContext(destFile, f, conf) + if err != nil { + return err + } + + for _, fName := range inFiles { + if err := func() error { + f, err := os.Open(fName) + if err != nil { + return err + } + defer f.Close() + + if log.CLIEnabled() { + log.CLI.Println(fName) + } + if err = appendTo(f, filepath.Base(fName), ctxDest, dividerPage); err != nil { + return err + } + + return nil + + }(); err != nil { + return err + } + } + + if err := OptimizeContext(ctxDest); err != nil { + return err + } + + return WriteContext(ctxDest, w) +} + +// MergeCreateFile merges inFiles and writes the result to outFile. +func MergeCreateFile(inFiles []string, outFile string, dividerPage bool, conf *model.Configuration) (err error) { + f, err := os.Create(outFile) + if err != nil { + return err + } + + defer func() { + if err != nil { + if err1 := f.Close(); err1 != nil { + return + } + os.Remove(outFile) + return + } + if err = f.Close(); err != nil { + return + } + }() + + logWritingTo(outFile) + return Merge("", inFiles, f, conf, dividerPage) +} + +// MergeAppendFile appends inFiles to outFile. +func MergeAppendFile(inFiles []string, outFile string, dividerPage bool, conf *model.Configuration) (err error) { + tmpFile := outFile + overWrite := false + destFile := "" + + if fileExists(outFile) { + overWrite = true + destFile = outFile + tmpFile += ".tmp" + if log.CLIEnabled() { + log.CLI.Printf("appending to %s...\n", outFile) + } + } else { + logWritingTo(outFile) + } + + f, err := os.Create(tmpFile) + if err != nil { + return err + } + + defer func() { + if err != nil { + if err1 := f.Close(); err1 != nil { + return + } + os.Remove(tmpFile) + return + } + if err = f.Close(); err != nil { + return + } + if overWrite { + err = os.Rename(tmpFile, outFile) + } + }() + + err = Merge(destFile, inFiles, f, conf, dividerPage) + return err +} + +// MergeCreateZip zips rs1 and rs2 into w. +func MergeCreateZip(rs1, rs2 io.ReadSeeker, w io.Writer, conf *model.Configuration) error { + if rs1 == nil { + return errors.New("pdfcpu: MergeCreateZip: missing rs1") + } + if rs2 == nil { + return errors.New("pdfcpu: MergeCreateZip: missing rs2") + } + if w == nil { + return errors.New("pdfcpu: MergeCreateZip: missing w") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.MERGECREATEZIP + conf.ValidationMode = model.ValidationRelaxed + + ctxDest, err := ReadAndValidate(rs1, conf) + if err != nil { + return err + } + if ctxDest.Version() == model.V20 { + return pdfcpu.ErrUnsupportedVersion + } + ctxDest.EnsureVersionForWriting() + + if _, err = pdfcpu.RemoveBookmarks(ctxDest); err != nil { + return err + } + + ctxSrc, err := ReadAndValidate(rs2, conf) + if err != nil { + return err + } + if ctxSrc.Version() == model.V20 { + return pdfcpu.ErrUnsupportedVersion + } + + if err := pdfcpu.MergeXRefTables("", ctxSrc, ctxDest, true, false); err != nil { + return err + } + + if err := OptimizeContext(ctxDest); err != nil { + return err + } + + return WriteContext(ctxDest, w) +} + +// MergeCreateZipFile zips inFile1 and inFile2 into outFile. +func MergeCreateZipFile(inFile1, inFile2, outFile string, conf *model.Configuration) (err error) { + f1, err := os.Open(inFile1) + if err != nil { + return err + } + + f2, err := os.Open(inFile2) + if err != nil { + return err + } + + f, err := os.Create(outFile) + if err != nil { + return err + } + + defer func() { + cerr := f.Close() + if err == nil { + err = cerr + } + }() + + logWritingTo(outFile) + + err = MergeCreateZip(f1, f2, f, conf) + return err +} diff --git a/pkg/api/nup.go b/pkg/api/nup.go new file mode 100644 index 0000000000000000000000000000000000000000..4dfcabf8eca1b6bed84caab667dfdad277138a99 --- /dev/null +++ b/pkg/api/nup.go @@ -0,0 +1,174 @@ +/* + Copyright 2020 The model Authors. + + 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 + + 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 api + +import ( + "io" + "os" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +// PDFNUpConfig returns an NUp configuration for Nup-ing PDF files. +func PDFNUpConfig(val int, desc string, conf *model.Configuration) (*model.NUp, error) { + return pdfcpu.PDFNUpConfig(val, desc, conf) +} + +// ImageNUpConfig returns an NUp configuration for Nup-ing image files. +func ImageNUpConfig(val int, desc string, conf *model.Configuration) (*model.NUp, error) { + return pdfcpu.ImageNUpConfig(val, desc, conf) +} + +// PDFGridConfig returns a grid configuration for Grid-ing PDF files. +func PDFGridConfig(rows, cols int, desc string, conf *model.Configuration) (*model.NUp, error) { + return pdfcpu.PDFGridConfig(rows, cols, desc, conf) +} + +// ImageGridConfig returns a grid configuration for Grid-ing image files. +func ImageGridConfig(rows, cols int, desc string, conf *model.Configuration) (*model.NUp, error) { + return pdfcpu.ImageGridConfig(rows, cols, desc, conf) +} + +// PDFBookletConfig returns an NUp configuration for Booklet-ing PDF files. +func PDFBookletConfig(val int, desc string, conf *model.Configuration) (*model.NUp, error) { + return pdfcpu.PDFBookletConfig(val, desc, conf) +} + +// ImageBookletConfig returns an NUp configuration for Booklet-ing image files. +func ImageBookletConfig(val int, desc string, conf *model.Configuration) (*model.NUp, error) { + return pdfcpu.ImageBookletConfig(val, desc, conf) +} + +// NUpFromImage creates a single page n-up PDF for one image +// or a sequence of n-up pages for more than one image. +func NUpFromImage(conf *model.Configuration, imageFileNames []string, nup *model.NUp) (*model.Context, error) { + if nup.PageDim == nil { + // Set default paper size. + nup.PageDim = types.PaperSize[nup.PageSize] + } + + ctx, err := pdfcpu.CreateContextWithXRefTable(conf, nup.PageDim) + if err != nil { + return nil, err + } + + pagesIndRef, err := ctx.Pages() + if err != nil { + return nil, err + } + + // This is the page tree root. + pagesDict, err := ctx.DereferenceDict(*pagesIndRef) + if err != nil { + return nil, err + } + + if len(imageFileNames) == 1 { + err = pdfcpu.NUpFromOneImage(ctx, imageFileNames[0], nup, pagesDict, pagesIndRef) + } else { + err = pdfcpu.NUpFromMultipleImages(ctx, imageFileNames, nup, pagesDict, pagesIndRef) + } + + return ctx, err +} + +// NUp rearranges PDF pages or images into page grids and writes the result to w. +// Either rs or imgFiles will be used. +func NUp(rs io.ReadSeeker, w io.Writer, imgFiles, selectedPages []string, nup *model.NUp, conf *model.Configuration) error { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.NUP + + if log.InfoEnabled() { + log.Info.Printf("%s", nup) + } + + var ( + ctx *model.Context + err error + ) + + if nup.ImgInputFile { + + if ctx, err = NUpFromImage(conf, imgFiles, nup); err != nil { + return err + } + + } else { + + if ctx, err = ReadAndValidate(rs, conf); err != nil { + return err + } + + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) + if err != nil { + return err + } + + // New pages get added to ctx while old pages get deleted. + // This way we avoid migrating objects between contexts. + if err = pdfcpu.NUpFromPDF(ctx, pages, nup); err != nil { + return err + } + + } + + return Write(ctx, w, conf) +} + +// NUpFile rearranges PDF pages or images into page grids and writes the result to outFile. +func NUpFile(inFiles []string, outFile string, selectedPages []string, nup *model.NUp, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if !nup.ImgInputFile { + // Nup from a PDF page. + if f1, err = os.Open(inFiles[0]); err != nil { + return err + } + } + + if f2, err = os.Create(outFile); err != nil { + if f1 != nil { + f1.Close() + } + return err + } + logWritingTo(outFile) + + defer func() { + if err != nil { + f2.Close() + if f1 != nil { + f1.Close() + } + os.Remove(outFile) + return + } + if err = f2.Close(); err != nil { + return + } + if f1 != nil { + err = f1.Close() + } + }() + + return NUp(f1, f2, inFiles, selectedPages, nup, conf) +} diff --git a/pkg/api/optimize.go b/pkg/api/optimize.go new file mode 100644 index 0000000000000000000000000000000000000000..799ab0ee44e2fa2a2d3bdafc5d02efb6f7897522 --- /dev/null +++ b/pkg/api/optimize.go @@ -0,0 +1,109 @@ +/* + Copyright 2020 The pdfcpu Authors. + + 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 + + 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 api + +import ( + "io" + "os" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pkg/errors" +) + +// Optimize reads a PDF stream from rs and writes the optimized PDF stream to w. +func Optimize(rs io.ReadSeeker, w io.Writer, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: Optimize: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + if log.StatsEnabled() { + log.Stats.Printf("XRefTable:\n%s\n", ctx) + } + + if err = WriteContext(ctx, w); err != nil { + return err + } + + // For Optimize only. + if ctx.StatsFileName != "" { + err = pdfcpu.AppendStatsFile(ctx) + if err != nil { + return errors.Wrap(err, "Write stats failed.") + } + } + + return nil +} + +// OptimizeFile reads inFile and writes the optimized PDF to outFile. +// If outFile is not provided then inFile gets overwritten +// which leads to the same result as when inFile equals outFile. +func OptimizeFile(inFile, outFile string, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + logWritingTo(outFile) + } else { + logWritingTo(inFile) + } + + if f2, err = os.Create(tmpFile); err != nil { + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.OPTIMIZE + + return Optimize(f1, f2, conf) +} diff --git a/pkg/api/page.go b/pkg/api/page.go new file mode 100644 index 0000000000000000000000000000000000000000..cf87ca67d955a5c735e9e7b057c6db5a96c02fc2 --- /dev/null +++ b/pkg/api/page.go @@ -0,0 +1,250 @@ +/* + Copyright 2020 The pdfcpu Authors. + + 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 + + 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 api + +import ( + "io" + "os" + "sort" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// InsertPages inserts a blank page before or after every page selected of rs and writes the result to w. +func InsertPages(rs io.ReadSeeker, w io.Writer, selectedPages []string, before bool, pageConf *pdfcpu.PageConfiguration, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: InsertPages: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.INSERTPAGESAFTER + if before { + conf.Cmd = model.INSERTPAGESBEFORE + } + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) + if err != nil { + return err + } + + var dim *types.Dim + if pageConf != nil { + dim = pageConf.PageDim + } + + if err = ctx.InsertBlankPages(pages, dim, before); err != nil { + return err + } + + return Write(ctx, w, conf) +} + +// InsertPagesFile inserts a blank page before or after every inFile page selected and writes the result to w. +func InsertPagesFile(inFile, outFile string, selectedPages []string, before bool, pageConf *pdfcpu.PageConfiguration, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + logWritingTo(outFile) + } else { + logWritingTo(inFile) + } + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return InsertPages(f1, f2, selectedPages, before, pageConf, conf) +} + +// RemovePages removes selected pages from rs and writes the result to w. +func RemovePages(rs io.ReadSeeker, w io.Writer, selectedPages []string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: RemovePages: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.REMOVEPAGES + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + pages, err := RemainingPagesForPageRemoval(ctx.PageCount, selectedPages, true) + if err != nil { + return err + } + + if len(pages) == 0 { + if log.CLIEnabled() { + log.CLI.Println("aborted: missing page numbers!") + } + return nil + } + + var pageNrs []int + for k, v := range pages { + if v { + pageNrs = append(pageNrs, k) + } + } + sort.Ints(pageNrs) + + ctxDest, err := pdfcpu.ExtractPages(ctx, pageNrs, false) + if err != nil { + return err + } + + return Write(ctxDest, w, conf) +} + +// RemovePagesFile removes selected inFile pages and writes the result to outFile.. +func RemovePagesFile(inFile, outFile string, selectedPages []string, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + logWritingTo(outFile) + } else { + logWritingTo(inFile) + } + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return RemovePages(f1, f2, selectedPages, conf) +} + +// PageCount returns rs's page count. +func PageCount(rs io.ReadSeeker, conf *model.Configuration) (int, error) { + if rs == nil { + return 0, errors.New("pdfcpu: PageCount: missing rs") + } + + ctx, err := ReadAndValidate(rs, conf) + if err != nil { + return 0, err + } + + return ctx.PageCount, nil +} + +// PageCountFile returns inFile's page count. +func PageCountFile(inFile string) (int, error) { + f, err := os.Open(inFile) + if err != nil { + return 0, err + } + defer f.Close() + + return PageCount(f, model.NewDefaultConfiguration()) +} + +// PageDims returns a sorted slice of mediaBox dimensions for rs. +func PageDims(rs io.ReadSeeker, conf *model.Configuration) ([]types.Dim, error) { + if rs == nil { + return nil, errors.New("pdfcpu: PageDims: missing rs") + } + + ctx, err := ReadAndValidate(rs, conf) + if err != nil { + return nil, err + } + + pd, err := ctx.PageDims() + if err != nil { + return nil, err + } + + if len(pd) != ctx.PageCount { + return nil, errors.New("pdfcpu: corrupt page dimensions") + } + + return pd, nil +} + +// PageDimsFile returns a sorted slice of mediaBox dimensions for inFile. +func PageDimsFile(inFile string) ([]types.Dim, error) { + f, err := os.Open(inFile) + if err != nil { + return nil, err + } + defer f.Close() + + return PageDims(f, model.NewDefaultConfiguration()) +} diff --git a/pkg/api/pageLayout.go b/pkg/api/pageLayout.go new file mode 100644 index 0000000000000000000000000000000000000000..e7f143dfd2e56bd6d594ab2784cdcb7c0e3e5916 --- /dev/null +++ b/pkg/api/pageLayout.go @@ -0,0 +1,216 @@ +/* + Copyright 2023 The pdfcpu Authors. + + 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 + + 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 api + +import ( + "io" + "os" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// PageLayout returns rs's page layout. +func PageLayout(rs io.ReadSeeker, conf *model.Configuration) (*model.PageLayout, error) { + if rs == nil { + return nil, errors.New("pdfcpu: PageLayout: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.LISTPAGELAYOUT + + ctx, err := ReadAndValidate(rs, conf) + if err != nil { + return nil, err + } + + return ctx.PageLayout, nil +} + +// PageLayoutFile returns inFile's page layout. +func PageLayoutFile(inFile string, conf *model.Configuration) (*model.PageLayout, error) { + f, err := os.Open(inFile) + if err != nil { + return nil, err + } + defer f.Close() + + return PageLayout(f, conf) +} + +// ListPageLayout lists rs's page layout. +func ListPageLayout(rs io.ReadSeeker, conf *model.Configuration) ([]string, error) { + if rs == nil { + return nil, errors.New("pdfcpu: ListPageLayout: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.LISTPAGELAYOUT + + ctx, err := ReadAndValidate(rs, conf) + if err != nil { + return nil, err + } + + if ctx.PageLayout != nil { + return []string{ctx.PageLayout.String()}, nil + } + + return []string{"No page layout set, PDF viewers will default to \"SinglePage\""}, nil +} + +// ListPageLayoutFile lists inFile's page layout. +func ListPageLayoutFile(inFile string, conf *model.Configuration) ([]string, error) { + f, err := os.Open(inFile) + if err != nil { + return nil, err + } + defer f.Close() + + return ListPageLayout(f, conf) +} + +// SetPageLayout sets rs's page layout and writes the result to w. +func SetPageLayout(rs io.ReadSeeker, w io.Writer, val model.PageLayout, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: SetPageLayout: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.SETPAGELAYOUT + + ctx, err := ReadAndValidate(rs, conf) + if err != nil { + return err + } + + ctx.RootDict["PageLayout"] = types.Name(val.String()) + + return Write(ctx, w, conf) +} + +// SetPageLayoutFile sets inFile's page layout and writes the result to outFile. +func SetPageLayoutFile(inFile, outFile string, val model.PageLayout, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + } + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return SetPageLayout(f1, f2, val, conf) +} + +// ResetPageLayout resets rs's page layout and writes the result to w. +func ResetPageLayout(rs io.ReadSeeker, w io.Writer, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: ResetPageLayout: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.RESETPAGELAYOUT + + ctx, err := ReadAndValidate(rs, conf) + if err != nil { + return err + } + + delete(ctx.RootDict, "PageLayout") + + return Write(ctx, w, conf) +} + +// ResetPageLayoutFile resets inFile's page layout and writes the result to outFile. +func ResetPageLayoutFile(inFile, outFile string, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + } + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return ResetPageLayout(f1, f2, conf) +} diff --git a/pkg/api/pageMode.go b/pkg/api/pageMode.go new file mode 100644 index 0000000000000000000000000000000000000000..1ae43b3537f438b083096c5a49ed68fd88ee8132 --- /dev/null +++ b/pkg/api/pageMode.go @@ -0,0 +1,216 @@ +/* + Copyright 2023 The pdfcpu Authors. + + 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 + + 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 api + +import ( + "io" + "os" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// PageMode returns rs's page mode. +func PageMode(rs io.ReadSeeker, conf *model.Configuration) (*model.PageMode, error) { + if rs == nil { + return nil, errors.New("pdfcpu: PageMode: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.LISTPAGEMODE + + ctx, err := ReadAndValidate(rs, conf) + if err != nil { + return nil, err + } + + return ctx.PageMode, nil +} + +// PageModeFile returns inFile's page mode. +func PageModeFile(inFile string, conf *model.Configuration) (*model.PageMode, error) { + f, err := os.Open(inFile) + if err != nil { + return nil, err + } + defer f.Close() + + return PageMode(f, conf) +} + +// ListPageMode lists rs's page mode. +func ListPageMode(rs io.ReadSeeker, conf *model.Configuration) ([]string, error) { + if rs == nil { + return nil, errors.New("pdfcpu: ListPageMode: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.LISTPAGEMODE + + ctx, err := ReadAndValidate(rs, conf) + if err != nil { + return nil, err + } + + if ctx.PageMode != nil { + return []string{ctx.PageMode.String()}, nil + } + + return []string{"No page mode set, PDF viewers will default to \"UseNone\""}, nil +} + +// ListPageModeFile lists inFile's page mode. +func ListPageModeFile(inFile string, conf *model.Configuration) ([]string, error) { + f, err := os.Open(inFile) + if err != nil { + return nil, err + } + defer f.Close() + + return ListPageMode(f, conf) +} + +// SetPageMode sets rs's page mode and writes the result to w. +func SetPageMode(rs io.ReadSeeker, w io.Writer, val model.PageMode, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: SetPageMode: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.SETPAGEMODE + + ctx, err := ReadAndValidate(rs, conf) + if err != nil { + return err + } + + ctx.RootDict["PageMode"] = types.Name(val.String()) + + return Write(ctx, w, conf) +} + +// SetPageModeFile sets inFile's page mode and writes the result to outFile. +func SetPageModeFile(inFile, outFile string, val model.PageMode, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + } + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return SetPageMode(f1, f2, val, conf) +} + +// ResetPageMode resets rs's page mode and writes the result to w. +func ResetPageMode(rs io.ReadSeeker, w io.Writer, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: ResetPageMode: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.RESETPAGEMODE + + ctx, err := ReadAndValidate(rs, conf) + if err != nil { + return err + } + + delete(ctx.RootDict, "PageMode") + + return Write(ctx, w, conf) +} + +// ResetPageModeFile resets inFile's page mode and writes the result to outFile. +func ResetPageModeFile(inFile, outFile string, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + } + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return ResetPageMode(f1, f2, conf) +} diff --git a/pkg/api/permission.go b/pkg/api/permission.go new file mode 100644 index 0000000000000000000000000000000000000000..572db8b78fb5e1db3cdd9ba4e318930a0d0fe552 --- /dev/null +++ b/pkg/api/permission.go @@ -0,0 +1,152 @@ +/* + Copyright 2020 The pdfcpu Authors. + + 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 + + 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 api + +import ( + "io" + "os" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pkg/errors" +) + +// Permissions returns user access permissions for rs. +func Permissions(rs io.ReadSeeker, conf *model.Configuration) (int, error) { + if rs == nil { + return 0, errors.New("pdfcpu: Permissions: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTPERMISSIONS + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return 0, err + } + + p := 0 + if ctx.E != nil { + p = ctx.E.P + } + + return p, nil +} + +// SetPermissions sets user access permissions. +// inFile has to be encrypted. +// A configuration containing the current passwords is required. +func SetPermissions(rs io.ReadSeeker, w io.Writer, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: SetPermissions: missing rs") + } + + if conf == nil { + return errors.New("pdfcpu: missing configuration for setting permissions") + } + conf.Cmd = model.SETPERMISSIONS + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + return WriteContext(ctx, w) +} + +// SetPermissionsFile sets inFile's user access permissions. +// inFile has to be encrypted. +// A configuration containing the current passwords is required. +func SetPermissionsFile(inFile, outFile string, conf *model.Configuration) (err error) { + if conf == nil { + return errors.New("pdfcpu: missing configuration for setting permissions") + } + + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + logWritingTo(outFile) + } else { + logWritingTo(inFile) + } + if f2, err = os.Create(tmpFile); err != nil { + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return SetPermissions(f1, f2, conf) +} + +// GetPermissions returns the permissions for rs. +func GetPermissions(rs io.ReadSeeker, conf *model.Configuration) (*int16, error) { + if rs == nil { + return nil, errors.New("pdfcpu: GetPermissions: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + // No cmd available. + + ctx, err := ReadAndValidate(rs, conf) + if err != nil { + return nil, err + } + + if ctx.E == nil { + // Full access - permissions don't apply. + return nil, nil + } + p := int16(ctx.E.P) + + return &p, nil +} + +// GetPermissionsFile returns the permissions for inFile. +func GetPermissionsFile(inFile string, conf *model.Configuration) (*int16, error) { + f, err := os.Open(inFile) + if err != nil { + return nil, err + } + defer f.Close() + + return GetPermissions(f, conf) +} diff --git a/pkg/api/property.go b/pkg/api/property.go new file mode 100644 index 0000000000000000000000000000000000000000..38767488994287644516b5af43c97286c4000988 --- /dev/null +++ b/pkg/api/property.go @@ -0,0 +1,176 @@ +/* + Copyright 2020 The pdfcpu Authors. + + 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 + + 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 api + +import ( + "io" + "os" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pkg/errors" +) + +// Properties returns rs's properties as recorded in infoDict. +func Properties(rs io.ReadSeeker, conf *model.Configuration) (map[string]string, error) { + if rs == nil { + return nil, errors.New("pdfcpu: ListProperties: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.LISTPROPERTIES + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return nil, err + } + + return ctx.Properties, nil +} + +// AddProperties adds properties to rs's infodict and writes the result to w. +func AddProperties(rs io.ReadSeeker, w io.Writer, properties map[string]string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: AddProperties: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.ADDPROPERTIES + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + if err = pdfcpu.PropertiesAdd(ctx, properties); err != nil { + return err + } + + return Write(ctx, w, conf) +} + +// AddPropertiesFile adds properties to inFile's infodict and writes the result to outFile. +func AddPropertiesFile(inFile, outFile string, properties map[string]string, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + } + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return AddProperties(f1, f2, properties, conf) +} + +// RemoveProperties deletes properties from rs's infodict and writes the result to w. +func RemoveProperties(rs io.ReadSeeker, w io.Writer, properties []string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: RemoveProperties: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.REMOVEPROPERTIES + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + var ok bool + if ok, err = pdfcpu.PropertiesRemove(ctx, properties); err != nil { + return err + } + if !ok { + return errors.New("no property removed") + } + + return Write(ctx, w, conf) +} + +// RemovePropertiesFile deletes properties from inFile's infodict and writes the result to outFile. +func RemovePropertiesFile(inFile, outFile string, properties []string, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + } + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return RemoveProperties(f1, f2, properties, conf) +} diff --git a/pkg/api/resize.go b/pkg/api/resize.go new file mode 100644 index 0000000000000000000000000000000000000000..0eeff5e8682eed3f385e0a634d48895bc128ab3a --- /dev/null +++ b/pkg/api/resize.go @@ -0,0 +1,108 @@ +/* +Copyright 2023 The pdfcpu Authors. + +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 + + 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 api + +import ( + "io" + "os" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pkg/errors" +) + +// Resize applies resizeConf for selected pages of rs and writes result to w. +func Resize(rs io.ReadSeeker, w io.Writer, selectedPages []string, resize *model.Resize, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: Resize: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.RESIZE + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) + if err != nil { + return err + } + + if err = pdfcpu.Resize(ctx, pages, resize); err != nil { + return err + } + + return Write(ctx, w, conf) +} + +// ResizeFile applies resizeConf for selected pages of inFile and writes result to outFile. +func ResizeFile(inFile, outFile string, selectedPages []string, resize *model.Resize, conf *model.Configuration) (err error) { + if log.CLIEnabled() { + log.CLI.Printf("resizing %s\n", inFile) + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + logWritingTo(outFile) + } else { + logWritingTo(inFile) + } + + var ( + f1, f2 *os.File + ) + + if f1, err = os.Open(inFile); err != nil { + return err + } + + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.RESIZE + + return Resize(f1, f2, selectedPages, resize, conf) +} diff --git a/pkg/api/rotate.go b/pkg/api/rotate.go new file mode 100644 index 0000000000000000000000000000000000000000..55a5382809cd02e68b3563bb641261eab618edd1 --- /dev/null +++ b/pkg/api/rotate.go @@ -0,0 +1,95 @@ +/* + Copyright 2020 The pdfcpu Authors. + + 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 + + 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 api + +import ( + "io" + "os" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pkg/errors" +) + +// Rotate rotates selected pages of rs clockwise by rotation degrees and writes the result to w. +func Rotate(rs io.ReadSeeker, w io.Writer, rotation int, selectedPages []string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: Rotate: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.ROTATE + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) + if err != nil { + return err + } + + if err = pdfcpu.RotatePages(ctx, pages, rotation); err != nil { + return err + } + + return Write(ctx, w, conf) +} + +// RotateFile rotates selected pages of inFile clockwise by rotation degrees and writes the result to outFile. +func RotateFile(inFile, outFile string, rotation int, selectedPages []string, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + logWritingTo(outFile) + } else { + logWritingTo(inFile) + } + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return Rotate(f1, f2, rotation, selectedPages, conf) +} diff --git a/pkg/api/selectPages.go b/pkg/api/selectPages.go new file mode 100644 index 0000000000000000000000000000000000000000..aaf84528f25f4a7fef3994256684a42ed6c432e9 --- /dev/null +++ b/pkg/api/selectPages.go @@ -0,0 +1,687 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 api + +import ( + "fmt" + "regexp" + "sort" + "strconv" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +var ( + selectedPagesRegExp *regexp.Regexp +) + +func setupRegExpForPageSelection() *regexp.Regexp { + e := "(\\d+)?-l(-\\d+)?|l(-(\\d+)-?)?" + e = "[!n]?((-\\d+)|(\\d+(-(\\d+)?)?)|" + e + ")" + e = "\\Qeven\\E|\\Qodd\\E|" + e + exp := "^" + e + "(," + e + ")*$" + re, _ := regexp.Compile(exp) + return re +} + +func init() { + selectedPagesRegExp = setupRegExpForPageSelection() +} + +// ParsePageSelection ensures a correct page selection expression. +func ParsePageSelection(s string) ([]string, error) { + if s == "" { + return nil, nil + } + + // Ensure valid comma separated expression of:{ {even|odd}{!}{-}# | {even|odd}{!}#-{#} }* + // + // Negated expressions: + // '!' negates an expression + // since '!' needs to be part of a single quoted string in bash + // as an alternative also 'n' works instead of "!" + // + // Extract all but page 4 may be expressed as: "1-,!4" or "1-,n4" + // + // The pageSelection is evaluated strictly from left to right! + // e.g. "!3,1-5" extracts pages 1-5 whereas "1-5,!3" extracts pages 1,2,4,5 + // + + if !selectedPagesRegExp.MatchString(s) { + return nil, errors.Errorf("-pages \"%s\" => syntax error\n", s) + } + + //log.CLI.Printf("pageSelection: %s\n", s) + + return strings.Split(s, ","), nil +} + +func handlePrefix(v string, negated bool, pageCount int, selectedPages types.IntSet) error { + // -l + if v == "l" { + for j := 1; j <= pageCount; j++ { + selectedPages[j] = !negated + } + return nil + } + + // -l-# + if strings.HasPrefix(v, "l-") { + i, err := strconv.Atoi(v[2:]) + if err != nil { + return err + } + if pageCount-i < 1 { + return nil + } + for j := 1; j <= pageCount-i; j++ { + selectedPages[j] = !negated + } + return nil + } + + // -# + i, err := strconv.Atoi(v) + if err != nil { + return err + } + + // Handle overflow gracefully + if i > pageCount { + i = pageCount + } + + // identified + // -# ... select all pages up to and including # + // or !-# ... deselect all pages up to and including # + for j := 1; j <= i; j++ { + selectedPages[j] = !negated + } + + return nil +} + +func handleSuffix(v string, negated bool, pageCount int, selectedPages types.IntSet) error { + // must be #- ... select all pages from here until the end. + // or !#- ... deselect all pages from here until the end. + + i, err := strconv.Atoi(v) + if err != nil { + return err + } + + // Handle overflow gracefully + if i > pageCount { + return nil + } + + for j := i; j <= pageCount; j++ { + selectedPages[j] = !negated + } + + return nil +} + +func handleSpecificPageOrLastXPages(s string, negated bool, pageCount int, selectedPages types.IntSet) error { + // l + if s == "l" { + selectedPages[pageCount] = !negated + return nil + } + + // l-# + if strings.HasPrefix(s, "l-") { + pr := strings.Split(s[2:], "-") + i, err := strconv.Atoi(pr[0]) + if err != nil { + return err + } + if pageCount-i < 1 { + return nil + } + j := pageCount - i + + // l-#- + if strings.HasSuffix(s, "-") { + j = pageCount + } + for i := pageCount - i; i <= j; i++ { + selectedPages[i] = !negated + } + return nil + } + + // must be # ... select a specific page + // or !# ... deselect a specific page + i, err := strconv.Atoi(s) + if err != nil { + return err + } + + // Handle overflow gracefully + if i > pageCount { + return nil + } + + selectedPages[i] = !negated + + return nil +} + +func negation(c byte) bool { + return c == '!' || c == 'n' +} + +func selectEvenPages(selectedPages types.IntSet, pageCount int) { + for i := 2; i <= pageCount; i += 2 { + _, found := selectedPages[i] + if !found { + selectedPages[i] = true + } + } +} + +func selectOddPages(selectedPages types.IntSet, pageCount int) { + for i := 1; i <= pageCount; i += 2 { + _, found := selectedPages[i] + if !found { + selectedPages[i] = true + } + } +} + +func parsePageRange(pr []string, pageCount int, negated bool, selectedPages types.IntSet) error { + from, err := strconv.Atoi(pr[0]) + if err != nil { + return err + } + + // Handle overflow gracefully + if from > pageCount { + return nil + } + + var thru int + if pr[1] == "l" { + // #-l + thru = pageCount + if len(pr) == 3 { + // #-l-# + i, err := strconv.Atoi(pr[2]) + if err != nil { + return err + } + thru -= i + } + } else { + // #-# + var err error + thru, err = strconv.Atoi(pr[1]) + if err != nil { + return err + } + } + + // Handle overflow gracefully + if thru < from { + return nil + } + + if thru > pageCount { + thru = pageCount + } + + for i := from; i <= thru; i++ { + selectedPages[i] = !negated + } + + return nil +} + +func sortedPages(selectedPages types.IntSet) []int { + p := []int(nil) + for i, v := range selectedPages { + if v { + p = append(p, i) + } + } + sort.Ints(p) + return p +} + +func logSelPages(selectedPages types.IntSet) { + if !log.CLIEnabled() || len(selectedPages) == 0 { + return + } + var b strings.Builder + for _, i := range sortedPages(selectedPages) { + fmt.Fprintf(&b, "%d,", i) + } + s := b.String() + if len(s) > 1 { + s = s[:len(s)-1] + } + // TODO Suppress for multifile cmds + if log.CLIEnabled() { + log.CLI.Printf("pages: %s\n", s) + } +} + +func calcSelPages(pageCount int, pageSelection []string, selectedPages types.IntSet) error { + for _, v := range pageSelection { + + //log.Stats.Printf("pageExp: <%s>\n", v) + + if v == "even" { + selectEvenPages(selectedPages, pageCount) + continue + } + + if v == "odd" { + selectOddPages(selectedPages, pageCount) + continue + } + + var negated bool + if negation(v[0]) { + negated = true + //logInfoAPI.Printf("is a negated exp\n") + v = v[1:] + } + + // -# + if v[0] == '-' { + + v = v[1:] + + if err := handlePrefix(v, negated, pageCount, selectedPages); err != nil { + return err + } + + continue + } + + // #- + if v[0] != 'l' && strings.HasSuffix(v, "-") { + + if err := handleSuffix(v[:len(v)-1], negated, pageCount, selectedPages); err != nil { + return err + } + + continue + } + + // l l-# l-#- + if v[0] == 'l' { + if err := handleSpecificPageOrLastXPages(v, negated, pageCount, selectedPages); err != nil { + return err + } + continue + } + + pr := strings.Split(v, "-") + if len(pr) >= 2 { + // v contains '-' somewhere in the middle + // #-# #-l #-l-# + if err := parsePageRange(pr, pageCount, negated, selectedPages); err != nil { + return err + } + + continue + } + + // # + if err := handleSpecificPageOrLastXPages(pr[0], negated, pageCount, selectedPages); err != nil { + return err + } + + } + + return nil +} + +// selectedPages returns a set of used page numbers. +// key==page# => key 0 unused! +func selectedPages(pageCount int, pageSelection []string, log bool) (types.IntSet, error) { + selectedPages := types.IntSet{} + + if err := calcSelPages(pageCount, pageSelection, selectedPages); err != nil { + return nil, err + } + + if log { + logSelPages(selectedPages) + } + + return selectedPages, nil +} + +// PagesForPageSelection ensures a set of page numbers for an ascending page sequence +// where each page number may appear only once. +func PagesForPageSelection(pageCount int, pageSelection []string, ensureAllforNone bool, log bool) (types.IntSet, error) { + if len(pageSelection) > 0 { + return selectedPages(pageCount, pageSelection, log) + } + if !ensureAllforNone { + //log.CLI.Printf("pages: none\n") + return nil, nil + } + m := types.IntSet{} + for i := 1; i <= pageCount; i++ { + m[i] = true + } + //log.CLI.Printf("pages: all\n") + return m, nil +} + +func RemainingPagesForPageRemoval(pageCount int, pageSelection []string, log bool) (types.IntSet, error) { + pagesToRemove, err := selectedPages(pageCount, pageSelection, log) + if err != nil { + return nil, err + } + + m := types.IntSet{} + for i := 1; i <= pageCount; i++ { + m[i] = true + } + + for k, v := range pagesToRemove { + if v { + m[k] = false + } + } + + return m, nil +} + +func deletePageFromCollection(cp *[]int, p int) { + a := []int{} + for _, i := range *cp { + if i != p { + a = append(a, i) + } + } + *cp = a +} + +func processPageForCollection(cp *[]int, negated bool, i int) { + if !negated { + *cp = append(*cp, i) + } else { + deletePageFromCollection(cp, i) + } +} + +func collectEvenPages(cp *[]int, pageCount int) { + for i := 2; i <= pageCount; i += 2 { + *cp = append(*cp, i) + } +} + +func collectOddPages(cp *[]int, pageCount int) { + for i := 1; i <= pageCount; i += 2 { + *cp = append(*cp, i) + } +} + +func handlePrefixForCollection(v string, negated bool, pageCount int, cp *[]int) error { + // -l + if v == "l" { + for j := 1; j <= pageCount; j++ { + processPageForCollection(cp, negated, j) + } + return nil + } + + // -l-# + if strings.HasPrefix(v, "l-") { + i, err := strconv.Atoi(v[2:]) + if err != nil { + return err + } + if pageCount-i < 1 { + return nil + } + for j := 1; j <= pageCount-i; j++ { + processPageForCollection(cp, negated, j) + } + return nil + } + + // -# + i, err := strconv.Atoi(v) + if err != nil { + return err + } + + // Handle overflow gracefully + if i > pageCount { + i = pageCount + } + + // identified + // -# ... select all pages up to and including # + // or !-# ... deselect all pages up to and including # + for j := 1; j <= i; j++ { + processPageForCollection(cp, negated, j) + } + + return nil +} + +func handleSuffixForCollection(v string, negated bool, pageCount int, cp *[]int) error { + // must be #- ... select all pages from here until the end. + // or !#- ... deselect all pages from here until the end. + + i, err := strconv.Atoi(v) + if err != nil { + return err + } + + // Handle overflow gracefully + if i > pageCount { + return nil + } + + for j := i; j <= pageCount; j++ { + processPageForCollection(cp, negated, j) + } + + return nil +} + +func handleSpecificPageOrLastXPagesForCollection(s string, negated bool, pageCount int, cp *[]int) error { + // l + if s == "l" { + processPageForCollection(cp, negated, pageCount) + return nil + } + + // l-# + if strings.HasPrefix(s, "l-") { + pr := strings.Split(s[2:], "-") + i, err := strconv.Atoi(pr[0]) + if err != nil { + return err + } + if pageCount-i < 1 { + return nil + } + j := pageCount - i + + // l-#- + if strings.HasSuffix(s, "-") { + j = pageCount + } + for i := pageCount - i; i <= j; i++ { + processPageForCollection(cp, negated, i) + } + return nil + } + + // must be # ... select a specific page + // or !# ... deselect a specific page + i, err := strconv.Atoi(s) + if err != nil { + return err + } + + // Handle overflow gracefully + if i > pageCount { + return nil + } + + processPageForCollection(cp, negated, i) + + return nil +} + +func parsePageRangeForCollection(pr []string, pageCount int, negated bool, cp *[]int) error { + from, err := strconv.Atoi(pr[0]) + if err != nil { + return err + } + + // Handle overflow gracefully + if from > pageCount { + return nil + } + + var thru int + if pr[1] == "l" { + // #-l + thru = pageCount + if len(pr) == 3 { + // #-l-# + i, err := strconv.Atoi(pr[2]) + if err != nil { + return err + } + thru -= i + } + } else { + // #-# + var err error + thru, err = strconv.Atoi(pr[1]) + if err != nil { + return err + } + } + + // Handle overflow gracefully + if thru < from { + return nil + } + + if thru > pageCount { + thru = pageCount + } + + for i := from; i <= thru; i++ { + processPageForCollection(cp, negated, i) + } + + return nil +} + +// PagesForPageCollection returns a slice of page numbers for a page collection. +// Any page number in any order any number of times allowed. +func PagesForPageCollection(pageCount int, pageSelection []string) ([]int, error) { + collectedPages := []int{} + for _, v := range pageSelection { + + if v == "even" { + collectEvenPages(&collectedPages, pageCount) + continue + } + + if v == "odd" { + collectOddPages(&collectedPages, pageCount) + continue + } + + var negated bool + if negation(v[0]) { + negated = true + //logInfoAPI.Printf("is a negated exp\n") + v = v[1:] + } + + // -# + if v[0] == '-' { + + v = v[1:] + + if err := handlePrefixForCollection(v, negated, pageCount, &collectedPages); err != nil { + return nil, err + } + + continue + } + + // #- + if v[0] != 'l' && strings.HasSuffix(v, "-") { + + if err := handleSuffixForCollection(v[:len(v)-1], negated, pageCount, &collectedPages); err != nil { + return nil, err + } + + continue + } + + // l l-# l-#- + if v[0] == 'l' { + if err := handleSpecificPageOrLastXPagesForCollection(v, negated, pageCount, &collectedPages); err != nil { + return nil, err + } + continue + } + + pr := strings.Split(v, "-") + if len(pr) >= 2 { + // v contains '-' somewhere in the middle + // #-# #-l #-l-# + if err := parsePageRangeForCollection(pr, pageCount, negated, &collectedPages); err != nil { + return nil, err + } + + continue + } + + // # + if err := handleSpecificPageOrLastXPagesForCollection(pr[0], negated, pageCount, &collectedPages); err != nil { + return nil, err + } + } + if len(collectedPages) == 0 { + return nil, errors.Errorf("pdfcpu: no page selected") + } + + return collectedPages, nil +} + +// PagesForPageRange returns a slice of page numbers for a page range. +func PagesForPageRange(from, thru int) []int { + s := make([]int, thru-from+1) + for i := 0; i < len(s); i++ { + s[i] = from + i + } + return s +} diff --git a/pkg/api/split.go b/pkg/api/split.go new file mode 100644 index 0000000000000000000000000000000000000000..15fe26aa76e972889aae7a72bbe98561fc1f369c --- /dev/null +++ b/pkg/api/split.go @@ -0,0 +1,319 @@ +/* + Copyright 2020 The pdfcpu Authors. + + 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 + + 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 api + +import ( + "bytes" + "io" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pkg/errors" +) + +type PageSpan struct { + From int + Thru int + Reader io.Reader +} + +func pageSpan(ctx *model.Context, from, thru int) (*PageSpan, error) { + ctxNew, err := pdfcpu.ExtractPages(ctx, PagesForPageRange(from, thru), false) + if err != nil { + return nil, err + } + + var b bytes.Buffer + if err := WriteContext(ctxNew, &b); err != nil { + return nil, err + } + + return &PageSpan{From: from, Thru: thru, Reader: &b}, nil +} + +func spanFileName(fileName string, from, thru int) string { + baseFileName := filepath.Base(fileName) + fn := strings.TrimSuffix(baseFileName, ".pdf") + fn = fn + "_" + strconv.Itoa(from) + if from == thru { + return fn + ".pdf" + } + return fn + "-" + strconv.Itoa(thru) + ".pdf" +} + +func splitOutPath(outDir, fileName string, forBookmark bool, from, thru int) string { + p := filepath.Join(outDir, fileName+".pdf") + if !forBookmark { + p = filepath.Join(outDir, spanFileName(fileName, from, thru)) + } + return p +} + +func writePageSpan(ctx *model.Context, from, thru int, outPath string) error { + ps, err := pageSpan(ctx, from, thru) + if err != nil { + return err + } + logWritingTo(outPath) + return pdfcpu.WriteReader(outPath, ps.Reader) +} + +func context(rs io.ReadSeeker, conf *model.Configuration) (*model.Context, error) { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.SPLIT + + return ReadValidateAndOptimize(rs, conf) +} + +func pageSpansSplitAlongBookmarks(ctx *model.Context) ([]*PageSpan, error) { + pss := []*PageSpan{} + + bms, err := pdfcpu.Bookmarks(ctx) + if err != nil { + return nil, err + } + + for _, bm := range bms { + + from, thru := bm.PageFrom, bm.PageThru + if thru == 0 { + thru = ctx.PageCount + } + + ps, err := pageSpan(ctx, from, thru) + if err != nil { + return nil, err + } + pss = append(pss, ps) + + } + + return pss, nil +} + +func pageSpans(ctx *model.Context, span int) ([]*PageSpan, error) { + pss := []*PageSpan{} + + for i := 0; i < ctx.PageCount/span; i++ { + start := i * span + from := start + 1 + thru := start + span + ps, err := pageSpan(ctx, from, thru) + if err != nil { + return nil, err + } + pss = append(pss, ps) + } + + // A possible last file has less than span pages. + if ctx.PageCount%span > 0 { + start := (ctx.PageCount / span) * span + from := start + 1 + thru := ctx.PageCount + ps, err := pageSpan(ctx, from, thru) + if err != nil { + return nil, err + } + pss = append(pss, ps) + } + + return pss, nil +} + +func writePageSpans(ctx *model.Context, span int, outDir, fileName string) error { + forBookmark := false + + for i := 0; i < ctx.PageCount/span; i++ { + start := i * span + from, thru := start+1, start+span + path := splitOutPath(outDir, fileName, forBookmark, from, thru) + if err := writePageSpan(ctx, from, thru, path); err != nil { + return err + } + } + + // A possible last file has less than span pages. + if ctx.PageCount%span > 0 { + start := (ctx.PageCount / span) * span + from, thru := start+1, ctx.PageCount + path := splitOutPath(outDir, fileName, forBookmark, from, thru) + if err := writePageSpan(ctx, from, thru, path); err != nil { + return err + } + } + + return nil +} + +func writePageSpansSplitAlongBookmarks(ctx *model.Context, outDir string) error { + forBookmark := true + + bms, err := pdfcpu.Bookmarks(ctx) + if err != nil { + return err + } + + for _, bm := range bms { + fileName := strings.Replace(bm.Title, " ", "_", -1) + from, thru := bm.PageFrom, bm.PageThru + if thru == 0 { + thru = ctx.PageCount + } + path := splitOutPath(outDir, fileName, forBookmark, from, thru) + if err := writePageSpan(ctx, from, thru, path); err != nil { + return err + } + } + + return nil +} + +func writePageSpansSplitAlongPages(ctx *model.Context, pageNrs []int, outDir, fileName string) error { + // pageNumbers is a a sorted sequence of page numbers. + forBookmark := false + from, thru := 1, 0 + + if len(pageNrs) < 1 { + return errors.New("pdfcpu: split along pageNrs - missing pageNrs") + } + + if pageNrs[0] > ctx.PageCount { + return errors.New("pdfcpu: split along pageNrs - invalid page number sequence.") + } + + for i := 0; i < len(pageNrs); i++ { + thru = pageNrs[i] - 1 + if thru >= ctx.PageCount { + break + } + path := splitOutPath(outDir, fileName, forBookmark, from, thru) + if err := writePageSpan(ctx, from, thru, path); err != nil { + return err + } + from = thru + 1 + } + + thru = ctx.PageCount + path := splitOutPath(outDir, fileName, forBookmark, from, thru) + return writePageSpan(ctx, from, thru, path) +} + +// SplitRaw returns page spans for the PDF stream read from rs obeying given split span. +// If span == 1 splitting results in single page PDFs. +// If span == 0 we split along given bookmarks (level 1 only). +// Default span: 1 +func SplitRaw(rs io.ReadSeeker, span int, conf *model.Configuration) ([]*PageSpan, error) { + if rs == nil { + return nil, errors.New("pdfcpu: SplitRaw: missing rs") + } + + ctx, err := context(rs, conf) + if err != nil { + return nil, err + } + + if span == 0 { + return pageSpansSplitAlongBookmarks(ctx) + } + return pageSpans(ctx, span) +} + +// Split generates a sequence of PDF files in outDir for the PDF stream read from rs obeying given split span. +// If span == 1 splitting results in single page PDFs. +// If span == 0 we split along given bookmarks (level 1 only). +// Default span: 1 +func Split(rs io.ReadSeeker, outDir, fileName string, span int, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: Split: missing rs") + } + + ctx, err := context(rs, conf) + if err != nil { + return err + } + + if span == 0 { + return writePageSpansSplitAlongBookmarks(ctx, outDir) + } + return writePageSpans(ctx, span, outDir, fileName) +} + +// SplitFile generates a sequence of PDF files in outDir for inFile obeying given split span. +// If span == 1 splitting results in single page PDFs. +// If span == 0 we split along given bookmarks (level 1 only). +// Default span: 1 +func SplitFile(inFile, outDir string, span int, conf *model.Configuration) error { + f, err := os.Open(inFile) + if err != nil { + return err + } + if log.CLIEnabled() { + log.CLI.Printf("splitting %s to %s/...\n", inFile, outDir) + } + + defer func() { + if err != nil { + f.Close() + return + } + err = f.Close() + }() + + return Split(f, outDir, filepath.Base(inFile), span, conf) +} + +// SplitFile generates a sequence of PDF files in outDir for rs splitting along pageNrs. +func SplitByPageNr(rs io.ReadSeeker, outDir, fileName string, pageNrs []int, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: SplitByPageNr: missing rs") + } + + ctx, err := context(rs, conf) + if err != nil { + return err + } + + return writePageSpansSplitAlongPages(ctx, pageNrs, outDir, fileName) +} + +// SplitFile generates a sequence of PDF files in outDir for inFile splitting it along pageNrs. +func SplitByPageNrFile(inFile, outDir string, pageNrs []int, conf *model.Configuration) error { + f, err := os.Open(inFile) + if err != nil { + return err + } + if log.CLIEnabled() { + log.CLI.Printf("splitting %s to %s/...\n", inFile, outDir) + } + + defer func() { + if err != nil { + f.Close() + return + } + err = f.Close() + }() + + return SplitByPageNr(f, outDir, filepath.Base(inFile), pageNrs, conf) +} diff --git a/pkg/api/stamp.go b/pkg/api/stamp.go new file mode 100644 index 0000000000000000000000000000000000000000..d84f2c7d3d0c8c1a556b2e1e9167c63f69ab5f3b --- /dev/null +++ b/pkg/api/stamp.go @@ -0,0 +1,531 @@ +/* + Copyright 2020 The pdfcpu Authors. + + 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 + + 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 api + +import ( + "io" + "os" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// WatermarkContext applies wm for selected pages to ctx. +func WatermarkContext(ctx *model.Context, selectedPages types.IntSet, wm *model.Watermark) error { + return pdfcpu.AddWatermarks(ctx, selectedPages, wm) +} + +// AddWatermarksMap adds watermarks in m to corresponding pages in rs and writes the result to w. +func AddWatermarksMap(rs io.ReadSeeker, w io.Writer, m map[int]*model.Watermark, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: AddWatermarksMap: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.ADDWATERMARKS + + if len(m) == 0 { + return errors.New("pdfcpu: missing watermarks") + } + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + if err = pdfcpu.AddWatermarksMap(ctx, m); err != nil { + return err + } + + return Write(ctx, w, conf) +} + +// AddWatermarksMapFile adds watermarks to corresponding pages in m of inFile and writes the result to outFile. +func AddWatermarksMapFile(inFile, outFile string, m map[int]*model.Watermark, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + logWritingTo(outFile) + } else { + logWritingTo(inFile) + } + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return AddWatermarksMap(f1, f2, m, conf) +} + +// AddWatermarksSliceMap adds watermarks in m to corresponding pages in rs and writes the result to w. +func AddWatermarksSliceMap(rs io.ReadSeeker, w io.Writer, m map[int][]*model.Watermark, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: AddWatermarksSliceMap: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.ADDWATERMARKS + + if len(m) == 0 { + return errors.New("pdfcpu: missing watermarks") + } + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + if err = pdfcpu.AddWatermarksSliceMap(ctx, m); err != nil { + return err + } + + return Write(ctx, w, conf) +} + +// AddWatermarksSliceMapFile adds watermarks to corresponding pages in m of inFile and writes the result to outFile. +func AddWatermarksSliceMapFile(inFile, outFile string, m map[int][]*model.Watermark, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + logWritingTo(outFile) + } else { + logWritingTo(inFile) + } + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return AddWatermarksSliceMap(f1, f2, m, conf) +} + +// AddWatermarks adds watermarks to all pages selected in rs and writes the result to w. +func AddWatermarks(rs io.ReadSeeker, w io.Writer, selectedPages []string, wm *model.Watermark, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: AddWatermarks: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.ADDWATERMARKS + conf.OptimizeDuplicateContentStreams = false + + if wm == nil { + return errors.New("pdfcpu: missing watermark configuration") + } + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + var pages types.IntSet + pages, err = PagesForPageSelection(ctx.PageCount, selectedPages, true, true) + if err != nil { + return err + } + + if err = pdfcpu.AddWatermarks(ctx, pages, wm); err != nil { + return err + } + + return Write(ctx, w, conf) +} + +// AddWatermarksFile adds watermarks to all selected pages of inFile and writes the result to outFile. +func AddWatermarksFile(inFile, outFile string, selectedPages []string, wm *model.Watermark, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + logWritingTo(outFile) + } else { + logWritingTo(inFile) + } + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return AddWatermarks(f1, f2, selectedPages, wm, conf) +} + +// RemoveWatermarks removes watermarks from all pages selected in rs and writes the result to w. +func RemoveWatermarks(rs io.ReadSeeker, w io.Writer, selectedPages []string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: RemoveWatermarks: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.REMOVEWATERMARKS + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) + if err != nil { + return err + } + + if err = pdfcpu.RemoveWatermarks(ctx, pages); err != nil { + return err + } + + return Write(ctx, w, conf) +} + +// RemoveWatermarksFile removes watermarks from all selected pages of inFile and writes the result to outFile. +func RemoveWatermarksFile(inFile, outFile string, selectedPages []string, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + logWritingTo(outFile) + } else { + logWritingTo(inFile) + } + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return RemoveWatermarks(f1, f2, selectedPages, conf) +} + +// HasWatermarks checks rs for watermarks. +func HasWatermarks(rs io.ReadSeeker, conf *model.Configuration) (bool, error) { + if rs == nil { + return false, errors.New("pdfcpu: HasWatermarks: missing rs") + } + + ctx, err := ReadContext(rs, conf) + if err != nil { + return false, err + } + + if err := pdfcpu.DetectWatermarks(ctx); err != nil { + return false, err + } + + return ctx.Watermarked, nil +} + +// HasWatermarksFile checks inFile for watermarks. +func HasWatermarksFile(inFile string, conf *model.Configuration) (bool, error) { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + + f, err := os.Open(inFile) + if err != nil { + return false, err + } + + defer f.Close() + + return HasWatermarks(f, conf) +} + +// TextWatermark returns a text watermark configuration. +func TextWatermark(text, desc string, onTop, update bool, u types.DisplayUnit) (*model.Watermark, error) { + wm, err := pdfcpu.ParseTextWatermarkDetails(text, desc, onTop, u) + if err != nil { + return nil, err + } + + wm.Update = update + + return wm, nil +} + +// ImageWatermark returns an image watermark configuration. +func ImageWatermark(fileName, desc string, onTop, update bool, u types.DisplayUnit) (*model.Watermark, error) { + wm, err := pdfcpu.ParseImageWatermarkDetails(fileName, desc, onTop, u) + if err != nil { + return nil, err + } + + wm.Update = update + + return wm, nil +} + +// ImageWatermarkForReader returns an image watermark configuration for r. +func ImageWatermarkForReader(r io.Reader, desc string, onTop, update bool, u types.DisplayUnit) (*model.Watermark, error) { + wm, err := pdfcpu.ParseImageWatermarkDetails("", desc, onTop, u) + if err != nil { + return nil, err + } + + wm.Update = update + wm.Image = r + + return wm, nil +} + +// PDFWatermark returns a PDF watermark configuration. +func PDFWatermark(fileName, desc string, onTop, update bool, u types.DisplayUnit) (*model.Watermark, error) { + wm, err := pdfcpu.ParsePDFWatermarkDetails(fileName, desc, onTop, u) + if err != nil { + return nil, err + } + + wm.Update = update + + return wm, nil +} + +// PDFWatermarkForReadSeeker returns a PDF watermark configuration. +// Apply watermark/stamp to destination file with pageNrSrc of rs for selected pages. +// If pageNr == 0 apply a multi watermark/stamp applying all src pages in ascending manner to destination pages. +func PDFWatermarkForReadSeeker(rs io.ReadSeeker, pageNrSrc int, desc string, onTop, update bool, u types.DisplayUnit) (*model.Watermark, error) { + wm, err := pdfcpu.ParsePDFWatermarkDetails("", desc, onTop, u) + if err != nil { + return nil, err + } + + wm.Update = update + wm.PDF = rs + wm.PdfPageNrSrc = pageNrSrc + + return wm, nil +} + +// PDFMultiWatermarkForReadSeeker returns a PDF watermark configuration. +// Define a source PDF watermark/stamp sequence using rs from page startPageNrSrc thru the last page of rs. +// Apply this sequence to the destination PDF file starting at page startPageNrDest for selected pages. +func PDFMultiWatermarkForReadSeeker(rs io.ReadSeeker, startPageNrSrc, startPageNrDest int, desc string, onTop, update bool, u types.DisplayUnit) (*model.Watermark, error) { + wm, err := pdfcpu.ParsePDFWatermarkDetails("", desc, onTop, u) + if err != nil { + return nil, err + } + + wm.Update = update + wm.PDF = rs + wm.PdfMultiStartPageNrSrc = startPageNrSrc + wm.PdfMultiStartPageNrDest = startPageNrDest + + return wm, nil +} + +// AddTextWatermarksFile adds text stamps/watermarks to all selected pages of inFile and writes the result to outFile. +func AddTextWatermarksFile(inFile, outFile string, selectedPages []string, onTop bool, text, desc string, conf *model.Configuration) error { + unit := types.POINTS + if conf != nil { + unit = conf.Unit + } + + wm, err := TextWatermark(text, desc, onTop, false, unit) + if err != nil { + return err + } + + return AddWatermarksFile(inFile, outFile, selectedPages, wm, conf) +} + +// AddImageWatermarksFile adds image stamps/watermarks to all selected pages of inFile and writes the result to outFile. +func AddImageWatermarksFile(inFile, outFile string, selectedPages []string, onTop bool, fileName, desc string, conf *model.Configuration) error { + unit := types.POINTS + if conf != nil { + unit = conf.Unit + } + + wm, err := ImageWatermark(fileName, desc, onTop, false, unit) + if err != nil { + return err + } + + return AddWatermarksFile(inFile, outFile, selectedPages, wm, conf) +} + +// AddImageWatermarksForReaderFile adds image stamps/watermarks to all selected pages of inFile for r and writes the result to outFile. +func AddImageWatermarksForReaderFile(inFile, outFile string, selectedPages []string, onTop bool, r io.Reader, desc string, conf *model.Configuration) error { + unit := types.POINTS + if conf != nil { + unit = conf.Unit + } + + wm, err := ImageWatermarkForReader(r, desc, onTop, false, unit) + if err != nil { + return err + } + + return AddWatermarksFile(inFile, outFile, selectedPages, wm, conf) +} + +// AddPDFWatermarksFile adds PDF stamps/watermarks to inFile and writes the result to outFile. +func AddPDFWatermarksFile(inFile, outFile string, selectedPages []string, onTop bool, fileName, desc string, conf *model.Configuration) error { + unit := types.POINTS + if conf != nil { + unit = conf.Unit + } + + wm, err := PDFWatermark(fileName, desc, onTop, false, unit) + if err != nil { + return err + } + + return AddWatermarksFile(inFile, outFile, selectedPages, wm, conf) +} + +// UpdateTextWatermarksFile adds text stamps/watermarks to all selected pages of inFile and writes the result to outFile. +func UpdateTextWatermarksFile(inFile, outFile string, selectedPages []string, onTop bool, text, desc string, conf *model.Configuration) error { + unit := types.POINTS + if conf != nil { + unit = conf.Unit + } + + wm, err := TextWatermark(text, desc, onTop, true, unit) + if err != nil { + return err + } + + return AddWatermarksFile(inFile, outFile, selectedPages, wm, conf) +} + +// UpdateImageWatermarksFile adds image stamps/watermarks to all selected pages of inFile and writes the result to outFile. +func UpdateImageWatermarksFile(inFile, outFile string, selectedPages []string, onTop bool, fileName, desc string, conf *model.Configuration) error { + unit := types.POINTS + if conf != nil { + unit = conf.Unit + } + wm, err := ImageWatermark(fileName, desc, onTop, true, unit) + if err != nil { + return err + } + return AddWatermarksFile(inFile, outFile, selectedPages, wm, conf) +} + +// UpdatePDFWatermarksFile adds PDF stamps/watermarks to all selected pages of inFile and writes the result to outFile. +func UpdatePDFWatermarksFile(inFile, outFile string, selectedPages []string, onTop bool, fileName, desc string, conf *model.Configuration) error { + unit := types.POINTS + if conf != nil { + unit = conf.Unit + } + + wm, err := PDFWatermark(fileName, desc, onTop, true, unit) + if err != nil { + return err + } + + return AddWatermarksFile(inFile, outFile, selectedPages, wm, conf) +} diff --git a/pkg/api/test/annotation_test.go b/pkg/api/test/annotation_test.go new file mode 100644 index 0000000000000000000000000000000000000000..af78329299c3fc8bbc97b2334fadae2ffbe9171c --- /dev/null +++ b/pkg/api/test/annotation_test.go @@ -0,0 +1,1077 @@ +/* +Copyright 2021 The pdfcpu Authors. + +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 + + 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 test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +var textAnn model.AnnotationRenderer = model.NewTextAnnotation( + *types.NewRectangle(0, 0, 100, 100), // rect + "Text Annotation", // contents + "ID1", // id + "", // modDate + 0, // f + &color.Gray, // col + "Title1", // title + nil, // popupIndRef + nil, // ca + "", // rc + "", // subject + 0, // borderRadX + 0, // borderRadY + 2, // borderWidth + false, // displayOpen + "Comment") // name + +var textAnnCJK model.AnnotationRenderer = model.NewTextAnnotation( + *types.NewRectangle(0, 100, 100, 200), // rect + "文字注释", // contents + "ID1CJK", // id + "", // modDate + 0, // f + &color.Gray, // col + "标题1", // title + nil, // popupIndRef + nil, // ca + "RC", // rc + "", // subject + 0, // borderRadX + 0, // borderRadY + 2, // borderWidth + true, // displayOpen + "Comment") // name + +var freeTextAnn model.AnnotationRenderer = model.NewFreeTextAnnotation( + *types.NewRectangle(200, 300, 400, 500), // rect + `Mac Preview shows "Contents" +line 2 +line 3`, // contents + "ID1", // id + "", // modDate + model.AnnLocked, // f + &color.Gray, // col + "Title1", // title + nil, // popupIndRef + nil, // ca + "", // rc + "", // subject + `A.Reader renders rich text ("RC"). +line 2 +line 3`, + // ` + // + // + //

This is some rich text.

+ // + // `, // rich text (ignored by Mac Preview and rendered mediocre by Adobe Reader) + types.AlignCenter, // horizontal alignment + "Helvetica", // font name (TODO) + 12, // font size in points (TODO) + &color.Green, // font color + "", // DS (default style string) + nil, // Intent + nil, // callOutLine + nil, // callOutLineEndingStyle + 0, 0, 0, 0, // margin + 0, // borderWidth + model.BSSolid, // borderStyle + false, // cloudyBorder + 0) // cloudyBorderIntensity + +var linkAnn model.AnnotationRenderer = model.NewLinkAnnotation( + *types.NewRectangle(200, 0, 300, 100), // rect + "", // contents + "ID2", // id + "", // modDate + 0, // f + &color.Red, // borderCol + nil, // dest + "https://pdfcpu.io", // uri + nil, // quad + true, // border + 1, // borderWidth + model.BSSolid, // borderStyle +) + +var squareAnn model.AnnotationRenderer = model.NewSquareAnnotation( + *types.NewRectangle(300, 0, 350, 50), // rect + "Square Annotation", // contents + "ID3", // id + "", // modDate + 0, // f + &color.Gray, // col + "Title1", // title + nil, // popupIndRef + nil, // ca + "", // rc + "", // subject + &color.Blue, // fillCol + 0, // MLeft + 0, // MTop + 0, // MRight + 0, // MBot + 1, // borderWidth + model.BSSolid, // borderStyle + false, // cloudyBorder + 0, // cloudyBorderIntensity +) + +var squareAnnCJK model.AnnotationRenderer = model.NewSquareAnnotation( + *types.NewRectangle(300, 50, 350, 100), // rect + "方形注释", // contents + "ID3CJK", // id + "", // modDate + 0, // f + &color.Gray, // col + "Title1", // title + nil, // popupIndRef + nil, // ca + "", // rc + "", // subject + &color.Green, // fillCol + 0, // MLeft + 0, // MTop + 0, // MRight + 0, // MBot + 1, // borderWidth + model.BSDashed, // borderStyle + false, // cloudyBorder + 0, // cloudyBorderIntensity +) + +var circleAnn model.AnnotationRenderer = model.NewCircleAnnotation( + *types.NewRectangle(400, 0, 450, 50), // rect + "Circle Annotation", // contents + "ID4", // id + "", // modDate + 0, // f + &color.Gray, // col + "Title1", // title + nil, // popupIndRef + nil, // ca + "", // rc + "", // subject + &color.Blue, // fillCol + 0, // MLeft + 0, // MTop + 0, // MRight + 0, // MBot + 1, // borderWidth + model.BSSolid, // borderStyle + false, // cloudyBorder + 0, // cloudyBorderIntensity +) + +var circleAnnCJK model.AnnotationRenderer = model.NewCircleAnnotation( + *types.NewRectangle(400, 50, 450, 100), // rect + "圆圈注释", // contents + "ID4CJK", // id + "", // modDate + 0, // f + &color.Green, // col + "Title1", // title + nil, // popupIndRef + nil, // ca + "", // rc + "", // subject + &color.Blue, // fillCol + 10, // MLeft + 10, // MTop + 10, // MRight + 10, // MBot + 1, // borderWidth + model.BSBeveled, // borderStyle + false, // cloudyBorder + 0, // cloudyBorderIntensity +) + +func annotationCount(t *testing.T, inFile string) int { + t.Helper() + + msg := "annotationCount" + + f, err := os.Open(inFile) + if err != nil { + t.Fatalf("%s open: %v\n", msg, err) + } + defer f.Close() + + annots, err := api.Annotations(f, nil, conf) + if err != nil { + t.Fatalf("%s annotations: %v\n", msg, err) + } + + count, _, err := pdfcpu.ListAnnotations(annots) + if err != nil { + t.Fatalf("%s listAnnotations: %v\n", msg, err) + } + + return count +} + +func add2Annotations(t *testing.T, msg, inFile string, incr bool) { + t.Helper() + + // We start with 0 annotations. + if i := annotationCount(t, inFile); i > 0 { + t.Fatalf("%s count: got %d want 0\n", msg, i) + } + + // Add a text annotation to page 1. + if err := api.AddAnnotationsFile(inFile, "", []string{"1"}, textAnn, nil, incr); err != nil { + t.Fatalf("%s add: %v\n", msg, err) + } + + // Add a link annotation to page 1. + if err := api.AddAnnotationsFile(inFile, "", []string{"1"}, linkAnn, nil, incr); err != nil { + t.Fatalf("%s add: %v\n", msg, err) + } + + // Now we should have 2 annotations. + if i := annotationCount(t, inFile); i != 2 { + t.Fatalf("%s count: got %d want 2\n", msg, i) + } +} + +func TestAddRemoveAnnotationsByAnnotType(t *testing.T) { + msg := "TestAddRemoveAnnotationsByAnnotType" + + incr := false // incremental updates + + fn := "test.pdf" + copyFile(t, filepath.Join(inDir, fn), filepath.Join(outDir, fn)) + inFile := filepath.Join(outDir, fn) + + add2Annotations(t, msg, inFile, incr) + + // Remove annotations by annotation type. + if err := api.RemoveAnnotationsFile(inFile, "", nil, []string{"Link", "Text"}, nil, nil, false); err != nil { + t.Fatalf("%s remove: %v\n", msg, err) + } + + // We should have 0 annotations as at the beginning. + if i := annotationCount(t, inFile); i > 0 { + t.Fatalf("%s count: got %d want 0\n", msg, i) + } +} + +func TestAddRemoveAnnotationsById(t *testing.T) { + msg := "TestAddRemoveAnnotationsById" + + incr := false // incremental updates + + fn := "test.pdf" + copyFile(t, filepath.Join(inDir, fn), filepath.Join(outDir, fn)) + inFile := filepath.Join(outDir, fn) + + add2Annotations(t, msg, inFile, incr) + + // Remove annotations by id. + if err := api.RemoveAnnotationsFile(inFile, "", nil, []string{"ID1", "ID2"}, nil, nil, incr); err != nil { + t.Fatalf("%s remove: %v\n", msg, err) + } + + // We should have 0 annotations as at the beginning. + if i := annotationCount(t, inFile); i > 0 { + t.Fatalf("%s count: got %d want 0\n", msg, i) + } +} + +func TestAddRemoveAnnotationsByIdAndAnnotType(t *testing.T) { + msg := "TestAddRemoveAnnotationsByIdAndAnnotType" + + incr := false // incremental updates + + fn := "test.pdf" + copyFile(t, filepath.Join(inDir, fn), filepath.Join(outDir, fn)) + inFile := filepath.Join(outDir, fn) + + add2Annotations(t, msg, inFile, incr) + + // Remove annotations by id annotation type. + if err := api.RemoveAnnotationsFile(inFile, "", nil, []string{"ID1", "Link"}, nil, nil, incr); err != nil { + t.Fatalf("%s remove: %v\n", msg, err) + } + + // We should have 0 annotations as at the beginning. + if i := annotationCount(t, inFile); i > 0 { + t.Fatalf("%s count: got %d want 0\n", msg, i) + } +} + +func TestAddRemoveAnnotationsByObjNr(t *testing.T) { + msg := "TestAddRemoveAnnotationsByObjNr" + + fn := "test.pdf" + copyFile(t, filepath.Join(inDir, fn), filepath.Join(outDir, fn)) + inFile := filepath.Join(outDir, fn) + + // Create a context. + ctx, err := api.ReadContextFile(inFile) + if err != nil { + t.Fatalf("%s readContext: %v\n", msg, err) + } + + allPages, err := api.PagesForPageSelection(ctx.PageCount, nil, true, true) + if err != nil { + t.Fatalf("%s pagesForPageSelection: %v\n", msg, err) + } + + // Add link annotation to all pages. + ok, err := pdfcpu.AddAnnotations(ctx, allPages, linkAnn, false) + if err != nil || !ok { + t.Fatalf("%s add: %v\n", msg, err) + } + + // Write context to file. + err = api.WriteContextFile(ctx, inFile) + if err != nil { + t.Fatalf("%s write: %v\n", msg, err) + } + + // We should have 1 annotation + if i := annotationCount(t, inFile); i != 1 { + t.Fatalf("%s count: got %d want 0\n", msg, i) + } + + // Create a context. + ctx, err = api.ReadContextFile(inFile) + if err != nil { + t.Fatalf("%s readContext: %v\n", msg, err) + } + + // Identify object numbers for located annotations + objNrs, err := pdfcpu.CachedAnnotationObjNrs(ctx) + if err != nil { + t.Fatalf("%s annObjNrs: %v\n", msg, err) + } + if len(objNrs) != 1 { + t.Fatalf("%s want 1 annotation, got: %d\n", msg, len(objNrs)) + } + + // Remove annotations by their object numbers + // We could also do: api.RemoveAnnotationsFile + // but since we already have the ctx this is more straight forward. + _, err = pdfcpu.RemoveAnnotations(ctx, allPages, nil, objNrs, false) + if err != nil { + t.Fatalf("%s remove: %v\n", msg, err) + } + + // Write context to file. + err = api.WriteContextFile(ctx, inFile) + if err != nil { + t.Fatalf("%s write: %v\n", msg, err) + } + + // We should have 0 annotations like at the beginning. + if i := annotationCount(t, inFile); i > 0 { + t.Fatalf("%s count: got %d want 0\n", msg, i) + } +} + +func TestAddRemoveAnnotationsByObjNrAndAnnotType(t *testing.T) { + msg := "TestAddRemoveAnnotationsByObjNrAndAnnotType" + + incr := false // incremental updates + + fn := "test.pdf" + copyFile(t, filepath.Join(inDir, fn), filepath.Join(outDir, fn)) + inFile := filepath.Join(outDir, fn) + + add2Annotations(t, msg, inFile, incr) + + // Remove annotations by obj and annotation type. + // Here we use the obj# of the link Annotation to be removed. + if err := api.RemoveAnnotationsFile(inFile, "", nil, []string{"Link"}, []int{6}, nil, incr); err != nil { + t.Fatalf("%s remove: %v\n", msg, err) + } + + // We should have 1 annotations. + if i := annotationCount(t, inFile); i != 1 { + t.Fatalf("%s count: got %d want 0\n", msg, i) + } +} + +func TestAddRemoveAnnotationsByIdAndObjNrAndAnnotType(t *testing.T) { + msg := "TestAddRemoveAnnotationsByObjNrAndAnnotType" + + incr := false // incremental updates + + fn := "test.pdf" + copyFile(t, filepath.Join(inDir, fn), filepath.Join(outDir, fn)) + inFile := filepath.Join(outDir, fn) + + add2Annotations(t, msg, inFile, incr) + + // Remove annotations by id annotation type. + if err := api.RemoveAnnotationsFile(inFile, "", nil, []string{"ID1", "Link"}, nil, nil, incr); err != nil { + t.Fatalf("%s remove: %v\n", msg, err) + } + + // We should have 0 annotations as at the beginning. + if i := annotationCount(t, inFile); i > 0 { + t.Fatalf("%s count: got %d want 0\n", msg, i) + } +} + +func TestRemoveAllAnnotations(t *testing.T) { + msg := "TestRemoveAllAnnotations" + + incr := false + + fn := "test.pdf" + copyFile(t, filepath.Join(inDir, fn), filepath.Join(outDir, fn)) + inFile := filepath.Join(outDir, fn) + + m := map[int][]model.AnnotationRenderer{} + anns := make([]model.AnnotationRenderer, 2) + anns[0] = textAnn + anns[1] = linkAnn + m[1] = anns + + err := api.AddAnnotationsMapFile(inFile, "", m, nil, incr) + if err != nil { + t.Fatalf("%s add: %v\n", msg, err) + } + + // We should have 2 annotations. + if i := annotationCount(t, inFile); i != 2 { + t.Fatalf("%s count: got %d want 0\n", msg, i) + } + + // Remove all annotations. + err = api.RemoveAnnotationsFile(inFile, "", nil, nil, nil, nil, incr) + if err != nil { + t.Fatalf("%s remove: %v\n", msg, err) + } + + // We should have 0 annotations like at the beginning. + if i := annotationCount(t, inFile); i > 0 { + t.Fatalf("%s count: got %d want 0\n", msg, i) + } +} + +func TestAddRemoveAllAnnotationsAsIncrements(t *testing.T) { + msg := "TestAddRemoveAnnotationsAsIncrements" + + incr := true // incremental updates + + fn := "test.pdf" + copyFile(t, filepath.Join(inDir, fn), filepath.Join(outDir, fn)) + inFile := filepath.Join(outDir, fn) + + add2Annotations(t, msg, inFile, incr) + + // Remove all page annotations and append the result as PDF increment to inFile. + if err := api.RemoveAnnotationsFile(inFile, "", nil, nil, nil, nil, true); err != nil { + t.Fatalf("%s remove: %v\n", msg, err) + } + + // We should have 0 annotations like at the beginning. + if i := annotationCount(t, inFile); i > 0 { + t.Fatalf("%s count: got %d want 0\n", msg, i) + } +} + +func TestAddAnnotationsLowLevel(t *testing.T) { + msg := "TestAddAnnotationsLowLevel" + + fn := "test.pdf" + inFile := filepath.Join(inDir, fn) + outFile := filepath.Join(outDir, fn) + + // Create a context. + ctx, err := api.ReadContextFile(inFile) + if err != nil { + t.Fatalf("%s readContext: %v\n", msg, err) + } + + m := map[int][]model.AnnotationRenderer{} + anns := make([]model.AnnotationRenderer, 2) + anns[0] = textAnn + anns[1] = linkAnn + m[1] = anns + + // Add 2 annotations to page 1. + if ok, err := pdfcpu.AddAnnotationsMap(ctx, m, false); err != nil || !ok { + t.Fatalf("%s add: %v\n", msg, err) + } + + // Write context to file. + if err := api.WriteContextFile(ctx, outFile); err != nil { + t.Fatalf("%s write: %v\n", msg, err) + } + + // Create a context. + ctx, err = api.ReadContextFile(outFile) + if err != nil { + t.Fatalf("%s readContext: %v\n", msg, err) + } + + // We should have 2 annotations. + i, _, err := pdfcpu.ListAnnotations(ctx.PageAnnots) + if err != nil || i != 2 { + t.Fatalf("%s list: %v\n", msg, err) + } + + // Remove all annotations. + _, err = pdfcpu.RemoveAnnotations(ctx, nil, nil, nil, false) + if err != nil { + t.Fatalf("%s remove: %v\n", msg, err) + } + + // (before writing) We should have 0 annotations like at the beginning. + i, _, err = pdfcpu.ListAnnotations(ctx.PageAnnots) + if err != nil || i != 0 { + t.Fatalf("%s list: %v\n", msg, err) + } + + // Write context to file. + if err := api.WriteContextFile(ctx, outFile); err != nil { + t.Fatalf("%s write: %v\n", msg, err) + } + + // (after writing) We should have 0 annotations like at the beginning. + if i := annotationCount(t, inFile); i > 0 { + t.Fatalf("%s count: got %d want 0\n", msg, i) + } +} + +func TestAddLinkAnnotationWithDest(t *testing.T) { + msg := "TestAddLinkAnnotationWithDest" + + // Best viewed with Adobe Reader. + + inFile := filepath.Join(inDir, "Walden.pdf") + outFile := filepath.Join(samplesDir, "annotations", "LinkAnnotWithDestTopLeft.pdf") + + // Create internal link: + // Add a 100x100 link rectangle on the bottom left corner of page 2. + // Set destination to top left corner of page 1. + dest := &model.Destination{Typ: model.DestXYZ, PageNr: 1, Left: -1, Top: -1} + + internalLink := model.NewLinkAnnotation( + *types.NewRectangle(0, 0, 100, 100), // rect + "", // contents + "ID2", // id + "", // modDate + 0, // f + &color.Red, // borderCol + dest, // dest + "", // uri + nil, // quad + true, // border + 1, // borderWidth + model.BSSolid, // borderStyle + ) + + err := api.AddAnnotationsFile(inFile, outFile, []string{"2"}, internalLink, nil, false) + if err != nil { + t.Fatalf("%s add: %v\n", msg, err) + } +} + +func TestAddAnnotationsFile(t *testing.T) { + msg := "TestAddAnnotationsFile" + + // Best viewed with Adobe Reader. + + inFile := filepath.Join(inDir, "test.pdf") + outFile := filepath.Join(samplesDir, "annotations", "Annotations.pdf") + + // Add text annotation. + if err := api.AddAnnotationsFile(inFile, outFile, nil, textAnn, nil, false); err != nil { + t.Fatalf("%s add: %v\n", msg, err) + } + + // Add CJK text annotation. + if err := api.AddAnnotationsFile(outFile, outFile, nil, textAnnCJK, nil, false); err != nil { + t.Fatalf("%s add: %v\n", msg, err) + } + + // Add link annotation. + if err := api.AddAnnotationsFile(outFile, outFile, nil, linkAnn, nil, false); err != nil { + t.Fatalf("%s add: %v\n", msg, err) + } + + // Add square annotation. + if err := api.AddAnnotationsFile(outFile, outFile, nil, squareAnn, nil, false); err != nil { + t.Fatalf("%s add: %v\n", msg, err) + } + + // Add CJK square annotation. + if err := api.AddAnnotationsFile(outFile, outFile, nil, squareAnnCJK, nil, false); err != nil { + t.Fatalf("%s add: %v\n", msg, err) + } + + // Add circle annotation. + if err := api.AddAnnotationsFile(outFile, outFile, nil, circleAnn, nil, false); err != nil { + t.Fatalf("%s add: %v\n", msg, err) + } + + // Add CJK circle annotation. + if err := api.AddAnnotationsFile(outFile, outFile, nil, circleAnnCJK, nil, false); err != nil { + t.Fatalf("%s add: %v\n", msg, err) + } + +} + +func TestAddAnnotations(t *testing.T) { + msg := "TestAddAnnotations" + + inFile := filepath.Join(inDir, "test.pdf") + outFile := filepath.Join(outDir, "Annotations.pdf") + + // Create a context from inFile. + ctx, err := api.ReadContextFile(inFile) + if err != nil { + t.Fatalf("%s readContext: %v\n", msg, err) + } + + // Prepare annotations for page 1. + m := map[int][]model.AnnotationRenderer{} + anns := make([]model.AnnotationRenderer, 7) + + anns[0] = textAnn + anns[1] = textAnnCJK + anns[2] = squareAnn + anns[3] = squareAnnCJK + anns[4] = circleAnn + anns[5] = circleAnnCJK + anns[6] = linkAnn + + m[1] = anns + + // Add 7 annotations to page 1. + if ok, err := pdfcpu.AddAnnotationsMap(ctx, m, false); err != nil || !ok { + t.Fatalf("%s add: %v\n", msg, err) + } + + // Write context to outFile. + if err := api.WriteContextFile(ctx, outFile); err != nil { + t.Fatalf("%s write: %v\n", msg, err) + } + +} + +func TestPopupAnnotation(t *testing.T) { + msg := "TestPopupAnnotation" + + // Add a Markup annotation and a linked Popup annotation. + // Best viewed with Adobe Reader. + + inFile := filepath.Join(inDir, "test.pdf") + outFile := filepath.Join(samplesDir, "annotations", "PopupAnnotation.pdf") + + incr := false + pageNr := 1 + + // Create a context. + ctx, err := api.ReadContextFile(inFile) + if err != nil { + t.Fatalf("%s readContext: %v\n", msg, err) + } + + // Add Markup annotation. + parentIndRef, textAnnotDict, err := pdfcpu.AddAnnotationToPage(ctx, pageNr, textAnn, incr) + if err != nil { + t.Fatalf("%s Add Text AnnotationToPage: %v\n", msg, err) + } + + // Add Markup annotation as parent of Popup annotation. + popupAnn := model.NewPopupAnnotation( + *types.NewRectangle(0, 0, 100, 100), // rect + "Popup content", // contents + "IDPopup", // id + "", // modDate + 0, // f + &color.Green, // col + 0, // borderRadX + 0, // borderRadY + 2, // borderWidth + parentIndRef, // parentIndRef, + false, // displayOpen + ) + + // Add Popup annotation. + popupIndRef, _, err := pdfcpu.AddAnnotationToPage(ctx, pageNr, popupAnn, incr) + if err != nil { + t.Fatalf("%s Add Popup AnnotationToPage: %v\n", msg, err) + } + + // Add Popup annotation to Markup annotation. + textAnnotDict["Popup"] = *popupIndRef + + // Write context to file. + if err := api.WriteContextFile(ctx, outFile); err != nil { + t.Fatalf("%s write: %v\n", msg, err) + } +} + +func TestInkAnnotation(t *testing.T) { + msg := "TestInkAnnotation" + + // Best viewed with Adobe Reader. + + inFile := filepath.Join(inDir, "test.pdf") + outFile := filepath.Join(samplesDir, "annotations", "InkAnnotation.pdf") + + p1 := model.InkPath{100., 542., 150., 492., 200., 542.} + p2 := model.InkPath{100, 592, 150, 592} + + inkAnn := model.NewInkAnnotation( + *types.NewRectangle(0, 0, 100, 100), // rect + "Ink content", // contents + "IDInk", // id + "", // modDate + 0, // f + &color.Red, // col + "Title1", // title + nil, // popupIndRef + nil, // ca + "", // rc + "", // subject + []model.InkPath{p1, p2}, // InkList + 0, // borderWidth + model.BSSolid, // borderStyle + ) + + // Add Ink annotation. + if err := api.AddAnnotationsFile(inFile, outFile, nil, inkAnn, nil, false); err != nil { + t.Fatalf("%s add: %v\n", msg, err) + } +} + +func TestHighlightAnnotation(t *testing.T) { + msg := "TestHighlightAnnotation" + + // Best viewed with Adobe Reader. + + inFile := filepath.Join(inDir, "testWithText.pdf") + outFile := filepath.Join(samplesDir, "annotations", "HighlightAnnotation.pdf") + + r := types.NewRectangle(205, 624.16, 400, 645.88) + + ql := types.NewQuadLiteralForRect(r) + + inkAnn := model.NewHighlightAnnotation( + *r, // rect + "Highlight content", // contents + "IDHighlight", // id + "", // modDate + model.AnnLocked, // f + &color.Yellow, // col + 0, // borderRadX + 0, // borderRadY + 2, // borderWidth + "Comment by Horst", // title + nil, // popupIndRef + nil, // ca + "", // rc + "Subject", // subject + types.QuadPoints{*ql}, // quad points + ) + + // Add Highlight annotation. + if err := api.AddAnnotationsFile(inFile, outFile, nil, inkAnn, nil, false); err != nil { + t.Fatalf("%s add: %v\n", msg, err) + } +} + +func TestUnderlineAnnotation(t *testing.T) { + msg := "TestUnderlineAnnotation" + + // Best viewed with Adobe Reader. + + inFile := filepath.Join(inDir, "testWithText.pdf") + outFile := filepath.Join(samplesDir, "annotations", "UnderlineAnnotation.pdf") + + r := types.NewRectangle(205, 624.16, 400, 645.88) + + ql := types.NewQuadLiteralForRect(r) + + underlineAnn := model.NewUnderlineAnnotation( + *r, // rect + "Underline content", // contents + "IDUnderline", // id + "", // modDate + model.AnnLocked, // f + &color.Yellow, // col + 0, // borderRadX + 0, // borderRadY + 2, // borderWidth + "Title1", // title + nil, // popupIndRef + nil, // ca + "", // rc + "", // subject + types.QuadPoints{*ql}, // quad points + ) + + // Add Underline annotation. + if err := api.AddAnnotationsFile(inFile, outFile, nil, underlineAnn, nil, false); err != nil { + t.Fatalf("%s add: %v\n", msg, err) + } +} + +func TestSquigglyAnnotation(t *testing.T) { + msg := "TestSquigglyAnnotation" + + // Best viewed with Adobe Reader. + + inFile := filepath.Join(inDir, "testWithText.pdf") + outFile := filepath.Join(samplesDir, "annotations", "SquigglyAnnotation.pdf") + + r := types.NewRectangle(205, 624.16, 400, 645.88) + + ql := types.NewQuadLiteralForRect(r) + + squigglyAnn := model.NewSquigglyAnnotation( + *r, // rect + "Squiggly content", // contents + "IDSquiggly", // id + "", // modDate + model.AnnLocked, // f + &color.Yellow, // col + 0, // borderRadX + 0, // borderRadY + 2, // borderWidth + "Title1", // title + nil, // popupIndRef + nil, // ca + "", // rc + "", // subject + types.QuadPoints{*ql}, // quad points + ) + + // Add Squiggly annotation. + if err := api.AddAnnotationsFile(inFile, outFile, nil, squigglyAnn, nil, false); err != nil { + t.Fatalf("%s add: %v\n", msg, err) + } +} + +func TestStrikeOutAnnotation(t *testing.T) { + msg := "TestStrikeOutAnnotation" + + // Best viewed with Adobe Reader. + + inFile := filepath.Join(inDir, "testWithText.pdf") + outFile := filepath.Join(samplesDir, "annotations", "StrikeOutAnnotation.pdf") + + r := types.NewRectangle(205, 624.16, 400, 645.88) + + ql := types.NewQuadLiteralForRect(r) + + strikeOutAnn := model.NewStrikeOutAnnotation( + *r, // rect + "StrikeOut content", // contents + "IDStrikeOut", // id + "", // modDate + model.AnnLocked, // f + &color.Yellow, // col + 0, // borderRadX + 0, // borderRadY + 2, // borderWidth + "Title1", // title + nil, // popupIndRef + nil, // ca + "", // rc + "", // subject + types.QuadPoints{*ql}, // quad points + ) + + // Add StrikeOut annotation. + if err := api.AddAnnotationsFile(inFile, outFile, nil, strikeOutAnn, nil, false); err != nil { + t.Fatalf("%s add: %v\n", msg, err) + } +} + +func TestFreeTextAnnotation(t *testing.T) { + msg := "TestFreeTextAnnotation" + + // Best viewed with Adobe Reader. + + inFile := filepath.Join(inDir, "test.pdf") + outFile := filepath.Join(samplesDir, "annotations", "FreeTextAnnotation.pdf") + + // Add Free text annotation. + if err := api.AddAnnotationsFile(inFile, outFile, nil, freeTextAnn, nil, false); err != nil { + t.Fatalf("%s add: %v\n", msg, err) + } +} + +func TestPolyLineAnnotation(t *testing.T) { + msg := "TestPolyLineAnnotation" + + // Best viewed with Adobe Reader. + + inFile := filepath.Join(inDir, "test.pdf") + outFile := filepath.Join(samplesDir, "annotations", "PolyLineAnnotation.pdf") + + leButt := model.LEButt + leOpenArrow := model.LEOpenArrow + + polyLineAnn := model.NewPolyLineAnnotation( + *types.NewRectangle(30, 30, 110, 110), // rect + "PolyLine Annotation", // contents + "IDPolyLine", // id + "", // modDate + 0, // f + &color.Gray, // col + "Title1", // title + nil, // popupIndRef + nil, // ca + "", // rc + "", // subject + types.NewNumberArray(30, 30, 110, 110, 110, 30), // vertices + nil, // path + nil, // intent + nil, // measure + &color.Green, // fillCol + 1, // borderWidth + model.BSDashed, // borderStyle + &leButt, // start lineEndingStyle + &leOpenArrow, // end lineEndingStyle + ) + + // Add PolyLine annotation. + if err := api.AddAnnotationsFile(inFile, outFile, nil, polyLineAnn, nil, false); err != nil { + t.Fatalf("%s add: %v\n", msg, err) + } +} + +func TestPolygonAnnotation(t *testing.T) { + msg := "TestPolygonAnnotation" + + // Best viewed with Adobe Reader. + + inFile := filepath.Join(inDir, "test.pdf") + outFile := filepath.Join(samplesDir, "annotations", "PolygonAnnotation.pdf") + + polygonAnn := model.NewPolygonAnnotation( + *types.NewRectangle(30, 30, 110, 110), // rect + "Polygon Annotation", // contents + "IDPolygon", // id + "", // modDate + 0, // f + &color.Gray, // col + "Title1", // title + nil, // popupIndRef + nil, // ca + "", // rc + "", // subject + types.NewNumberArray(30, 30, 110, 110, 110, 30), // vertices + nil, // path + nil, // intent + nil, // measure + &color.Green, // fillCol + 5, // borderWidth + model.BSDashed, // borderStyle + true, // cloudyBorder + 2) // cloudyBorderIntensity + + // Add Polygon annotation. + if err := api.AddAnnotationsFile(inFile, outFile, nil, polygonAnn, nil, false); err != nil { + t.Fatalf("%s add: %v\n", msg, err) + } +} + +func TestLineAnnotation(t *testing.T) { + msg := "TestLineAnnotation" + + // Best viewed with Adobe Reader. + + inFile := filepath.Join(inDir, "test.pdf") + outFile := filepath.Join(samplesDir, "annotations", "LineAnnotation.pdf") + + leOpenArrow := model.LEOpenArrow + + lineAnn := model.NewLineAnnotation( + *types.NewRectangle(30, 30, 110, 110), // rect + "Diagonal", // contents + "IDLine", // id + "", // modDate + 0, // f + &color.DarkGray, // col + "Title1", // title + nil, // popupIndRef + nil, // ca + "", // rc + "", // subject + types.NewPoint(148.75, 140.33), // P1 + types.NewPoint(297.5, 280.66), // P2 + &leOpenArrow, // start lineEndingStyle + &leOpenArrow, // end lineEndingStyle + 50, // leader line length + 0, // leader line offset + 10, // leader line extension length + nil, // intent + nil, // measure + true, // caption + false, // caption position top + 0, // caption offset X + 0, // caption offset Y + nil, // fillCol + 1, // borderWidth + model.BSSolid) // borderStyle + + // Add line annotation. + if err := api.AddAnnotationsFile(inFile, outFile, nil, lineAnn, nil, false); err != nil { + t.Fatalf("%s add: %v\n", msg, err) + } +} + +func TestCaretAnnotation(t *testing.T) { + msg := "TestCaretAnnotation" + + // Best viewed with Adobe Reader. + + inFile := filepath.Join(inDir, "test.pdf") + outFile := filepath.Join(samplesDir, "annotations", "CaretAnnotation.pdf") + + caretAnn := model.NewCaretAnnotation( + *types.NewRectangle(30, 30, 110, 110), // rect + "Caret Annotation", // contents + "IDCaret", // id + "", // modDate + 0, // f, + nil, // col + 0, // borderRadX + 0, // borderRadY + 0, // borderWidth + "Title1", // title + nil, // popupIndRef + nil, // ca + "", // rc + "", // subject + types.NewRectangle(20, 20, 20, 20), // RD + true) // paragraph symbol + + // Add line annotation. + if err := api.AddAnnotationsFile(inFile, outFile, nil, caretAnn, nil, false); err != nil { + t.Fatalf("%s add: %v\n", msg, err) + } +} diff --git a/pkg/api/test/api_test.go b/pkg/api/test/api_test.go new file mode 100644 index 0000000000000000000000000000000000000000..2128326d399c9d9b1cab2c6bccfca45896f5a96c --- /dev/null +++ b/pkg/api/test/api_test.go @@ -0,0 +1,240 @@ +/* +Copyright 2018 The pdf Authors. + +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 + + 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 test + +import ( + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strings" + "testing" + "time" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +var inDir, outDir, resDir, samplesDir string +var conf *model.Configuration + +func isTrueType(filename string) bool { + s := strings.ToLower(filename) + return strings.HasSuffix(s, ".ttf") || strings.HasSuffix(s, ".ttc") +} + +func userFonts(dir string) ([]string, error) { + files, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + ff := []string(nil) + for _, f := range files { + if isTrueType(f.Name()) { + fn := filepath.Join(dir, f.Name()) + ff = append(ff, fn) + } + } + return ff, nil +} + +func TestMain(m *testing.M) { + inDir = filepath.Join("..", "..", "testdata") + resDir = filepath.Join(inDir, "resources") + samplesDir = filepath.Join("..", "..", "samples") + + conf = api.LoadConfiguration() + + // Install test user fonts from pkg/testdata/fonts. + fonts, err := userFonts(filepath.Join(inDir, "fonts")) + if err != nil { + fmt.Printf("%v", err) + os.Exit(1) + } + + if err := api.InstallFonts(fonts); err != nil { + fmt.Printf("%v", err) + os.Exit(1) + } + + if outDir, err = os.MkdirTemp("", "pdfcpu_api_tests"); err != nil { + fmt.Printf("%v", err) + os.Exit(1) + } + // fmt.Printf("outDir = %s\n", outDir) + + exitCode := m.Run() + + if err = os.RemoveAll(outDir); err != nil { + fmt.Printf("%v", err) + os.Exit(1) + } + + os.Exit(exitCode) +} + +func copyFile(t *testing.T, srcFileName, destFileName string) error { + t.Helper() + from, err := os.Open(srcFileName) + if err != nil { + return err + } + defer from.Close() + to, err := os.Create(destFileName) + if err != nil { + return err + } + defer to.Close() + _, err = io.Copy(to, from) + return err +} + +func imageFileNames(t *testing.T, dir string) []string { + t.Helper() + fn, err := model.ImageFileNames(dir, types.MB) + if err != nil { + t.Fatal(err) + } + sort.Strings(fn) + return fn +} + +func BenchmarkValidate(b *testing.B) { + msg := "BenchmarkValidate" + b.ResetTimer() + for n := 0; n < b.N; n++ { + f, err := os.Open(filepath.Join(inDir, "gobook.0.pdf")) + if err != nil { + b.Fatalf("%s: %v\n", msg, err) + } + if err = api.Validate(f, nil); err != nil { + b.Fatalf("%s: %v\n", msg, err) + } + if err = f.Close(); err != nil { + b.Fatalf("%s: %v\n", msg, err) + } + } +} + +func isPDF(filename string) bool { + return strings.HasSuffix(strings.ToLower(filename), ".pdf") +} + +func AllPDFs(t *testing.T, dir string) []string { + t.Helper() + files, err := os.ReadDir(dir) + if err != nil { + t.Fatalf("pdfFiles from %s: %v\n", dir, err) + } + ff := []string(nil) + for _, f := range files { + if isPDF(f.Name()) { + ff = append(ff, f.Name()) + } + } + return ff +} + +func TestPageCount(t *testing.T) { + msg := "TestPageCount" + + fn := "5116.DCT_Filter.pdf" + wantPageCount := 52 + inFile := filepath.Join(inDir, fn) + + // Retrieve page count for inFile. + gotPageCount, err := api.PageCountFile(inFile) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + if wantPageCount != gotPageCount { + t.Fatalf("%s %s: pageCount want:%d got:%d\n", msg, inFile, wantPageCount, gotPageCount) + } +} + +func TestPageDimensions(t *testing.T) { + msg := "TestPageDimensions" + for _, fn := range AllPDFs(t, inDir) { + inFile := filepath.Join(inDir, fn) + + // Retrieve page dimensions for inFile. + if _, err := api.PageDimsFile(inFile); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + } +} + +func TestValidate(t *testing.T) { + msg := "TestValidate" + inFile := filepath.Join(inDir, "Acroforms2.pdf") + + // Validate inFile. + if err := api.ValidateFile(inFile, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestManipulateContext(t *testing.T) { + msg := "TestManipulateContext" + inFile := filepath.Join(inDir, "5116.DCT_Filter.pdf") + outFile := filepath.Join(outDir, "abc.pdf") + + // Read a PDF Context from inFile. + ctx, err := api.ReadContextFile(inFile) + if err != nil { + t.Fatalf("%s: ReadContextFile %s: %v\n", msg, inFile, err) + } + + // Manipulate the PDF Context. + // Eg. Let's stamp all pages with pageCount and current timestamp. + text := fmt.Sprintf("Pages: %d \n Current time: %v", ctx.PageCount, time.Now()) + wm, err := api.TextWatermark(text, "font:Times-Italic, scale:.9", true, false, types.POINTS) + if err != nil { + t.Fatalf("%s: ParseTextWatermarkDetails: %v\n", msg, err) + } + if err := pdfcpu.AddWatermarks(ctx, nil, wm); err != nil { + t.Fatalf("%s: WatermarkContext: %v\n", msg, err) + } + + // Write the manipulated PDF context to outFile. + if err := api.WriteContextFile(ctx, outFile); err != nil { + t.Fatalf("%s: WriteContextFile %s: %v\n", msg, outFile, err) + } +} + +func TestInfo(t *testing.T) { + msg := "TestInfo" + inFile := filepath.Join(inDir, "5116.DCT_Filter.pdf") + + f, err := os.Open(inFile) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + defer f.Close() + + info, err := api.PDFInfo(f, inFile, nil, conf) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + if info == nil { + t.Fatalf("%s: missing Info\n", msg) + } +} diff --git a/pkg/api/test/attachment_test.go b/pkg/api/test/attachment_test.go new file mode 100644 index 0000000000000000000000000000000000000000..397d24967fcc63545dc1f895b7ac53572b89be34 --- /dev/null +++ b/pkg/api/test/attachment_test.go @@ -0,0 +1,262 @@ +/* +Copyright 2019 The pdf Authors. + +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 + + 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 test + +import ( + "io" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" +) + +func prepareForAttachmentTest(t *testing.T) error { + t.Helper() + for _, fileName := range []string{"go.pdf", "golang.pdf", "T4.pdf", "go-lecture.pdf"} { + inFile := filepath.Join(inDir, fileName) + outFile := filepath.Join(outDir, fileName) + if err := copyFile(t, inFile, outFile); err != nil { + return err + } + } + return copyFile(t, filepath.Join(resDir, "test.wav"), filepath.Join(outDir, "test.wav")) +} + +func listAttachments(t *testing.T, msg, fileName string, want int) { + t.Helper() + + f, err := os.Open(fileName) + if err != nil { + t.Fatalf("%s open: %v\n", msg, err) + } + defer f.Close() + + aa, err := api.Attachments(f, nil) + if err != nil { + t.Fatalf("%s list attachments: %v\n", msg, err) + } + + got := len(aa) + if got != want { + t.Fatalf("%s: list attachments %s: want %d got %d\n", msg, fileName, want, got) + } +} + +func TestAttachments(t *testing.T) { + msg := "testAttachments" + + if err := prepareForAttachmentTest(t); err != nil { + t.Fatalf("%s prepare for attachments: %v\n", msg, err) + } + + fileName := filepath.Join(outDir, "go.pdf") + + // # of attachments must be 0 + listAttachments(t, msg, fileName, 0) + + // attach add 4 files + files := []string{ + filepath.Join(outDir, "golang.pdf"), + filepath.Join(outDir, "T4.pdf"), + filepath.Join(outDir, "go-lecture.pdf"), + filepath.Join(outDir, "test.wav")} + + if err := api.AddAttachmentsFile(fileName, "", files, false, nil); err != nil { + t.Fatalf("%s add attachments: %v\n", msg, err) + } + + listAttachments(t, msg, fileName, 4) + + // Extract all attachments. + if err := api.ExtractAttachmentsFile(fileName, outDir, nil, nil); err != nil { + t.Fatalf("%s extract all attachments: %v\n", msg, err) + } + + // Extract 1 attachment. + if err := api.ExtractAttachmentsFile(fileName, outDir, []string{"golang.pdf"}, nil); err != nil { + t.Fatalf("%s extract one attachment: %v\n", msg, err) + } + + // Remove 1 attachment. + if err := api.RemoveAttachmentsFile(fileName, "", []string{"golang.pdf"}, nil); err != nil { + t.Fatalf("%s remove one attachment: %v\n", msg, err) + } + listAttachments(t, msg, fileName, 3) + + // Remove all attachments. + if err := api.RemoveAttachmentsFile(fileName, "", nil, nil); err != nil { + t.Fatalf("%s remove all attachments: %v\n", msg, err) + } + listAttachments(t, msg, fileName, 0) + + // Validate the processed file. + if err := api.ValidateFile(fileName, nil); err != nil { + t.Fatalf("%s: validate: %v\n", msg, err) + } +} + +// timeEqualsTimeFromDateTime returns true if t1 equals t2 +// working on the assumption that t2 is restored from a PDF +// date string that does not have a way to include nanoseconds. +func timeEqualsTimeFromDateTime(t1, t2 *time.Time) bool { + if t1 == nil && t2 == nil { + return true + } + if t1 == nil || t2 == nil { + return false + } + nanos := t1.Nanosecond() + t11 := t1.Add(-time.Duration(nanos) * time.Nanosecond) + return t11.Equal(*t2) +} + +func addAttachment(t *testing.T, msg, outFile, id, desc, want string, modTime time.Time, ctx *model.Context) { + t.Helper() + + a := model.Attachment{ + Reader: strings.NewReader(want), + ID: id, + Desc: desc, + ModTime: &modTime} + + var err error + useCollection := false + if err = ctx.AddAttachment(a, useCollection); err != nil { + t.Fatalf("%s addAttachment: %v\n", msg, err) + } + + // Write context to outFile after adding attachment. + if err = api.WriteContextFile(ctx, outFile); err != nil { + t.Fatalf("%s writeContext: %v\n", msg, err) + } +} + +func extractAttachment(t *testing.T, msg string, a model.Attachment, ctx *model.Context) model.Attachment { + t.Helper() + + a1, err := ctx.ExtractAttachment(a) + if err != nil { + t.Fatalf("%s extractAttachment: %v\n", msg, err) + } + if a1.ID != a.ID || + a1.FileName != a.FileName || + a1.Desc != a.Desc || + !timeEqualsTimeFromDateTime(a.ModTime, a1.ModTime) { + t.Fatalf("%s extractAttachment: unexpected attachment: %s\n", msg, a1) + } + return *a1 +} + +func removeAttachment(t *testing.T, msg, outFile string, a model.Attachment, ctx *model.Context) { + t.Helper() + ok, err := ctx.RemoveAttachment(a) + if err != nil { + t.Fatalf("%s removeAttachment: %v\n", msg, err) + } + if !ok { + t.Fatalf("%s removeAttachment: attachment %s not found\n", msg, a.FileName) + } + + // Write context to outFile after removing attachment. + if err := api.WriteContextFile(ctx, outFile); err != nil { + t.Fatalf("%s writeContext: %v\n", msg, err) + } + + // Read outfile once again into a PDFContext. + ctx, err = api.ReadContextFile(outFile) + if err != nil { + t.Fatalf("%s readContext: %v\n", msg, err) + } + + // List attachment. + aa, err := ctx.ListAttachments() + if err != nil { + t.Fatalf("%s listAttachments: %v\n", msg, err) + } + if len(aa) != 0 { + t.Fatalf("%s listAttachments: want 0 got %d\n", msg, len(aa)) + } +} + +func TestAttachmentsLowLevel(t *testing.T) { + msg := "TestAttachmentsLowLevel" + + file := "go.pdf" + inFile := filepath.Join(inDir, file) + outFile := filepath.Join(outDir, file) + if err := copyFile(t, inFile, outFile); err != nil { + t.Fatalf("%s copyFile: %v\n", msg, err) + } + + // Create a context. + ctx, err := api.ReadContextFile(outFile) + if err != nil { + t.Fatalf("%s readContext: %v\n", msg, err) + } + + // Ensure zero attachments. + if aa, err := ctx.ListAttachments(); err != nil || len(aa) > 0 { + t.Fatalf("%s listAttachments: %v\n", msg, err) + } + + id := "attachment1" + desc := "description" + want := "12345" + modTime := time.Now() + addAttachment(t, msg, outFile, id, desc, want, modTime, ctx) + + // Read outfile again into a PDFContext. + ctx, err = api.ReadContextFile(outFile) + if err != nil { + t.Fatalf("%s readContext: %v\n", msg, err) + } + + // List attachments. + aa, err := ctx.ListAttachments() + if err != nil { + t.Fatalf("%s listAttachments: %v\n", msg, err) + } + if len(aa) != 1 { + t.Fatalf("%s listAttachments: want 1 got %d\n", msg, len(aa)) + } + if aa[0].FileName != id || + aa[0].Desc != desc || + !timeEqualsTimeFromDateTime(&modTime, aa[0].ModTime) { + t.Fatalf("%s listAttachments: unexpected attachment: %s\n", msg, aa[0]) + } + + a := extractAttachment(t, msg, aa[0], ctx) + + // Compare extracted attachment bytes. + gotBytes, err := io.ReadAll(a) + if err != nil { + t.Fatalf("%s extractAttachment: attachment %s no data available\n", msg, id) + } + got := string(gotBytes) + if got != want { + t.Fatalf("%s\ngot:%s\nwant:%s", msg, got, want) + } + + // Optional processing of attachment bytes: + // Process gotBytes.. + + removeAttachment(t, msg, outFile, a, ctx) +} diff --git a/pkg/api/test/booklet_test.go b/pkg/api/test/booklet_test.go new file mode 100644 index 0000000000000000000000000000000000000000..1c15d9657a90de09061ebe4ece77a460d0e92f91 --- /dev/null +++ b/pkg/api/test/booklet_test.go @@ -0,0 +1,257 @@ +/* +Copyright 2021 The pdf Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" +) + +func testBooklet(t *testing.T, msg string, inFiles []string, outFile string, selectedPages []string, desc string, n int, isImg bool, conf *model.Configuration) { + t.Helper() + + var ( + booklet *model.NUp + err error + ) + + if isImg { + if booklet, err = api.ImageBookletConfig(n, desc, conf); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + } else { + if booklet, err = api.PDFBookletConfig(n, desc, conf); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + } + + if err := api.BookletFile(inFiles, outFile, selectedPages, booklet, conf); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + if err := api.ValidateFile(outFile, conf); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestBooklet(t *testing.T) { + outDir := filepath.Join("..", "..", "samples", "booklet") + + for _, tt := range []struct { + msg string + inFiles []string + outFile string + selectedPages []string + desc string + unit string + n int + isImg bool + }{ + // 2-up booklet from images on A4 + {"TestBookletFromImagesA42Up", + imageFileNames(t, resDir), + filepath.Join(outDir, "BookletFromImagesA4_2Up.pdf"), + nil, + "p:A4, border:false, g:on, ma:25, bgcol:#beded9", + "points", + 2, + true, + }, + + // 4-up booklet from images on A4 + {"TestBookletFromImagesA44Up", + imageFileNames(t, resDir), + filepath.Join(outDir, "BookletFromImagesA4_4Up.pdf"), + nil, + "p:A4, border:false, g:on, ma:25, bgcol:#beded9", + "points", + 4, + true, + }, + + // 2-up booklet from PDF on A4 + {"TestBookletFromPDF2UpA4", + []string{filepath.Join(inDir, "zineTest.pdf")}, + filepath.Join(outDir, "BookletFromPDFA4_2Up.pdf"), + nil, // all pages + "p:A4, border:false, g:on, ma:10, bgcol:#beded9", + "points", + 2, + false, + }, + + // 4-up booklet from PDF on A4 + {"TestBookletFromPDF4UpA4", + []string{filepath.Join(inDir, "zineTest.pdf")}, + filepath.Join(outDir, "BookletFromPDFA4_4Up.pdf"), + []string{"1-"}, // all pages + "p:A4, border:off, guides:on, ma:10, bgcol:#beded9", + "points", + 4, + false, + }, + + // 4-up booklet from PDF on Ledger + {"TestBookletFromPDF4UpLedger", + []string{filepath.Join(inDir, "bookletTest.pdf")}, + filepath.Join(outDir, "BookletFromPDFLedger_4Up.pdf"), + []string{"1-24"}, + "p:LedgerP, g:on, ma:10, bgcol:#f7e6c7", + "points", + 4, + false, + }, + + // 4-up booklet from PDF on Ledger where the number of pages don't fill the whole sheet + {"TestBookletFromPDF4UpLedgerWithTrailingBlankPages", + []string{filepath.Join(inDir, "bookletTest.pdf")}, + filepath.Join(outDir, "BookletFromPDFLedger_4UpWithTrailingBlankPages.pdf"), + []string{"1-21"}, + "p:LedgerP, g:on, ma:10, bgcol:#f7e6c7", + "points", + 4, + false, + }, + + // 2-up booklet from PDF on Letter + {"TestBookletFromPDF2UpLetter", + []string{filepath.Join(inDir, "bookletTest.pdf")}, + filepath.Join(outDir, "BookletFromPDFLetter_2Up.pdf"), + []string{"1-16"}, + "p:LetterP, g:on, ma:10, bgcol:#f7e6c7", + "points", + 2, + false, + }, + + // 2-up booklet from PDF on Letter where the number of pages don't fill the whole sheet + {"TestBookletFromPDF2UpLetterWithTrailingBlankPages", + []string{filepath.Join(inDir, "bookletTest.pdf")}, + filepath.Join(outDir, "BookletFromPDFLetter_2UpWithTrailingBlankPages.pdf"), + []string{"1-14"}, + "p:LetterP, g:on, ma:10, bgcol:#f7e6c7", + "points", + 2, + false, + }, + + // more nup + {"TestBookletFromPDF_2up_perfectbound", + []string{filepath.Join(inDir, "bookletTest.pdf")}, + filepath.Join(outDir, "BookletFromPDFLetter_2Up_perfectbound.pdf"), + []string{"1-24"}, + "p:LetterP, g:on, btype:perfectbound", + "points", + 2, + false, + }, + {"TestBookletFromPDF_6up", + []string{filepath.Join(inDir, "bookletTest.pdf")}, + filepath.Join(outDir, "BookletFromPDFLedger_6Up.pdf"), + []string{"1-24"}, + "p:LedgerP, g:on", + "points", + 6, + false, + }, + {"TestBookletFromPDF_8up", + []string{filepath.Join(inDir, "bookletTest.pdf")}, + filepath.Join(outDir, "BookletFromPDFLedger_8Up.pdf"), + []string{"1-32"}, + "p:LedgerP, g:on", + "points", + 8, + false, + }, + + // misc orientations and booklet types on 4-up + {"TestBookletFromPDF_4up_portrait_short", + []string{filepath.Join(inDir, "bookletTest.pdf")}, + filepath.Join(outDir, "BookletFromPDFLedger_4Up_portrait_short.pdf"), + []string{"1-24"}, + "p:LedgerP, g:on, binding:short", + "points", + 4, + false, + }, + {"TestBookletFromPDF_4up_landscape_long", + []string{filepath.Join(inDir, "bookletTestLandscape.pdf")}, + filepath.Join(outDir, "BookletFromPDFLedger_4Up_landscape_long.pdf"), + []string{"1-24"}, + "p:LedgerL, g:on", + "points", + 4, + false, + }, + {"TestBookletFromPDF_4up_landscape_short", + []string{filepath.Join(inDir, "bookletTestLandscape.pdf")}, + filepath.Join(outDir, "BookletFromPDFLedger_4Up_landscape_short.pdf"), + []string{"1-24"}, + "p:LedgerL, g:on, binding:short", + "points", + 4, + false, + }, + {"TestBookletFromPDF_4up-portrait_long_advanced", + []string{filepath.Join(inDir, "bookletTest.pdf")}, + filepath.Join(outDir, "BookletFromPDFLedger_4Up_portrait_long_advanced.pdf"), + []string{"1-24"}, + "p:LedgerP, g:on, btype:bookletadvanced", + "points", + 4, + false, + }, + {"TestBookletFromPDF_4up_landscape_short_advanced", + []string{filepath.Join(inDir, "bookletTestLandscape.pdf")}, + filepath.Join(outDir, "BookletFromPDFLedger_4Up_landscape_short_advanced.pdf"), + []string{"1-24"}, + "p:LedgerL, g:on, binding:short, btype:bookletadvanced", + "points", + 4, + false, + }, + {"TestBookletFromPDF_4up_perfectbound", + []string{filepath.Join(inDir, "bookletTest.pdf")}, + filepath.Join(outDir, "BookletFromPDFLedger_4Up_perfectbound.pdf"), + []string{"1-24"}, + "p:LedgerP, g:on, btype:perfectbound", + "points", + 4, + false, + }, + + // 2-up multi folio booklet from PDF on A4 using 8 sheets per folio + // using the default foliosize:8 + // Here we print 2 complete folios (2 x 8 sheets) + 1 partial folio + // See also https://www.instructables.com/How-to-bind-your-own-Hardback-Book/ + {"TestHardbackBookFromPDF", + []string{filepath.Join(inDir, "WaldenFull.pdf")}, + filepath.Join(outDir, "HardbackBookFromPDF.pdf"), + []string{"1-70"}, + "p:A4, multifolio:on, border:off, g:on, ma:10, bgcol:#beded9", + "points", + 2, + false, + }, + } { + conf := model.NewDefaultConfiguration() + conf.SetUnit(tt.unit) + testBooklet(t, tt.msg, tt.inFiles, tt.outFile, tt.selectedPages, tt.desc, tt.n, tt.isImg, conf) + } +} diff --git a/pkg/api/test/bookmark_test.go b/pkg/api/test/bookmark_test.go new file mode 100644 index 0000000000000000000000000000000000000000..fa4c131bd4d30b0207e13a7f28252da48a8ca9ad --- /dev/null +++ b/pkg/api/test/bookmark_test.go @@ -0,0 +1,187 @@ +/* + Copyright 2020 The pdfcpu Authors. + + 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 + + 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 test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" +) + +// Acrobat Reader "Bookmarks" = Mac Preview "Table of Contents". +// Mac Preview limitations: does not render color, style, outline tree collapsed by default. + +func listBookmarksFile(t *testing.T, fileName string, conf *model.Configuration) ([]string, error) { + t.Helper() + + msg := "listBookmarks" + + f, err := os.Open(fileName) + if err != nil { + t.Fatalf("%s open: %v\n", msg, err) + } + defer f.Close() + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.LISTBOOKMARKS + + ctx, err := api.ReadValidateAndOptimize(f, conf) + if err != nil { + t.Fatalf("%s ReadValidateAndOptimize: %v\n", msg, err) + } + + return pdfcpu.BookmarkList(ctx) +} + +func TestListBookmarks(t *testing.T) { + msg := "TestListBookmarks" + inDir := filepath.Join("..", "..", "samples", "bookmarks") + inFile := filepath.Join(inDir, "bookmarkTree.pdf") + + if _, err := listBookmarksFile(t, inFile, nil); err != nil { + t.Fatalf("%s list bookmarks: %v\n", msg, err) + } +} + +func InactiveTestAddDuplicateBookmarks(t *testing.T) { + msg := "TestAddDuplicateBookmarks" + inFile := filepath.Join(inDir, "CenterOfWhy.pdf") + outFile := filepath.Join("..", "..", "samples", "bookmarks", "bookmarkDuplicates.pdf") + + bms := []pdfcpu.Bookmark{ + {PageFrom: 2, Title: "Duplicate Name"}, + {PageFrom: 3, Title: "Duplicate Name"}, + {PageFrom: 5, Title: "Duplicate Name"}, + } + + replace := true // Replace existing bookmarks. + if err := api.AddBookmarksFile(inFile, outFile, bms, replace, nil); err != nil { + t.Fatalf("%s addBookmarks: %v\n", msg, err) + } + if err := api.ValidateFile(outFile, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestAddSimpleBookmarks(t *testing.T) { + msg := "TestAddSimpleBookmarks" + inFile := filepath.Join(inDir, "CenterOfWhy.pdf") + outFile := filepath.Join("..", "..", "samples", "bookmarks", "bookmarkSimple.pdf") + + bookmarkColor := color.NewSimpleColor(0xab6f30) + + // TODO Emoji support! + + bms := []pdfcpu.Bookmark{ + {PageFrom: 1, Title: "Page 1: Applicant’s Form"}, + {PageFrom: 2, Title: "Page 2: Bold 这是一个测试", Bold: true}, + {PageFrom: 3, Title: "Page 3: Italic 测试 尾巴", Italic: true, Bold: true}, + {PageFrom: 4, Title: "Page 4: Bold & Italic", Bold: true, Italic: true}, + {PageFrom: 16, Title: "Page 16: The birthday of Smalltalk", Color: &bookmarkColor}, + {PageFrom: 17, Title: "Page 17: Gray", Color: &color.Gray}, + {PageFrom: 18, Title: "Page 18: Red", Color: &color.Red}, + {PageFrom: 19, Title: "Page 19: Bold Red", Color: &color.Red, Bold: true}, + } + + replace := true // Replace existing bookmarks. + if err := api.AddBookmarksFile(inFile, outFile, bms, replace, nil); err != nil { + t.Fatalf("%s addBookmarks: %v\n", msg, err) + } + if err := api.ValidateFile(outFile, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestAddBookmarkTree2Levels(t *testing.T) { + msg := "TestAddBookmarkTree2Levels" + inFile := filepath.Join(inDir, "CenterOfWhy.pdf") + outFile := filepath.Join("..", "..", "samples", "bookmarks", "bookmarkTree.pdf") + + bms := []pdfcpu.Bookmark{ + {PageFrom: 1, Title: "Page 1: Level 1", Color: &color.Green, + Kids: []pdfcpu.Bookmark{ + {PageFrom: 2, Title: "Page 2: Level 1.1"}, + {PageFrom: 3, Title: "Page 3: Level 1.2", + Kids: []pdfcpu.Bookmark{ + {PageFrom: 4, Title: "Page 4: Level 1.2.1"}, + }}, + }}, + {PageFrom: 5, Title: "Page 5: Level 2", Color: &color.Blue, + Kids: []pdfcpu.Bookmark{ + {PageFrom: 6, Title: "Page 6: Level 2.1"}, + {PageFrom: 7, Title: "Page 7: Level 2.2"}, + {PageFrom: 8, Title: "Page 8: Level 2.3"}, + }}, + } + + if err := api.AddBookmarksFile(inFile, outFile, bms, false, nil); err != nil { + t.Fatalf("%s addBookmarks: %v\n", msg, err) + } + if err := api.ValidateFile(outFile, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestRemoveBookmarks(t *testing.T) { + msg := "TestRemoveBookmarks" + inDir := filepath.Join("..", "..", "samples", "bookmarks") + inFile := filepath.Join(inDir, "bookmarkTree.pdf") + outFile := filepath.Join(inDir, "bookmarkTreeNoBookmarks.pdf") + + if err := api.RemoveBookmarksFile(inFile, outFile, nil); err != nil { + t.Fatalf("%s removeBookmarks: %v\n", msg, err) + } + if err := api.ValidateFile(outFile, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestExportBookmarks(t *testing.T) { + msg := "TestExportBookmarks" + inDir := filepath.Join("..", "..", "samples", "bookmarks") + inFile := filepath.Join(inDir, "bookmarkTree.pdf") + outFile := filepath.Join(inDir, "bookmarkTree.json") + + if err := api.ExportBookmarksFile(inFile, outFile, nil); err != nil { + t.Fatalf("%s export bookmarks: %v\n", msg, err) + } +} + +func TestImportBookmarks(t *testing.T) { + msg := "TestImportBookmarks" + inDir := filepath.Join("..", "..", "samples", "bookmarks") + inFile := filepath.Join(inDir, "bookmarkTree.pdf") + inFileJSON := filepath.Join(inDir, "bookmarkTree.json") + outFile := filepath.Join(inDir, "bookmarkTreeImported.pdf") + + replace := true + if err := api.ImportBookmarksFile(inFile, inFileJSON, outFile, replace, nil); err != nil { + t.Fatalf("%s importBookmarks: %v\n", msg, err) + } + if err := api.ValidateFile(outFile, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} diff --git a/pkg/api/test/box_test.go b/pkg/api/test/box_test.go new file mode 100644 index 0000000000000000000000000000000000000000..ca0a392f09cb82f2d573a3ff15aa6f8268b35eaa --- /dev/null +++ b/pkg/api/test/box_test.go @@ -0,0 +1,152 @@ +/* +Copyright 2019 The pdf Authors. + +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 + + 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 test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +func listBoxes(t *testing.T, fileName string, pb *model.PageBoundaries) ([]string, error) { + t.Helper() + + msg := "listBoxes" + + f, err := os.Open(fileName) + if err != nil { + t.Fatalf("%s open: %v\n", msg, err) + } + defer f.Close() + + ctx, err := api.ReadValidateAndOptimize(f, conf) + if err != nil { + t.Fatalf("%s ReadValidateAndOptimize: %v\n", msg, err) + } + + if pb == nil { + pb = &model.PageBoundaries{} + pb.SelectAll() + } + + return ctx.ListPageBoundaries(nil, pb) +} + +func TestListBoxes(t *testing.T) { + msg := "TestListBoxes" + inFile := filepath.Join(inDir, "5116.DCT_Filter.pdf") + + if _, err := listBoxes(t, inFile, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + // List crop box for all pages. + pb, err := api.PageBoundariesFromBoxList("crop") + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + if _, err := listBoxes(t, inFile, pb); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestCrop(t *testing.T) { + msg := "TestCrop" + inFile := filepath.Join(inDir, "test.pdf") + outFile := filepath.Join(outDir, "out.pdf") + + for _, tt := range []struct { + s string + u types.DisplayUnit + }{ + {"[0 0 5 5]", types.CENTIMETRES}, + {"100", types.POINTS}, + {"20% 40%", types.POINTS}, + {"dim:30 30", types.POINTS}, + {"dim:50% 50%", types.POINTS}, + {"pos:bl, dim:50% 50%", types.POINTS}, + {"pos:tl, off: 10 -10, dim:50% 50%", types.POINTS}, + {"pos:tl, dim:.5 1 rel", types.POINTS}, + {"-1", types.INCHES}, + {"-25%", types.POINTS}, + } { + box, err := api.Box(tt.s, tt.u) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + if err := api.CropFile(inFile, outFile, nil, box, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + } +} + +func TestAddBoxes(t *testing.T) { + msg := "TestAddBoxes" + inFile := filepath.Join(inDir, "test.pdf") + outFile := filepath.Join(outDir, "out.pdf") + + for _, tt := range []struct { + s string + u types.DisplayUnit + }{ + {"art:10%", types.POINTS}, // When using relative positioning unit is irrelevant. + {"trim:10", types.POINTS}, + {"crop:[0 0 5 5]", types.CENTIMETRES}, // Crop 5 x 5 cm at bottom left corner + {"crop:10", types.POINTS}, + {"crop:-10", types.POINTS}, + {"crop:10 20, trim:crop, art:bleed, bleed:art", types.POINTS}, + {"crop:10 20, trim:crop, art:bleed, bleed:media", types.POINTS}, + {"c:10 20, t:c, a:b, b:m", types.POINTS}, + {"crop:10, trim:20, art:trim", types.POINTS}, + } { + pb, err := api.PageBoundaries(tt.s, tt.u) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + if err := api.AddBoxesFile(inFile, outFile, nil, pb, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + } +} + +func TestAddRemoveBoxes(t *testing.T) { + msg := "TestAddRemoveBoxes" + inFile := filepath.Join(inDir, "test.pdf") + outFile := filepath.Join(outDir, "out.pdf") + + pb, err := api.PageBoundaries("crop:[0 0 100 100]", types.POINTS) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + if err := api.AddBoxesFile(inFile, outFile, nil, pb, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + pb, err = api.PageBoundariesFromBoxList("crop") + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + if err := api.RemoveBoxesFile(outFile, outFile, nil, pb, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} diff --git a/pkg/api/test/collect_test.go b/pkg/api/test/collect_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d3183deb41b23923dd06e723f3f9e6186e31196f --- /dev/null +++ b/pkg/api/test/collect_test.go @@ -0,0 +1,72 @@ +/* +Copyright 2020 The pdf Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" +) + +func TestCollect(t *testing.T) { + msg := "TestCollect" + + inFile := filepath.Join(inDir, "pike-stanford.pdf") + outFile := filepath.Join(outDir, "myPageSequence.pdf") + + // Start with all odd pages but page 1, then append pages 8-11 and the last page. + if err := api.CollectFile(inFile, outFile, []string{"odd", "!1", "8-11", "l"}, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + if err := api.ValidateFile(outFile, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestCollectLowLevel(t *testing.T) { + msg := "TestCollectLowLevel" + inFile := filepath.Join(inDir, "pike-stanford.pdf") + outFile := filepath.Join(outDir, "MyCollectedPages.pdf") + + // Create a context. + ctx, err := api.ReadContextFile(inFile) + if err != nil { + t.Fatalf("%s readContext: %v\n", msg, err) + } + + // Collect pages. + selectedPages, err := api.PagesForPageCollection(ctx.PageCount, []string{"odd", "!1", "8-11", "l"}) + if err != nil { + t.Fatalf("%s PagesForPageCollection: %v\n", msg, err) + } + + usePgCache := true + ctxNew, err := pdfcpu.ExtractPages(ctx, selectedPages, usePgCache) + if err != nil { + t.Fatalf("%s ExtractPages: %v\n", msg, err) + } + + // Here you can process this single page PDF context. + + // Write context to file. + if err := api.WriteContextFile(ctxNew, outFile); err != nil { + t.Fatalf("%s write: %v\n", msg, err) + } +} diff --git a/pkg/api/test/concurrency_test.go b/pkg/api/test/concurrency_test.go new file mode 100644 index 0000000000000000000000000000000000000000..3654c361f0bdc615a4fbc08a7f13c3ac1fc05336 --- /dev/null +++ b/pkg/api/test/concurrency_test.go @@ -0,0 +1,49 @@ +/* +Copyright 2023 The pdfcpu Authors. + +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 + + 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 test + +import ( + "sync" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" +) + +func TestDisableConfigDir(t *testing.T) { + t.Parallel() + api.DisableConfigDir() + + if model.ConfigPath != "disable" { + t.Errorf("model.ConfigPath != \"disable\" (%s)", model.ConfigPath) + } +} + +func TestDisableConfigDir_Parallel(t *testing.T) { + t.Parallel() + + var wg sync.WaitGroup + for i := 0; i < 100; i++ { + wg.Add(1) + go func() { + defer wg.Done() + api.DisableConfigDir() + }() + } + wg.Wait() + t.Log("DisableConfigDir passed") +} diff --git a/pkg/api/test/createFromJSON_test.go b/pkg/api/test/createFromJSON_test.go new file mode 100644 index 0000000000000000000000000000000000000000..8bbd8302e32322134e8d270228176689e5def542 --- /dev/null +++ b/pkg/api/test/createFromJSON_test.go @@ -0,0 +1,356 @@ +/* +Copyright 2023 The pdf Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" +) + +/************************************************************** + * All form related processing is optimized for Adobe Reader! * + **************************************************************/ + +func createPDF(t *testing.T, msg, inFile, inFileJSON, outFile string, conf *model.Configuration) { + + t.Helper() + + // inFile inFileJSON outFile action + // --------------------------------------------------------------- + // "" jsonFile outfile write outFile + // inFile jsonFile "" update (read and write inFile) + // inFile jsonFile outFile read inFile and write outFile + + if outFile == "" { + outFile = inFile + } + + if err := api.CreateFile(inFile, inFileJSON, outFile, conf); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + if err := api.ValidateFile(outFile, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + +} + +func TestCreateContentPrimitivesViaJson(t *testing.T) { + + t.Helper() + inDir := filepath.Join(inDir, "json", "create") + outDir := filepath.Join(samplesDir, "create", "primitives") + + for _, tt := range []struct { + msg string + inFileJSON string + outFile string + }{ + // Render page content samples. + + // Font + {"TestFonts", "fonts.json", "fonts.pdf"}, + + // Text + {"TestTextAnchored", "textAnchored.json", "textAnchored.pdf"}, + {"TestTextBordersAndPaddings", "textBordersAndPaddings.json", "textBordersAndPaddings.pdf"}, + {"TestTextAlignment", "textAndAlignment.json", "textAndAlignment.pdf"}, + + // Image + {"TestImages", "images.json", "images.pdf"}, + {"TestImagesOptimized", "imagesOptimized.json", "imagesOptimized.pdf"}, + {"TestImagesDirsFiles", "imagesDirsFiles.json", "imagesDirsFiles.pdf"}, + + // Box + {"TestBoxesAndColors", "boxesAndColors.json", "boxesAndColors.pdf"}, + {"TestBoxesAndMargin", "boxesAndMargin.json", "boxesAndMargin.pdf"}, + {"TestBoxesAndRotation", "boxesAndRotation.json", "boxesAndRotation.pdf"}, + + // Table + {"TestTable", "table.json", "table.pdf"}, + {"TestTableRTL", "tableRTL.json", "tableRTL.pdf"}, + {"TestTableCJK", "tableCJK.json", "tableCJK.pdf"}, + + // Content Region + {"TestRegions", "regions.json", "regions.pdf"}, + {"TestRegionsMarginBorderPadding", "regionsMargBordPadd.json", "regionsMarginBorderPadding.pdf"}, + } { + inFileJSON := filepath.Join(inDir, tt.inFileJSON) + outFile := filepath.Join(outDir, tt.outFile) + createPDF(t, tt.msg, "", inFileJSON, outFile, conf) + } + +} + +func TestCreateFormPrimitivesViaJson(t *testing.T) { + + inDirForm := filepath.Join(inDir, "json", "form") + outDirForm := filepath.Join(samplesDir, "form", "primitives") + + for _, tt := range []struct { + msg string + inFileJSON string + outFile string + }{ + // Render form field samples. + + // Textfield + {"TestTextfield", "textfield.json", "textfield.pdf"}, + {"TestTextfieldGroup", "textfieldGroup.json", "textfieldGroup.pdf"}, + {"TestTextfieldGroupSingle", "textfieldGroupSingle.json", "textfieldGroupSingle.pdf"}, + + // Textarea + {"TestTextarea", "textarea.json", "textarea.pdf"}, + {"TestTextareaGroup", "textareaGroup.json", "textareaGroup.pdf"}, + + // Datefield + {"TestDatefield", "datefield.json", "datefield.pdf"}, + {"TestDatefieldGroup", "datefieldGroup.json", "datefieldGroup.pdf"}, + + // Checkbox + {"TestCheckbox", "checkbox.json", "checkbox.pdf"}, + {"TestCheckboxGroup", "checkboxGroup.json", "checkboxGroup.pdf"}, + + // Radio button group + {"TestRadiobuttonsHor", "radiobuttonsHor.json", "radiobuttonsHor.pdf"}, + {"TestRadiobuttonsHorGroup", "radiobuttonsHorGroup.json", "radiobuttonsHorGroup.pdf"}, + {"TestRadiobuttonsVertLeft", "radiobuttonsVertL.json", "radiobuttonsVertL.pdf"}, + {"TestRadiobuttonsVertLeftGroup", "radiobuttonsVertLGroup.json", "radiobuttonsVertLGroup.pdf"}, + {"TestRadiobuttonsVertRight", "radiobuttonsVertR.json", "radiobuttonsVertR.pdf"}, + {"TestRadiobuttonsVertRightGroup", "radiobuttonsVertRGroup.json", "radiobuttonsVertRGroup.pdf"}, + + // Combobox + {"TestCombobox", "combobox.json", "combobox.pdf"}, + {"TestComboboxGroup", "comboboxGroup.json", "comboboxGroup.pdf"}, + + // Listbox + {"TestListbox", "listbox.json", "listbox.pdf"}, + {"TestListboxGroup", "listboxGroup.json", "listboxGroup.pdf"}, + } { + inFileJSON := filepath.Join(inDirForm, tt.inFileJSON) + outFile := filepath.Join(outDirForm, tt.outFile) + createPDF(t, tt.msg, "", inFileJSON, outFile, conf) + } + +} + +func TestCreateSinglePageDemoFormsViaJson(t *testing.T) { + + // Render single page demo forms for export, reset, lock, unlock and fill tests. + + inDirFormDemo := filepath.Join(inDir, "json", "form", "demoSinglePage") + outDirFormDemo := filepath.Join(samplesDir, "form", "demoSinglePage") + + for _, tt := range []struct { + msg string + inFileJSON string + outFile string + }{ + {"TestFormDemoEN", "english.json", "english.pdf"}, // Core font (Helvetica) + {"TestFormDemoUK", "ukrainian.json", "ukrainian.pdf"}, // User font (Roboto-Regular) + {"TestFormDemoAR", "arabic.json", "arabic.pdf"}, // User font RTL (Roboto-Regular) + {"TestFormDemoSC", "chineseSimple.json", "chineseSimple.pdf"}, // User font CJK (UnifontMedium) + {"TestPersonFormDemo", "person.json", "person.pdf"}, // Person Form + } { + inFileJSON := filepath.Join(inDirFormDemo, tt.inFileJSON) + outFile := filepath.Join(outDirFormDemo, tt.outFile) + createPDF(t, tt.msg, "", inFileJSON, outFile, conf) + } + +} + +func TestCreateDemoFormsViaJson(t *testing.T) { + + inDirFormDemo := filepath.Join(inDir, "json", "form", "demo") + outDirFormDemo := filepath.Join(samplesDir, "form", "demo") + + for _, tt := range []struct { + msg string + inFileJSON string + outFile string + }{ + // Render demo forms. + + // For corrections please open an issue on Github. + + // Core font (Helvetica) + {"TestFormDemoDM", "danish.json", "danish.pdf"}, + {"TestFormDemoNL", "dutch.json", "dutch.pdf"}, + {"TestFormDemoEN", "english.json", "english.pdf"}, + {"TestFormDemoFI", "finnish.json", "finnish.pdf"}, + {"TestFormDemoFR", "french.json", "french.pdf"}, + {"TestFormDemoDE", "german.json", "german.pdf"}, + {"TestFormDemoHU", "hungarian.json", "hungarian.pdf"}, + {"TestFormDemoIN", "indonesian.json", "indonesian.pdf"}, + {"TestFormDemoIC", "icelandic.json", "icelandic.pdf"}, + {"TestFormDemoIT", "italian.json", "italian.pdf"}, + {"TestFormDemoNO", "norwegian.json", "norwegian.pdf"}, + {"TestFormDemoPT", "portuguese.json", "portuguese.pdf"}, + {"TestFormDemoSK", "slovak.json", "slovak.pdf"}, + {"TestFormDemoSL", "slovenian.json", "slovenian.pdf"}, + {"TestFormDemoES", "spanish.json", "spanish.pdf"}, + {"TestFormDemoSWA", "swahili.json", "swahili.pdf"}, + {"TestFormDemoSWE", "swedish.json", "swedish.pdf"}, + + // User font (Roboto-Regular) + {"TestFormDemoBR", "belarusian.json", "belarusian.pdf"}, + {"TestFormDemoBG", "bulgarian.json", "bulgarian.pdf"}, + {"TestFormDemoCR", "croatian.json", "croatian.pdf"}, + {"TestFormDemoCZ", "czech.json", "czech.pdf"}, + {"TestFormDemoGR", "greek.json", "greek.pdf"}, + {"TestFormDemoKU", "kurdish.json", "kurdish.pdf"}, + {"TestFormDemoPO", "polish.json", "polish.pdf"}, + {"TestFormDemoRO", "romanian.json", "romanian.pdf"}, + {"TestFormDemoRU", "russian.json", "russian.pdf"}, + {"TestFormDemoTK", "turkish.json", "turkish.pdf"}, + {"TestFormDemoUK", "ukrainian.json", "ukrainian.pdf"}, + {"TestFormDemoVI", "vietnamese.json", "vietnamese.pdf"}, + + // User font (UnifontMedium) + {"TestFormDemoAR", "arabic.json", "arabic.pdf"}, + {"TestFormDemoARM", "armenian.json", "armenian.pdf"}, + {"TestFormDemoAZ", "azerbaijani.json", "azerbaijani.pdf"}, + {"TestFormDemoBA", "bangla.json", "bangla.pdf"}, + {"TestFormDemoSC", "chineseSimple.json", "chineseSimple.pdf"}, + {"TestFormDemoTC", "chineseTrad.json", "chineseTraditional.pdf"}, + {"TestFormDemoHE", "hebrew.json", "hebrew.pdf"}, + {"TestFormDemoHI", "hindi.json", "hindi.pdf"}, + {"TestFormDemoJP", "japanese.json", "japanese.pdf"}, + {"TestFormDemoKR", "korean.json", "korean.pdf"}, + {"TestFormDemoMA", "marathi.json", "marathi.pdf"}, + {"TestFormDemoPE", "persian.json", "persian.pdf"}, + {"TestFormDemoUR", "thai.json", "thai.pdf"}, + {"TestFormDemoUR", "urdu.json", "urdu.pdf"}, + } { + inFileJSON := filepath.Join(inDirFormDemo, tt.inFileJSON) + outFile := filepath.Join(outDirFormDemo, tt.outFile) + createPDF(t, tt.msg, "", inFileJSON, outFile, conf) + } + +} + +func TestCreateAndUpdatePageViaJson(t *testing.T) { + + // CREATE PDF, UPDATE/ADD PAGE + // 1. Create PDF page + // 2. Add textbox and reuse corefont/userfont/cjkfont + // a) from same page + // b) on different page + + jsonDir := filepath.Join(inDir, "json", "create", "flow") + outDir := filepath.Join(samplesDir, "create", "flow") + + // Create PDF in outDir. + inFileJSON1 := filepath.Join(jsonDir, "createPage.json") + file := filepath.Join(outDir, "createAndUpdatePage.pdf") + createPDF(t, "pass1", "", inFileJSON1, file, conf) + + // Update PDF in outDir: reuse fonts from (same) page 1 + inFileJSON2 := filepath.Join(jsonDir, "updatePage1.json") + createPDF(t, "pass2", file, inFileJSON2, file, conf) + + // Update PDF in outDir: reuse fonts from (different) page 1 + inFileJSON3 := filepath.Join(jsonDir, "updatePage2.json") + createPDF(t, "pass2", file, inFileJSON3, file, conf) +} + +func TestReadAndUpdatePageViaJson(t *testing.T) { + + // READ PDF, UPDATE/ADD PAGE + // 1. Read any PDF + // 2. Add textbox and reuse corefont/userfont/cjkfont + // a) from same page + // b) on different page + + jsonDir := filepath.Join(inDir, "json", "create", "flow") + outDir := filepath.Join(samplesDir, "create", "flow") + + // Update PDF in outDir. + inFile := filepath.Join(inDir, "Walden.pdf") + inFileJSON1 := filepath.Join(jsonDir, "updateAnyPage1.json") + outFile := filepath.Join(outDir, "readAndUpdatePage.pdf") + createPDF(t, "pass", inFile, inFileJSON1, outFile, conf) + + // Update PDF in outDir: reuse fonts from (different) page 1 and create new page + inFileJSON2 := filepath.Join(jsonDir, "updateAnyPage2.json") + createPDF(t, "pass2", outFile, inFileJSON2, outFile, conf) +} + +func TestCreateFormAndUpdatePageViaJson(t *testing.T) { + + // CREATE FORM, UPDATE/ADD PAGE + // 1. Create PDF form + // 2. Add content + + jsonDir := filepath.Join(inDir, "json", "form") + outDir := filepath.Join(samplesDir, "form", "flow") + + // Create PDF form in outDir and add content using corefont. + inFileJSON1 := filepath.Join(jsonDir, "demo", "english.json") + file := filepath.Join(outDir, "createFormAndUpdatePageCoreFont.pdf") + createPDF(t, "pass1", "", inFileJSON1, file, conf) + // Update PDF form in outDir reusing page font. + inFileJSON2 := filepath.Join(jsonDir, "flow", "updatePageCoreFont.json") + createPDF(t, "pass2", file, inFileJSON2, file, conf) + + // Create PDF form in outDir and add content using userfont. + inFileJSON1 = filepath.Join(jsonDir, "demo", "ukrainian.json") + file = filepath.Join(outDir, "createFormAndUpdatePageUserFont.pdf") + createPDF(t, "pass1", "", inFileJSON1, file, conf) + // Update PDF form in outDir reusing page font. + inFileJSON2 = filepath.Join(jsonDir, "flow", "updatePageUserFont.json") + createPDF(t, "pass2", file, inFileJSON2, file, conf) + + // Create PDF form in outDir and add content using CJK userfont. + inFileJSON1 = filepath.Join(jsonDir, "demo", "chineseSimple.json") + file = filepath.Join(outDir, "createFormAndUpdatePageCJKUserFont.pdf") + createPDF(t, "pass1", "", inFileJSON1, file, conf) + // Update PDF form in outDir reusing page font. + inFileJSON2 = filepath.Join(jsonDir, "flow", "updatePageCJKUserFont.json") + createPDF(t, "pass2", file, inFileJSON2, file, conf) +} + +func TestReadFormAndUpdateFormViaJson(t *testing.T) { + + // READ FORM, UPDATE FORM + // 1. Read PDF form + // 2. Add fields + + jsonDir := filepath.Join(inDir, "json", "form", "flow") + outDir := filepath.Join(samplesDir, "form", "flow") + + // Read demo form and update with corefont in outDir. + inFile := filepath.Join(samplesDir, "form", "demo", "english.pdf") + inFileJSON := filepath.Join(jsonDir, "updateFormCoreFont.json") + outFile := filepath.Join(outDir, "readFormAndUpdateFormCoreFont.pdf") + createPDF(t, "pass1", inFile, inFileJSON, outFile, conf) + + // Read demo form and update with userfont in outDir. + inFile = filepath.Join(samplesDir, "form", "demo", "ukrainian.pdf") + inFileJSON = filepath.Join(jsonDir, "updateFormUserFont.json") + outFile = filepath.Join(outDir, "readFormAndUpdateFormUserFont.pdf") + createPDF(t, "pass1", inFile, inFileJSON, outFile, conf) + + // Read demo form and update with CJK userfont in outDir. + inFile = filepath.Join(samplesDir, "form", "demo", "chineseSimple.pdf") + inFileJSON = filepath.Join(jsonDir, "updateFormCJK.json") + outFile = filepath.Join(outDir, "readFormAndUpdateFormCJK.pdf") + createPDF(t, "pass1", inFile, inFileJSON, outFile, conf) +} diff --git a/pkg/api/test/createUserFont_test.go b/pkg/api/test/createUserFont_test.go new file mode 100644 index 0000000000000000000000000000000000000000..ac47b98998130f6183c851782bc0d92d53002926 --- /dev/null +++ b/pkg/api/test/createUserFont_test.go @@ -0,0 +1,580 @@ +/* +Copyright 2020 The pdf Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +const ( + sampleArabic = `الإعلان العالمي لحقوق الإنسان + +المادة 1. +يولد جميع الناس أحرارًا ومتساوين في الكرامة والحقوق. +وقد وهبوا عقلاً وضميرًا وعليهم أن يعامل بعضهم بعضًا بروح الإخاء. + +المادة 2. +لكل فرد الحق في جميع الحقوق والحريات المنصوص عليها في هذا الإعلان ، دون تمييز من أي نوع ، +مثل العرق أو اللون أو الجنس أو اللغة أو الدين أو الرأي السياسي أو غير السياسي أو الأصل القومي أو الاجتماعي أو الملكية أو +ولادة أو حالة أخرى. علاوة على ذلك ، لا يجوز التمييز على أساس سياسي أو قضائي أو +الوضع الدولي للبلد أو الإقليم الذي ينتمي إليه الشخص ، سواء كان مستقلاً ، أو محل ثقة ، +غير متمتع بالحكم الذاتي أو تحت أي قيود أخرى على السيادة.` + + sampleArmenian = `Մարդու իրավունքների համընդհանուր հռչակագիր + +Հոդված 1. +Բոլոր մարդիկ ծնվում են ազատ և հավասար ՝ արժանապատվությամբ և իրավունքներով: +Նրանք օժտված են բանականությամբ և խղճով և պետք է եղբայրության ոգով գործեն միմյանց նկատմամբ: + +Հոդված 2. +Յուրաքանչյուր ոք ունի սույն Հռչակագրում ամրագրված բոլոր իրավունքներն ու ազատությունները առանց որևէ տեսակի տարբերակման, +ինչպիսիք են ռասան, գույնը, սեռը, լեզուն, կրոնը, քաղաքական կամ այլ կարծիքը, ազգային կամ սոցիալական ծագումը, սեփականությունը, +ծնունդ կամ այլ կարգավիճակ: Ավելին, որևէ տարբերակում չի կարող դրվել քաղաքական, իրավասության կամ իրավունքի հիման վրա +երկրի կամ տարածքի միջազգային կարգավիճակը, որին պատկանում է անձը, անկախ այն լինելուց, վստահություն, +ոչ ինքնակառավարվող կամ ինքնիշխանության որևէ այլ սահմանափակման ներքո:` + + sampleAzerbaijani = `Ümumdünya İnsan Haqları Bəyannaməsi + +Maddə 1 +Bütün insanlar azad və ləyaqət və hüquqlara bərabər olaraq doğulurlar. +Onlara ağıl və vicdan bəxş edilmişdir və bir-birlərinə qardaşlıq ruhunda davranmalıdırlar. + +Maddə 2 +Hər kəs bu Bəyannamədə göstərilən bütün hüquq və azadlıqlara, heç bir fərq qoymadan, +irqi, rəngi, cinsi, dili, dini, siyasi və ya digər fikri, milli və ya sosial mənşəyi, mülkiyyəti, +doğum və ya digər vəziyyət. Bundan əlavə, siyasi, yurisdiksiyalı və ya əsas götürülərək heç bir fərq qoyulmayacaqdır +bir şəxsin mənsub olduğu ölkənin və ya ərazinin beynəlxalq statusu, müstəqil, etibarlı, +özünüidarə etməmək və ya suverenliyin hər hansı digər məhdudiyyəti altında.` + + sampleBangla = `মানবাধিকারের সর্বজনীন ঘোষণা: + +অনুচ্ছেদ 1। +সমস্ত মানুষ মর্যাদা ও অধিকারে স্বাধীন ও সমান জন্মগ্রহণ করে। +এগুলি যুক্তি ও বিবেকের অধিকারী এবং ভ্রাতৃত্বের মনোভাবের সাথে একে অপরের প্রতি আচরণ করা উচিত। + +অনুচ্ছেদ 2। +এই ঘোষণাপত্রে নির্ধারিত সমস্ত অধিকার এবং স্বাধীনতার জন্য প্রত্যেকেই অধিকারপ্রাপ্ত, +কোনও প্রকারভেদ ছাড়াই, যেমন জাতি, বর্ণ, লিঙ্গ, ভাষা, ধর্ম, রাজনৈতিক বা অন্যান্য মতামত, +জাতীয় বা সামাজিক উত্স, সম্পত্তি, জন্ম বা অন্য অবস্থা। তদুপরি, রাজনৈতিক, +এখতিয়ার বা ভিত্তিতে কোনও পার্থক্য করা হবে না একজন ব্যক্তি যার দেশ বা অঞ্চলে তার আন্তর্জাতিক +অবস্থান, এটি স্বাধীন, বিশ্বাস, স্ব-শাসন পরিচালনা বা সার্বভৌমত্বের অন্য কোনও সীমাবদ্ধতার অধীনে।` + + sampleBelarusian = `УСЕАГУЛЬНАЯ ДЭКЛАРАЦЫЯ ПРАВОЎ ЧАЛАВЕКА + +Артыкул 1. +Усе людзi нараджаюцца свабоднымi i роўнымi ў сваёй годнасцi i правах. Яны надзелены розумам i сумленнем i павiнны ставiцца +адзiн да аднаго ў духу брацтва. + +Артыкул 2. +Кожны чалавек павiнен валодаць усiмi правамi i ўсiмi свабодамi, што абвешчаны гэтай Дэкларацыяй, без якога б там нi было адрознення, +як напрыклад у адносiнах расы, колеру скуры, полу, мовы, рэлiгii, палiтычных або iншых перакананняў, нацыянальнага або сацыяльнага +паходжання, маёмаснага, саслоўнага або iншага становiшча. +Апрача таго, не павiнна рабiцца нiякага адрознення на аснове палiтычнага, прававога або мiжнароднага статуса краiны або тэрыторыi, +да якой чалавек належыць, незалежна ад таго, цi з’яўляецца гэта тэрыторыя незалежнай, падапечнай, несамакiравальнай, +або як-небудзь iнакш абмежаванай у сваiм суверэнiтэце.` + + sampleChineseSimple = `世界人权宣言 + +第一条 +人人生而自由,在尊严和权利上一律平等。他们赋有理性和良心,并应以兄弟关系的精神相对待。 + +第二条 +人人有资格享有本宣言所载的一切权利和自由,不分种族、肤色、性别、语言、宗教、政治或其他见解、国籍或社会出身、财产、出生或其他身分等任何区别。 + +并且不得因一人所属的国家或领土的政治的、行政的或者国际的地位之不同而有所区别,无论该领土是独立领土、托管领土、非自治领土或者处于其他任何主权受限制的情况之下。` + + sampleChineseTraditional = `世界人權宣言 + +第一條 +人人生而自由,在尊嚴和權利上一律平等。他們賦有理性和良心,並應以兄弟關係的精神相對待。 + +第二條 +人人有資格享有本宣言所載的一切權利和自由,不分種族、膚色、性別、語言、宗教、政治或其他見解、國籍或社會出身、財產、出生或其他身分等任何區別。 + +並且不得因一人所屬的國家或領土的政治的、行政的或者國際的地位之不同而有所區別,無論該領土是獨立領土、託管領土、非自治領土或者處於其他任何主權受限制的情況之下。` + + sampleEnglish = `Universal Declaration of Human Rights + +Article 1. +All human beings are born free and equal in dignity and rights. +They are endowed with reason and conscience and should act towards one another in a spirit of brotherhood. + +Article 2. +Everyone is entitled to all the rights and freedoms set forth in this Declaration, without distinction of any kind, +such as race, colour, sex, language, religion, political or other opinion, national or social origin, property, +birth or other status. Furthermore, no distinction shall be made on the basis of the political, jurisdictional or +international status of the country or territory to which a person belongs, whether it be independent, trust, +non-self-governing or under any other limitation of sovereignty.` + + sampleFrench = `Déclaration universelle des droits de l'Homme + +Article 1. +Tous les êtres humains naissent libres et égaux en dignité et en droits. Ils sont doués de raison et de conscience et doivent agir les uns envers les autres +dans un esprit de fraternité. + +Article 2. +Chacun peut se prévaloir de tous les droits et de toutes les libertés proclamés dans la présente Déclaration, sans distinction aucune, notamment de race, +de couleur, de sexe, de langue, de religion, d'opinion politique ou de toute autre opinion, d'origine nationale ou sociale, de fortune, de naissance ou de toute +autre situation. De plus, il ne sera fait aucune distinction fondée sur le statut politique, juridique ou international du pays ou du territoire dont une personne +est ressortissante, que ce pays ou territoire soit indépendant, sous tutelle, non autonome ou soumis à une limitation quelconque de souveraineté.` + + sampleGerman = `Die Allgemeine Erklärung der Menschenrechte + + Artikel 1. +Alle Menschen sind frei und gleich an Würde und Rechten geboren. +Sie sind mit Vernunft und Gewissen begabt und sollen einander im Geist der Brüderlichkeit begegnen. + +Artikel 2. +Jeder hat Anspruch auf die in dieser Erklärung verkündeten Rechte und Freiheiten ohne irgendeinen Unterschied, +etwa nach Rasse, Hautfarbe, Geschlecht, Sprache, Religion, politischer oder sonstiger Überzeugung, nationaler +oder sozialer Herkunft, Vermögen, Geburt oder sonstigem Stand. +Des weiteren darf kein Unterschied gemacht werden auf Grund der politischen, rechtlichen oder internationalen +Stellung des Landes oder Gebiets, dem eine Person angehört, gleichgültig ob dieses unabhängig ist, unter Treuhandschaft steht, +keine Selbstregierung besitzt oder sonst in seiner Souveränität eingeschränkt ist.` + + sampleGreek = `ΟΙΚΟΥΜΕΝΙΚΗ ΔΙΑΚΗΡΥΞΗ ΓΙΑ ΤΑ ΑΝΘΡΩΠΙΝΑ ΔΙΚΑΙΩΜΑΤΑ + +ΑΡΘΡΟ 1 +Ολοι οι άνθρωποι γεννιούνται ελεύθεροι και ίσοι στην αξιοπρέπεια και τα δικαιώματα. +Είναι προικισμένοι με λογική και συνείδηση, και οφείλουν να συμπεριφέρονται μεταξύ τους με πνεύμα αδελφοσύνης. + +ΑΡΘΡΟ 2 +Κάθε άνθρωπος δικαιούται να επικαλείται όλα τα δικαιώματα και όλες τις ελευθερίες που προκηρύσσει η παρούσα Διακήρυξη, +χωρίς καμία απολύτως διάκριση, ειδικότερα ως προς τη φυλή, το χρώμα, το φύλο, τη γλώσσα, τις θρησκείες, τις πολιτικές +ή οποιεσδήποτε άλλες πεποιθήσεις, την εθνική ή κοινωνική καταγωγή, την περιουσία, τη γέννηση ή οποιαδήποτε άλλη κατάσταση.` + + sampleHebrew = `הצהרה האוניברסלית של זכויות האדם + +מאמר 1. +כל בני האדם נולדים חופשיים ושווים בכבודם ובזכויותיהם. +הם ניחנים בתבונה ובמצפון ועליהם לפעול זה כלפי זה ברוח אחווה. + +סעיף 2. +כל אחד זכאי לכל הזכויות והחירויות המופיעות בהצהרה זו, ללא הבחנה מכל סוג שהוא, +כגון גזע, צבע, מין, שפה, דת, דעה פוליטית או אחרת, מקור לאומי או חברתי, רכוש, +לידה או מעמד אחר. יתר על כן, לא תיעשה הבחנה על בסיס הפוליטי, השיפוט או +מעמד בינלאומי של המדינה או השטח שאדם משתייך אליו, בין אם זה עצמאי, אמון, +לא ממשל עצמי או תחת כל מגבלה אחרת של ריבונות.` + + sampleHindi = `मानव अधिकारों की सार्वभौम घोषणा + +लेख 1। +सभी मनुष्यों का जन्म स्वतंत्र और समान सम्मान और अधिकार में हुआ है। +वे तर्क और विवेक के साथ संपन्न होते हैं और भाईचारे की भावना से एक दूसरे के प्रति कार्य करना चाहिए। + +अनुच्छेद 2। +हर कोई इस घोषणा में उल्लिखित सभी अधिकारों और स्वतंत्रता का हकदार है, किसी भी प्रकार का भेद किए बिना, +जैसे कि जाति, रंग, लिंग, भाषा, धर्म, राजनीतिक या अन्य मत, राष्ट्रीय या सामाजिक मूल, संपत्ति, +जन्म या अन्य स्थिति। इसके अलावा, राजनीतिक, अधिकार क्षेत्र या के आधार पर कोई भेद नहीं किया जाएगा +देश या क्षेत्र की अंतर्राष्ट्रीय स्थिति, जो किसी व्यक्ति की है, चाहे वह स्वतंत्र हो, भरोसा हो, +स्व-शासन या संप्रभुता के किसी अन्य सीमा के तहत।` + + sampleHungarian = `Az Emberi Jogok Egyetemes Nyilatkozata + +1. cikk +Minden. emberi lény szabadon születik és egyenlő méltósága és joga van. Az emberek, ésszel és lelkiismerettel bírván, +egymással szemben testvéri szellemben kell hogy viseltessenek. + +2. cikk +Mindenki, bármely megkülönböztetésre, nevezetesen fajra, színre, nemre, nyelvre, vallásra, politikai vagy bármely +más véleményre, nemzeti vagy társadalmi eredetre, vagyonra, születésre, vagy bármely más körülményre való tekintet +nélkül hivatkozhat a jelen Nyilatkozatban kinyilvánított összes jogokra és szabadságokra. +Ezenfelül nem lehet semmiféle megkülönböztetést tenni annak az országnak, vagy területnek politikai, jogi vagy +nemzetközi helyzete alapján sem, amelynek a személy állampolgára, aszerint, hogy az illető ország vagy terület független, +gyámság alatt áll, nem autonóm vagy szuverenitása bármely vonatkozásban korlátozott.` + + sampleIndonesian = `Pernyataan Umum tentang Hak-Hak Asasi Manusia + +Pasal 1 +Semua orang dilahirkan merdeka dan mempunyai martabat dan hak-hak yang sama. +Mereka dikaruniai akal dan hati nurani dan hendaknya bergaul satu sama lain dalam semangat persaudaraan. + +Pasal 2 +Setiap orang berhak atas semua hak dan kebebasan-kebebasan yang tercantum di dalam Pernyataan ini tanpa perkecualian apapun, +seperti ras, warna kulit, jenis kelamin, bahasa, agama, politik atau pendapat yang berlainan, asal mula kebangsaan atau +kemasyarakatan, hak milik, kelahiran ataupun kedudukan lain. +Di samping itu, tidak diperbolehkan melakukan perbedaan atas dasar kedudukan politik, hukum atau kedudukan internasional +dari negara atau daerah dari mana seseorang berasal, baik dari negara yang merdeka, yang berbentuk wilayah-wilayah perwalian, +jajahan atau yang berada di bawah batasan kedaulatan yang lain.` + + sampleItalian = `DICHIARAZIONE UNIVERSALE DEI DIRITTI UMANI + +Articolo 1 +Tutti gli esseri umani nascono liberi ed eguali in dignità e diritti. +Essi sono dotati di ragione e di coscienza e devono agire gli uni verso gli altri in spirito di fratellanza. + +Articolo 2 +Ad ogni individuo spettano tutti i diritti e tutte le libertà enunciate nella presente Dichiarazione, +senza distinzione alcuna, per ragioni di razza, di colore, di sesso, di lingua, di religione, +di opinione politica o di altro genere, di origine nazionale o sociale, di ricchezza, di nascita o di altra condizione. +Nessuna distinzione sarà inoltre stabilita sulla base dello statuto politico, giuridico o internazionale del paese +o del territorio cui una persona appartiene, sia indipendente, o sottoposto ad amministrazione fiduciaria o non autonomo, +o soggetto a qualsiasi limitazione di sovranità.` + + sampleJapanese = `『世界人権宣言』 + +第1条 + +すべての人間は、生まれながらにして自由であり、かつ、尊厳と権利と について平等である。人間は、理性と良心とを授けられており、互いに同 胞の精神をもって行動しなければならない。 + +第2条 + +すべて人は、人種、皮膚の色、性、言語、宗教、政治上その他の意見、    +国民的もしくは社会的出身、財産、門地その他の地位又はこれに類するい    +かなる自由による差別をも受けることなく、この宣言に掲げるすべての権    +利と自由とを享有することができる。 + +さらに、個人の属する国又は地域が独立国であると、信託統治地域で    +あると、非自治地域であると、又は他のなんらかの主権制限の下にあると    +を問わず、その国又は地域の政治上、管轄上又は国際上の地位に基ずくい    +かなる差別もしてはならない。` + + sampleKorean = `세 계 인 권 선 언 + +제 1 조 +모든 인간은 태어날 때부터 자유로우며 그 존엄과 권리에 있어 동등하다. 인간은 천부적으로 이성과 양심을 부여받았으며 서로 형제애의 정신으로 행동하여야 한다. + +제 2 조 +모든 사람은 인종, 피부색, 성, 언어, 종교, 정치적 또는 기타의 견해, 민족적 또는 사회적 출신, 재산, 출생 또는 기타의 신분과 같은 어떠한 종류의 차별이 없이, +이 선언에 규정된 모든 권리와 자유를 향유할 자격이 있다 . 더 나아가 개인이 속한 국가 또는 영토가 독립국 , 신탁통치지역 , 비자치지역이거나 또는 주권에 대한 여타의 제약을 받느냐에 관계없이 , +그 국가 또는 영토의 정치적, 법적 또는 국제적 지위에 근거하여 차별이 있어서는 아니된다 .` + + sampleMarathi = `मानवाधिकारांची सार्वत्रिक घोषणा + +अनुच्छेद १. +सर्व मानव स्वतंत्र आणि समान सन्मान आणि अधिकारात जन्माला येतात. ते तर्क आणि विवेकबुद्धीने संपन्न आहेत आणि त्यांनी बंधुत्वाच्या भावनेने एकमेकांशी वागले पाहिजे. + +अनुच्छेद २. +या घोषणेमध्ये वंश, रंग, लिंग, भाषा, धर्म, राजकीय किंवा अन्य मत, राष्ट्रीय किंवा सामाजिक मूळ, मालमत्ता, जन्म किंवा इतर कोणत्याही प्रकारचे भेद न +करता प्रत्येकजण या घोषणेत नमूद केलेले सर्व अधिकार आणि स्वातंत्र्य मिळविण्यास पात्र आहे. इतर स्थिती. याव्यतिरिक्त, एखादा देश ज्याच्या ताब्यात आहे तो +देशाच्या राजकीय, कार्यकक्षात्मक किंवा आंतरराष्ट्रीय दर्जाच्या आधारे कोणताही भेदभाव केला जाणार नाही, तो स्वतंत्र, विश्वास असो, स्वराज्य असो किंवा +सार्वभौमत्वाच्या कोणत्याही अन्य मर्यादेखाली असो.` + + samplePersian = `اعلامیه جهانی حقوق بشر + +مقاله 1. +همه انسانها آزاد و از نظر کرامت و حقوق برابر به دنیا می آیند. +آنها از عقل و وجدان برخوردارند و باید با روحیه برادری نسبت به یکدیگر رفتار کنند. + +ماده 2 +هر کس بدون هیچ گونه تمایزی از کلیه حقوق و آزادی های مندرج در این بیانیه برخوردار است ، +مانند نژاد ، رنگ ، جنس ، زبان ، مذهب ، عقاید سیاسی یا عقاید دیگر ، منشا national ملی یا اجتماعی ، دارایی ، +تولد یا وضعیت دیگر بعلاوه ، هیچ تفکیکی نباید بر اساس حوزه های سیاسی ، قضایی یا قضایی قائل شود +وضعیت بین المللی کشور یا سرزمینی که شخص به آن تعلق دارد ، خواه استقلال باشد ، +غیر خود حاکم یا تحت هر محدودیت دیگری در حاکمیت.` + + samplePolish = `POWSZECHNA DEKLARACJA PRAW CZŁOWIEKA + +Artykuł 1 +Wszyscy ludzie rodzą się wolni i równi pod względem swej godności i swych praw. Są oni obdarzeni rozumem i sumieniem +i powinni postępować wobec innych w duchu braterstwa. + +Artykuł 2 +Każdy człowiek posiada wszystkie prawa i wolności zawarte w niniejszej Deklaracji bez względu na jakiekolwiek różnice rasy, +koloru, płci, języka, wyznania, poglądów politycznych i innych, narodowości, pochodzenia społecznego, majątku, +urodzenia lub jakiegokolwiek innego stanu. +Nie wolno ponadto czynić żadnej różnicy w zależności od sytuacji politycznej, prawnej lub międzynarodowej kraju lub obszaru, +do którego dana osoba przynależy, bez względu na to, czy dany kraj lub obszar jest niepodległy, czy też podlega systemowi +powiernictwa, nie rządzi się samodzielnie lub jest w jakikolwiek sposób ograniczony w swej niepodległości.` + + samplePortuguese = `Declaração Universal dos Direitos Humanos + +Artigo 1° +Todos os seres humanos nascem livres e iguais em dignidade e em direitos. Dotados de razão e de consciência, +devem agir uns para com os outros em espírito de fraternidade. + +Artigo 2° +Todos os seres humanos podem invocar os direitos e as liberdades proclamados na presente Declaração, +sem distinção alguma, nomeadamente de raça, de cor, de sexo, de língua, de religião, de opinião política ou outra, +de origem nacional ou social, de fortuna, de nascimento ou de qualquer outra situação. +Além disso, não será feita nenhuma distinção fundada no estatuto político, jurídico ou internacional do país ou do +território da naturalidade da pessoa, seja esse país ou território independente, sob tutela, autônomo ou sujeito +a alguma limitação de soberania.` + + sampleRussian = `Всеобщая декларация прав человека + +Статья 1 +Все люди рождаются свободными и равными в своем достоинстве и правах. +Они наделены разумом и совестью и должны поступать в отношении друг друга в духе братства. + +Статья 2 +Каждый человек должен обладать всеми правами и всеми свободами, провозглашенными настоящей +Декларацией, без какого бы то ни было различия, как-то в отношении расы, цвета кожи, пола, +языка, религии, политических или иных убеждений, национального или социального происхождения, +имущественного, сословного или иного положения. +Кроме того, не должно проводиться никакого различия на основе политического, правового или +международного статуса страны или территории, к которой человек принадлежит, независимо от того, +является ли эта территория независимой, подопечной, несамоуправляющейся или как-либо иначе +ограниченной в своем суверенитете.` + + sampleSpanish = `Declaración Universal de Derechos Humanos + +Artículo 1. +Todos los seres humanos nacen libres e iguales en dignidad y derechos y, dotados como están de razón y conciencia, +deben comportarse fraternalmente los unos con los otros. + +Artículo 2. +Toda persona tiene los derechos y libertades proclamados en esta Declaración, sin distinción alguna de raza, color, +sexo, idioma, religión, opinión política o de cualquier otra índole, origen nacional o social, posición económica, +nacimiento o cualquier otra condición. Además, no se hará distinción alguna fundada en la condición política,jurídica +o internacional del país o territorio de cuya jurisdicción dependa una persona, tanto si se trata de un país independiente, +como de un territorio bajo administración fiduciaria, no autónomo o sometido a cualquier otra limitación de soberanía.` + + sampleSwahili = `UMOJA WA MATAIFA OFISI YA IDARA YA HABARI TAARIFA YA ULIMWENGU JUU YA HAKI ZA BINADAMU + +Kifungu cha 1. +Watu wote wamezaliwa huru, hadhi na haki zao ni sawa. Wote wamejaliwa akili na dhamiri, hivyo yapasa watendeane kindugu. + +Kifungu cha 2. +Kila mtu anastahili kuwa na haki zote na uhuru wote ambao umeelezwa katika Taarifa hii bila ubaguzi wo wote. Yaani bila kubaguana kwa rangi, +taifa, wanaume kwa wanawake, dini, siasa, fikara, asili ya taifa la mtu, mali, kwa kizazi au kwa hali nyingine yo yote. +Juu ya hayo usifanye ubaguzi kwa kutegemea siasa, utawala au kwa kutegemea uhusiano wa nchi fulani na mataifa mengine au nchi ya asili ya mtu, +haidhuru nchi hiyo iwe inayojitawala, ya udhamini, isiyojitawala au inayotawaliwa na nchi nyingine kwa hali ya namna yo yote.` + + sampleSwedish = `ALLMÄN FÖRKLARING OM DE MÄNSKLIGA RÄTTIGHETERNA + +Artikel 1. +Alla människor äro födda fria och lika i värde och rättigheter. +De äro utrustade med förnuft och samvete och böra handla gentemot varandra i en anda av broderskap. + +Artikel 2. +Envar är berättigad till alla de fri- och rättigheter, som uttalas i denna förklaring, utan åtskillnad av något slag, +såsom ras, hudfärg, kön, språk, religion, politisk eller annan uppfattning, nationellt eller socialt ursprung, +egendom, börd eller ställning i övrigt. +Ingen åtskillnad må vidare göras på grund av den politiska, juridiska eller internationella ställning, som intages +v det land eller område, till vilket en person hör, vare sig detta land eller område är oberoende, står under +förvaltarskap, är icke-självstyrande eller är underkastat någon annan begränsning av sin suveränitet.` + + sampleThai = `ปฏิญญาสากลว่าด้วยสิทธิมนุษยชน + +หัวข้อที่ 1. +มนุษย์ทุกคนเกิดมาโดยเสรีและเท่าเทียมกันในศักดิ์ศรีและสิทธิ +พวกเขากอปรด้วยเหตุผลและมโนธรรมและควรปฏิบัติต่อกันด้วยจิตวิญญาณแห่งความเป็นพี่น้องกัน + +ข้อ 2. +ทุกคนมีสิทธิได้รับสิทธิและเสรีภาพทั้งหมดที่กำหนดไว้ในปฏิญญานี้โดยไม่มีความแตกต่างใด ๆ +เช่นเชื้อชาติสีผิวเพศภาษาศาสนาความคิดเห็นทางการเมืองหรืออื่น ๆ ชาติกำเนิดหรือสังคมทรัพย์สิน +การเกิดหรือสถานะอื่น ๆ นอกจากนี้จะไม่มีการสร้างความแตกต่างใด ๆ บนพื้นฐานของการเมืองเขตอำนาจศาลหรือ +สถานะระหว่างประเทศของประเทศหรือดินแดนที่บุคคลเป็นอยู่ไม่ว่าจะเป็นอิสระความไว้วางใจ +ไม่ปกครองตนเองหรืออยู่ภายใต้ข้อ จำกัด อื่นใดของอำนาจอธิปไตย` + + sampleTurkish = `İnsan hakları evrensel beyannamesi + +Madde 1 +Bütün insanlar hür, haysiyet ve haklar bakımından eşit doğarlar. +Akıl ve vicdana sahiptirler ve birbirlerine karşı kardeşlik zihniyeti ile hareket etmelidirler. + +Madde 2 +Herkes, ırk, renk, cinsiyet, dil, din, siyasi veya diğer herhangi bir akide, milli veya içtimai menşe, +servet, doğuş veya herhangi diğer bir fark gözetilmeksizin işbu Beyannamede ilan olunan tekmil haklardan +ve bütün hürriyetlerden istifade edebilir. +Bundan başka, bağımsız memleket uyruğu olsun, vesayet altında bulunan, gayri muhtar veya sair bir egemenlik +kayıtlamasına tabi ülke uyruğu olsun, bir şahıs hakkında, uyruğu bulunduğu memleket veya ülkenin siyasi, +hukuki veya milletlerarası statüsü bakımından hiçbir ayrılık gözetilmeyecektir.` + + sampleUrdu = `انسانی حقوق کا عالمی اعلان + +آرٹیکل 1۔ +تمام انسان وقار اور حقوق میں آزاد اور برابر پیدا ہوئے ہیں۔ +وہ استدلال اور ضمیر کے مالک ہیں اور بھائی چارے کے جذبے سے ایک دوسرے کے ساتھ کام کریں۔ + +آرٹیکل 2۔ +ہر شخص کسی بھی طرح کے امتیاز کے بغیر ، اس اعلامیے میں بیان کردہ تمام حقوق اور آزادی کا حقدار ہے ، +جیسے نسل ، رنگ ، جنس ، زبان ، مذہب ، سیاسی یا دوسری رائے ، قومی یا معاشرتی اصل ، املاک ، +پیدائش یا دوسری حیثیت مزید برآں ، سیاسی ، دائرہ اختیار یا کی بنیاد پر کوئی امتیاز نہیں برپا کیا جائے گا +ملک یا علاقے کی بین الاقوامی حیثیت جس سے کسی شخص کا تعلق ہے ، خواہ وہ آزاد ہو ، اعتماد ، +غیر خود حکمرانی یا خود مختاری کی کسی بھی دوسری حد کے تحت۔` + + sampleVietnamese = `Tuyên ngôn nhân quyền + +Điều 1. +Tất cả con người sinh ra đều tự do, bình đẳng về nhân phẩm và quyền. +Họ được phú cho lý trí và lương tâm và nên hành động với nhau trong tinh thần anh em. + +Điều 2. +Mọi người được hưởng tất cả các quyền và tự do được nêu trong Tuyên bố này, không phân biệt bất kỳ +hình thức nào, chẳng hạn như chủng tộc, màu da, giới tính, ngôn ngữ, tôn giáo, chính trị hoặc quan +điểm khác, nguồn gốc quốc gia hoặc xã hội, tài sản, nơi sinh hoặc trạng thái khác. +Hơn nữa, không có sự phân biệt nào được thực hiện trên cơ sở địa vị chính trị, quyền tài phán hoặc +quốc tế của quốc gia hoặc vùng lãnh thổ mà một người thuộc về, cho dù đó là quốc gia độc lập, +tin cậy, không tự quản hay theo bất kỳ giới hạn chủ quyền nào khác.` +) + +type sample struct { + fontName string + lang string + text string + rtl bool +} + +var langSamples = []sample{ + {"UnifontMedium", "Arabic", sampleArabic, true}, + {"UnifontMedium", "Armenian", sampleArmenian, false}, + {"Roboto-Regular", "Azerbaijani", sampleAzerbaijani, false}, + {"UnifontMedium", "Bangla", sampleBangla, false}, + {"Roboto-Regular", "Belarusian", sampleBelarusian, false}, + {"UnifontMedium", "Chinese simple", sampleChineseSimple, false}, + {"UnifontMedium", "Chinese traditional", sampleChineseTraditional, false}, + {"Roboto-Regular", "English", sampleEnglish, false}, + {"Roboto-Regular", "French", sampleFrench, false}, + {"Roboto-Regular", "German", sampleGerman, false}, + {"Roboto-Regular", "Greek", sampleGreek, false}, + {"UnifontMedium", "Hebrew", sampleHebrew, true}, + {"UnifontMedium", "Hindi", sampleHindi, false}, + {"Roboto-Regular", "Hungarian", sampleHungarian, false}, + {"Roboto-Regular", "Indonesian", sampleIndonesian, false}, + {"Roboto-Regular", "Italian", sampleItalian, false}, + {"Unifont-JPMedium", "Japanese", sampleJapanese, false}, + {"UnifontMedium", "Korean", sampleKorean, false}, + {"UnifontMedium", "Marathi", sampleMarathi, false}, + {"UnifontMedium", "Persian", samplePersian, true}, + {"Roboto-Regular", "Portuguese", samplePortuguese, false}, + {"Roboto-Regular", "Polish", samplePolish, false}, + {"Roboto-Regular", "Russian", sampleRussian, false}, + {"Roboto-Regular", "Spanish", sampleSpanish, false}, + {"Roboto-Regular", "Swahili", sampleSwahili, false}, + {"Roboto-Regular", "Swedish", sampleSwedish, false}, + {"UnifontMedium", "Thai", sampleThai, false}, + {"Roboto-Regular", "Turkish", sampleTurkish, false}, + {"UnifontMedium", "Urdu", sampleUrdu, true}, + {"Roboto-Regular", "Vietnamese", sampleVietnamese, false}, +} + +func renderArticle(xRefTable *model.XRefTable, p model.Page, row, col, lang int) { + mediaBox := p.MediaBox + w := mediaBox.Width() / 6 + h := mediaBox.Height() / 5 + region := types.RectForWidthAndHeight(float64(col)*w, float64(4-row)*h, w, h) + buf := p.Buf + sample := langSamples[lang] + + if lang%2 > 0 { + draw.FillRectNoBorder(buf, region, color.SimpleColor{R: .75, G: .75, B: 1}) + } + + fontName := "Helvetica" + k := p.Fm.EnsureKey("Helvetica") + + td := model.TextDescriptor{ + Text: sample.lang, + FontName: fontName, + FontKey: k, + FontSize: 24, + MLeft: 5, + MRight: 5, + MTop: 5, + MBot: 5, + Scale: 1., + ScaleAbs: false, + HAlign: types.AlignLeft, + VAlign: types.AlignMiddle, + RMode: draw.RMFill, + StrokeCol: color.NewSimpleColor(0x206A29), + FillCol: color.NewSimpleColor(0x206A29), + ShowBackground: true, + BackgroundCol: color.SimpleColor{R: 1., G: .98, B: .77}, + ShowBorder: true, + ShowLineBB: false, + ShowTextBB: true, + HairCross: false, + } + + model.WriteColumnAnchored(xRefTable, buf, mediaBox, region, td, types.TopLeft, 0) + + fontName = sample.fontName + k = p.Fm.EnsureKey(fontName) + + td = model.TextDescriptor{ + Text: sample.text, + FontName: fontName, + Embed: true, + RTL: sample.rtl, + FontKey: k, + FontSize: 16, + MLeft: 5, + MRight: 5, + MTop: 5, + MBot: 5, + X: -1, + Y: -1, + Scale: .9, + ScaleAbs: false, + HAlign: types.AlignJustify, + VAlign: types.AlignMiddle, + RMode: draw.RMFill, + StrokeCol: color.NewSimpleColor(0x206A29), + FillCol: color.NewSimpleColor(0x206A29), + ShowBackground: true, + BackgroundCol: color.SimpleColor{R: 1., G: .98, B: .77}, + ShowBorder: true, + ShowLineBB: false, + ShowTextBB: false, + HairCross: false, + } + + if sample.lang == "Japanese" { + model.WriteColumn(xRefTable, buf, mediaBox, region, td, mediaBox.Width()*.9) + return + } + + if sample.lang == "Thai" { + td.HAlign = types.AlignLeft + model.WriteColumn(xRefTable, buf, mediaBox, region, td, mediaBox.Width()*.9) + return + } + + model.WriteMultiLine(xRefTable, buf, mediaBox, region, td) +} + +func TestUserFonts(t *testing.T) { + msg := "TestUserFonts" + + w, h := 600., 600. + mediaBox := types.RectForDim(w, h) + p := model.NewPageWithBg(mediaBox, color.NewSimpleColor(0xbeded9)) + + xRefTable, err := pdfcpu.CreateDemoXRef() + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + lang := 0 + for row := 0; row < 5; row++ { + for col := 0; col < 6; col++ { + renderArticle(xRefTable, p, row, col, lang) + lang++ + } + } + + rootDict, err := xRefTable.Catalog() + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + if err = pdfcpu.AddPageTreeWithSamplePage(xRefTable, rootDict, p); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + outDir := filepath.Join("..", "..", "samples", "basic") + outFile := filepath.Join(outDir, "UserFont_HumanRights.pdf") + createAndValidate(t, xRefTable, outFile, msg) +} diff --git a/pkg/api/test/create_test.go b/pkg/api/test/create_test.go new file mode 100644 index 0000000000000000000000000000000000000000..7b5b942bbe04c0f1155dc5f5ebe148b4e47ef661 --- /dev/null +++ b/pkg/api/test/create_test.go @@ -0,0 +1,2069 @@ +/* +Copyright 2019 The pdf Authors. + +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 + + 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 test + +import ( + "bytes" + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +var sampleText string = `MOST of the adventures recorded in this book really occurred; one or +two were experiences of my own, the rest those of boys who were +schoolmates of mine. Huck Finn is drawn from life; Tom Sawyer also, but +not from an individual--he is a combination of the characteristics of +three boys whom I knew, and therefore belongs to the composite order of +architecture. + +The odd superstitions touched upon were all prevalent among children +and slaves in the West at the period of this story--that is to say, +thirty or forty years ago. + +Although my book is intended mainly for the entertainment of boys and +girls, I hope it will not be shunned by men and women on that account, +for part of my plan has been to try to pleasantly remind adults of what +they once were themselves, and of how they felt and thought and talked, +and what queer enterprises they sometimes engaged in.` + +var sampleTextArabic = `حدثت بالفعل معظم المغامرات المسجلة في هذا الكتاب ؛ واحد أو +كانت اثنتان من تجربتي الخاصة ، والباقي تجارب الأولاد الذين كانوا كذلك +زملائي في المدرسة. هاك فين مستوحى من الحياة ؛ توم سوير أيضا ولكن +ليس من فرد - إنه مزيج من خصائص +ثلاثة أولاد أعرفهم ، وبالتالي ينتمون إلى الترتيب المركب لـ +هندسة معمارية. + +كانت الخرافات الغريبة التي تم التطرق إليها سائدة بين الأطفال +والعبيد في الغرب في فترة هذه القصة - أي +قبل ثلاثين أو أربعين سنة. + +على الرغم من أن كتابي مخصص بشكل أساسي للترفيه عن الأولاد و +الفتيات ، أتمنى ألا يتجنب الرجال والنساء ذلك الحساب ، +جزء من خطتي كان محاولة تذكير البالغين بما يحدث +كانوا أنفسهم ذات مرة ، وكيف شعروا وفكروا وتحدثوا ، +وما هي المؤسسات الكويرية التي شاركوا فيها أحيانًا.` + +var sampleTextHebrew = `רוב ההרפתקאות שתועדו בספר זה באמת התרחשו; אחד או +שתיים היו חוויות משלי, והשאר אלה של בנים שהיו +חברי לבית הספר שלי. האק פין נשאב מהחיים; גם טום סוייר, אבל +לא מאדם - הוא שילוב של המאפיינים של +שלושה בנים שהכרתי ולכן שייכים לסדר המורכב של +ארכיטקטורה. + +האמונות הטפלות המוזרות בהן נגעו היו כולן רווחות בקרב ילדים +ועבדים במערב בתקופת הסיפור הזה - כלומר, +לפני שלושים או ארבעים שנה. + +למרות שהספר שלי מיועד בעיקר לבידור של בנים ו +בנות, אני מקווה שזה לא יימנע מגברים ונשים בגלל זה, +חלק מהתוכנית שלי הייתה לנסות להזכיר למבוגרים בנעימות מה +פעם הם היו עצמם, ועל איך שהם הרגישו וחשבו ודיברו, +ובאילו מפעלים מוזרים הם עסקו לפעמים.` + +var sampleTextPersian = `بیشتر ماجراهای ثبت شده در این کتاب واقعاً اتفاق افتاده است. یکی یا +دو مورد از تجربه های خودم بود ، بقیه از پسران بودند +هم مدرسه ای های من. هاک فین از زندگی کشیده شده است. تام سویر نیز ، اما +نه از یک فرد - او ترکیبی از ویژگی های است +سه پسر که من آنها را می شناختم و بنابراین به ترتیب مرکب تعلق دارند +معماری. + +خرافات عجیب و غریب لمس شده همه در میان کودکان شایع بود +و بردگان در غرب این دوره از داستان - یعنی اینکه ، +سی چهل سال پیش + +اگرچه کتاب من عمدتا برای سرگرمی پسران و +دختران ، امیدوارم با این حساب مردان و زنان از آن اجتناب نکنند ، +زیرا بخشی از برنامه من این بوده است که سعی کنم چه چیزی را به بزرگسالان یادآوری کنم +آنها یک بار خودشان بودند ، و از احساس و تفکر و صحبت کردن ، +و بعضی اوقات چه کارهایی را انجام می دادند` + +var sampleTextUrdu = `اس کتاب میں درج کی گئی زیادہ تر مہم جوئی واقعتا؛ پیش آئی ہے۔ ایک یا +دو میرے اپنے تجربات تھے ، باقی جو لڑکے تھے +میرے اسکول کے ساتھیوں. ہک فن زندگی سے نکالا گیا ہے۔ ٹام ساویر بھی ، لیکن +کسی فرد سے نہیں - وہ کی خصوصیات کا ایک مجموعہ ہے +تین لڑکے جن کو میں جانتا تھا ، اور اس وجہ سے یہ جامع ترتیب سے ہے +فن تعمیر + +بچوں میں عجیب و غریب اندوشواس کا اثر تمام تھا +اور اس کہانی کے دور میں مغرب میں غلام۔ +تیس یا چالیس سال پہلے کی بات ہے۔ + +اگرچہ میری کتاب بنیادی طور پر لڑکوں اور تفریح ​​کے لئے ہے +لڑکیاں ، مجھے امید ہے کہ اس وجہ سے مرد اور خواتین اس سے باز نہیں آئیں گے ، +میرے منصوبے کا ایک حص adultsہ یہ رہا ہے کہ بالغوں کو خوشی سے اس کی یاد دلانے کی کیا کوشش کی جائے +وہ ایک بار خود تھے ، اور یہ کہ وہ کیسے محسوس کرتے ہیں ، سوچتے اور بات کرتے ہیں ، +اور کن کن کن کاروباری اداروں میں وہ کبھی کبھی مشغول رہتے ہیں۔` + +var sampleTextRTL = map[string]string{ + "Arabic": sampleTextArabic, + "Hebrew": sampleTextHebrew, + "Persian": sampleTextPersian, + "Urdu": sampleTextUrdu, +} + +var sampleText2 = `THE two boys flew on and on, toward the village, speechless with +horror. They glanced backward over their shoulders from time to time, +apprehensively, as if they feared they might be followed. Every stump +that started up in their path seemed a man and an enemy, and made them +catch their breath; and as they sped by some outlying cottages that lay +near the village, the barking of the aroused watch-dogs seemed to give +wings to their feet. + +"If we can only get to the old tannery before we break down!" +whispered Tom, in short catches between breaths. "I can't stand it much +longer." + +Huckleberry's hard pantings were his only reply, and the boys fixed +their eyes on the goal of their hopes and bent to their work to win it. +They gained steadily on it, and at last, breast to breast, they burst +through the open door and fell grateful and exhausted in the sheltering +shadows beyond. By and by their pulses slowed down, and Tom whispered: + +"Huckleberry, what do you reckon'll come of this?"` + +var sampleText3 = `Even the Glorious Fourth was in some sense a failure, for it rained +hard, there was no procession in consequence, and the greatest man in +the world (as Tom supposed), Mr. Benton, an actual United States +Senator, proved an overwhelming disappointment--for he was not +twenty-five feet high, nor even anywhere in the neighborhood of it. + +A circus came. The boys played circus for three days afterward in +tents made of rag carpeting--admission, three pins for boys, two for +girls--and then circusing was abandoned. + +A phrenologist and a mesmerizer came--and went again and left the +village duller and drearier than ever. + +There were some boys-and-girls' parties, but they were so few and so +delightful that they only made the aching voids between ache the harder. + +Becky Thatcher was gone to her Constantinople home to stay with her +parents during vacation--so there was no bright side to life anywhere.` + +func createAndValidate(t *testing.T, xRefTable *model.XRefTable, outFile, msg string) { + t.Helper() + outDir := "../../samples/basic" + outFile = filepath.Join(outDir, outFile) + if err := api.CreatePDFFile(xRefTable, outFile, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + if err := api.ValidateFile(outFile, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestCreateDemoPDF(t *testing.T) { + msg := "TestCreateDemoPDF" + mediaBox := types.RectForFormat("A4") + p := model.Page{MediaBox: mediaBox, Fm: model.FontMap{}, Buf: new(bytes.Buffer)} + pdfcpu.CreateTestPageContent(p) + xRefTable, err := pdfcpu.CreateDemoXRef() + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + rootDict, err := xRefTable.Catalog() + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + if err = pdfcpu.AddPageTreeWithSamplePage(xRefTable, rootDict, p); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + createAndValidate(t, xRefTable, "Test.pdf", msg) +} + +func TestResourceDictInheritanceDemoPDF(t *testing.T) { + // Create a test page proofing resource inheritance. + // Resources may be inherited from ANY parent node. + // Case in point: fonts + msg := "TestResourceDictInheritanceDemoPDF" + xRefTable, err := pdfcpu.CreateResourceDictInheritanceDemoXRef() + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + createAndValidate(t, xRefTable, "ResourceDictInheritanceDemo.pdf", msg) +} + +func TestAnnotationDemoPDF(t *testing.T) { + msg := "TestAnnotationDemoPDF" + xRefTable, err := pdfcpu.CreateAnnotationDemoXRef() + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + createAndValidate(t, xRefTable, "AnnotationDemo.pdf", msg) +} + +func writeTextDemoAlignedWidthAndMargin( + xRefTable *model.XRefTable, + p model.Page, + region *types.Rectangle, + hAlign types.HAlignment, + w, mLeft, mRight, mTop, mBot float64) { + + buf := p.Buf + mediaBox := p.MediaBox + + mediaBB := true + + var cr, cg, cb float32 + cr, cg, cb = .5, .75, 1. + r := mediaBox + if region != nil { + r = region + cr, cg, cb = .75, .75, 1 + } + if mediaBB { + draw.FillRectNoBorder(buf, r, color.SimpleColor{R: cr, G: cg, B: cb}) + } + + fontName := "Helvetica" + k := p.Fm.EnsureKey(fontName) + + td := model.TextDescriptor{ + FontName: fontName, + Embed: true, + FontKey: k, + FontSize: 24, + ShowMargins: true, + MLeft: mLeft, + MRight: mRight, + MTop: mTop, + MBot: mBot, + Scale: 1., + ScaleAbs: true, + HAlign: hAlign, + RMode: draw.RMFill, + StrokeCol: color.Black, + FillCol: color.Black, + ShowBackground: true, + BackgroundCol: color.SimpleColor{R: 1., G: .98, B: .77}, + ShowBorder: true, + ShowLineBB: true, + ShowTextBB: true, + HairCross: true, + } + + td.VAlign, td.X, td.Y, td.Text = types.AlignBaseline, -1, r.Height()*.75, "M\\u(lti\nline\n\nwith empty line" + model.WriteColumn(xRefTable, buf, mediaBox, region, td, w) + + td.VAlign, td.X, td.Y, td.Text = types.AlignBaseline, r.Width()*.75, r.Height()*.25, "Arbitrary\ntext\nlines" + model.WriteColumn(xRefTable, buf, mediaBox, region, td, w) + + // Multilines along the top of the page: + td.VAlign, td.X, td.Y, td.Text = types.AlignTop, 0, r.Height(), "0,h (topleft)\nand line2" + model.WriteColumn(xRefTable, buf, mediaBox, region, td, w) + + td.VAlign, td.X, td.Y, td.Text = types.AlignTop, -1, r.Height(), "-1,h (topcenter)\nand line2" + model.WriteColumn(xRefTable, buf, mediaBox, region, td, w) + + td.VAlign, td.X, td.Y, td.Text = types.AlignTop, r.Width(), r.Height(), "w,h (topright)\nand line2" + model.WriteColumn(xRefTable, buf, mediaBox, region, td, w) + + // Multilines along the center of the page: + // x = 0 centers the position of multilines horizontally + // y = 0 centers the position of multilines vertically and enforces alignMiddle + td.VAlign, td.X, td.Y, td.Text = types.AlignBaseline, 0, -1, "0,-1 (left)\nand line2" + model.WriteColumn(xRefTable, buf, mediaBox, region, td, w) + + td.VAlign, td.X, td.Y, td.Text = types.AlignMiddle, -1, -1, "-1,-1 (center)\nand line2" + model.WriteColumn(xRefTable, buf, mediaBox, region, td, w) + + td.VAlign, td.X, td.Y, td.Text = types.AlignBaseline, r.Width(), -1, "w,-1 (right)\nand line2" + model.WriteColumn(xRefTable, buf, mediaBox, region, td, w) + + // Multilines along the bottom of the page: + td.VAlign, td.X, td.Y, td.Text = types.AlignBottom, 0, 0, "0,0 (botleft)\nand line2" + model.WriteColumn(xRefTable, buf, mediaBox, region, td, w) + + td.VAlign, td.X, td.Y, td.Text = types.AlignBottom, -1, 0, "-1,0 (botcenter)\nand line2" + model.WriteColumn(xRefTable, buf, mediaBox, region, td, w) + + td.VAlign, td.X, td.Y, td.Text = types.AlignBottom, r.Width(), 0, "w,0 (botright)\nand line2" + model.WriteColumn(xRefTable, buf, mediaBox, region, td, w) + + draw.DrawHairCross(buf, 0, 0, r) +} + +func createTextDemoAlignedWidthAndMargin(xRefTable *model.XRefTable, mediaBox *types.Rectangle, hAlign types.HAlignment, w, mLeft, mRight, mTop, mBot float64) model.Page { + p := model.NewPage(mediaBox, nil) + var region *types.Rectangle + writeTextDemoAlignedWidthAndMargin(xRefTable, p, region, hAlign, w, mLeft, mRight, mTop, mBot) + region = types.RectForWidthAndHeight(50, 70, 200, 200) + writeTextDemoAlignedWidthAndMargin(xRefTable, p, region, hAlign, w, mLeft, mRight, mTop, mBot) + return p +} + +func createTextDemoAlignLeft(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + return createTextDemoAlignedWidthAndMargin(xRefTable, mediaBox, types.AlignLeft, 0, 0, 0, 0, 0) +} + +func createTextDemoAlignLeftMargin(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + return createTextDemoAlignedWidthAndMargin(xRefTable, mediaBox, types.AlignLeft, 0, 5, 10, 15, 20) +} + +func createTextDemoAlignRight(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + return createTextDemoAlignedWidthAndMargin(xRefTable, mediaBox, types.AlignRight, 0, 0, 0, 0, 0) +} + +func createTextDemoAlignRightMargin(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + return createTextDemoAlignedWidthAndMargin(xRefTable, mediaBox, types.AlignRight, 0, 5, 10, 15, 20) +} + +func createTextDemoAlignCenter(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + return createTextDemoAlignedWidthAndMargin(xRefTable, mediaBox, types.AlignCenter, 0, 0, 0, 0, 0) +} + +func createTextDemoAlignCenterMargin(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + return createTextDemoAlignedWidthAndMargin(xRefTable, mediaBox, types.AlignCenter, 0, 5, 10, 15, 20) +} + +func createTextDemoAlignJustify(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + return createTextDemoAlignedWidthAndMargin(xRefTable, mediaBox, types.AlignJustify, 0, 0, 0, 0, 0) +} + +func createTextDemoAlignJustifyMargin(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + return createTextDemoAlignedWidthAndMargin(xRefTable, mediaBox, types.AlignJustify, 0, 5, 10, 15, 20) +} + +func createTextDemoAlignLeftWidth(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + return createTextDemoAlignedWidthAndMargin(xRefTable, mediaBox, types.AlignLeft, 250, 0, 0, 0, 0) +} + +func createTextDemoAlignLeftWidthAndMargin(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + return createTextDemoAlignedWidthAndMargin(xRefTable, mediaBox, types.AlignLeft, 250, 5, 10, 15, 20) +} + +func createTextDemoAlignRightWidth(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + return createTextDemoAlignedWidthAndMargin(xRefTable, mediaBox, types.AlignRight, 250, 0, 0, 0, 0) +} + +func createTextDemoAlignRightWidthAndMargin(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + return createTextDemoAlignedWidthAndMargin(xRefTable, mediaBox, types.AlignRight, 250, 5, 10, 15, 20) +} + +func createTextDemoAlignCenterWidth(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + return createTextDemoAlignedWidthAndMargin(xRefTable, mediaBox, types.AlignCenter, 250, 0, 0, 0, 0) +} + +func createTextDemoAlignCenterWidthAndMargin(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + return createTextDemoAlignedWidthAndMargin(xRefTable, mediaBox, types.AlignCenter, 250, 5, 40, 15, 20) +} + +func createTextDemoAlignJustifyWidth(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + return createTextDemoAlignedWidthAndMargin(xRefTable, mediaBox, types.AlignJustify, 250, 0, 0, 0, 0) +} + +func createTextDemoAlignJustifyWidthAndMargin(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + return createTextDemoAlignedWidthAndMargin(xRefTable, mediaBox, types.AlignJustify, 250, 5, 10, 15, 20) +} + +func writeTextAlignJustifyDemo(xRefTable *model.XRefTable, p model.Page, region *types.Rectangle, fontName string) { + mediaBox := p.MediaBox + buf := p.Buf + + mediaBB := true + + var cr, cg, cb float32 + cr, cg, cb = .5, .75, 1. + r := mediaBox + if region != nil { + r = region + cr, cg, cb = .75, .75, 1 + } + if mediaBB { + draw.FillRectNoBorder(buf, r, color.SimpleColor{R: cr, G: cg, B: cb}) + } + + k := p.Fm.EnsureKey(fontName) + + td := model.TextDescriptor{ + Text: sampleText, + FontName: fontName, + Embed: true, + FontKey: k, + FontSize: 12, + MLeft: 5, + MRight: 5, + MTop: 5, + MBot: 5, + X: -1, + Y: -1, + Scale: 1., + ScaleAbs: true, + HAlign: types.AlignJustify, + VAlign: types.AlignMiddle, + RMode: draw.RMFill, + StrokeCol: color.NewSimpleColor(0x206A29), + FillCol: color.NewSimpleColor(0x206A29), + ShowBackground: true, + BackgroundCol: color.SimpleColor{R: 1., G: .98, B: .77}, + ShowBorder: true, + ShowLineBB: false, + ShowTextBB: true, + HairCross: false, + } + + model.WriteMultiLine(xRefTable, buf, mediaBox, region, td) + + draw.DrawHairCross(p.Buf, 0, 0, mediaBox) +} + +func writeTextAlignJustifyColumnDemo(xRefTable *model.XRefTable, p model.Page, region *types.Rectangle) { + mediaBox := p.MediaBox + buf := p.Buf + + mediaBB := true + + var cr, cg, cb float32 + cr, cg, cb = .5, .75, 1. + r := mediaBox + if region != nil { + r = region + cr, cg, cb = .75, .75, 1 + } + if mediaBB { + draw.FillRectNoBorder(buf, r, color.SimpleColor{R: cr, G: cg, B: cb}) + } + + fontName := "Times-Roman" + fontName2 := "Helvetica" + k1 := p.Fm.EnsureKey(fontName) + k2 := p.Fm.EnsureKey(fontName2) + + td := model.TextDescriptor{ + Text: sampleText, + Embed: true, + MLeft: 5, + MRight: 5, + MTop: 5, + MBot: 5, + Scale: 1., + ScaleAbs: true, + HAlign: types.AlignJustify, + RMode: draw.RMFill, + StrokeCol: color.Black, + FillCol: color.Black, + ShowBackground: true, + BackgroundCol: color.SimpleColor{R: 1., G: .98, B: .77}, + ShowBorder: true, + ShowLineBB: false, + ShowTextBB: true, + HairCross: false, + } + + td.BackgroundCol = color.White + td.FillCol = color.Black + td.FontName, td.FontKey, td.FontSize = fontName, k1, 9 + td.ParIndent = true + td.VAlign, td.X, td.Y, td.Dx, td.Dy = types.AlignTop, 0, r.Height(), 5, -5 + model.WriteColumn(xRefTable, buf, mediaBox, region, td, 150) + + td.BackgroundCol = color.Black + td.FillCol = color.White + td.FontName, td.FontKey, td.FontSize = fontName2, k2, 12 + td.ParIndent = true + td.VAlign, td.X, td.Y, td.Dx, td.Dy = types.AlignTop, -1, -1, 0, 0 + model.WriteColumn(xRefTable, buf, mediaBox, region, td, 290) + + draw.DrawHairCross(p.Buf, 0, 0, mediaBox) +} + +func createTextAlignJustifyDemo(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + p := model.NewPage(mediaBox, nil) + var region *types.Rectangle + fontName := "Times-Roman" + writeTextAlignJustifyDemo(xRefTable, p, region, fontName) + region = types.RectForWidthAndHeight(0, 0, 200, 200) + writeTextAlignJustifyDemo(xRefTable, p, region, fontName) + return p +} + +func createTextAlignJustifyColumnDemo(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + p := model.NewPage(mediaBox, nil) + var region *types.Rectangle + writeTextAlignJustifyColumnDemo(xRefTable, p, region) + region = types.RectForWidthAndHeight(0, 0, 200, 200) + writeTextAlignJustifyColumnDemo(xRefTable, p, region) + return p +} + +func writeTextDemoAnchorsWithOffset(xRefTable *model.XRefTable, p model.Page, region *types.Rectangle, dx, dy float64) { + mediaBox := p.MediaBox + buf := p.Buf + + mediaBB := true + + var cr, cg, cb float32 + cr, cg, cb = .5, .75, 1. + r := mediaBox + if region != nil { + r = region + cr, cg, cb = .75, .75, 1 + } + if mediaBB { + draw.FillRectNoBorder(buf, r, color.SimpleColor{R: cr, G: cg, B: cb}) + } + + fontName := "Helvetica" + k := p.Fm.EnsureKey(fontName) + + td := model.TextDescriptor{ + FontName: fontName, + Embed: true, + FontKey: k, + FontSize: 24, + MLeft: 10, + MRight: 10, + MTop: 10, + MBot: 10, + Scale: 1., + ScaleAbs: true, + RMode: draw.RMFill, + StrokeCol: color.Black, + FillCol: color.Black, + ShowBackground: true, + BackgroundCol: color.SimpleColor{R: 1., G: .98, B: .77}, + ShowBorder: true, + ShowLineBB: true, + ShowTextBB: true, + HairCross: false, + } + + td.Dx, td.Dy, td.Text = dx, -dy, "topleft\nandLine2" + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.TopLeft) + + td.Dx, td.Dy, td.Text = 0, -dy, "topcenter\nandLine2" + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.TopCenter) + + td.Dx, td.Dy, td.Text = -dx, -dy, "topright\nandLine2" + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.TopRight) + + td.Dx, td.Dy, td.Text = dx, 0, "left\nandline2" + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.Left) + + td.Dx, td.Dy, td.Text = 0, 0, "center\nandLine2" + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.Center) + + td.Dx, td.Dy, td.Text = -dx, 0, "right\nandLine2" + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.Right) + + td.Dx, td.Dy, td.Text = dx, dy, "botleft\nandLine2" + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.BottomLeft) + + td.Dx, td.Dy, td.Text = 0, dy, "botcenter\nandLine2" + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.BottomCenter) + + td.Dx, td.Dy, td.Text = -dx, dy, "botright\nandLine2" + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.BottomRight) + + draw.DrawHairCross(buf, 0, 0, r) +} + +func writeTextDemoAnchors(xRefTable *model.XRefTable, p model.Page, region *types.Rectangle) { + writeTextDemoAnchorsWithOffset(xRefTable, p, region, 0, 0) +} + +func createTextDemoAnchors(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + p := model.NewPage(mediaBox, nil) + var region *types.Rectangle + writeTextDemoAnchors(xRefTable, p, region) + region = types.RectForWidthAndHeight(50, 70, 200, 200) + writeTextDemoAnchors(xRefTable, p, region) + return p +} + +func createTextDemoAnchorsWithOffset(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + p := model.NewPage(mediaBox, nil) + dx, dy := 20., 20. + var region *types.Rectangle + writeTextDemoAnchorsWithOffset(xRefTable, p, region, dx, dy) + region = types.RectForWidthAndHeight(50, 70, 200, 200) + writeTextDemoAnchorsWithOffset(xRefTable, p, region, dx, dy) + return p +} + +func writeTextDemoColumnAnchoredWithOffset(xRefTable *model.XRefTable, p model.Page, region *types.Rectangle, dx, dy float64) { + mediaBox := p.MediaBox + buf := p.Buf + + mediaBB := true + + var cr, cg, cb float32 + cr, cg, cb = .5, .75, 1. + r := mediaBox + if region != nil { + r = region + cr, cg, cb = .75, .75, 1 + } + if mediaBB { + draw.FillRectNoBorder(buf, r, color.SimpleColor{R: cr, G: cg, B: cb}) + } + + wSmall := 100. + wBig := 300. + + fontName := "Helvetica" + k := p.Fm.EnsureKey(fontName) + + td := model.TextDescriptor{ + Text: sampleText, + FontName: fontName, + Embed: true, + FontKey: k, + FontSize: 6, + MLeft: 5, + MRight: 5, + MTop: 5, + MBot: 5, + Scale: 1., + ScaleAbs: true, + RMode: draw.RMFill, + StrokeCol: color.Black, + FillCol: color.Black, + ShowBackground: true, + BackgroundCol: color.SimpleColor{R: 1., G: .98, B: .77}, + ShowBorder: true, + ShowLineBB: false, + ShowTextBB: true, + HairCross: false, + } + + td.Dx, td.Dy = dx, -dy + model.WriteColumnAnchored(xRefTable, buf, mediaBox, region, td, types.TopLeft, wSmall) + model.WriteColumnAnchored(xRefTable, buf, mediaBox, region, td, types.TopLeft, 0) + model.WriteColumnAnchored(xRefTable, buf, mediaBox, region, td, types.TopLeft, wBig) + + td.Dx, td.Dy = 0, -dy + model.WriteColumnAnchored(xRefTable, buf, mediaBox, region, td, types.TopCenter, wSmall) + model.WriteColumnAnchored(xRefTable, buf, mediaBox, region, td, types.TopCenter, 0) + model.WriteColumnAnchored(xRefTable, buf, mediaBox, region, td, types.TopCenter, wBig) + + td.Dx, td.Dy = -dx, -dy + model.WriteColumnAnchored(xRefTable, buf, mediaBox, region, td, types.TopRight, wSmall) + model.WriteColumnAnchored(xRefTable, buf, mediaBox, region, td, types.TopRight, 0) + model.WriteColumnAnchored(xRefTable, buf, mediaBox, region, td, types.TopRight, wBig) + + td.Dx, td.Dy = dx, 0 + model.WriteColumnAnchored(xRefTable, buf, mediaBox, region, td, types.Left, wSmall) + model.WriteColumnAnchored(xRefTable, buf, mediaBox, region, td, types.Left, 0) + model.WriteColumnAnchored(xRefTable, buf, mediaBox, region, td, types.Left, wBig) + + td.Dx, td.Dy = 0, 0 + model.WriteColumnAnchored(xRefTable, buf, mediaBox, region, td, types.Center, wSmall) + model.WriteColumnAnchored(xRefTable, buf, mediaBox, region, td, types.Center, 0) + model.WriteColumnAnchored(xRefTable, buf, mediaBox, region, td, types.Center, wBig) + + td.Dx, td.Dy = -dx, 0 + model.WriteColumnAnchored(xRefTable, buf, mediaBox, region, td, types.Right, wSmall) + model.WriteColumnAnchored(xRefTable, buf, mediaBox, region, td, types.Right, 0) + model.WriteColumnAnchored(xRefTable, buf, mediaBox, region, td, types.Right, wBig) + + td.Dx, td.Dy = dx, dy + model.WriteColumnAnchored(xRefTable, buf, mediaBox, region, td, types.BottomLeft, wSmall) + model.WriteColumnAnchored(xRefTable, buf, mediaBox, region, td, types.BottomLeft, 0) + model.WriteColumnAnchored(xRefTable, buf, mediaBox, region, td, types.BottomLeft, wBig) + + td.Dx, td.Dy = 0, dy + model.WriteColumnAnchored(xRefTable, buf, mediaBox, region, td, types.BottomCenter, wSmall) + model.WriteColumnAnchored(xRefTable, buf, mediaBox, region, td, types.BottomCenter, 0) + model.WriteColumnAnchored(xRefTable, buf, mediaBox, region, td, types.BottomCenter, wBig) + + td.Dx, td.Dy = -dx, dy + model.WriteColumnAnchored(xRefTable, buf, mediaBox, region, td, types.BottomRight, wSmall) + model.WriteColumnAnchored(xRefTable, buf, mediaBox, region, td, types.BottomRight, 0) + model.WriteColumnAnchored(xRefTable, buf, mediaBox, region, td, types.BottomRight, wBig) + + draw.DrawHairCross(buf, 0, 0, mediaBox) +} + +func writeTextDemoColumnAnchored(xRefTable *model.XRefTable, p model.Page, region *types.Rectangle) { + writeTextDemoColumnAnchoredWithOffset(xRefTable, p, region, 0, 0) +} + +func createTextDemoColumnAnchored(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + p := model.NewPage(mediaBox, nil) + var region *types.Rectangle + writeTextDemoColumnAnchored(xRefTable, p, region) + region = types.RectForWidthAndHeight(50, 70, 400, 400) + writeTextDemoColumnAnchored(xRefTable, p, region) + return p +} + +func createTextDemoColumnAnchoredWithOffset(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + p := model.NewPage(mediaBox, nil) + var region *types.Rectangle + dx, dy := 20., 20. + writeTextDemoColumnAnchoredWithOffset(xRefTable, p, region, dx, dy) + region = types.RectForWidthAndHeight(50, 70, 400, 400) + writeTextDemoColumnAnchoredWithOffset(xRefTable, p, region, dx, dy) + return p +} + +func writeTextRotateDemoWithOffset(xRefTable *model.XRefTable, p model.Page, region *types.Rectangle, dx, dy float64) { + mediaBox := p.MediaBox + buf := p.Buf + + mediaBB := true + + var cr, cg, cb float32 + cr, cg, cb = .5, .75, 1. + r := mediaBox + if region != nil { + r = region + cr, cg, cb = .75, .75, 1 + } + if mediaBB { + draw.FillRectNoBorder(buf, r, color.SimpleColor{R: cr, G: cg, B: cb}) + draw.DrawHairCross(buf, 0, 0, r) + } + + fillCol := color.Black + + fontName := "Helvetica" + k := p.Fm.EnsureKey(fontName) + + td := model.TextDescriptor{ + Text: "Hello Gopher!\nLine 2", + FontName: fontName, + Embed: true, + FontKey: k, + FontSize: 24, + MLeft: 10, + MRight: 10, + MTop: 10, + MBot: 10, + Scale: 1., + ScaleAbs: true, + RMode: draw.RMFill, + StrokeCol: color.Black, + ShowBackground: true, + BackgroundCol: color.SimpleColor{R: 1., G: .98, B: .77}, + ShowBorder: true, + ShowLineBB: false, + ShowTextBB: true, + HairCross: false, + } + + td.Dx, td.Dy = dx, -dy + td.Rotation, td.FillCol = 0, fillCol + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.TopLeft) + td.Rotation, td.FillCol = 45, color.SimpleColor{R: 1} + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.TopLeft) + td.Rotation, td.FillCol = 90, color.SimpleColor{R: .5} + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.TopLeft) + + td.Dx, td.Dy = 0, -dy + td.Rotation, td.FillCol = 0, fillCol + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.TopCenter) + td.Rotation, td.FillCol = 45, color.SimpleColor{G: 1} + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.TopCenter) + td.Rotation, td.FillCol = 90, color.SimpleColor{G: .5} + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.TopCenter) + + td.Dx, td.Dy = -dx, -dy + td.Rotation, td.FillCol = 0, fillCol + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.TopRight) + td.Rotation, td.FillCol = 45, color.SimpleColor{B: 1} + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.TopRight) + td.Rotation, td.FillCol = 90, color.SimpleColor{B: .5} + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.TopRight) + + td.Dx, td.Dy = dx, 0 + td.Rotation, td.FillCol = 0, fillCol + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.Left) + td.Rotation, td.FillCol = 45, color.SimpleColor{R: 1} + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.Left) + td.Rotation, td.FillCol = 90, color.SimpleColor{R: .5} + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.Left) + + td.Dx, td.Dy = 0, 0 + td.Rotation, td.FillCol = 0, fillCol + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.Center) + td.Rotation, td.FillCol = 45, color.SimpleColor{G: 1} + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.Center) + td.Rotation, td.FillCol = 90, color.SimpleColor{G: .5} + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.Center) + + td.Dx, td.Dy = -dx, 0 + td.Rotation, td.FillCol = 0, fillCol + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.Right) + td.Rotation, td.FillCol = 45, color.SimpleColor{B: 1} + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.Right) + td.Rotation, td.FillCol = 90, color.SimpleColor{B: .5} + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.Right) + + td.Dx, td.Dy = dx, dy + td.Rotation, td.FillCol = 0, fillCol + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.BottomLeft) + td.Rotation, td.FillCol = 45, color.SimpleColor{R: 1} + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.BottomLeft) + td.Rotation, td.FillCol = 90, color.SimpleColor{R: .5} + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.BottomLeft) + + td.Dx, td.Dy = 0, dy + td.Rotation, td.FillCol = 0, fillCol + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.BottomCenter) + td.Rotation, td.FillCol = 45, color.SimpleColor{G: 1} + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.BottomCenter) + td.Rotation, td.FillCol = 90, color.SimpleColor{G: .5} + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.BottomCenter) + + td.Dx, td.Dy = -dx, dy + td.Rotation, td.FillCol = 0, fillCol + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.BottomRight) + td.Rotation, td.FillCol = 45, color.SimpleColor{B: 1} + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.BottomRight) + td.Rotation, td.FillCol = 90, color.SimpleColor{B: .5} + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.BottomRight) +} + +func writeTextRotateDemo(xRefTable *model.XRefTable, p model.Page, region *types.Rectangle) { + writeTextRotateDemoWithOffset(xRefTable, p, region, 0, 0) +} + +func createTextRotateDemo(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + p := model.NewPage(mediaBox, nil) + var region *types.Rectangle + writeTextRotateDemo(xRefTable, p, region) + region = types.RectForWidthAndHeight(150, 150, 300, 300) + writeTextRotateDemo(xRefTable, p, region) + return p +} + +func createTextRotateDemoWithOffset(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + p := model.NewPage(mediaBox, nil) + var region *types.Rectangle + dx, dy := 20., 20. + writeTextRotateDemoWithOffset(xRefTable, p, region, dx, dy) + region = types.RectForWidthAndHeight(150, 150, 300, 300) + writeTextRotateDemoWithOffset(xRefTable, p, region, dx, dy) + return p +} + +func writeTextScaleAbsoluteDemoWithOffset(xRefTable *model.XRefTable, p model.Page, region *types.Rectangle, dx, dy float64) { + mediaBox := p.MediaBox + buf := p.Buf + + mediaBB := true + + var cr, cg, cb float32 + cr, cg, cb = .5, .75, 1. + r := mediaBox + if region != nil { + r = region + cr, cg, cb = .75, .75, 1 + } + if mediaBB { + draw.FillRectNoBorder(buf, r, color.SimpleColor{R: cr, G: cg, B: cb}) + } + + fillCol := color.Black + bgCol := color.SimpleColor{R: 1., G: .98, B: .77} + + fontName := "Helvetica" + k := p.Fm.EnsureKey(fontName) + + td := model.TextDescriptor{ + Text: sampleText, + FontName: fontName, + Embed: true, + FontKey: k, + FontSize: 18, + MLeft: 5, + MRight: 5, + MTop: 5, + MBot: 5, + ScaleAbs: true, + RMode: draw.RMFill, + StrokeCol: color.Black, + ShowBackground: true, + BackgroundCol: bgCol, + ShowBorder: true, + ShowLineBB: false, + ShowTextBB: true, + HairCross: false, + } + + td.HAlign, td.VAlign, td.X, td.Y, td.FontSize = types.AlignJustify, types.AlignMiddle, -1, r.Height()*.72, 9 + td.Scale, td.FillCol = 1, fillCol + model.WriteMultiLine(xRefTable, buf, mediaBox, region, td) + td.Scale, td.FillCol = 1.5, color.SimpleColor{R: 1} + model.WriteMultiLine(xRefTable, buf, mediaBox, region, td) + td.Scale, td.FillCol = 2, color.SimpleColor{R: .5} + model.WriteMultiLine(xRefTable, buf, mediaBox, region, td) + + width := 130. + + td.HAlign, td.VAlign, td.X = types.AlignJustify, types.AlignMiddle, r.Width()*.75 + td.FillCol, td.Text = fillCol, "Justified column\nWidth=130" + + td.FontSize, td.Y = 24, r.Height()*.35 + td.Scale = 1 + model.WriteColumn(xRefTable, buf, mediaBox, region, td, width) + td.Scale = 1.5 + model.WriteColumn(xRefTable, buf, mediaBox, region, td, width) + + td.FontSize, td.Y = 12, r.Height()*.22 + td.Scale = 1 + model.WriteColumn(xRefTable, buf, mediaBox, region, td, width) + td.Scale = 1.5 + model.WriteColumn(xRefTable, buf, mediaBox, region, td, width) + + td.FontSize = 9 + td.Scale, td.Y = 1, r.Height()*.15 + model.WriteColumn(xRefTable, buf, mediaBox, region, td, width) + td.Scale, td.Y = 1.5, r.Height()*.13 + model.WriteColumn(xRefTable, buf, mediaBox, region, td, width) + + td = model.TextDescriptor{ + FontName: fontName, + Embed: true, + FontKey: k, + FontSize: 12, + MLeft: 5, + MRight: 5, + MTop: 5, + MBot: 5, + ScaleAbs: true, + RMode: draw.RMFill, + StrokeCol: color.Black, + ShowBackground: true, + BackgroundCol: bgCol, + ShowBorder: true, + ShowLineBB: false, + ShowTextBB: true, + HairCross: false, + } + + text15 := "Hello Gopher!\nAbsolute Width=1.5" + text1 := "Hello Gopher!\nAbsolute Width=1" + text5 := "Hello Gopher!\nAbsolute Width=.5" + + td.Dx, td.Dy = dx, -dy + td.Scale, td.FillCol, td.Text = 1.5, fillCol, text15 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.TopLeft) + td.Scale, td.FillCol, td.Text = 1, color.SimpleColor{R: 1}, text1 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.TopLeft) + td.Scale, td.FillCol, td.Text = .5, color.SimpleColor{R: .5}, text5 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.TopLeft) + + td.Dx, td.Dy = 0, -dy + td.Scale, td.FillCol, td.Text = 1.5, fillCol, text15 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.TopCenter) + td.Scale, td.FillCol, td.Text = 1, color.SimpleColor{G: 1}, text1 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.TopCenter) + td.Scale, td.FillCol, td.Text = .5, color.SimpleColor{G: .5}, text5 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.TopCenter) + + td.Dx, td.Dy = -dx, -dy + td.Scale, td.FillCol, td.Text = 1.5, fillCol, text15 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.TopRight) + td.Scale, td.FillCol, td.Text = 1, color.SimpleColor{B: 1}, text1 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.TopRight) + td.Scale, td.FillCol, td.Text = .5, color.SimpleColor{B: .5}, text5 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.TopRight) + + td.Dx, td.Dy = dx, 0 + td.Scale, td.FillCol, td.Text = 1.5, fillCol, text15 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.Left) + td.Scale, td.FillCol, td.Text = 1, color.SimpleColor{R: 1}, text1 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.Left) + td.Scale, td.FillCol = .5, color.SimpleColor{R: .5} + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.Left) + + td.Dx, td.Dy = 0, 0 + td.Scale, td.FillCol, td.Text = 1.5, fillCol, text15 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.Center) + td.Scale, td.FillCol, td.Text = 1, color.SimpleColor{G: 1}, text1 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.Center) + td.Scale, td.FillCol, td.Text = .5, color.SimpleColor{G: .5}, text5 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.Center) + + td.Dx, td.Dy = -dx, 0 + td.Scale, td.FillCol, td.Text = 1.5, fillCol, text15 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.Right) + td.Scale, td.FillCol, td.Text = 1, color.SimpleColor{B: 1}, text1 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.Right) + td.Scale, td.FillCol, td.Text = .5, color.SimpleColor{B: .5}, text5 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.Right) + + td.Dx, td.Dy = dx, dy + td.Scale, td.FillCol, td.Text = 1.5, fillCol, text15 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.BottomLeft) + td.Scale, td.FillCol, td.Text = 1, color.SimpleColor{R: 1}, text1 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.BottomLeft) + td.Scale, td.FillCol = .5, color.SimpleColor{R: .5} + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.BottomLeft) + + td.Dx, td.Dy = 0, dy + td.Scale, td.FillCol, td.Text = 1.5, fillCol, text15 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.BottomCenter) + td.Scale, td.FillCol, td.Text = 1, color.SimpleColor{G: 1}, text1 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.BottomCenter) + td.Scale, td.FillCol, td.Text = .5, color.SimpleColor{G: .5}, text5 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.BottomCenter) + + td.Dx, td.Dy = -dx, +dy + td.Scale, td.FillCol, td.Text = 1.5, fillCol, text15 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.BottomRight) + td.Scale, td.FillCol, td.Text = 1, color.SimpleColor{B: 1}, text1 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.BottomRight) + td.Scale, td.FillCol, td.Text = .5, color.SimpleColor{B: .5}, text5 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, r, td, types.BottomRight) + + draw.DrawHairCross(buf, 0, 0, r) +} + +func writeTextScaleAbsoluteDemo(xRefTable *model.XRefTable, p model.Page, region *types.Rectangle) { + writeTextScaleAbsoluteDemoWithOffset(xRefTable, p, region, 0, 0) +} + +func createTextScaleAbsoluteDemo(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + p := model.NewPage(mediaBox, nil) + var region *types.Rectangle + writeTextScaleAbsoluteDemo(xRefTable, p, region) + region = types.RectForWidthAndHeight(20, 70, 180, 180) + writeTextScaleAbsoluteDemo(xRefTable, p, region) + return p +} + +func createTextScaleAbsoluteDemoWithOffset(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + p := model.NewPage(mediaBox, nil) + dx, dy := 20., 20. + var region *types.Rectangle + writeTextScaleAbsoluteDemoWithOffset(xRefTable, p, region, dx, dy) + region = types.RectForWidthAndHeight(20, 70, 180, 180) + writeTextScaleAbsoluteDemoWithOffset(xRefTable, p, region, dx, dy) + return p +} + +func writeTextScaleRelativeDemoWithOffset(xRefTable *model.XRefTable, p model.Page, region *types.Rectangle, dx, dy float64) { + mediaBox := p.MediaBox + buf := p.Buf + + mediaBB := true + + var cr, cg, cb float32 + cr, cg, cb = .5, .75, 1. + r := mediaBox + if region != nil { + r = region + cr, cg, cb = .75, .75, 1 + } + if mediaBB { + draw.FillRectNoBorder(buf, r, color.SimpleColor{R: cr, G: cg, B: cb}) + } + + fillCol := color.Black + bgCol := color.SimpleColor{R: 1., G: .98, B: .77} + + fontName := "Helvetica" + k := p.Fm.EnsureKey(fontName) + + td := model.TextDescriptor{ + Text: sampleText, + FontName: fontName, + Embed: true, + FontKey: k, + FontSize: 18, + MLeft: 5, + MRight: 5, + MTop: 5, + MBot: 5, + HAlign: types.AlignJustify, + VAlign: types.AlignMiddle, + X: -1, + Y: r.Height() * .73, + ScaleAbs: false, + RMode: draw.RMFill, + StrokeCol: color.Black, + ShowBackground: true, + BackgroundCol: bgCol, + ShowBorder: true, + ShowLineBB: false, + ShowTextBB: true, + HairCross: false, + } + + td.FontSize, td.Scale, td.FillCol = 9, .4, fillCol + model.WriteMultiLine(xRefTable, buf, mediaBox, region, td) + td.FontSize, td.Scale, td.FillCol = 9, .6, color.SimpleColor{R: 1} + model.WriteMultiLine(xRefTable, buf, mediaBox, region, td) + td.FontSize, td.Scale, td.FillCol = 9, .8, color.SimpleColor{R: .5} + model.WriteMultiLine(xRefTable, buf, mediaBox, region, td) + + width := 130. + + td = model.TextDescriptor{ + Text: "Justified column\nWidth=130", + FontName: fontName, + Embed: true, + FontKey: k, + FontSize: 18, + MLeft: 5, + MRight: 5, + MTop: 5, + MBot: 5, + HAlign: types.AlignJustify, + VAlign: types.AlignMiddle, + X: r.Width() * .75, + Y: r.Height() * .25, + ScaleAbs: false, + RMode: draw.RMFill, + StrokeCol: color.Black, + ShowBackground: true, + BackgroundCol: bgCol, + ShowBorder: true, + ShowLineBB: false, + ShowTextBB: true, + HairCross: false, + } + td.Scale, td.FillCol = .5, fillCol + model.WriteColumn(xRefTable, buf, mediaBox, region, td, width) + td.Scale, td.FillCol = .3, color.SimpleColor{G: 1} + model.WriteColumn(xRefTable, buf, mediaBox, region, td, width) + td.Scale, td.FillCol = .20, color.SimpleColor{G: .5} + model.WriteColumn(xRefTable, buf, mediaBox, region, td, width) + + td = model.TextDescriptor{ + FontName: fontName, + Embed: true, + FontKey: k, + FontSize: 18, + MLeft: 5, + MRight: 5, + MTop: 5, + MBot: 5, + HAlign: types.AlignJustify, + VAlign: types.AlignMiddle, + X: r.Width() * .75, + Y: r.Height() * .25, + ScaleAbs: false, + RMode: draw.RMFill, + StrokeCol: color.Black, + ShowBackground: true, + BackgroundCol: bgCol, + ShowBorder: true, + ShowLineBB: false, + ShowTextBB: true, + HairCross: false, + } + + text10 := "Hello Gopher!\nRelative Width=10%" + text20 := "Hello Gopher!\nRelative Width=20%" + text30 := "Hello Gopher!\nRelative Width=30%" + + td.Dx, td.Dy = dx, -dy + td.Scale, td.FillCol, td.Text = .3, fillCol, text30 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.TopLeft) + td.Scale, td.FillCol, td.Text = .2, color.SimpleColor{R: 1}, text20 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.TopLeft) + td.Scale, td.FillCol, td.Text = .1, color.SimpleColor{R: .5}, text10 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.TopLeft) + + td.Dx, td.Dy = 0, -dy + td.Scale, td.FillCol, td.Text = .3, fillCol, text30 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.TopCenter) + td.Scale, td.FillCol, td.Text = .2, color.SimpleColor{G: 1}, text20 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.TopCenter) + td.Scale, td.FillCol, td.Text = .1, color.SimpleColor{G: .5}, text10 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.TopCenter) + + td.Dx, td.Dy = -dx, -dy + td.Scale, td.FillCol, td.Text = .3, fillCol, text30 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.TopRight) + td.Scale, td.FillCol, td.Text = .2, color.SimpleColor{B: 1}, text20 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.TopRight) + td.Scale, td.FillCol, td.Text = .1, color.SimpleColor{B: .5}, text10 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.TopRight) + + td.Dx, td.Dy = dx, 0 + td.Scale, td.FillCol, td.Text = .3, fillCol, text30 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.Left) + td.Scale, td.FillCol, td.Text = .2, color.SimpleColor{R: 1}, text20 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.Left) + td.Scale, td.FillCol, td.Text = .1, color.SimpleColor{R: .5}, text10 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.Left) + + td.Dx, td.Dy = 0, 0 + td.Scale, td.FillCol, td.Text = .3, fillCol, text30 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.Center) + td.Scale, td.FillCol, td.Text = .2, color.SimpleColor{G: 1}, text20 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.Center) + td.Scale, td.FillCol, td.Text = .1, color.SimpleColor{G: .5}, text10 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.Center) + + td.Dx, td.Dy = -dx, 0 + td.Scale, td.FillCol, td.Text = .3, fillCol, text30 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.Right) + td.Scale, td.FillCol, td.Text = .2, color.SimpleColor{B: 1}, text20 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.Right) + td.Scale, td.FillCol, td.Text = .1, color.SimpleColor{B: .5}, text10 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.Right) + + td.Dx, td.Dy = dx, dy + td.Scale, td.FillCol, td.Text = .3, fillCol, text30 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.BottomLeft) + td.Scale, td.FillCol, td.Text = .2, color.SimpleColor{R: 1}, text20 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.BottomLeft) + td.Scale, td.FillCol, td.Text = .1, color.SimpleColor{R: .5}, text10 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.BottomLeft) + + td.Dx, td.Dy = 0, dy + td.Scale, td.FillCol, td.Text = .3, fillCol, text30 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.BottomCenter) + td.Scale, td.FillCol, td.Text = .2, color.SimpleColor{G: 1}, text20 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.BottomCenter) + td.Scale, td.FillCol, td.Text = .1, color.SimpleColor{G: .5}, text10 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.BottomCenter) + + td.Dx, td.Dy = -dx, dy + td.Scale, td.FillCol, td.Text = .3, fillCol, text30 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.BottomRight) + td.Scale, td.FillCol, td.Text = .2, color.SimpleColor{B: 1}, text20 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.BottomRight) + td.Scale, td.FillCol, td.Text = .1, color.SimpleColor{B: .5}, text10 + model.WriteMultiLineAnchored(xRefTable, buf, mediaBox, region, td, types.BottomRight) + + draw.DrawHairCross(buf, 0, 0, r) +} + +func writeTextScaleRelativeDemo(xRefTable *model.XRefTable, p model.Page, region *types.Rectangle) { + writeTextScaleRelativeDemoWithOffset(xRefTable, p, region, 0, 0) +} + +func createTextScaleRelativeDemo(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + p := model.NewPage(mediaBox, nil) + var region *types.Rectangle + writeTextScaleRelativeDemo(xRefTable, p, region) + region = types.RectForWidthAndHeight(50, 70, 200, 200) + writeTextScaleRelativeDemo(xRefTable, p, region) + return p +} + +func createTextScaleRelativeDemoWithOffset(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + p := model.NewPage(mediaBox, nil) + var region *types.Rectangle + dx, dy := 20., 20. + writeTextScaleRelativeDemoWithOffset(xRefTable, p, region, dx, dy) + region = types.RectForWidthAndHeight(50, 70, 200, 200) + writeTextScaleRelativeDemoWithOffset(xRefTable, p, region, dx, dy) + return p +} + +func createTextDemoColumns(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + p := model.NewPageWithBg(mediaBox, color.NewSimpleColor(0xbeded9)) + fontName := "Times-Roman" + k := p.Fm.EnsureKey(fontName) + td := model.TextDescriptor{ + FontName: fontName, + Embed: true, + FontKey: k, + FontSize: 9, + MLeft: 10, + MRight: 10, + MTop: 10, + MBot: 10, + Scale: 1., + ScaleAbs: true, + RMode: draw.RMFill, + StrokeCol: color.Black, + ShowBackground: true, + BorderWidth: 3, + } + + // 1st row: 3 side by side columns using anchors, width and a background color. + + width := mediaBox.Width() / 3 + td.MinHeight = mediaBox.Height() / 2 + + // Render left column. + // Draw the bounding box with rounded corners but no borders. + td.Text = sampleText + td.ShowTextBB, td.ShowBorder = true, false + td.BackgroundCol = color.SimpleColor{R: .4, G: .98, B: .77} + td.BorderStyle = types.LJRound + model.WriteColumnAnchored(xRefTable, p.Buf, mediaBox, nil, td, types.TopLeft, width) + + // Render middle column. + // Draw the bounding box with regular corners but no border. + td.Text = sampleText2 + td.Dx = -width / 2 + td.ShowTextBB, td.ShowBorder = true, false + td.BackgroundCol = color.SimpleColor{R: .6, G: .98, B: .77} + td.BorderStyle = types.LJMiter + model.WriteColumnAnchored(xRefTable, p.Buf, mediaBox, nil, td, types.TopCenter, width) + + // Render right column. + // Draw bounding box and a border with rounded corners. + + td.Text = sampleText3 + td.Dx = 0 + td.ShowTextBB, td.ShowBorder = true, true + td.BackgroundCol = color.SimpleColor{R: 1., G: .98, B: .77} + td.BorderCol = color.SimpleColor{R: .2, G: .5, B: .2} + td.BorderStyle = types.LJRound + model.WriteColumnAnchored(xRefTable, p.Buf, mediaBox, nil, td, types.TopRight, width) + + // 2nd row: 3 side by side columns below using relative scaling, + // Indent paragraph beginnings and don't draw the background. + relScaleFactor := .334 + td.Dy = mediaBox.Height() / 2 + td.Scale = relScaleFactor + td.ScaleAbs = false + td.ParIndent = true + td.ShowBackground, td.ShowBorder = false, true + td.HAlign, td.VAlign = types.AlignJustify, types.AlignTop + + // Render left column. + td.Text = sampleText + td.X = 0 + td.ShowTextBB = true + td.BorderStyle = types.LJBevel + model.WriteMultiLine(xRefTable, p.Buf, mediaBox, nil, td) + + // Render middle column. + td.Text = sampleText2 + td.X = mediaBox.Width() / 2 + td.Dx = -width / 2 + td.ShowTextBB = false + model.WriteMultiLine(xRefTable, p.Buf, mediaBox, nil, td) + + // Render right column. + td.Text = sampleText3 + td.X = mediaBox.Width() + td.Dx = 0 + td.ShowTextBB = true + td.BorderStyle = types.LJMiter + model.WriteMultiLine(xRefTable, p.Buf, mediaBox, nil, td) + + draw.DrawHairCross(p.Buf, 0, 0, mediaBox) + + return p +} + +func writeTextBorderTest(xRefTable *model.XRefTable, p model.Page, region *types.Rectangle) model.Page { + mediaBox := p.MediaBox + buf := p.Buf + + mediaBB := true + + var cr, cg, cb float32 + cr, cg, cb = .5, .75, 1. + r := mediaBox + if region != nil { + r = region + cr, cg, cb = .75, .75, 1 + } + if mediaBB { + draw.FillRectNoBorder(buf, r, color.SimpleColor{R: cr, G: cg, B: cb}) + } + + fontName := "Times-Roman" + k := p.Fm.EnsureKey(fontName) + td := model.TextDescriptor{ + FontName: fontName, + Embed: true, + FontKey: k, + FontSize: 7, + MLeft: 10, + MRight: 10, + MTop: 10, + MBot: 10, + Scale: 1., + ScaleAbs: true, + RMode: draw.RMFill, + BorderCol: color.NewSimpleColor(0xabe003), + ShowTextBB: true, + } + + w := mediaBox.Width() / 2 + + // no background, no margin, no border + td.Text = sampleText2 + td.ShowBackground, td.ShowBorder, td.ShowMargins = false, false, false + td.MBot, td.MTop, td.MLeft, td.MRight = 0, 0, 0, 0 + td.BorderWidth = 0 + td.BackgroundCol = color.SimpleColor{R: .6, G: .98, B: .77} + td.BorderStyle = types.LJMiter + model.WriteColumnAnchored(xRefTable, p.Buf, mediaBox, region, td, types.TopLeft, w) + + // with background, no margin, no border + td.Text = sampleText2 + td.ShowBackground, td.ShowBorder, td.ShowMargins = true, false, false + td.MBot, td.MTop, td.MLeft, td.MRight = 0, 0, 0, 0 + td.BorderWidth = 0 + td.BackgroundCol = color.SimpleColor{R: .6, G: .98, B: .77} + model.WriteColumnAnchored(xRefTable, p.Buf, mediaBox, region, td, types.TopCenter, w) + + // with background, with margins, no border + td.Text = sampleText2 + td.ShowBackground, td.ShowBorder, td.ShowMargins = true, false, false + td.MBot, td.MTop, td.MLeft, td.MRight = 10, 10, 10, 10 + td.BackgroundCol = color.SimpleColor{R: .6, G: .98, B: .77} + td.Dy = 100 + model.WriteColumnAnchored(xRefTable, p.Buf, mediaBox, region, td, types.Left, w) + + // with background, with margins, show margins, no border + td.Text = sampleText2 + td.ShowBackground, td.ShowBorder, td.ShowMargins = true, false, true + td.MBot, td.MTop, td.MLeft, td.MRight = 10, 10, 10, 10 + td.BackgroundCol = color.SimpleColor{R: .6, G: .98, B: .77} + td.BorderStyle = types.LJMiter + td.Dy = 100 + bb := model.WriteColumnAnchored(xRefTable, p.Buf, mediaBox, region, td, types.Center, w) + + // with background, no margin, with border, without border background + td.Text = sampleText2 + td.ShowBackground, td.ShowBorder, td.ShowMargins = true, false, false + td.BorderWidth = 5 + td.MBot, td.MTop, td.MLeft, td.MRight = 0, 0, 0, 0 + td.BackgroundCol = color.SimpleColor{R: .6, G: .98, B: .77} + td.BorderStyle = types.LJRound + td.Dy = -bb.Height() / 2 + model.WriteColumnAnchored(xRefTable, p.Buf, mediaBox, region, td, types.Left, w) + + // with background, no margin, with border, with border background + td.Text = sampleText2 + td.ShowBackground, td.ShowBorder, td.ShowMargins = true, true, false + td.BorderWidth = 5 + td.MBot, td.MTop, td.MLeft, td.MRight = 0, 0, 0, 0 + td.BackgroundCol = color.SimpleColor{R: .6, G: .98, B: .77} + td.BorderStyle = types.LJRound + td.Dy = -bb.Height() / 2 + model.WriteColumnAnchored(xRefTable, p.Buf, mediaBox, region, td, types.Center, w) + + // with background, with margins, with border, with border background + td.Text = sampleText2 + td.ShowBackground, td.ShowBorder, td.ShowMargins = true, true, false + td.BorderWidth = 5 + td.MBot, td.MTop, td.MLeft, td.MRight = 10, 10, 10, 10 + td.BackgroundCol = color.SimpleColor{R: .6, G: .98, B: .77} + td.BorderStyle = types.LJRound + td.Dy = 0 + model.WriteColumnAnchored(xRefTable, p.Buf, mediaBox, region, td, types.BottomLeft, w) + + // with background, with margins, show margins, with border, with border background + td.Text = sampleText2 + td.ShowBackground, td.ShowBorder, td.ShowMargins = true, true, true + td.BorderWidth = 5 + td.MBot, td.MTop, td.MLeft, td.MRight = 10, 10, 10, 10 + td.BackgroundCol = color.SimpleColor{R: .6, G: .98, B: .77} + td.BorderStyle = types.LJRound + td.Dy = 0 + model.WriteColumnAnchored(xRefTable, p.Buf, mediaBox, region, td, types.BottomCenter, w) + + draw.DrawHairCross(p.Buf, 0, 0, r) + + return p +} + +func createTextBorderTest(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + p := model.NewPageWithBg(mediaBox, color.NewSimpleColor(0xbeded9)) + var region *types.Rectangle + writeTextBorderTest(xRefTable, p, region) + region = types.RectForWidthAndHeight(70, 200, 200, 200) + writeTextBorderTest(xRefTable, p, region) + return p +} + +func createTextBorderNoMarginAlignLeftTest(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + p := model.NewPageWithBg(mediaBox, color.NewSimpleColor(0xbeded9)) + fontName := "Times-Roman" + k := p.Fm.EnsureKey(fontName) + td := model.TextDescriptor{ + Text: sampleText2, + FontName: fontName, + Embed: true, + FontKey: k, + FontSize: 12, + Scale: 1., + ScaleAbs: true, + RMode: draw.RMFill, + ShowBackground: true, + BackgroundCol: color.SimpleColor{R: .6, G: .98, B: .77}, + ShowBorder: true, + BorderWidth: 10, + ShowMargins: true, + MLeft: 10, + MRight: 10, + MTop: 10, + MBot: 10, + BorderCol: color.NewSimpleColor(0xabe003), + ShowTextBB: true, + } + + td.X, td.Y, td.HAlign, td.VAlign = 100, 450, types.AlignLeft, types.AlignTop + td.MinHeight = 300 + model.WriteColumn(xRefTable, p.Buf, mediaBox, nil, td, 400) + + draw.SetLineWidth(p.Buf, 0) + draw.SetStrokeColor(p.Buf, color.Black) + draw.DrawLineSimple(p.Buf, 100, 0, 100, 600) + draw.DrawLineSimple(p.Buf, 500, 0, 500, 600) + draw.DrawLineSimple(p.Buf, 110, 0, 110, 600) + draw.DrawLineSimple(p.Buf, 490, 0, 490, 600) + draw.DrawLineSimple(p.Buf, 0, 150, 600, 150) + draw.DrawLineSimple(p.Buf, 0, 450, 600, 450) + draw.DrawLineSimple(p.Buf, 0, 160, 600, 160) + draw.DrawLineSimple(p.Buf, 0, 440, 600, 440) + //pdf.DrawHairCross(p.Buf, 0, 0, mediaBox) + return p +} + +func createTextBorderNoMarginAlignRightTest(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + p := model.NewPageWithBg(mediaBox, color.NewSimpleColor(0xbeded9)) + fontName := "Times-Roman" + k := p.Fm.EnsureKey(fontName) + td := model.TextDescriptor{ + Text: sampleText2, + FontName: fontName, + Embed: true, + FontKey: k, + FontSize: 12, + Scale: 1., + ScaleAbs: true, + RMode: draw.RMFill, + ShowBackground: true, + BackgroundCol: color.SimpleColor{R: .6, G: .98, B: .77}, + ShowBorder: true, + BorderWidth: 10, + ShowMargins: true, + MLeft: 10, + MRight: 10, + MTop: 10, + MBot: 10, + BorderCol: color.NewSimpleColor(0xabe003), + ShowTextBB: true, + } + + td.X, td.Y, td.HAlign, td.VAlign = 500, 450, types.AlignRight, types.AlignTop + td.MinHeight = 300 + model.WriteColumn(xRefTable, p.Buf, mediaBox, nil, td, 400) + + draw.SetLineWidth(p.Buf, 0) + draw.SetStrokeColor(p.Buf, color.Black) + draw.DrawLineSimple(p.Buf, 100, 0, 100, 600) + draw.DrawLineSimple(p.Buf, 500, 0, 500, 600) + draw.DrawLineSimple(p.Buf, 110, 0, 110, 600) + draw.DrawLineSimple(p.Buf, 490, 0, 490, 600) + draw.DrawLineSimple(p.Buf, 0, 150, 600, 150) + draw.DrawLineSimple(p.Buf, 0, 450, 600, 450) + draw.DrawLineSimple(p.Buf, 0, 160, 600, 160) + draw.DrawLineSimple(p.Buf, 0, 440, 600, 440) + //pdf.DrawHairCross(p.Buf, 0, 0, mediaBox) + return p +} + +func createTextBorderNoMarginAlignCenterTest(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + p := model.NewPageWithBg(mediaBox, color.NewSimpleColor(0xbeded9)) + fontName := "Times-Roman" + k := p.Fm.EnsureKey(fontName) + td := model.TextDescriptor{ + Text: sampleText2, + FontName: fontName, + Embed: true, + FontKey: k, + FontSize: 12, + Scale: 1., + ScaleAbs: true, + RMode: draw.RMFill, + ShowBackground: true, + BackgroundCol: color.SimpleColor{R: .6, G: .98, B: .77}, + ShowBorder: true, + BorderWidth: 10, + BorderCol: color.NewSimpleColor(0xabe003), + ShowMargins: true, + MLeft: 10, + MRight: 10, + MTop: 10, + MBot: 10, + ShowTextBB: true, + } + + td.X, td.Y, td.HAlign, td.VAlign = 300, 450, types.AlignCenter, types.AlignTop + td.MinHeight = 300 + model.WriteColumn(xRefTable, p.Buf, mediaBox, nil, td, 400) + + draw.SetLineWidth(p.Buf, 0) + draw.SetStrokeColor(p.Buf, color.Black) + draw.DrawLineSimple(p.Buf, 100, 0, 100, 600) + draw.DrawLineSimple(p.Buf, 500, 0, 500, 600) + draw.DrawLineSimple(p.Buf, 110, 0, 110, 600) + draw.DrawLineSimple(p.Buf, 490, 0, 490, 600) + draw.DrawLineSimple(p.Buf, 0, 150, 600, 150) + draw.DrawLineSimple(p.Buf, 0, 450, 600, 450) + draw.DrawLineSimple(p.Buf, 0, 440, 600, 440) + //pdf.DrawHairCross(p.Buf, 0, 0, mediaBox) + return p +} + +func createTextBorderNoMarginAlignJustifyTest(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + p := model.NewPageWithBg(mediaBox, color.NewSimpleColor(0xbeded9)) + fontName := "Times-Roman" + k := p.Fm.EnsureKey(fontName) + td := model.TextDescriptor{ + Text: sampleText2, + FontName: fontName, + Embed: true, + FontKey: k, + FontSize: 12, + Scale: 1., + ScaleAbs: true, + RMode: draw.RMFill, + ShowBackground: true, + BackgroundCol: color.SimpleColor{R: .6, G: .98, B: .77}, + ShowBorder: true, + BorderWidth: 10, + BorderCol: color.NewSimpleColor(0xabe003), + ShowTextBB: true, + ShowMargins: true, + MLeft: 10, + MRight: 10, + MTop: 10, + MBot: 10, + } + + td.X, td.Y, td.HAlign, td.VAlign = 100, 450, types.AlignJustify, types.AlignTop + td.MinHeight = 300 + model.WriteColumn(xRefTable, p.Buf, mediaBox, nil, td, 400) + + draw.SetLineWidth(p.Buf, 0) + draw.SetStrokeColor(p.Buf, color.Black) + draw.DrawLineSimple(p.Buf, 100, 0, 100, 600) + draw.DrawLineSimple(p.Buf, 500, 0, 500, 600) + draw.DrawLineSimple(p.Buf, 110, 0, 110, 600) + draw.DrawLineSimple(p.Buf, 490, 0, 490, 600) + draw.DrawLineSimple(p.Buf, 0, 150, 600, 150) + draw.DrawLineSimple(p.Buf, 0, 450, 600, 450) + draw.DrawLineSimple(p.Buf, 0, 160, 600, 160) + draw.DrawLineSimple(p.Buf, 0, 440, 600, 440) + //pdf.DrawHairCross(p.Buf, 0, 0, mediaBox) + return p +} + +func createXRefAndWritePDF(t *testing.T, msg, fileName string, mediaBox *types.Rectangle, f func(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page) { + t.Helper() + xRefTable, err := pdfcpu.CreateDemoXRef() + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + p := f(xRefTable, mediaBox) + + rootDict, err := xRefTable.Catalog() + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + if err = pdfcpu.AddPageTreeWithSamplePage(xRefTable, rootDict, p); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + outDir := filepath.Join("..", "..", "samples", "basic") + outFile := filepath.Join(outDir, fileName+".pdf") + createAndValidate(t, xRefTable, outFile, msg) +} + +func testTextDemoPDF(t *testing.T, msg, fileName string, w, h int, hAlign types.HAlignment) { + t.Helper() + + var f1, f2, f3, f4 func(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page + + switch hAlign { + case types.AlignLeft: + f1 = createTextDemoAlignLeft + f2 = createTextDemoAlignLeftMargin + f3 = createTextDemoAlignLeftWidth + f4 = createTextDemoAlignLeftWidthAndMargin + case types.AlignCenter: + f1 = createTextDemoAlignCenter + f2 = createTextDemoAlignCenterMargin + f3 = createTextDemoAlignCenterWidth + f4 = createTextDemoAlignCenterWidthAndMargin + case types.AlignRight: + f1 = createTextDemoAlignRight + f2 = createTextDemoAlignRightMargin + f3 = createTextDemoAlignRightWidth + f4 = createTextDemoAlignRightWidthAndMargin + case types.AlignJustify: + f1 = createTextDemoAlignJustify + f2 = createTextDemoAlignJustifyMargin + f3 = createTextDemoAlignJustifyWidth + f4 = createTextDemoAlignJustifyWidthAndMargin + } + + mediaBox := types.RectForDim(float64(w), float64(h)) + createXRefAndWritePDF(t, msg, "TextDemo"+fileName, mediaBox, f1) + createXRefAndWritePDF(t, msg, "TextDemo"+fileName+"Margin", mediaBox, f2) + createXRefAndWritePDF(t, msg, "TextDemo"+fileName+"Width", mediaBox, f3) + createXRefAndWritePDF(t, msg, "TextDemo"+fileName+"WidthAndMargin", mediaBox, f4) +} + +func TestTextDemoPDF(t *testing.T) { + msg := "TestTextDemoPDF" + w, h := 600, 600 + + for _, tt := range []struct { + fileName string + w, h int + hAlign types.HAlignment + }{ + {"AlignLeft", w, h, types.AlignLeft}, + {"AlignCenter", w, h, types.AlignCenter}, + {"AlignRight", w, h, types.AlignRight}, + {"AlignJustify", w, h, types.AlignJustify}, + } { + testTextDemoPDF(t, msg, tt.fileName, tt.w, tt.h, tt.hAlign) + } +} + +func TestColumnDemoPDF(t *testing.T) { + msg := "TestColumnDemoPDF" + + for _, tt := range []struct { + fileName string + w, h int + f func(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page + }{ + {"TestTextAlignJustifyDemo", 600, 600, createTextAlignJustifyDemo}, + {"TestTextAlignJustifyColumnDemo", 600, 600, createTextAlignJustifyColumnDemo}, + {"TextDemoAnchors", 600, 600, createTextDemoAnchors}, + {"TextDemoAnchorsWithOffset", 600, 600, createTextDemoAnchorsWithOffset}, + {"TextDemoColumnAnchored", 1200, 1200, createTextDemoColumnAnchored}, + {"TextDemoColumnAnchoredWithOffset", 1200, 1200, createTextDemoColumnAnchoredWithOffset}, + {"TextRotateDemo", 1200, 1200, createTextRotateDemo}, + {"TextRotateDemoWithOffset", 1200, 1200, createTextRotateDemoWithOffset}, + {"TextScaleAbsoluteDemo", 600, 600, createTextScaleAbsoluteDemo}, + {"TextScaleAbsoluteDemoWithOffset", 600, 600, createTextScaleAbsoluteDemoWithOffset}, + {"TextScaleRelativeDemo", 600, 600, createTextScaleRelativeDemo}, + {"TextScaleRelativeDemoWithOffset", 600, 600, createTextScaleRelativeDemoWithOffset}, + {"TextDemoColumns", 600, 600, createTextDemoColumns}, + {"TextBorderTest", 600, 600, createTextBorderTest}, + {"TextBorderNoMarginAlignLeftTest", 600, 600, createTextBorderNoMarginAlignLeftTest}, + {"TextBorderNoMarginAlignRightTest", 600, 600, createTextBorderNoMarginAlignRightTest}, + {"TextBorderNoMarginAlignCenterTest", 600, 600, createTextBorderNoMarginAlignCenterTest}, + {"TextBorderNoMarginAlignJustifyTest", 600, 600, createTextBorderNoMarginAlignJustifyTest}, + } { + mediaBox := types.RectForDim(float64(tt.w), float64(tt.h)) + createXRefAndWritePDF(t, msg, tt.fileName, mediaBox, tt.f) + } +} + +func writecreateTestRTLUserFont(xRefTable *model.XRefTable, p model.Page, region *types.Rectangle, fontName, s string) { + mediaBox := p.MediaBox + buf := p.Buf + + mediaBB := true + + var cr, cg, cb float32 + cr, cg, cb = .5, .75, 1. + r := mediaBox + if region != nil { + r = region + cr, cg, cb = .75, .75, 1 + } + if mediaBB { + draw.FillRectNoBorder(buf, r, color.SimpleColor{R: cr, G: cg, B: cb}) + } + + k := p.Fm.EnsureKey(fontName) + + td := model.TextDescriptor{ + Text: s, + FontName: fontName, + Embed: true, + FontKey: k, + FontSize: 12, + RTL: true, + MLeft: 5, + MRight: 5, + MTop: 5, + MBot: 5, + X: mediaBox.Width(), + Y: -1, + Scale: 1., + ScaleAbs: true, + HAlign: types.AlignRight, + VAlign: types.AlignMiddle, + RMode: draw.RMFill, + StrokeCol: color.NewSimpleColor(0x206A29), + FillCol: color.NewSimpleColor(0x206A29), + ShowBackground: true, + BackgroundCol: color.SimpleColor{R: 1., G: .98, B: .77}, + ShowBorder: true, + ShowLineBB: false, + ShowTextBB: true, + HairCross: false, + } + + model.WriteMultiLine(xRefTable, buf, mediaBox, region, td) + + draw.DrawHairCross(p.Buf, 0, 0, mediaBox) +} + +func createTestRTLUserFont(xRefTable *model.XRefTable, mediaBox *types.Rectangle, language, fontName string) model.Page { + p := model.NewPage(mediaBox, nil) + var region *types.Rectangle + text := sampleTextRTL[language] + writecreateTestRTLUserFont(xRefTable, p, region, fontName, text) + region = types.RectForWidthAndHeight(10, 10, mediaBox.Width()/4, mediaBox.Height()/4) + writecreateTestRTLUserFont(xRefTable, p, region, fontName, text) + return p +} + +func writecreateTestUserFontJustified(xRefTable *model.XRefTable, p model.Page, region *types.Rectangle, rtl bool) { + mediaBox := p.MediaBox + buf := p.Buf + + mediaBB := true + + var cr, cg, cb float32 + cr, cg, cb = .5, .75, 1. + r := mediaBox + if region != nil { + r = region + cr, cg, cb = .75, .75, 1 + } + if mediaBB { + draw.FillRectNoBorder(buf, r, color.SimpleColor{R: cr, G: cg, B: cb}) + } + + fontName := "Roboto-Regular" + k := p.Fm.EnsureKey(fontName) + + td := model.TextDescriptor{ + Text: sampleText, + FontName: fontName, + Embed: true, + FontKey: k, + FontSize: 12, + RTL: rtl, + MLeft: 5, + MRight: 5, + MTop: 5, + MBot: 5, + X: -1, + Y: -1, + Scale: 1., + ScaleAbs: true, + HAlign: types.AlignJustify, + VAlign: types.AlignMiddle, + RMode: draw.RMFill, + StrokeCol: color.NewSimpleColor(0x206A29), + FillCol: color.NewSimpleColor(0x206A29), + ShowBackground: true, + BackgroundCol: color.SimpleColor{R: 1., G: .98, B: .77}, + ShowBorder: true, + ShowLineBB: false, + ShowTextBB: true, + HairCross: false, + } + + model.WriteMultiLine(xRefTable, buf, mediaBox, region, td) + + draw.DrawHairCross(p.Buf, 0, 0, mediaBox) +} + +func createTestUserFontJustified(xRefTable *model.XRefTable, mediaBox *types.Rectangle, rtl bool) model.Page { + p := model.NewPage(mediaBox, nil) + var region *types.Rectangle + writecreateTestUserFontJustified(xRefTable, p, region, rtl) + return p +} + +func createXRefAndWriteJustifiedPDF(t *testing.T, msg, fileName string, mediaBox *types.Rectangle, rtl bool) { + t.Helper() + xRefTable, err := pdfcpu.CreateDemoXRef() + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + p := createTestUserFontJustified(xRefTable, mediaBox, rtl) + + rootDict, err := xRefTable.Catalog() + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + if err = pdfcpu.AddPageTreeWithSamplePage(xRefTable, rootDict, p); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + outDir := filepath.Join("..", "..", "samples", "basic") + outFile := filepath.Join(outDir, fileName+".pdf") + createAndValidate(t, xRefTable, outFile, msg) +} + +func TestUserFontJustified(t *testing.T) { + msg := "TestUserFontJustified" + mediaBox := types.RectForDim(600, 600) + createXRefAndWriteJustifiedPDF(t, msg, "UserFont_Justified", mediaBox, false) + createXRefAndWriteJustifiedPDF(t, msg, "UserFont_JustifiedRightToLeft", mediaBox, true) +} + +func createXRefAndWriteRTLPDF(t *testing.T, + msg, fileName string, + mediaBox *types.Rectangle, + language, fontName string, + f func(xRefTable *model.XRefTable, mediaBox *types.Rectangle, language, fontName string) model.Page) { + t.Helper() + + xRefTable, err := pdfcpu.CreateDemoXRef() + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + p := f(xRefTable, mediaBox, language, fontName) + + rootDict, err := xRefTable.Catalog() + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + if err = pdfcpu.AddPageTreeWithSamplePage(xRefTable, rootDict, p); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + outDir := filepath.Join("..", "..", "samples", "basic") + outFile := filepath.Join(outDir, fileName+".pdf") + createAndValidate(t, xRefTable, outFile, msg) +} + +func TestUserFontRTL(t *testing.T) { + msg := "TestUserFontRTL" + f := createTestRTLUserFont + mediaBox := types.RectForDim(600, 600) + + for _, tt := range []struct { + fileName string + language string + fontName string + }{ + {"UserFont_Arabic", "Arabic", "UnifontMedium"}, + {"UserFont_Hebrew", "Hebrew", "UnifontMedium"}, + {"UserFont_Persian", "Persian", "UnifontMedium"}, + {"UserFont_Urdu", "Urdu", "UnifontMedium"}, + } { + createXRefAndWriteRTLPDF(t, msg, tt.fileName, mediaBox, tt.language, tt.fontName, f) + } +} + +func createCJKVDemo(xRefTable *model.XRefTable, mediaBox *types.Rectangle) model.Page { + p := model.NewPage(mediaBox, nil) + mb := p.MediaBox + + textEnglish := `pdfcpu +Instant PDF processing for all your needs. +Now supporting CJKV!` + + textChineseSimple := `pdfcpu +即时处理PDF,满足您的所有需求。 +现在支持CJKV字体!` + + textJapanese := `pdfcpu +すべてのニーズに対応するインスタントPDF処理。 +CJKVフォントがサポートされるようになりました!` + + textKorean := `pdfcpu +모든 요구 사항에 맞는 즉각적인 PDF 처리. +이제 CJKV 글꼴을 지원합니다!` + + textVietnamese := `pdfcpu +Xử lý PDF tức thì cho mọi nhu cầu của bạn. +Bây giờ với sự hỗ trợ cho các phông chữ CJKV!` + + td := model.TextDescriptor{ + FontSize: 24, + Embed: true, + MLeft: 5, + MRight: 5, + MTop: 5, + MBot: 5, + Scale: 1, + ScaleAbs: true, + HAlign: types.AlignLeft, + VAlign: types.AlignMiddle, + RMode: draw.RMFill, + StrokeCol: color.NewSimpleColor(0x206A29), + FillCol: color.NewSimpleColor(0x206A29), + ShowBackground: true, + BackgroundCol: color.SimpleColor{R: 1., G: .98, B: .77}, + ShowBorder: true, + ShowLineBB: false, + ShowTextBB: true, + HairCross: false, + } + + td.Text, td.FontName, td.FontKey = textChineseSimple, "UnifontMedium", p.Fm.EnsureKey("UnifontMedium") + td.X, td.Y = 0, mb.Height() + model.WriteColumn(xRefTable, p.Buf, mediaBox, nil, td, 3*mb.Width()/4) + + td.Text, td.FontName, td.FontKey = textJapanese, "Unifont-JPMedium", p.Fm.EnsureKey("Unifont-JPMedium") + td.X, td.Y = mb.Width(), 2*mb.Height()/3 + model.WriteColumn(xRefTable, p.Buf, mediaBox, nil, td, 3*mb.Width()/4) + + td.Text, td.FontName, td.FontKey = textKorean, "UnifontMedium", p.Fm.EnsureKey("UnifontMedium") + td.X, td.Y = 0, mb.Height()/3 + model.WriteColumn(xRefTable, p.Buf, mediaBox, nil, td, 3*mb.Width()/4) + + td.Text, td.FontName, td.FontKey = textVietnamese, "Roboto-Regular", p.Fm.EnsureKey("Roboto-Regular") + td.X, td.Y = mb.Width(), 0 + model.WriteColumn(xRefTable, p.Buf, mediaBox, nil, td, 3*mb.Width()/4) + + td.Text, td.FontSize, td.ShowTextBB = textEnglish, 24, false + td.X, td.Y, td.HAlign = -1, -1, types.AlignCenter + model.WriteColumn(xRefTable, p.Buf, mediaBox, nil, td, 0) + + td.FontSize = 80 + td.Text, td.HAlign, td.X, td.Y = "C", types.AlignRight, mb.Width(), mb.Height() + model.WriteColumn(xRefTable, p.Buf, mediaBox, nil, td, 0) + + td.Text, td.HAlign, td.X, td.Y = "J", types.AlignLeft, 0, 2*mb.Height()/3 + model.WriteColumn(xRefTable, p.Buf, mediaBox, nil, td, 0) + + td.Text, td.HAlign, td.X, td.Y = "K", types.AlignRight, mb.Width(), mb.Height()/3 + model.WriteColumn(xRefTable, p.Buf, mediaBox, nil, td, 0) + + td.Text, td.HAlign, td.X, td.Y = "V", types.AlignLeft, 0, 0 + model.WriteColumn(xRefTable, p.Buf, mediaBox, nil, td, 0) + + return p +} + +func TestCJKV(t *testing.T) { + msg := "TestCJKV" + mediaBox := types.RectForDim(600, 600) + xRefTable, err := pdfcpu.CreateDemoXRef() + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + p := createCJKVDemo(xRefTable, mediaBox) + + rootDict, err := xRefTable.Catalog() + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + if err = pdfcpu.AddPageTreeWithSamplePage(xRefTable, rootDict, p); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + outDir := filepath.Join("..", "..", "samples", "basic") + outFile := filepath.Join(outDir, "UserFont_CJKV.pdf") + createAndValidate(t, xRefTable, outFile, msg) +} diff --git a/pkg/api/test/cut_test.go b/pkg/api/test/cut_test.go new file mode 100644 index 0000000000000000000000000000000000000000..4c7c7ac032cf8b900756d9d952e8f9f72bbf561d --- /dev/null +++ b/pkg/api/test/cut_test.go @@ -0,0 +1,213 @@ +/* +Copyright 2023 The pdf Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +func testCut(t *testing.T, msg, inFile, outDir, outFile string, unit types.DisplayUnit, cutConf string) { + t.Helper() + + cut, err := pdfcpu.ParseCutConfig(cutConf, unit) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + inFile = filepath.Join(inDir, inFile) + outDir = filepath.Join(samplesDir, outDir) + + if err := api.CutFile(inFile, outDir, outFile, nil, cut, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestCut(t *testing.T) { + for _, tt := range []struct { + msg string + inFile string + outDir, outFile string + unit types.DisplayUnit + cutConf string + }{ + {"TestRotatedCutHor", + "testRot.pdf", + "cut", + "cutHorRot", + types.CENTIMETRES, + "hor:.5, margin:1, border:on"}, + + {"TestCutHor", + "test.pdf", + "cut", + "cutHor", + types.CENTIMETRES, + "hor:.5, margin:1, bgcol:#E9967A, border:on"}, + + {"TestCutVer", + "test.pdf", + "cut", + "cutVer", + types.CENTIMETRES, + "ver:.5, margin:1, bgcol:#E9967A"}, + + {"TestCutHorAndVerQuarter", + "test.pdf", + "cut", + "cutHorAndVerQuarter", + types.POINTS, + "h:.5, v:.5"}, + + {"TestCutHorAndVerThird", + "test.pdf", + "cut", + "cutHorAndVerThird", + types.POINTS, + "h:.33333, h:.66666, v:.33333, v:.66666"}, + + {"Test", + "test.pdf", + "cut", + "cutCustom", + types.POINTS, + "h:.25, v:.5"}, + } { + testCut(t, tt.msg, tt.inFile, tt.outDir, tt.outFile, tt.unit, tt.cutConf) + } +} + +func testNDown(t *testing.T, msg, inFile, outDir, outFile string, n int, unit types.DisplayUnit, cutConf string) { + t.Helper() + + cut, err := pdfcpu.ParseCutConfigForN(n, cutConf, unit) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + inFile = filepath.Join(inDir, inFile) + outDir = filepath.Join(samplesDir, outDir) + + if err := api.NDownFile(inFile, outDir, outFile, nil, n, cut, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestNDown(t *testing.T) { + for _, tt := range []struct { + msg string + inFile string + outDir, outFile string + n int + unit types.DisplayUnit + cutConf string + }{ + {"TestNDownRot2", + "testRot.pdf", + "cut", + "ndownRot2", + 2, + types.CENTIMETRES, + "margin:1, bgcol:#E9967A"}, + + {"TestNDown2", + "test.pdf", + "cut", + "ndown2", + 2, + types.CENTIMETRES, + "margin:1, border:on"}, + + {"TestNDown9", + "test.pdf", + "cut", + "ndown9", + 9, + types.CENTIMETRES, + "margin:1, bgcol:#E9967A, border:on"}, + + {"TestNDown16", + "test.pdf", + "cut", + "ndown16", + 16, + types.CENTIMETRES, + ""}, // optional border, margin, bgcolor + } { + testNDown(t, tt.msg, tt.inFile, tt.outDir, tt.outFile, tt.n, tt.unit, tt.cutConf) + } +} + +func testPoster(t *testing.T, msg, inFile, outDir, outFile string, unit types.DisplayUnit, cutConf string) { + t.Helper() + + cut, err := pdfcpu.ParseCutConfigForPoster(cutConf, unit) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + inFile = filepath.Join(inDir, inFile) + outDir = filepath.Join(samplesDir, outDir) + + if err := api.PosterFile(inFile, outDir, outFile, nil, cut, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestPoster(t *testing.T) { + for _, tt := range []struct { + msg string + inFile string + outDir, outFile string + unit types.DisplayUnit + cutConf string + }{ + {"TestPoster", // 2x2 grid of A6 => A4 + "test.pdf", // A4 + "cut", + "poster", + types.POINTS, + "f:A6"}, + + {"TestPosterScaled", // 4x4 grid of A6 => A2 + "test.pdf", // A4 + "cut", + "posterScaled", + types.CENTIMETRES, + "f:A6, scale:2.0, margin:1, bgcol:#E9967A"}, + + {"TestPosterDim", // grid made up of 15x10 cm tiles => A4 + "test.pdf", // A4 + "cut", + "posterDim", + types.CENTIMETRES, + "dim:15 10, margin:1, border:on"}, + + {"TestPosterDimScaled", // grid made up of 15x10 cm tiles => A2 + "test.pdf", // A4 + "cut", + "posterDimScaled", + types.CENTIMETRES, + "dim:15 10, scale:2.0, margin:1, bgcol:#E9967A, border:on"}, + } { + testPoster(t, tt.msg, tt.inFile, tt.outDir, tt.outFile, tt.unit, tt.cutConf) + } +} diff --git a/pkg/api/test/encryption_test.go b/pkg/api/test/encryption_test.go new file mode 100644 index 0000000000000000000000000000000000000000..6ab8c236eb6ed3480fad896a0c1d5fd5418b16cb --- /dev/null +++ b/pkg/api/test/encryption_test.go @@ -0,0 +1,232 @@ +/* +Copyright 2020 The pdf Authors. + +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 + + 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 test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" +) + +func listPermissions(t *testing.T, fileName string) ([]string, error) { + t.Helper() + + msg := "listPermissions" + + f, err := os.Open(fileName) + if err != nil { + t.Fatalf("%s open: %v\n", msg, err) + } + defer f.Close() + + conf := model.NewDefaultConfiguration() + conf.Cmd = model.LISTPERMISSIONS + + ctx, err := api.ReadValidateAndOptimize(f, conf) + if err != nil { + return nil, err + } + + return pdfcpu.Permissions(ctx), nil +} + +func confForAlgorithm(aes bool, keyLength int, upw, opw string) *model.Configuration { + if aes { + return model.NewAESConfiguration(upw, opw, keyLength) + } + return model.NewRC4Configuration(upw, opw, keyLength) +} + +func setPermissions(t *testing.T, aes bool, keyLength int, msg, outFile string) { + t.Helper() + // Set all permissions of encrypted file w/o passwords should fail. + conf := confForAlgorithm(aes, keyLength, "", "") + conf.Permissions = model.PermissionsAll + if err := api.SetPermissionsFile(outFile, "", conf); err == nil { + t.Fatalf("%s: set all permissions w/o pw for %s\n", msg, outFile) + } + + // Set all permissions of encrypted file with user password should fail. + conf = confForAlgorithm(aes, keyLength, "upw", "") + conf.Permissions = model.PermissionsAll + if err := api.SetPermissionsFile(outFile, "", conf); err == nil { + t.Fatalf("%s: set all permissions w/o opw for %s\n", msg, outFile) + } + + // Set all permissions of encrypted file with owner password should fail. + conf = confForAlgorithm(aes, keyLength, "", "opw") + conf.Permissions = model.PermissionsAll + if err := api.SetPermissionsFile(outFile, "", conf); err == nil { + t.Fatalf("%s: set all permissions w/o both pws for %s\n", msg, outFile) + } + + // Set all permissions of encrypted file using both passwords. + conf = confForAlgorithm(aes, keyLength, "upw", "opw") + conf.Permissions = model.PermissionsAll + if err := api.SetPermissionsFile(outFile, "", conf); err != nil { + t.Fatalf("%s: set all permissions for %s: %v\n", msg, outFile, err) + } + + // List permissions using the owner password. + conf = confForAlgorithm(aes, keyLength, "", "opw") + p, err := api.GetPermissionsFile(outFile, conf) + if err != nil { + t.Fatalf("%s: get permissions %s: %v\n", msg, outFile, err) + } + + // Ensure permissions all. + if p == nil || uint16(*p) != uint16(model.PermissionsAll) { + t.Fatal() + } + +} + +func testEncryption(t *testing.T, fileName string, alg string, keyLength int) { + t.Helper() + msg := "testEncryption" + + aes := alg == "aes" + inFile := filepath.Join(inDir, fileName) + outFile := filepath.Join(outDir, "test.pdf") + t.Log(inFile) + + p, err := api.GetPermissionsFile(inFile, nil) + if err != nil { + t.Fatalf("%s: get permissions %s: %v\n", msg, inFile, err) + } + // Ensure full access. + if p != nil { + t.Fatal() + } + + // Encrypt file. + conf := confForAlgorithm(aes, keyLength, "upw", "opw") + if err := api.EncryptFile(inFile, outFile, conf); err != nil { + t.Fatalf("%s: encrypt %s: %v\n", msg, outFile, err) + } + + // List permissions of encrypted file w/o passwords should fail. + if list, err := listPermissions(t, outFile); err == nil { + t.Fatalf("%s: list permissions w/o pw %s: %v\n", msg, outFile, list) + } + + // List permissions of encrypted file using the user password. + conf = confForAlgorithm(aes, keyLength, "upw", "") + p, err = api.GetPermissionsFile(outFile, conf) + if err != nil { + t.Fatalf("%s: get permissions %s: %v\n", msg, inFile, err) + } + // Ensure permissions none. + if p == nil || uint16(*p) != uint16(model.PermissionsNone) { + t.Fatal() + } + + // List permissions of encrypted file using the owner password. + conf = confForAlgorithm(aes, keyLength, "", "opw") + p, err = api.GetPermissionsFile(outFile, conf) + if err != nil { + t.Fatalf("%s: get permissions %s: %v\n", msg, inFile, err) + } + // Ensure permissions none. + if p == nil || uint16(*p) != uint16(model.PermissionsNone) { + t.Fatal() + } + + setPermissions(t, aes, keyLength, msg, outFile) + + // Change user password. + conf = confForAlgorithm(aes, keyLength, "upw", "opw") + if err = api.ChangeUserPasswordFile(outFile, "", "upw", "upwNew", conf); err != nil { + t.Fatalf("%s: change upw %s: %v\n", msg, outFile, err) + } + + // Change owner password. + conf = confForAlgorithm(aes, keyLength, "upwNew", "opw") + if err = api.ChangeOwnerPasswordFile(outFile, "", "opw", "opwNew", conf); err != nil { + t.Fatalf("%s: change opw %s: %v\n", msg, outFile, err) + } + + // Decrypt file using both passwords. + conf = confForAlgorithm(aes, keyLength, "upwNew", "opwNew") + if err = api.DecryptFile(outFile, "", conf); err != nil { + t.Fatalf("%s: decrypt %s: %v\n", msg, outFile, err) + } + + // Validate decrypted file. + if err = api.ValidateFile(outFile, nil); err != nil { + t.Fatalf("%s: validate %s: %v\n", msg, outFile, err) + } +} + +func TestEncryption(t *testing.T) { + for _, fileName := range []string{ + "5116.DCT_Filter.pdf", + "adobe_errata.pdf", + } { + testEncryption(t, fileName, "rc4", 40) + testEncryption(t, fileName, "rc4", 128) + testEncryption(t, fileName, "aes", 40) + testEncryption(t, fileName, "aes", 128) + testEncryption(t, fileName, "aes", 256) + } +} + +func TestPDF20Encryption(t *testing.T) { + // PDF 2.0 encryption assumes aes/256. + for _, fileName := range []string{ + "i277.pdf", + "imageWithBPC.pdf", + "pageLevelOutputIntent.pdf", + "SimplePDF2.0.pdf", + "utf8stringAndAnnotation.pdf", + "utf8test.pdf", + "viaIncrementalSave.pdf", + "withOffsetStart.pdf", + } { + testEncryption(t, filepath.Join("pdf20", fileName), "aes", 256) + } +} + +func TestSetPermissions(t *testing.T) { + msg := "TestSetPermissions" + inFile := filepath.Join(inDir, "5116.DCT_Filter.pdf") + outFile := filepath.Join(outDir, "out.pdf") + + conf := confForAlgorithm(true, 256, "upw", "opw") + permNew := model.PermissionsNone | model.PermissionPrintRev2 | model.PermissionPrintRev3 + conf.Permissions = permNew + + if err := api.EncryptFile(inFile, outFile, conf); err != nil { + t.Fatalf("%s: encrypt %s: %v\n", msg, outFile, err) + } + + conf = confForAlgorithm(true, 256, "upw", "opw") + p, err := api.GetPermissionsFile(outFile, conf) + if err != nil { + t.Fatalf("%s: get permissions %s: %v\n", msg, outFile, err) + } + if p == nil { + t.Fatalf("%s: missing permissions", msg) + } + if uint16(*p) != uint16(permNew) { + t.Fatalf("%s: got: %d want: %d", msg, uint16(*p), uint16(permNew)) + } +} diff --git a/pkg/api/test/extract_test.go b/pkg/api/test/extract_test.go new file mode 100644 index 0000000000000000000000000000000000000000..ac80e43aafb259a7486a9a69f3318096a2a3bd93 --- /dev/null +++ b/pkg/api/test/extract_test.go @@ -0,0 +1,320 @@ +/* +Copyright 2020 The pdf Authors. + +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 + + 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 test + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +func TestExtractImages(t *testing.T) { + msg := "TestExtractImages" + // Extract images for all pages into outDir. + for _, fn := range []string{"5116.DCT_Filter.pdf", "testImage.pdf", "go.pdf"} { + // Test writing files + fn = filepath.Join(inDir, fn) + if err := api.ExtractImagesFile(fn, outDir, nil, nil); err != nil { + t.Fatalf("%s %s: %v\n", msg, fn, err) + } + } + // Extract images for inFile starting with page 1 into outDir. + inFile := filepath.Join(inDir, "testImage.pdf") + if err := api.ExtractImagesFile(inFile, outDir, []string{"1-"}, nil); err != nil { + t.Fatalf("%s %s: %v\n", msg, inFile, err) + } +} + +func compare(t *testing.T, fn1, fn2 string) { + + f1, err := os.Open(fn1) + if err != nil { + t.Errorf("%s: %v", fn1, err) + return + } + defer f1.Close() + + bb1, err := io.ReadAll(f1) + if err != nil { + t.Errorf("%s: %v", fn1, err) + return + } + + f2, err := os.Open(fn2) + if err != nil { + t.Errorf("%s: %v", fn2, err) + return + } + defer f1.Close() + + bb2, err := io.ReadAll(f2) + if err != nil { + t.Errorf("%s: %v", fn2, err) + return + } + + if len(bb1) != len(bb2) { + t.Errorf("%s <-> %s: length mismatch %d != %d", fn1, fn2, len(bb1), len(bb2)) + return + } + + for i := 0; i < len(bb1); i++ { + if bb1[i] != bb2[i] { + t.Errorf("%s <-> %s: mismatch at %d, 0x%02x != 0x%02x\n", fn1, fn2, i, bb1[i], bb2[i]) + return + } + } + +} + +func TestExtractImagesSoftMasks(t *testing.T) { + inFile := filepath.Join(inDir, "VectorApple.pdf") + ctx, err := api.ReadContextFile(inFile) + if err != nil { + t.Fatal(err) + } + + images := make(map[int]*types.StreamDict) + for objId, obj := range ctx.XRefTable.Table { + if obj != nil { + if dict, ok := obj.Object.(types.StreamDict); ok { + if subtype := dict.Dict.NameEntry("Subtype"); subtype != nil && *subtype == "Image" { + images[objId] = &dict + } + } + } + } + + expected := map[int]string{ + 36: "VectorApple_36.tif", // IndexedCMYK w/ softmask + 245: "VectorApple_245.tif", // DeviceCMYK w/ softmask + } + + for objId, filename := range expected { + sd := images[objId] + + if err := sd.Decode(); err != nil { + t.Fatal(err) + } + + tmpFileName := filepath.Join(outDir, filename) + fmt.Printf("tmpFileName: %s\n", tmpFileName) + + // Write the image object (as TIFF file) to disk. + // fn1 is the resulting fileName path including the suffix (aka filetype extension). + fn1, err := pdfcpu.WriteImage(ctx.XRefTable, tmpFileName, sd, false, objId) + if err != nil { + t.Fatalf("err: %v\n", err) + } + + fn2 := filepath.Join(resDir, filename) + + compare(t, fn1, fn2) + } +} + +func TestExtractImagesLowLevel(t *testing.T) { + msg := "TestExtractImagesLowLevel" + fileName := "testImage.pdf" + inFile := filepath.Join(inDir, fileName) + + // Create a context. + ctx, err := api.ReadContextFile(inFile) + if err != nil { + t.Fatalf("%s readContext: %v\n", msg, err) + } + + // Optimize resource usage of this context. + if err := api.OptimizeContext(ctx); err != nil { + t.Fatalf("%s optimizeContext: %v\n", msg, err) + } + + // Extract images for page 1. + i := 1 + ii, err := pdfcpu.ExtractPageImages(ctx, i, false) + if err != nil { + t.Fatalf("%s extractPageFonts(%d): %v\n", msg, i, err) + } + + baseFileName := strings.TrimSuffix(filepath.Base(fileName), ".pdf") + + // Process extracted images. + for _, img := range ii { + fn := filepath.Join(outDir, fmt.Sprintf("%s_%d_%s.%s", baseFileName, i, img.Name, img.FileType)) + if err := pdfcpu.WriteReader(fn, img); err != nil { + t.Fatalf("%s write: %s", msg, fn) + } + } +} + +func TestExtractFonts(t *testing.T) { + msg := "TestExtractFonts" + // Extract fonts for all pages into outDir. + for _, fn := range []string{"5116.DCT_Filter.pdf", "testImage.pdf", "go.pdf"} { + fn = filepath.Join(inDir, fn) + if err := api.ExtractFontsFile(fn, outDir, nil, nil); err != nil { + t.Fatalf("%s %s: %v\n", msg, fn, err) + } + } + // Extract fonts for inFile for pages 1-3 into outDir. + inFile := filepath.Join(inDir, "go.pdf") + if err := api.ExtractFontsFile(inFile, outDir, []string{"1-3"}, nil); err != nil { + t.Fatalf("%s %s: %v\n", msg, inFile, err) + } +} + +func TestExtractFontsLowLevel(t *testing.T) { + msg := "TestExtractFontsLowLevel" + inFile := filepath.Join(inDir, "go.pdf") + + // Create a context. + ctx, err := api.ReadContextFile(inFile) + if err != nil { + t.Fatalf("%s readContext: %v\n", msg, err) + } + + // Optimize resource usage of this context. + if err := api.OptimizeContext(ctx); err != nil { + t.Fatalf("%s optimizeContext: %v\n", msg, err) + } + + // Extract fonts for page 1. + i := 1 + ff, err := pdfcpu.ExtractPageFonts(ctx, i) + if err != nil { + t.Fatalf("%s extractPageFonts(%d): %v\n", msg, i, err) + } + + // Process extracted fonts. + for _, f := range ff { + fn := filepath.Join(outDir, fmt.Sprintf("%s.%s", f.Name, f.Type)) + if err := pdfcpu.WriteReader(fn, f); err != nil { + t.Fatalf("%s write: %s", msg, fn) + } + } +} + +func TestExtractPages(t *testing.T) { + msg := "TestExtractPages" + // Extract page #1 into outDir. + inFile := filepath.Join(inDir, "TheGoProgrammingLanguageCh1.pdf") + if err := api.ExtractPagesFile(inFile, outDir, []string{"1"}, nil); err != nil { + t.Fatalf("%s %s: %v\n", msg, inFile, err) + } +} + +func TestExtractPagesLowLevel(t *testing.T) { + msg := "TestExtractPagesLowLevel" + inFile := filepath.Join(inDir, "TheGoProgrammingLanguageCh1.pdf") + outFile := "MyExtractedAndProcessedSinglePage.pdf" + + // Create a context. + ctx, err := api.ReadContextFile(inFile) + if err != nil { + t.Fatalf("%s readContext: %v\n", msg, err) + } + + // Extract page 1. + i := 1 + + r, err := api.ExtractPage(ctx, i) + if err != nil { + t.Fatalf("%s extractPage(%d): %v\n", msg, i, err) + } + + if err := api.WritePage(r, outDir, outFile, i); err != nil { + t.Fatalf("%s writePage(%d): %v\n", msg, i, err) + } + +} + +func TestExtractContent(t *testing.T) { + msg := "TestExtractContent" + // Extract content of all pages into outDir. + inFile := filepath.Join(inDir, "5116.DCT_Filter.pdf") + if err := api.ExtractContentFile(inFile, outDir, nil, nil); err != nil { + t.Fatalf("%s %s: %v\n", msg, inFile, err) + } +} + +func TestExtractContentLowLevel(t *testing.T) { + msg := "TestExtractContentLowLevel" + inFile := filepath.Join(inDir, "5116.DCT_Filter.pdf") + + // Create a context. + ctx, err := api.ReadContextFile(inFile) + if err != nil { + t.Fatalf("%s readContext: %v\n", msg, err) + } + + // Extract page content for page 2. + i := 2 + r, err := pdfcpu.ExtractPageContent(ctx, i) + if err != nil { + t.Fatalf("%s extractPageContent(%d): %v\n", msg, i, err) + } + + // Process page content. + bb, err := io.ReadAll(r) + if err != nil { + t.Fatalf("%s readAll: %v\n", msg, err) + } + t.Logf("Page content (PDF-syntax) for page %d:\n%s", i, string(bb)) +} + +func TestExtractMetadata(t *testing.T) { + msg := "TestExtractMetadata" + // Extract all metadata into outDir. + inFile := filepath.Join(inDir, "TheGoProgrammingLanguageCh1.pdf") + if err := api.ExtractMetadataFile(inFile, outDir, nil); err != nil { + t.Fatalf("%s %s: %v\n", msg, inFile, err) + } +} + +func TestExtractMetadataLowLevel(t *testing.T) { + msg := "TestExtractMedadataLowLevel" + inFile := filepath.Join(inDir, "TheGoProgrammingLanguageCh1.pdf") + + // Create a context. + ctx, err := api.ReadContextFile(inFile) + if err != nil { + t.Fatalf("%s readContext: %v\n", msg, err) + } + + // Extract all metadata. + mm, err := pdfcpu.ExtractMetadata(ctx) + if err != nil { + t.Fatalf("%s ExtractMetadata: %v\n", msg, err) + } + + // Process metadata. + for _, md := range mm { + bb, err := io.ReadAll(md) + if err != nil { + t.Fatalf("%s metadata readAll: %v\n", msg, err) + } + t.Logf("Metadata: objNr=%d parentDictObjNr=%d parentDictType=%s\n%s\n", + md.ObjNr, md.ParentObjNr, md.ParentType, string(bb)) + } +} diff --git a/pkg/api/test/font_test.go b/pkg/api/test/font_test.go new file mode 100644 index 0000000000000000000000000000000000000000..b248fa785ea4f0c80e327deea73ed4dcf186c843 --- /dev/null +++ b/pkg/api/test/font_test.go @@ -0,0 +1,130 @@ +/* +Copyright 2020 The pdf Authors. + +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 + + 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 test + +import ( + "fmt" + + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/font" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +func writeCoreFontDemoContent(xRefTable *model.XRefTable, p model.Page, fontName string) { + baseFontName := "Helvetica" + baseFontSize := 24 + baseFontKey := p.Fm.EnsureKey(baseFontName) + + fontKey := p.Fm.EnsureKey(fontName) + fontSize := 24 + + fillCol := color.NewSimpleColor(0xf7e6c7) + draw.DrawGrid(p.Buf, 16, 14, types.RectForWidthAndHeight(55, 2, 480, 422), color.Black, &fillCol) + + td := model.TextDescriptor{ + FontName: baseFontName, + FontKey: baseFontKey, + FontSize: baseFontSize, + HAlign: types.AlignCenter, + VAlign: types.AlignBaseline, + Scale: 1.0, + ScaleAbs: true, + RMode: draw.RMFill, + StrokeCol: color.Black, + FillCol: color.NewSimpleColor(0xab6f30), + ShowBackground: true, + BackgroundCol: color.SimpleColor{R: 1., G: .98, B: .77}, + } + + s := fmt.Sprintf("%s %d points", fontName, fontSize) + if fontName != "ZapfDingbats" && fontName != "Symbol" { + s = s + " (CP1252)" + } + td.X, td.Y, td.Text = p.MediaBox.Width()/2, 500, s + td.StrokeCol, td.FillCol = color.NewSimpleColor(0x77bdbd), color.NewSimpleColor(0xab6f30) + model.WriteMultiLine(xRefTable, p.Buf, p.MediaBox, nil, td) + + for i := 0; i < 16; i++ { + s = fmt.Sprintf("#%02X", i) + td.X, td.Y, td.Text, td.FontSize = float64(70+i*30), 427, s, 14 + td.StrokeCol, td.FillCol = color.Black, color.SimpleColor{B: .8} + model.WriteMultiLine(xRefTable, p.Buf, p.MediaBox, nil, td) + } + + for j := 0; j < 14; j++ { + s = fmt.Sprintf("#%02X", j*16+32) + td.X, td.Y, td.Text = 41, float64(403-j*30), s + td.StrokeCol, td.FillCol = color.Black, color.SimpleColor{B: .8} + td.FontName, td.FontKey, td.FontSize = baseFontName, baseFontKey, 14 + model.WriteMultiLine(xRefTable, p.Buf, p.MediaBox, nil, td) + for i := 0; i < 16; i++ { + b := byte(32 + j*16 + i) + s = string([]byte{b}) + td.X, td.Y, td.Text = float64(70+i*30), float64(400-j*30), s + td.StrokeCol, td.FillCol = color.Black, color.Black + td.FontName, td.FontKey, td.FontSize = fontName, fontKey, fontSize + model.WriteMultiLine(xRefTable, p.Buf, p.MediaBox, nil, td) + } + } +} + +func createCoreFontDemoPage(xRefTable *model.XRefTable, w, h int, fontName string) model.Page { + mediaBox := types.RectForDim(float64(w), float64(h)) + p := model.NewPageWithBg(mediaBox, color.NewSimpleColor(0xbeded9)) + writeCoreFontDemoContent(xRefTable, p, fontName) + return p +} +func TestCoreFontDemoPDF(t *testing.T) { + msg := "TestCoreFontDemoPDF" + w, h := 600, 600 + for _, fn := range font.CoreFontNames() { + xRefTable, err := pdfcpu.CreateDemoXRef() + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + rootDict, err := xRefTable.Catalog() + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + p := createCoreFontDemoPage(xRefTable, w, h, fn) + if err = pdfcpu.AddPageTreeWithSamplePage(xRefTable, rootDict, p); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + outFile := filepath.Join("..", "..", "samples", "fonts", "core", fn+".pdf") + createAndValidate(t, xRefTable, outFile, msg) + } +} + +func TestUserFontDemoPDF(t *testing.T) { + msg := "TestUserFontDemoPDF" + + // For each installed user font create a single page pdf cheat sheet for every unicode plane covered + // in pkg/samples/fonts/user. + for _, fn := range font.UserFontNames() { + fmt.Println(fn) + if err := api.CreateUserFontDemoFiles(filepath.Join("..", "..", "samples", "fonts", "user"), fn); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + } +} diff --git a/pkg/api/test/form_test.go b/pkg/api/test/form_test.go new file mode 100644 index 0000000000000000000000000000000000000000..606f582bb96507dbb6924087b3b0a9587bfff764 --- /dev/null +++ b/pkg/api/test/form_test.go @@ -0,0 +1,302 @@ +/* +Copyright 2023 The pdf Authors. + +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 + + 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 test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/form" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" +) + +/************************************************************** + * All form related processing is optimized for Adobe Reader! * + **************************************************************/ + +func listFormFieldsFile(t *testing.T, inFile string, conf *model.Configuration) ([]string, error) { + t.Helper() + + msg := "listFormFields" + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTFORMFIELDS + + f, err := os.Open(inFile) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + defer f.Close() + + ctx, err := api.ReadValidateAndOptimize(f, conf) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + return form.ListFormFields(ctx) +} + +func TestListFormFields(t *testing.T) { + + msg := "TestListFormFields" + inFile := filepath.Join(samplesDir, "form", "demo", "english.pdf") + + ss, err := listFormFieldsFile(t, inFile, conf) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + if len(ss) != 27 { + t.Fatalf("%s: want 27, got %d lines\n", msg, len(ss)) + } +} + +func TestRemoveFormFields(t *testing.T) { + + msg := "TestRemoveFormFields" + inFile := filepath.Join(samplesDir, "form", "demo", "english.pdf") + outFile := filepath.Join(samplesDir, "form", "remove", "removedField.pdf") + + ss, err := listFormFieldsFile(t, inFile, conf) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + want := len(ss) - 2 + + if err := api.RemoveFormFieldsFile(inFile, outFile, []string{"dob1", "firstName1"}, conf); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + ss, err = listFormFieldsFile(t, outFile, conf) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + got := len(ss) + + if got != want { + t.Fail() + } +} + +func TestResetFormFields(t *testing.T) { + + for _, tt := range []struct { + msg string + inFile string + outFile string + }{ + {"TestResetFormCorefont", "english.pdf", "english-reset.pdf"}, // Core font (Helvetica) + {"TestResetFormUserfont", "ukrainian.pdf", "ukrainian-reset.pdf"}, // User font (Roboto-Regular) + {"TestFormRTL", "arabic.pdf", "arabic-reset.pdf"}, // User font RTL (Roboto-Regular) + {"TestResetFormCJK", "chineseSimple.pdf", "chineseSimple-reset.pdf"}, // User font CJK (UnifontMedium) + {"TestResetPersonForm", "person.pdf", "person-reset.pdf"}, // Person Form + } { + inFile := filepath.Join(samplesDir, "form", "demoSinglePage", tt.inFile) + outFile := filepath.Join(samplesDir, "form", "reset", tt.outFile) + if err := api.ResetFormFieldsFile(inFile, outFile, nil, conf); err != nil { + t.Fatalf("%s: %v\n", tt.msg, err) + } + } + +} + +func TestLockFormFields(t *testing.T) { + + for _, tt := range []struct { + msg string + inFile string + outFile string + }{ + {"TestLockFormEN", "english.pdf", "english-locked.pdf"}, // Core font (Helvetica) + {"TestLockFormUK", "ukrainian.pdf", "ukrainian-locked.pdf"}, // User font (Roboto-Regular) + {"TestLockFormRTL", "arabic.pdf", "arabic-locked.pdf"}, // User font RTL (Roboto-Regular) + {"TestLockFormCJK", "chineseSimple.pdf", "chineseSimple-locked.pdf"}, // User font CJK (UnifontMedium) + {"TestLockPersonForm", "person.pdf", "person-locked.pdf"}, // Person Form + } { + inFile := filepath.Join(samplesDir, "form", "demoSinglePage", tt.inFile) + outFile := filepath.Join(samplesDir, "form", "lock", tt.outFile) + if err := api.LockFormFieldsFile(inFile, outFile, nil, conf); err != nil { + t.Fatalf("%s: %v\n", tt.msg, err) + } + } +} + +func TestUnlockFormFields(t *testing.T) { + + for _, tt := range []struct { + msg string + inFile string + outFile string + }{ + {"TestUnlockFormEN", "english-locked.pdf", "english-unlocked.pdf"}, // Core font (Helvetica) + {"TestUnlockFormUK", "ukrainian-locked.pdf", "ukrainian-unlocked.pdf"}, // User font (Roboto-Regular) + {"TestUnlockFormRTL", "arabic-locked.pdf", "arabic-unlocked.pdf"}, // User font RTL (Roboto-Regular) + {"TestUnlockFormCJK", "chineseSimple-locked.pdf", "chineseSimple-unlocked.pdf"}, // User font CJK (UnifontMedium) + {"TestUnlockPersonForm", "person-locked.pdf", "person-unlocked.pdf"}, // Person Form + } { + inFile := filepath.Join(samplesDir, "form", "lock", tt.inFile) + outFile := filepath.Join(samplesDir, "form", "lock", tt.outFile) + if err := api.UnlockFormFieldsFile(inFile, outFile, nil, conf); err != nil { + t.Fatalf("%s: %v\n", tt.msg, err) + } + } +} + +func TestExportForm(t *testing.T) { + + inDir := filepath.Join(samplesDir, "form", "demoSinglePage") + outDir := filepath.Join(samplesDir, "form", "export") + + for _, tt := range []struct { + msg string + inFile string + outFile string + }{ + {"TestExportFormEN", "english.pdf", "english.json"}, // Core font (Helvetica) + {"TestExportFormUK", "ukrainian.pdf", "ukrainian.json"}, // User font (Roboto-Regular) + {"TestExportFormRTL", "arabic.pdf", "arabic.json"}, // User font RTL (Roboto-Regular) + {"TestExportFormCJK", "chineseSimple.pdf", "chineseSimple.json"}, // User font CJK (UnifontMedium) + {"TestExportPersonForm", "person.pdf", "person.json"}, // Person Form + } { + inFile := filepath.Join(inDir, tt.inFile) + outFile := filepath.Join(outDir, tt.outFile) + if err := api.ExportFormFile(inFile, outFile, conf); err != nil { + t.Fatalf("%s: %v\n", tt.msg, err) + } + } +} + +func TestFillForm(t *testing.T) { + + inDir := filepath.Join(samplesDir, "form", "demoSinglePage") + jsonDir := filepath.Join(samplesDir, "form", "fill") + outDir := jsonDir + + for _, tt := range []struct { + msg string + inFile string + inFileJSON string + outFile string + }{ + {"TestFillFormEN", "english.pdf", "english.json", "english.pdf"}, // Core font (Helvetica) + {"TestFillFormUK", "ukrainian.pdf", "ukrainian.json", "ukrainian.pdf"}, // User font (Roboto-Regular) + {"TestFillFormRTL", "arabic.pdf", "arabic.json", "arabic.pdf"}, // User font RTL (Roboto-Regular) + {"TestFillFormCJK", "chineseSimple.pdf", "chineseSimple.json", "chineseSimple.pdf"}, // User font CJK (UnifontMedium) + {"TestFillPersonForm", "person.pdf", "person.json", "person.pdf"}, // Person Form + } { + inFile := filepath.Join(inDir, tt.inFile) + inFileJSON := filepath.Join(jsonDir, tt.inFileJSON) + outFile := filepath.Join(outDir, tt.outFile) + if err := api.FillFormFile(inFile, inFileJSON, outFile, conf); err != nil { + t.Fatalf("%s: %v\n", tt.msg, err) + } + } +} + +func TestMultiFillFormJSON(t *testing.T) { + + inDir := filepath.Join(samplesDir, "form", "demoSinglePage") + jsonDir := filepath.Join(samplesDir, "form", "multifill", "json") + outDir := jsonDir + + for _, tt := range []struct { + msg string + inFile string + inFileJSON string + }{ + {"TestMultiFillFormJSONEnglish", "english.pdf", "english.json"}, + {"TestMultiFillFormJSONPerson", "person.pdf", "person.json"}, + } { + inFile := filepath.Join(inDir, tt.inFile) + inFileJSON := filepath.Join(jsonDir, tt.inFileJSON) + if err := api.MultiFillFormFile(inFile, inFileJSON, outDir, inFile, false, conf); err != nil { + t.Fatalf("%s: %v\n", tt.msg, err) + } + } +} + +func TestMultiFillFormJSONMerged(t *testing.T) { + + inDir := filepath.Join(samplesDir, "form", "demoSinglePage") + jsonDir := filepath.Join(samplesDir, "form", "multifill", "json") + outDir := filepath.Join(jsonDir, "merge") + + for _, tt := range []struct { + msg string + inFile string + inFileJSON string + }{ + {"TestMultiFillFormJSONEnglish", "english.pdf", "english.json"}, + {"TestMultiFillFormJSONPerson", "person.pdf", "person.json"}, + } { + inFile := filepath.Join(inDir, tt.inFile) + inFileJSON := filepath.Join(jsonDir, tt.inFileJSON) + if err := api.MultiFillFormFile(inFile, inFileJSON, outDir, inFile, true, conf); err != nil { + t.Fatalf("%s: %v\n", tt.msg, err) + } + } +} + +func TestMultiFillFormCSV(t *testing.T) { + + inDir := filepath.Join(samplesDir, "form", "demoSinglePage") + csvDir := filepath.Join(samplesDir, "form", "multifill", "csv") + outDir := csvDir + + for _, tt := range []struct { + msg string + inFile string + inFileCSV string + }{ + {"TestMultiFillFormCSVEnglish", "english.pdf", "english.csv"}, + {"TestMultiFillFormCSVPerson", "person.pdf", "person.csv"}, + } { + + inFile := filepath.Join(inDir, tt.inFile) + inFileCSV := filepath.Join(csvDir, tt.inFileCSV) + if err := api.MultiFillFormFile(inFile, inFileCSV, outDir, inFile, false, conf); err != nil { + t.Fatalf("%s: %v\n", tt.msg, err) + } + } +} + +func TestMultiFillFormCSVMerged(t *testing.T) { + + inDir := filepath.Join(samplesDir, "form", "demoSinglePage") + csvDir := filepath.Join(samplesDir, "form", "multifill", "csv") + outDir := filepath.Join(csvDir, "merge") + + for _, tt := range []struct { + msg string + inFile string + inFileCSV string + }{ + {"TestMultiFillFormCSVEnglish", "english.pdf", "english.csv"}, + {"TestMultiFillFormCSVPerson", "person.pdf", "person.csv"}, + } { + + inFile := filepath.Join(inDir, tt.inFile) + inFileCSV := filepath.Join(csvDir, tt.inFileCSV) + if err := api.MultiFillFormFile(inFile, inFileCSV, outDir, inFile, true, conf); err != nil { + t.Fatalf("%s: %v\n", tt.msg, err) + } + } +} diff --git a/pkg/api/test/grid_test.go b/pkg/api/test/grid_test.go new file mode 100644 index 0000000000000000000000000000000000000000..73c124475d6071bc82fd776ffaa6e6c23b6e287f --- /dev/null +++ b/pkg/api/test/grid_test.go @@ -0,0 +1,86 @@ +/* +Copyright 2020 The pdf Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" +) + +func testGrid(t *testing.T, msg string, inFiles []string, outFile string, selectedPages []string, desc string, rows, cols int, isImg bool, conf *model.Configuration) { + t.Helper() + + var ( + nup *model.NUp + err error + ) + + if isImg { + if nup, err = api.ImageGridConfig(rows, cols, desc, conf); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + } else { + if nup, err = api.PDFGridConfig(rows, cols, desc, conf); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + } + + if err := api.NUpFile(inFiles, outFile, selectedPages, nup, conf); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + if err := api.ValidateFile(outFile, conf); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestGrid(t *testing.T) { + + outDir := filepath.Join(samplesDir, "grid") + + for _, tt := range []struct { + msg string + inFiles []string + outFile string + selectedPages []string + desc string + unit string + rows, cols int + isImg bool + }{ + {"TestGridFromPDF", + []string{filepath.Join(inDir, "read.go.pdf")}, + filepath.Join(outDir, "GridFromPDF.pdf"), + nil, "form:LegalP, o:dr, border:off", "points", 4, 6, false}, + + {"TestGridFromPDFWithCropBox", + []string{filepath.Join(inDir, "grid_example.pdf")}, + filepath.Join(outDir, "GridFromPDFWithCropBox.pdf"), + nil, "form:A5L, border:on, margin:0", "points", 2, 1, false}, + + {"TestGridFromImages", + imageFileNames(t, resDir), + filepath.Join(outDir, "GridFromImages.pdf"), + nil, "d:500 500, margin:20, bo:off", "points", 1, 4, true}, + } { + conf := model.NewDefaultConfiguration() + conf.SetUnit(tt.unit) + testGrid(t, tt.msg, tt.inFiles, tt.outFile, tt.selectedPages, tt.desc, tt.rows, tt.cols, tt.isImg, conf) + } +} diff --git a/pkg/api/test/importImages_test.go b/pkg/api/test/importImages_test.go new file mode 100644 index 0000000000000000000000000000000000000000..192ba5dec28cc48143332ab39388f0633cdc0904 --- /dev/null +++ b/pkg/api/test/importImages_test.go @@ -0,0 +1,121 @@ +/* +Copyright 2020 The pdf Authors. + +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 + + 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 test + +import ( + "bufio" + "bytes" + "io" + "os" + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +func testImportImages(t *testing.T, msg string, imgFiles []string, outFile, impConf string) { + t.Helper() + var err error + + // The default import conf uses the special pos:full argument + // which overrides all other import conf parms. + imp := pdfcpu.DefaultImportConfig() + if impConf != "" { + if imp, err = api.Import(impConf, types.POINTS); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + } + if err := api.ImportImagesFile(imgFiles, outFile, imp, nil); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + if err := api.ValidateFile(outFile, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestImportImages(t *testing.T) { + + outDir := filepath.Join(samplesDir, "import") + + testFile1 := filepath.Join(outDir, "CenteredGraySepia.pdf") + os.Remove(testFile1) + + testFile2 := filepath.Join(outDir, "Full.pdf") + os.Remove(testFile2) + + for _, tt := range []struct { + msg string + imgFiles []string + outFile string + impConf string + }{ + // Render image on an A4 portrait mode page. + {"TestCenteredGraySepia", + []string{filepath.Join(resDir, "mountain.jpg")}, + testFile1, + "f:A4, pos:c, bgcol:#beded9"}, + + // Import another image as a new page of testfile1 and convert image to gray. + {"TestCenteredGraySepia", + []string{filepath.Join(resDir, "mountain.png")}, + testFile1, + "f:A4, pos:c, sc:.75, bgcol:#beded9, gray:true"}, + + // Import another image as a new page of testfile1 and apply a sepia filter. + {"TestCenteredGraySepia", + []string{filepath.Join(resDir, "mountain.webp")}, + testFile1, + "f:A4, pos:c, sc:.9, bgcol:#beded9, sepia:true"}, + + // Import another image as a new page of testfile1. + {"TestCenteredGraySepia", + []string{filepath.Join(resDir, "mountain.tif")}, + testFile1, + "f:A4, pos:c, sc:1, bgcol:#beded9"}, + + // Page dimensions match image dimensions. + {"TestFull", + imageFileNames(t, resDir), + testFile2, + "pos:full"}, + } { + testImportImages(t, tt.msg, tt.imgFiles, tt.outFile, tt.impConf) + } +} + +func TestMemBasedWriterPanic(t *testing.T) { + + imgFiles := []string{filepath.Join(resDir, "logoSmall.png")} + + rr := make([]io.Reader, len(imgFiles)) + for i, fn := range imgFiles { + f, err := os.Open(fn) + if err != nil { + t.Fatal(err) + } + rr[i] = bufio.NewReader(f) + } + + outBuf := &bytes.Buffer{} + + if err := api.ImportImages(nil, outBuf, rr, nil, nil); err != nil { + t.Fatal(err) + } + +} diff --git a/pkg/api/test/keyword_test.go b/pkg/api/test/keyword_test.go new file mode 100644 index 0000000000000000000000000000000000000000..3b0d8352a9aeea939012e6acedef5810121ca4e0 --- /dev/null +++ b/pkg/api/test/keyword_test.go @@ -0,0 +1,98 @@ +/* +Copyright 2020 The pdf Authors. + +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 + + 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 test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +func listKeywordsFile(t *testing.T, fileName string, conf *model.Configuration) ([]string, error) { + t.Helper() + + msg := "listKeywords" + + f, err := os.Open(fileName) + if err != nil { + t.Fatalf("%s open: %v\n", msg, err) + } + defer f.Close() + + return api.Keywords(f, conf) +} + +func listKeywords(t *testing.T, msg, fileName string, want []string) []string { + t.Helper() + + got, err := listKeywordsFile(t, fileName, nil) + if err != nil { + t.Fatalf("%s list keywords: %v\n", msg, err) + } + + // # of keywords must be want + if len(got) != len(want) { + t.Fatalf("%s: list keywords %s: want %d got %d\n", msg, fileName, len(want), len(got)) + } + + for _, v := range got { + if !types.MemberOf(v, want) { + t.Fatalf("%s: list keywords %s: want %v got %v\n", msg, fileName, want, got) + } + } + return got +} + +func TestKeywords(t *testing.T) { + msg := "TestKeywords" + + fileName := filepath.Join(outDir, "go.pdf") + if err := copyFile(t, filepath.Join(inDir, "go.pdf"), fileName); err != nil { + t.Fatalf("%s: copyFile: %v\n", msg, err) + } + + // # of keywords must be 0 + listKeywords(t, msg, fileName, nil) + + keywords := []string{"Ö", "你好"} + if err := api.AddKeywordsFile(fileName, "", keywords, nil); err != nil { + t.Fatalf("%s add keywords: %v\n", msg, err) + } + listKeywords(t, msg, fileName, keywords) + + keywords = []string{"world"} + if err := api.AddKeywordsFile(fileName, "", keywords, nil); err != nil { + t.Fatalf("%s add keywords: %v\n", msg, err) + } + listKeywords(t, msg, fileName, []string{"Ö", "你好", "world"}) + + if err := api.RemoveKeywordsFile(fileName, "", []string{"你好"}, nil); err != nil { + t.Fatalf("%s remove 1 keyword: %v\n", msg, err) + } + listKeywords(t, msg, fileName, []string{"Ö", "world"}) + + if err := api.RemoveKeywordsFile(fileName, "", nil, nil); err != nil { + t.Fatalf("%s remove all keywords: %v\n", msg, err) + } + + // # of keywords must be 0 + listKeywords(t, msg, fileName, nil) +} diff --git a/pkg/api/test/merge_test.go b/pkg/api/test/merge_test.go new file mode 100644 index 0000000000000000000000000000000000000000..631557572cf0241dc5c4fa63794f7def0a1f0e74 --- /dev/null +++ b/pkg/api/test/merge_test.go @@ -0,0 +1,174 @@ +/* +Copyright 2020 The pdf Authors. + +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 + + 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 test + +import ( + "bytes" + "io" + "os" + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" +) + +func TestMergeCreateNew(t *testing.T) { + msg := "TestMergeCreate" + inFiles := []string{ + filepath.Join(inDir, "Acroforms2.pdf"), + filepath.Join(inDir, "adobe_errata.pdf"), + } + + // Merge inFiles by concatenation in the order specified and write the result to outFile. + // outFile will be overwritten. + + // Bookmarks for the merged document will be created/preserved per default (see config.yaml) + + outFile := filepath.Join(outDir, "out.pdf") + if err := api.MergeCreateFile(inFiles, outFile, false, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + if err := api.ValidateFile(outFile, conf); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + // Insert an empty page between merged files. + outFile = filepath.Join(outDir, "outWithDivider.pdf") + dividerPage := true + if err := api.MergeCreateFile(inFiles, outFile, dividerPage, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + if err := api.ValidateFile(outFile, conf); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestMergeCreateZipped(t *testing.T) { + msg := "TestMergeCreateZipped" + + // The actual usecase for this is the recombination of 2 PDF files representing even and odd pages of some PDF source. + // See #716 + inFile2 := filepath.Join(inDir, "adobe_errata.pdf") + inFile1 := filepath.Join(inDir, "Acroforms2.pdf") + outFile := filepath.Join(outDir, "out.pdf") + + if err := api.MergeCreateZipFile(inFile1, inFile2, outFile, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + if err := api.ValidateFile(outFile, conf); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestMergeAppendNew(t *testing.T) { + msg := "TestMergeAppend" + inFiles := []string{ + filepath.Join(inDir, "Acroforms2.pdf"), + filepath.Join(inDir, "adobe_errata.pdf"), + } + outFile := filepath.Join(outDir, "test.pdf") + if err := copyFile(t, filepath.Join(inDir, "test.pdf"), outFile); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + // Merge inFiles by concatenation in the order specified and write the result to outFile. + // If outFile already exists its content will be preserved and serves as the beginning of the merge result. + + // Bookmarks for the merged document will be created/preserved per default (see config.yaml) + + if err := api.MergeAppendFile(inFiles, outFile, false, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + if err := api.ValidateFile(outFile, conf); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + anotherFile := filepath.Join(inDir, "testRot.pdf") + err := api.MergeAppendFile([]string{anotherFile}, outFile, false, nil) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + if err := api.ValidateFile(outFile, conf); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestMergeToBufNew(t *testing.T) { + msg := "TestMergeToBuf" + inFiles := []string{ + filepath.Join(inDir, "Acroforms2.pdf"), + filepath.Join(inDir, "adobe_errata.pdf"), + } + outFile := filepath.Join(outDir, "test.pdf") + + destFile := inFiles[0] + inFiles = inFiles[1:] + + buf := &bytes.Buffer{} + if err := api.Merge(destFile, inFiles, buf, nil, false); err != nil { + t.Fatalf("%s: merge: %v\n", msg, err) + } + + if err := os.WriteFile(outFile, buf.Bytes(), 0644); err != nil { + t.Fatalf("%s: write: %v\n", msg, err) + } + + if err := api.ValidateFile(outFile, conf); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestMergeRaw(t *testing.T) { + msg := "TestMergeRaw" + inFiles := []string{ + filepath.Join(inDir, "Acroforms2.pdf"), + filepath.Join(inDir, "adobe_errata.pdf"), + } + outFile := filepath.Join(outDir, "test.pdf") + + var rsc []io.ReadSeeker = make([]io.ReadSeeker, 2) + + f0, err := os.Open(inFiles[0]) + if err != nil { + t.Fatalf("%s: open file1: %v\n", msg, err) + } + defer f0.Close() + rsc[0] = f0 + + f1, err := os.Open(inFiles[1]) + if err != nil { + t.Fatalf("%s: open file2: %v\n", msg, err) + } + defer f1.Close() + rsc[1] = f1 + + buf := &bytes.Buffer{} + if err := api.MergeRaw(rsc, buf, false, nil); err != nil { + t.Fatalf("%s: merge: %v\n", msg, err) + } + + if err := os.WriteFile(outFile, buf.Bytes(), 0644); err != nil { + t.Fatalf("%s: write: %v\n", msg, err) + } + + if err := api.ValidateFile(outFile, conf); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} diff --git a/pkg/api/test/nup_test.go b/pkg/api/test/nup_test.go new file mode 100644 index 0000000000000000000000000000000000000000..b6ef4e85203a4115f05ad063d3427c6daa5b6de8 --- /dev/null +++ b/pkg/api/test/nup_test.go @@ -0,0 +1,111 @@ +/* +Copyright 2020 The pdf Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" +) + +func testNUp(t *testing.T, msg string, inFiles []string, outFile string, selectedPages []string, desc string, n int, isImg bool, conf *model.Configuration) { + t.Helper() + + var ( + nup *model.NUp + err error + ) + + if isImg { + if nup, err = api.ImageNUpConfig(n, desc, conf); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + } else { + if nup, err = api.PDFNUpConfig(n, desc, conf); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + } + + if err := api.NUpFile(inFiles, outFile, selectedPages, nup, conf); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + if err := api.ValidateFile(outFile, conf); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestNUp(t *testing.T) { + + outDir := filepath.Join(samplesDir, "nup") + + for _, tt := range []struct { + msg string + inFiles []string + outFile string + selectedPages []string + desc string + unit string + n int + isImg bool + }{ + // 4-Up a PDF + {"TestNUpFromPDF", + []string{filepath.Join(inDir, "WaldenFull.pdf")}, + filepath.Join(outDir, "NUpFromPDF.pdf"), + nil, + "dim: 400 800, margin:10, bgcol:#f7e6c7", + "mm", + 9, + false}, + + // 2-Up a PDF with CropBox + {"TestNUpFromPdfWithCropBox", + []string{filepath.Join(inDir, "grid_example.pdf")}, + filepath.Join(outDir, "NUpFromPDFWithCropBox.pdf"), + nil, + "form:A5L, border:on, margin:0, bgcol:#f7e6c7", + "points", + 2, + false}, + + // 16-Up an image + {"TestNUpFromSingleImage", + []string{filepath.Join(resDir, "logoSmall.png")}, + filepath.Join(outDir, "NUpFromSingleImage.pdf"), + nil, + "form:A3P, ma:10, bgcol:#f7e6c7", + "points", + 16, + true}, + + // 6-Up a sequence of images. + {"TestNUpFromImages", + imageFileNames(t, resDir), + filepath.Join(outDir, "NUpFromImages.pdf"), + nil, + "form:Tabloid, border:on, ma:10, bgcol:#f7e6c7", + "points", + 6, + true}, + } { + conf := model.NewDefaultConfiguration() + conf.SetUnit(tt.unit) + testNUp(t, tt.msg, tt.inFiles, tt.outFile, tt.selectedPages, tt.desc, tt.n, tt.isImg, conf) + } +} diff --git a/pkg/api/test/optimize_test.go b/pkg/api/test/optimize_test.go new file mode 100644 index 0000000000000000000000000000000000000000..4bc6b607820ea22e2e4ba0cc4b93259b911a41d0 --- /dev/null +++ b/pkg/api/test/optimize_test.go @@ -0,0 +1,43 @@ +/* +Copyright 2020 The pdf Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" +) + +func TestOptimize(t *testing.T) { + msg := "TestOptimize" + fileName := "Acroforms2.pdf" + inFile := filepath.Join(inDir, fileName) + outFile := filepath.Join(outDir, fileName) + + // Create an optimized version of inFile. + if err := api.OptimizeFile(inFile, outFile, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + // Create an optimized version of inFile. + // If you want to modify the original file, pass an empty string for outFile. + inFile = outFile + if err := api.OptimizeFile(inFile, "", nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} diff --git a/pkg/api/test/pageLayout_test.go b/pkg/api/test/pageLayout_test.go new file mode 100644 index 0000000000000000000000000000000000000000..7cdcccc87c0666452f04866286514274422236ff --- /dev/null +++ b/pkg/api/test/pageLayout_test.go @@ -0,0 +1,70 @@ +/* +Copyright 2023 The pdf Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" +) + +func TestPageLayout(t *testing.T) { + msg := "testPageLayout" + + fileName := "test.pdf" + inFile := filepath.Join(outDir, fileName) + copyFile(t, filepath.Join(inDir, fileName), inFile) + + pageLayout := model.PageLayoutTwoColumnLeft + + pl, err := api.PageLayoutFile(inFile, nil) + if err != nil { + t.Fatalf("%s %s: list pageLayout: %v\n", msg, inFile, err) + } + if pl != nil { + t.Fatalf("%s %s: list pageLayout, unexpected: %s\n", msg, inFile, pl) + } + + if err := api.SetPageLayoutFile(inFile, "", pageLayout, nil); err != nil { + t.Fatalf("%s %s: set pageLayout: %v\n", msg, inFile, err) + } + + pl, err = api.PageLayoutFile(inFile, nil) + if err != nil { + t.Fatalf("%s %s: list pageLayout: %v\n", msg, inFile, err) + } + if pl == nil { + t.Fatalf("%s %s: list pageLayout, missing page layout\n", msg, inFile) + } + if *pl != pageLayout { + t.Fatalf("%s %s: list pageLayout, want:%s, got:%s\n", msg, inFile, pageLayout.String(), pl.String()) + } + + if err := api.ResetPageLayoutFile(inFile, "", nil); err != nil { + t.Fatalf("%s %s: reset pageLayout: %v\n", msg, inFile, err) + } + + pl, err = api.PageLayoutFile(inFile, nil) + if err != nil { + t.Fatalf("%s %s: list page layout: %v\n", msg, inFile, err) + } + if pl != nil { + t.Fatalf("%s %s: list page layout, unexpected: %s\n", msg, inFile, pl) + } +} diff --git a/pkg/api/test/pageMode_test.go b/pkg/api/test/pageMode_test.go new file mode 100644 index 0000000000000000000000000000000000000000..277564c6160c32a8725ac183717f12592e5b9b5b --- /dev/null +++ b/pkg/api/test/pageMode_test.go @@ -0,0 +1,70 @@ +/* +Copyright 2023 The pdf Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" +) + +func TestPageMode(t *testing.T) { + msg := "testPageMode" + + fileName := "test.pdf" + inFile := filepath.Join(outDir, fileName) + copyFile(t, filepath.Join(inDir, fileName), inFile) + + pageMode := model.PageModeUseOutlines + + pl, err := api.PageModeFile(inFile, nil) + if err != nil { + t.Fatalf("%s %s: list pageMode: %v\n", msg, inFile, err) + } + if pl != nil { + t.Fatalf("%s %s: list pageMode, unexpected: %s\n", msg, inFile, pl) + } + + if err := api.SetPageModeFile(inFile, "", pageMode, nil); err != nil { + t.Fatalf("%s %s: set pageMode: %v\n", msg, inFile, err) + } + + pm, err := api.PageModeFile(inFile, nil) + if err != nil { + t.Fatalf("%s %s: list pageMode: %v\n", msg, inFile, err) + } + if pm == nil { + t.Fatalf("%s %s: list pageMode, missing page mode\n", msg, inFile) + } + if *pm != pageMode { + t.Fatalf("%s %s: list pageMode, want:%s, got:%s\n", msg, inFile, pageMode.String(), pm.String()) + } + + if err := api.ResetPageModeFile(inFile, "", nil); err != nil { + t.Fatalf("%s %s: reset pageMode: %v\n", msg, inFile, err) + } + + pl, err = api.PageModeFile(inFile, nil) + if err != nil { + t.Fatalf("%s %s: list pageMode: %v\n", msg, inFile, err) + } + if pl != nil { + t.Fatalf("%s %s: list pageMode, unexpected: %s\n", msg, inFile, pl) + } +} diff --git a/pkg/api/test/page_test.go b/pkg/api/test/page_test.go new file mode 100644 index 0000000000000000000000000000000000000000..1409766fd288cc91aa85ceb712307771389483ea --- /dev/null +++ b/pkg/api/test/page_test.go @@ -0,0 +1,100 @@ +/* +Copyright 2020 The pdf Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +func TestInsertRemovePages(t *testing.T) { + msg := "TestInsertRemovePages" + inFile := filepath.Join(inDir, "Acroforms2.pdf") + outFile := filepath.Join(outDir, "test.pdf") + + n1, err := api.PageCountFile(inFile) + if err != nil { + t.Fatalf("%s %s: %v\n", msg, inFile, err) + } + + // Insert an empty page before pages 1 and 2. + if err := api.InsertPagesFile(inFile, outFile, []string{"-2"}, true, nil, nil); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + if err := api.ValidateFile(outFile, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + n2, err := api.PageCountFile(outFile) + if err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + if n2 != n1+2 { + t.Fatalf("%s %s: pageCount want:%d got:%d\n", msg, inFile, n1+2, n2) + } + + // // Remove pages 1 and 2. + if err := api.RemovePagesFile(outFile, "", []string{"-2"}, nil); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + if err := api.ValidateFile(outFile, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + n2, err = api.PageCountFile(outFile) + if err != nil { + t.Fatalf("%s %s: %v\n", msg, inFile, err) + } + if n1 != n2 { + t.Fatalf("%s %s: pageCount want:%d got:%d\n", msg, inFile, n1, n2) + } +} + +func TestInsertCustomBlankPage(t *testing.T) { + msg := "TestInsertCustomBlankPage" + inFile := filepath.Join(inDir, "Acroforms2.pdf") + outFile := filepath.Join(outDir, "test.pdf") + + selectedPages := []string{"2"} + + before := false + + pageConf, err := pdfcpu.ParsePageConfiguration("f:A5L", conf.Unit) + if err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + // Insert an empty A5 page in landscape mode after page 5. + if err := api.InsertPagesFile(inFile, outFile, selectedPages, before, pageConf, conf); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + selectedPages = []string{"odd"} + + pageConf, err = pdfcpu.ParsePageConfiguration("dim:5 10", types.CENTIMETRES) + if err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + // Insert an empty page with dimensions 5 x 10 cm after every odd page. + if err := api.InsertPagesFile(inFile, outFile, selectedPages, before, pageConf, conf); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + +} diff --git a/pkg/api/test/portfolio_test.go b/pkg/api/test/portfolio_test.go new file mode 100644 index 0000000000000000000000000000000000000000..f70f2313a129dcd474229c95e5691db248ddc4d3 --- /dev/null +++ b/pkg/api/test/portfolio_test.go @@ -0,0 +1,78 @@ +/* +Copyright 2020 The pdf Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" +) + +func TestPortfolio(t *testing.T) { + msg := "testPortfolio" + + if err := prepareForAttachmentTest(t); err != nil { + t.Fatalf("%s prepare for portfolio: %v\n", msg, err) + } + + fileName := filepath.Join(outDir, "go.pdf") + + // # of portfolio entries must be 0 + listAttachments(t, msg, fileName, 0) + + // Attach 4 portfolio entries including descriptions. + files := []string{ + filepath.Join(outDir, "golang.pdf"), + filepath.Join(outDir, "T4.pdf") + ", CCITT spec", + filepath.Join(outDir, "go-lecture.pdf"), + filepath.Join(outDir, "test.wav") + ", test audio file", + } + if err := api.AddAttachmentsFile(fileName, "", files, true, nil); err != nil { + t.Fatalf("%s add portfolio entries: %v\n", msg, err) + } + + // List portfolio entries. + listAttachments(t, msg, fileName, 4) + + // Extract all portfolio entries. + if err := api.ExtractAttachmentsFile(fileName, outDir, nil, nil); err != nil { + t.Fatalf("%s extract all portfolio entries: %v\n", msg, err) + } + + // Extract 1 portfolio entry. + if err := api.ExtractAttachmentsFile(fileName, outDir, []string{"golang.pdf"}, nil); err != nil { + t.Fatalf("%s extract one portfolio entry: %v\n", msg, err) + } + + // Remove 1 portfolio entry. + if err := api.RemoveAttachmentsFile(fileName, "", []string{"golang.pdf"}, nil); err != nil { + t.Fatalf("%s remove one portfolio entry: %v\n", msg, err) + } + listAttachments(t, msg, fileName, 3) + + // Remove all portfolio entries. + if err := api.RemoveAttachmentsFile(fileName, "", nil, nil); err != nil { + t.Fatalf("%s remove all portfolio entries: %v\n", msg, err) + } + listAttachments(t, msg, fileName, 0) + + // Validate the processed file. + if err := api.ValidateFile(fileName, nil); err != nil { + t.Fatalf("%s: validate: %v\n", msg, err) + } +} diff --git a/pkg/api/test/property_test.go b/pkg/api/test/property_test.go new file mode 100644 index 0000000000000000000000000000000000000000..60f908a888ee74c26e8267089666a55dae940b3e --- /dev/null +++ b/pkg/api/test/property_test.go @@ -0,0 +1,105 @@ +/* +Copyright 2020 The pdf Authors. + +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 + + 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 test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" +) + +func listPropertiesFile(t *testing.T, fileName string, conf *model.Configuration) ([]string, error) { + t.Helper() + + msg := "listProperties" + + f, err := os.Open(fileName) + if err != nil { + t.Fatalf("%s open: %v\n", msg, err) + } + defer f.Close() + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.LISTPROPERTIES + + ctx, err := api.ReadValidateAndOptimize(f, conf) + if err != nil { + t.Fatalf("%s ReadValidateAndOptimize: %v\n", msg, err) + } + + return pdfcpu.PropertiesList(ctx) +} + +func listProperties(t *testing.T, msg, fileName string, want []string) []string { + t.Helper() + + got, err := listPropertiesFile(t, fileName, nil) + if err != nil { + t.Fatalf("%s list properties: %v\n", msg, err) + } + + // # of keywords must be want + if len(got) != len(want) { + t.Fatalf("%s: list properties %s: want %d got %d\n", msg, fileName, len(want), len(got)) + } + for i, v := range got { + if v != want[i] { + t.Fatalf("%s: list properties %s: want %v got %v\n", msg, fileName, want, got) + } + } + return got +} + +func TestProperties(t *testing.T) { + msg := "TestProperties" + + fileName := filepath.Join(outDir, "go.pdf") + if err := copyFile(t, filepath.Join(inDir, "go.pdf"), fileName); err != nil { + t.Fatalf("%s: copyFile: %v\n", msg, err) + } + + // # of properties must be 0 + listProperties(t, msg, fileName, nil) + + properties := map[string]string{"name1": "value1", "nameÖ": "valueö", "cjkv": "你好"} + if err := api.AddPropertiesFile(fileName, "", properties, nil); err != nil { + t.Fatalf("%s add properties: %v\n", msg, err) + } + + listProperties(t, msg, fileName, []string{"cjkv = 你好", "name1 = value1", "nameÖ = valueö"}) + + if err := api.RemovePropertiesFile(fileName, "", []string{"nameÖ"}, nil); err != nil { + t.Fatalf("%s remove 1 property: %v\n", msg, err) + } + + listProperties(t, msg, fileName, []string{"cjkv = 你好", "name1 = value1"}) + + if err := api.RemovePropertiesFile(fileName, "", nil, nil); err != nil { + t.Fatalf("%s remove all properties: %v\n", msg, err) + } + + // # of properties must be 0 + listProperties(t, msg, fileName, nil) +} diff --git a/pkg/api/test/resize_test.go b/pkg/api/test/resize_test.go new file mode 100644 index 0000000000000000000000000000000000000000..4b9ef38cf129da545a4170fb3d20c2dcdf448e79 --- /dev/null +++ b/pkg/api/test/resize_test.go @@ -0,0 +1,140 @@ +/* +Copyright 2023 The pdf Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +func TestResizeByScaleFactor(t *testing.T) { + msg := "TestResizeByScaleFactor" + + inFile := filepath.Join(inDir, "test.pdf") + + // Enlarge by scale factor 2. + res, err := pdfcpu.ParseResizeConfig("sc:2", types.POINTS) + if err != nil { + t.Fatalf("%s invalid resize configuration: %v\n", msg, err) + } + + outFile := filepath.Join(samplesDir, "resize", "enlargeByScaleFactor.pdf") + if err := api.ResizeFile(inFile, outFile, nil, res, nil); err != nil { + t.Fatalf("%s resize: %v\n", msg, err) + } + + // Shrink by 50%. + res, err = pdfcpu.ParseResizeConfig("sc:.5", types.POINTS) + if err != nil { + t.Fatalf("%s invalid resize configuration: %v\n", msg, err) + } + + outFile = filepath.Join(samplesDir, "resize", "shrinkByScaleFactor.pdf") + if err := api.ResizeFile(inFile, outFile, nil, res, nil); err != nil { + t.Fatalf("%s resize: %v\n", msg, err) + } +} + +func TestResizeByWidthOrHeight(t *testing.T) { + msg := "TestResizeByWidthOrHeight" + + inFile := filepath.Join(inDir, "test.pdf") + + // Set width to 200 points. + res, err := pdfcpu.ParseResizeConfig("dim:200 0", types.POINTS) + if err != nil { + t.Fatalf("%s invalid resize configuration: %v\n", msg, err) + } + + outFile := filepath.Join(samplesDir, "resize", "resizeByWidth.pdf") + if err := api.ResizeFile(inFile, outFile, nil, res, nil); err != nil { + t.Fatalf("%s resize: %v\n", msg, err) + } + + // Set height to 200 mm. + res, err = pdfcpu.ParseResizeConfig("dim:0 200", types.MILLIMETRES) + if err != nil { + t.Fatalf("%s invalid resize configuration: %v\n", msg, err) + } + + outFile = filepath.Join(samplesDir, "resize", "resizeByHeight.pdf") + if err := api.ResizeFile(inFile, outFile, nil, res, nil); err != nil { + t.Fatalf("%s resize: %v\n", msg, err) + } +} + +func TestResizeToFormSize(t *testing.T) { + msg := "TestResizeToPaperSize" + + inFile := filepath.Join(inDir, "test.pdf") + + // Resize to A3 and keep orientation. + res, err := pdfcpu.ParseResizeConfig("form:A3", types.POINTS) + if err != nil { + t.Fatalf("%s invalid resize configuration: %v\n", msg, err) + } + + outFile := filepath.Join(samplesDir, "resize", "resizeToA3.pdf") + if err := api.ResizeFile(inFile, outFile, nil, res, nil); err != nil { + t.Fatalf("%s resize: %v\n", msg, err) + } + + // Resize to A4 and enforce orientation (here landscape mode). + res, err = pdfcpu.ParseResizeConfig("form:A4L", types.POINTS) + if err != nil { + t.Fatalf("%s invalid resize configuration: %v\n", msg, err) + } + + outFile = filepath.Join(samplesDir, "resize", "resizeToA4L.pdf") + if err := api.ResizeFile(inFile, outFile, nil, res, nil); err != nil { + t.Fatalf("%s resize: %v\n", msg, err) + } +} + +func TestResizeToDimensions(t *testing.T) { + msg := "TestResizeToDimensions" + + inFile := filepath.Join(inDir, "test.pdf") + + // Resize to 400 x 200 and keep orientation of input file. + // Apply background color to unused space. + res, err := pdfcpu.ParseResizeConfig("dim:400 200, bgcol:#E9967A", types.POINTS) + if err != nil { + t.Fatalf("%s invalid resize configuration: %v\n", msg, err) + } + + outFile := filepath.Join(samplesDir, "resize", "resizeToDimensionsKeep.pdf") + if err := api.ResizeFile(inFile, outFile, nil, res, nil); err != nil { + t.Fatalf("%s resize: %v\n", msg, err) + } + + // Resize to 400 x 200 and enforce new orientation. + // Render border of original crop box. + res, err = pdfcpu.ParseResizeConfig("dim:400 200, enforce:true, border:on", types.POINTS) + if err != nil { + t.Fatalf("%s invalid resize configuration: %v\n", msg, err) + } + + outFile = filepath.Join(samplesDir, "resize", "resizeToDimensionsEnforce.pdf") + if err := api.ResizeFile(inFile, outFile, nil, res, nil); err != nil { + t.Fatalf("%s resize: %v\n", msg, err) + } +} diff --git a/pkg/api/test/rotate_test.go b/pkg/api/test/rotate_test.go new file mode 100644 index 0000000000000000000000000000000000000000..0b61aaa57ff6356d7efb460a3c2075286628ce30 --- /dev/null +++ b/pkg/api/test/rotate_test.go @@ -0,0 +1,43 @@ +/* +Copyright 2020 The pdf Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" +) + +func TestRotate(t *testing.T) { + msg := "TestRotate" + fileName := "Acroforms2.pdf" + inFile := filepath.Join(inDir, fileName) + outFile := filepath.Join(outDir, fileName) + + // Rotate all pages of inFile, clockwise by 90 degrees and write the result to outFile. + if err := api.RotateFile(inFile, outFile, 90, nil, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + // Rotate the first page of inFile by 180 degrees. + // If you want to modify the original file, pass an empty string for outFile. + inFile = outFile + if err := api.RotateFile(inFile, "", 180, []string{"1"}, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} diff --git a/pkg/api/test/selectPages_test.go b/pkg/api/test/selectPages_test.go new file mode 100644 index 0000000000000000000000000000000000000000..6f4060fd7f84372fa4ae0ad2e4b4c97ff3185501 --- /dev/null +++ b/pkg/api/test/selectPages_test.go @@ -0,0 +1,223 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 test + +import ( + "fmt" + "testing" + + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +func testPageSelectionSyntaxOk(t *testing.T, s string) { + t.Helper() + _, err := api.ParsePageSelection(s) + if err != nil { + t.Errorf("doTestPageSelectionSyntaxOk(%s)\n", s) + } +} + +func testPageSelectionSyntaxFail(t *testing.T, s string) { + t.Helper() + _, err := api.ParsePageSelection(s) + if err == nil { + t.Errorf("doTestPageSelectionSyntaxFail(%s)\n", s) + } +} + +// Test the pageSelection string. +// This is used to select specific pages for extraction and trimming. +func TestPageSelectionSyntax(t *testing.T) { + psOk := []string{"1", "!1", "n1", "1-", "!1-", "n1-", "-5", "!-5", "n-5", "3-5", "!3-5", "n3-5", + "1,2,3", "!-5,10-15,30-", "1-,n4", "odd", "even", " 1"} + + for _, s := range psOk { + testPageSelectionSyntaxOk(t, s) + } + + psFail := []string{"1,", "1 ", "-", " -", " !"} + + for _, s := range psFail { + testPageSelectionSyntaxFail(t, s) + } +} + +func selectedPagesString(sp types.IntSet, pageCount int) string { + s := []string{} + var t string + + for i := 1; i <= pageCount; i++ { + if sp[i] { + t = "1" + } else { + t = "0" + } + s = append(s, t) + } + + return strings.Join(s, "") +} + +func testSelectedPages(s string, pageCount int, compareString string, t *testing.T) { + pageSelection, err := api.ParsePageSelection(s) + if err != nil { + t.Fatalf("testSelectedPages(%s) %v\n", s, err) + } + + selectedPages, err := api.PagesForPageSelection(pageCount, pageSelection, false, true) + if err != nil { + t.Fatalf("testSelectedPages(%s) %v\n", s, err) + } + + resultString := selectedPagesString(selectedPages, pageCount) + + if resultString != compareString { + t.Fatalf("testSelectedPages(%s) expected:%s got%s\n", s, compareString, resultString) + } +} + +func TestSelectedPages(t *testing.T) { + pageCount := 5 + + testSelectedPages("even", pageCount, "01010", t) + testSelectedPages("even,even", pageCount, "01010", t) + testSelectedPages("odd", pageCount, "10101", t) + testSelectedPages("odd,odd", pageCount, "10101", t) + testSelectedPages("even,odd", pageCount, "11111", t) + testSelectedPages("odd,!1", pageCount, "00101", t) + testSelectedPages("odd,n1", pageCount, "00101", t) + testSelectedPages("!1,odd", pageCount, "00101", t) + testSelectedPages("n1,odd", pageCount, "00101", t) + testSelectedPages("!1,odd,even", pageCount, "01111", t) + + testSelectedPages("1", pageCount, "10000", t) + testSelectedPages("2", pageCount, "01000", t) + testSelectedPages("3", pageCount, "00100", t) + testSelectedPages("4", pageCount, "00010", t) + testSelectedPages("5", pageCount, "00001", t) + testSelectedPages("6", pageCount, "00000", t) + + testSelectedPages("-3", pageCount, "11100", t) + testSelectedPages("3-", pageCount, "00111", t) + testSelectedPages("2-4", pageCount, "01110", t) + + testSelectedPages("-2,4-", pageCount, "11011", t) + testSelectedPages("2-4,!3", pageCount, "01010", t) + testSelectedPages("-4,n2", pageCount, "10110", t) + + testSelectedPages("5-7", pageCount, "00001", t) + testSelectedPages("4-", pageCount, "00011", t) + testSelectedPages("5-", pageCount, "00001", t) + testSelectedPages("!4", pageCount, "00000", t) + + testSelectedPages("-l", pageCount, "11111", t) + testSelectedPages("-l-1", pageCount, "11110", t) + testSelectedPages("2-l", pageCount, "01111", t) + testSelectedPages("2-l-2", pageCount, "01100", t) + testSelectedPages("2-l-3", pageCount, "01000", t) + testSelectedPages("2-l-4", pageCount, "00000", t) + testSelectedPages("!l", pageCount, "00000", t) + testSelectedPages("nl", pageCount, "00000", t) + testSelectedPages("!l-2", pageCount, "00000", t) + testSelectedPages("nl-2", pageCount, "00000", t) + testSelectedPages("l", pageCount, "00001", t) + testSelectedPages("l-1", pageCount, "00010", t) + testSelectedPages("l-1-", pageCount, "00011", t) + testSelectedPages("!l,odd", pageCount, "10100", t) + testSelectedPages("l,even", pageCount, "01011", t) + + testSelectedPages("1-l,!2-l-1", pageCount, "10001", t) + testSelectedPages("1-l,!2-l-1", pageCount, "10001", t) +} + +func collectedPagesString(cp []int) string { + return fmt.Sprint(cp) +} + +func testCollectedPages(s string, pageCount int, want string, t *testing.T) { + pageSelection, err := api.ParsePageSelection(s) + if err != nil { + t.Fatalf("testCollectedPages(%s) %v\n", s, err) + } + + collectedPages, err := api.PagesForPageCollection(pageCount, pageSelection) + if err != nil { + t.Fatalf("testCollectedPages(%s) %v\n", s, err) + } + + got := collectedPagesString(collectedPages) + //fmt.Printf("%s\n", resultString) + + if got != want { + t.Fatalf("testCollectedPages(%s) want:%s got%s\n", s, want, got) + } +} + +func TestCollectedPages(t *testing.T) { + pageCount := 5 + + testCollectedPages("even", pageCount, "[2 4]", t) + testCollectedPages("even,even", pageCount, "[2 4 2 4]", t) + testCollectedPages("odd", pageCount, "[1 3 5]", t) + testCollectedPages("odd,odd", pageCount, "[1 3 5 1 3 5]", t) + testCollectedPages("even,odd", pageCount, "[2 4 1 3 5]", t) + testCollectedPages("odd,!1", pageCount, "[3 5]", t) + testCollectedPages("odd,n1", pageCount, "[3 5]", t) + testCollectedPages("!1,odd", pageCount, "[1 3 5]", t) + testCollectedPages("n1,odd", pageCount, "[1 3 5]", t) + testCollectedPages("!1,odd,even", pageCount, "[1 3 5 2 4]", t) + + testCollectedPages("1", pageCount, "[1]", t) + testCollectedPages("2", pageCount, "[2]", t) + testCollectedPages("3", pageCount, "[3]", t) + testCollectedPages("4", pageCount, "[4]", t) + testCollectedPages("5", pageCount, "[5]", t) + + testCollectedPages("-3", pageCount, "[1 2 3]", t) + testCollectedPages("3-", pageCount, "[3 4 5]", t) + testCollectedPages("2-4", pageCount, "[2 3 4]", t) + + testCollectedPages("-2,4-", pageCount, "[1 2 4 5]", t) + testCollectedPages("2-4,!3", pageCount, "[2 4]", t) + testCollectedPages("-4,n2", pageCount, "[1 3 4]", t) + + testCollectedPages("5-7", pageCount, "[5]", t) + testCollectedPages("4-", pageCount, "[4 5]", t) + testCollectedPages("5-", pageCount, "[5]", t) + + testCollectedPages("-l", pageCount, "[1 2 3 4 5]", t) + testCollectedPages("-l-1", pageCount, "[1 2 3 4]", t) + testCollectedPages("2-l", pageCount, "[2 3 4 5]", t) + testCollectedPages("2-l-2", pageCount, "[2 3]", t) + testCollectedPages("2-l-3", pageCount, "[2]", t) + testCollectedPages("l", pageCount, "[5]", t) + testCollectedPages("l-1", pageCount, "[4]", t) + testCollectedPages("l-1-", pageCount, "[4 5]", t) + testCollectedPages("!l,odd", pageCount, "[1 3 5]", t) + testCollectedPages("l,even", pageCount, "[5 2 4]", t) + + testCollectedPages("1-3,2,1,l", pageCount, "[1 2 3 2 1 5]", t) + testCollectedPages("1,1,1,l,l,l", pageCount, "[1 1 1 5 5 5]", t) + testCollectedPages("1-3,2-4,3-", pageCount, "[1 2 3 2 3 4 3 4 5]", t) + testCollectedPages("1-3,2-4,!3", pageCount, "[1 2 2 4]", t) + + testCollectedPages("1-,!l", pageCount, "[1 2 3 4]", t) + testCollectedPages("1-,nl", pageCount, "[1 2 3 4]", t) +} diff --git a/pkg/api/test/split_test.go b/pkg/api/test/split_test.go new file mode 100644 index 0000000000000000000000000000000000000000..686e66030c030e47c90b5b319263e78b25dc3c0d --- /dev/null +++ b/pkg/api/test/split_test.go @@ -0,0 +1,104 @@ +/* +Copyright 2020 The pdf Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" +) + +func TestSplitSpan1(t *testing.T) { + msg := "TestSplitSpan1" + fileName := "Acroforms2.pdf" + inFile := filepath.Join(inDir, fileName) + + // Create single page files of inFile in outDir. + span := 1 + if err := api.SplitFile(inFile, outDir, span, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestSplitSpan2(t *testing.T) { + msg := "TestSplitSpan2" + fileName := "Acroforms2.pdf" + inFile := filepath.Join(inDir, fileName) + + // Create dual page files of inFile in outDir. + span := 2 + if err := api.SplitFile(inFile, outDir, span, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestSplitByBookmark(t *testing.T) { + msg := "TestSplitByBookmark" + fileName := "5116.DCT_Filter.pdf" + inFile := filepath.Join(inDir, fileName) + + // Split along bookmarks. + span := 0 + if err := api.SplitFile(inFile, outDir, span, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestSplitByPageNr(t *testing.T) { + msg := "TestSplitByPageNr" + fileName := "5116.DCT_Filter.pdf" + inFile := filepath.Join(inDir, fileName) + + // Generate page section 1 + // Generate page section 2-9 + // Generate page section 10-49 + // Generate page section 50-last page + + if err := api.SplitByPageNrFile(inFile, outDir, []int{2, 10, 50}, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestSplitLowLevel(t *testing.T) { + msg := "TestSplitLowLevel" + inFile := filepath.Join(inDir, "TheGoProgrammingLanguageCh1.pdf") + outFile := filepath.Join(outDir, "MyExtractedPageSpan.pdf") + + // Create a context. + ctx, err := api.ReadContextFile(inFile) + if err != nil { + t.Fatalf("%s readContext: %v\n", msg, err) + } + + // Extract a page span. + from, thru := 2, 4 + selectedPages := api.PagesForPageRange(from, thru) + usePgCache := false + ctxNew, err := pdfcpu.ExtractPages(ctx, selectedPages, usePgCache) + if err != nil { + t.Fatalf("%s ExtractPages(%d,%d): %v\n", msg, from, thru, err) + } + + // Here you can process this single page PDF context. + + // Write context to file. + if err := api.WriteContextFile(ctxNew, outFile); err != nil { + t.Fatalf("%s write: %v\n", msg, err) + } +} diff --git a/pkg/api/test/stampUserFont_test.go b/pkg/api/test/stampUserFont_test.go new file mode 100644 index 0000000000000000000000000000000000000000..8e63b466fe59dc6aa312030b4aba56754eced8cf --- /dev/null +++ b/pkg/api/test/stampUserFont_test.go @@ -0,0 +1,47 @@ +/* +Copyright 2021 The pdfcpu Authors. + +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 + + 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 test + +import ( + "fmt" + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" +) + +func TestStampUserFont(t *testing.T) { + msg := "TestStampUserFont" + inFile := filepath.Join(inDir, "mountain.pdf") + outDir := filepath.Join("..", "..", "samples", "stamp", "text", "utf8") + + for _, sample := range langSamples { + outFile := filepath.Join(outDir, sample.lang+".pdf") + align, rtl := "l", "off" + if sample.rtl { + align, rtl = "r", "on" + } + desc := fmt.Sprintf("font:%s, rtl:%s, align:%s, scale:1.0 rel, rot:0, fillc:#000000, bgcol:#ab6f30, margin:10, border:10 round, opacity:.7", sample.fontName, rtl, align) + err := api.AddTextWatermarksFile(inFile, outFile, nil, true, sample.text, desc, nil) + if err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + if err := api.ValidateFile(outFile, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + } +} diff --git a/pkg/api/test/stampVersatile_test.go b/pkg/api/test/stampVersatile_test.go new file mode 100644 index 0000000000000000000000000000000000000000..bcf5aa9ae961b18bec341ce512c0d5a4ad478931 --- /dev/null +++ b/pkg/api/test/stampVersatile_test.go @@ -0,0 +1,392 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 test + +import ( + "fmt" + "os" + "path/filepath" + "testing" + "time" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +func TestAlternatingPageNumbersViaWatermarkMap(t *testing.T) { + msg := "TestAlternatingPageNumbersViaWatermarkMap" + inFile := filepath.Join(inDir, "WaldenFull.pdf") + outFile := filepath.Join(samplesDir, "stamp", "mixed", "AlternatingPageNumbersViaWatermarkMap.pdf") + + pageCount, err := api.PageCountFile(inFile) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + // Prepare a map of watermarks. + // This maps pages to corresponding watermarks. + // Any page may be assigned a single watermark of type text, image or PDF. + m := map[int]*model.Watermark{} + + // Start stamping with page 2. + // For odd page numbers add a blue stamp on the bottom right corner using Roboto-Regular + // For even page numbers add a green stamp on the bottom left corner using Times-Italic + for i := 2; i <= pageCount; i++ { + text := fmt.Sprintf("%d of %d", i, pageCount) + fontName := "Times-Italic" + pos := "bl" + dx := 10 + fillCol := "#008000" + if i%2 > 0 { + fontName = "Roboto-Regular" + pos = "br" + dx = -10 + fillCol = "#0000E0" + } + desc := fmt.Sprintf("font:%s, points:12, scale:1 abs, pos:%s, off:%d 10, fillcol:%s, rot:0", fontName, pos, dx, fillCol) + wm, err := api.TextWatermark(text, desc, true, false, types.POINTS) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + m[i] = wm + } + + if err := api.AddWatermarksMapFile(inFile, outFile, m, nil); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + // Add a stamp with the creation date on the center of the bottom of every page. + text := fmt.Sprintf("%%p of %%P - Creation date: %v", time.Now().Format("2006-01-02 15:04")) + if err := api.AddTextWatermarksFile(outFile, outFile, nil, true, text, "fo:Roboto-Regular, points:12, scale:1 abs, pos:bc, off:0 10, rot:0", nil); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + // Add a "Draft" stamp with opacity 0.6 along the 1st diagonale in light blue using Courier. + if err := api.AddTextWatermarksFile(outFile, outFile, nil, true, "Draft", "fo:Courier, scale:.9, fillcol:#00aacc, op:.6", nil); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } +} + +func TestAlternatingPageNumbersViaWatermarkMapLowLevel(t *testing.T) { + msg := "TestAlternatingPageNumbersViaWatermarkMapLowLevel" + inFile := filepath.Join(inDir, "WaldenFull.pdf") + outFile := filepath.Join(samplesDir, "stamp", "mixed", "AlternatingPageNumbersViaWatermarkMapLowLevel.pdf") + + // Create a context. + ctx, err := api.ReadContextFile(inFile) + if err != nil { + t.Fatalf("%s readContext: %v\n", msg, err) + } + + m := map[int]*model.Watermark{} + unit := types.POINTS + + // Start stamping with page 2. + // For odd page numbers add a blue stamp on the bottom right corner using Roboto-Regular + // For even page numbers add a green stamp on the bottom left corner using Times-Italic + for i := 2; i <= ctx.PageCount; i++ { + text := fmt.Sprintf("%d of %d", i, ctx.PageCount) + fontName := "Times-Italic" + pos := "bl" + dx := 10 + fillCol := "#008000" + if i%2 > 0 { + fontName = "Roboto-Regular" + pos = "br" + dx = -10 + fillCol = "#0000E0" + } + desc := fmt.Sprintf("font:%s, points:12, scale:1 abs, pos:%s, off:%d 10, fillcol:%s, rot:0", fontName, pos, dx, fillCol) + wm, err := api.TextWatermark(text, desc, true, false, unit) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + m[i] = wm + } + + if err := pdfcpu.AddWatermarksMap(ctx, m); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + // Add a stamp with the creation date on the center of the bottom of every page. + text := fmt.Sprintf("%%p of %%P - Creation date: %v", time.Now().Format("2006-01-02 15:04")) + wm, err := api.TextWatermark(text, "fo:Roboto-Regular, points:12, scale:1 abs, pos:bc, off:0 10, rot:0", true, false, unit) + if err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + if err := pdfcpu.AddWatermarks(ctx, nil, wm); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + // Add a "Draft" stamp with opacity 0.6 along the 1st diagonale in light blue using Courier. + wm, err = api.TextWatermark("Draft", "fo:Courier, scale:.9, fillcol:#00aacc, op:.6", true, false, unit) + if err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + if err := pdfcpu.AddWatermarks(ctx, nil, wm); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + // Write context to file. + if err := api.WriteContextFile(ctx, outFile); err != nil { + t.Fatalf("%s write: %v\n", msg, err) + } +} + +func TestAlternatingPageNumbersViaWatermarkSliceMap(t *testing.T) { + msg := "TestAlternatingPageNumbersViaWatermarkSliceMap" + inFile := filepath.Join(inDir, "WaldenFull.pdf") + outFile := filepath.Join(samplesDir, "stamp", "mixed", "AlternatingPageNumbersViaWatermarkSliceMap.pdf") + + pageCount, err := api.PageCountFile(inFile) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + m := map[int][]*model.Watermark{} + opacity := 1.0 + onTop := true // All stamps! + update := false + unit := types.POINTS + + // Prepare a map of watermark slices. + // This maps pages to corresponding watermarks. + // Each page may be assigned an arbitrary number of watermarks of type text, image or PDF. + for i := 2; i <= pageCount; i++ { + + wms := []*model.Watermark{} + + // 1st watermark on page + // For odd page numbers add a blue stamp on the bottom right corner using Roboto-Regular + // For even page numbers add a green stamp on the bottom left corner using Times-Italic + text := fmt.Sprintf("%d of %d", i, pageCount) + fontName := "Times-Italic" + pos := "bl" + dx := 10 + fillCol := "#008000" + if i%2 > 0 { + fontName = "Roboto-Regular" + pos = "br" + dx = -10 + fillCol = "#0000E0" + } + desc := fmt.Sprintf("font:%s, points:12, scale:1 abs, pos:%s, off:%d 10, fillcol:%s, rot:0, op:%f", fontName, pos, dx, fillCol, opacity) + wm, err := api.TextWatermark(text, desc, onTop, update, unit) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + wms = append(wms, wm) + + // 2nd watermark on page + // Add a stamp with the creation date on the center of the bottom of every page. + text = fmt.Sprintf("%%p of %%P - Creation date: %v", time.Now().Format("2006-01-02 15:04")) + desc = fmt.Sprintf("fo:Roboto-Regular, points:12, scale:1 abs, pos:bc, off:0 10, rot:0, op:%f", opacity) + wm, err = api.TextWatermark(text, desc, onTop, update, unit) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + wms = append(wms, wm) + + // 3rd watermark on page + // Add a "Draft" stamp with opacity 0.6 along the 1st diagonale in light blue using Courier. + text = "Draft" + desc = fmt.Sprintf("fo:Courier, scale:.9, fillcol:#00aacc, op:%f", opacity) + wm, err = api.TextWatermark(text, desc, onTop, update, unit) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + wms = append(wms, wm) + + m[i] = wms + } + + // Apply all watermarks in one Go. + // Assumption: All watermarks share the same opacity and onTop (all stamps or watermarks). + // If you cannot ensure this you have to do something along the lines of func TestAlternatingPageNumbersViaWatermarkMap + if err := api.AddWatermarksSliceMapFile(inFile, outFile, m, nil); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } +} + +func TestImagesTextAndPDFWMViaWatermarkMap(t *testing.T) { + msg := "TestImagesTextAndPDFWMViaWatermarkMap" + inFile := filepath.Join(inDir, "WaldenFull.pdf") + outFile := filepath.Join(samplesDir, "stamp", "mixed", "ImagesTextAndPDFWMViaWatermarkMap.pdf") + + pageCount, err := api.PageCountFile(inFile) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + m := map[int]*model.Watermark{} + fileNames := imageFileNames(t, resDir) + + opacity := 1.0 + onTop := true // All stamps! + update := false + unit := types.POINTS + + // Apply a mix of image, text and PDF watermarks in one go. + for i := 1; i <= pageCount; i++ { + if i <= len(fileNames) { + desc := fmt.Sprintf("pos:bl, scale:.25, rot:0, op:%f", opacity) + wm, err := api.ImageWatermark(fileNames[i-1], desc, onTop, update, unit) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + m[i] = wm + continue + } + + if i%2 > 0 { + desc := fmt.Sprintf("scale:.25, pos:br, rot:0, op:%f", opacity) + wm, err := api.PDFWatermark(inFile+":1", desc, onTop, update, unit) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + m[i] = wm + continue + } + + desc := fmt.Sprintf("rot:0, op:%f", opacity) + wm, err := api.TextWatermark("Even page number", desc, onTop, update, unit) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + m[i] = wm + } + + // Apply all watermarks in one Go. + // Assumption: All watermarks share the same opacity and onTop (all stamps or watermarks). + // If you cannot ensure this you have to do something along the lines of func TestAlternatingPageNumbersViaWatermarkMap + if err := api.AddWatermarksMapFile(inFile, outFile, m, nil); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } +} + +func TestPdfSingleStampVariations(t *testing.T) { + msg := "TestPdfSingleStampVariations" + inFile := filepath.Join(inDir, "zineTest.pdf") + stampFile := inFile + + // Stamp selected pages of inFile with one specific page of some PDF file - the stampFile. + + rs, err := os.Open(stampFile) + if err != nil { + t.Fatalf("%s %s: %v\n", msg, stampFile, err) + } + defer rs.Close() + + for _, tt := range []struct { + msg, outFile string + pageNrSrc int + }{ + {"TestPdfSingleStampDefault", // Use page 2 of stampFile to stamp inFile pages. + "PdfSingleStampDefault.pdf", + 2, + }, + {"TestPdfMultiStampDefault", // Start stamping at page 1 using page 1 of stampFile. + "TestPdfMultiStampDefault.pdf", + 0, // special case defaulting to multistamping + }, + } { + wm, err := api.PDFWatermarkForReadSeeker( + rs, + tt.pageNrSrc, + "scale:.2, pos:tr, off:-10 -10, rot:0", // scaled @ top right corner using some offset and 0 rotation. + true, // stamp + false, // no update + conf.Unit, + ) + + if err != nil { + t.Fatalf("%s: %v\n", tt.msg, err) + } + + outFile := filepath.Join(samplesDir, "stamp", "mixed", tt.outFile) + + if err = api.AddWatermarksFile(inFile, outFile, nil, wm, conf); err != nil { + t.Fatalf("%s %s: %v\n", tt.msg, outFile, err) + } + } +} + +func TestPdfMultiStampVariations(t *testing.T) { + msg := "TestPdfMultiStampVariations" + inFile := filepath.Join(inDir, "zineTest.pdf") + stampFile := inFile + + // Stamp selected pages of inFile with different pages of some PDF file - the stampFile. + // Stamping proceeds in ascending manner where each new inFile page gets stamped with the next page of stampFile. + // Set the first page of the stampFile initiating the sequence = startPageNrSrc + // Set the first page of inFile that gets stamped = startPageNrDest + + rs, err := os.Open(stampFile) + if err != nil { + t.Fatalf("%s %s: %v\n", msg, stampFile, err) + } + defer rs.Close() + + for _, tt := range []struct { + msg, outFile string + startPageNrSrc int + startPageNrDest int + }{ + {"TestPdfMultiStamp11", // Start stamping at page 1 using page 1 of stampFile. (=TestPdfMultiStampDefault) + "PdfMultiStamp11.pdf", + 1, + 1, + }, + {"TestPdfMultiStamp13", // Skip first 2 page and start stamping at page 3 using page 1 of stampFile. + "PdfMultiStamp13.pdf", + 1, + 3, + }, + {"TestPdfMultiStamp31", // Start stamping at page 1 using page 3 of stampFile. + "PdfMultiStamp31.pdf", + 3, + 1, + }, + {"TestPdfMultiStamp33", // Skip first 2 page and start stamping at page 3 using page 3 of stampFile. + "PdfMultiStamp33.pdf", + 3, + 3, + }, + } { + wm, err := api.PDFMultiWatermarkForReadSeeker( + rs, + tt.startPageNrSrc, + tt.startPageNrDest, + "scale:.2, pos:tr, off:-10 -10, rot:0", // scaled @ top right corner using some offset and 0 rotation. + true, // stamp + false, // no update + conf.Unit, + ) + + if err != nil { + t.Fatalf("%s: %v\n", tt.msg, err) + } + + outFile := filepath.Join(samplesDir, "stamp", "mixed", tt.outFile) + + if err = api.AddWatermarksFile(inFile, outFile, nil, wm, conf); err != nil { + t.Fatalf("%s %s: %v\n", tt.msg, outFile, err) + } + } +} diff --git a/pkg/api/test/stamp_test.go b/pkg/api/test/stamp_test.go new file mode 100644 index 0000000000000000000000000000000000000000..4c8b25dcb0a7eedf077f26d0e1673f7c8b130b7b --- /dev/null +++ b/pkg/api/test/stamp_test.go @@ -0,0 +1,639 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 test + +import ( + "fmt" + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +func testAddWatermarks(t *testing.T, msg, inFile, outFile string, selectedPages []string, mode, modeParam, desc string, onTop bool) { + t.Helper() + inFile = filepath.Join(inDir, inFile) + s := "watermark" + if onTop { + s = "stamp" + } + outFile = filepath.Join(samplesDir, s, mode, outFile) + + var err error + switch mode { + case "text": + err = api.AddTextWatermarksFile(inFile, outFile, selectedPages, onTop, modeParam, desc, nil) + case "image": + err = api.AddImageWatermarksFile(inFile, outFile, selectedPages, onTop, modeParam, desc, nil) + case "pdf": + err = api.AddPDFWatermarksFile(inFile, outFile, selectedPages, onTop, modeParam, desc, nil) + } + if err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + if err := api.ValidateFile(outFile, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestAddWatermarks(t *testing.T) { + for _, tt := range []struct { + msg string + inFile, outFile string + selectedPages []string + mode string + modeParm string + wmConf string + }{ + + // Avoid font embedding for CJK fonts like so: + + // Any font name ending with GB2312 will be recognized as using HANS: + + // {"TestWatermarkText", + // "sample.pdf", + // "chinese.pdf", + // []string{"1-"}, + // "text", + // "测试中文字体水印增加的文件大小\n2023-10-16", + // "font: KaiTi_GB2312, points: 36, scale: 1 abs, color: #ff0000, op: 0.3, ro: 30"}, + + // Configure script manually: + + // {"TestWatermarkText", + // "sample.pdf", + // "chinese1.pdf", + // []string{"1-"}, + // "text", + // "测试中文字体水印增加的文件大小\n2023-10-16", + // "font: KaiTi_GB2312, script: hans, points: 36, scale: 1 abs, color: #ff0000, op: 0.3, ro: 30"}, + + {"TestWatermarkText", + "Walden.pdf", + "TextDefaults.pdf", + []string{"1-"}, + "text", + "A simple text watermark using defaults:\n" + + "\"font:Helvetica, points:24, aligntext:center,\n" + + "position:c, offset:0 0, scale:0.5 rel, diagonal:1,\n" + + "opacity:1, rendermode:0, fillcolor: 0.5 0.5 0.5,\n" + + "strokecol: 0.5 0.5 0.5\"", + ""}, + + {"TestWatermarkText", + "Walden.pdf", + "TextDefaultsAbbr.pdf", + []string{"1-"}, + "text", + `A simple text watermark using defaults: + Unique abbreviations also work: + "fo:Helvetica, poi:24, align:c, + pos:c, off:0 0, scale:0.5 rel, d:1, + op:1, mode:0, fillc: 0.5 0.5 0.5, + strokec: #808080"`, + ""}, + + {"TestWatermarkText", + "Walden.pdf", + "TextAlongLeftBorder.pdf", + []string{"1-"}, + "text", + "Welcome to pdfcpu", + "pos:l, off:0 0, rot:-90"}, + + {"TestWatermarkText", + "Walden.pdf", + "TextPagenumbers.pdf", + []string{"1-"}, + "text", + "Page %p of %P", + "scale:1 abs, pos:bc, rot:0"}, + + {"TestWatermarkText", + "Walden.pdf", + "TextRenderMode0.pdf", + []string{"1-"}, + "text", + "Rendermode 0 fills text using fill color.\n" + + "\"rendermode\" or \"mode\" works - also abbreviated: \n" + + "\"mode:0, fillc:#3277d3, rot:0, scale:.8\"", + "mode:0, fillc:#3277d3, rot:0, scale:.8"}, + + {"TestWatermarkText", + "Walden.pdf", + "TextRenderMode1.pdf", + []string{"1-"}, + "text", + "Rendermode 1 strokes text using stroke color.\n" + + "\"rendermode\" or \"mode\" works - also abbreviated: \n" + + "\"mo:1, strokec:#335522, rot:0, scale:.8\"", + "mo:1, strokec:#335522, rot:0, scale:.8"}, + + {"TestWatermarkText", + "Walden.pdf", + "TextRenderMode2.pdf", + []string{"1-"}, + "text", + "Rendermode 2 strokes text using stroke color\n" + + "and fills text using fill color\n" + + "\"rendermode\" or \"mode\" works - also abbreviated: \n" + + "\"re:2, fillc:#3277d3, strokec:#335522, rot:0, scale:.8\"", + "re:2, fillc:#3277d3, strokec:#335522, rot:0, scale:.8"}, + + {"TestWatermarkText", + "Walden.pdf", + "TextAlignLeft.pdf", + []string{"1-"}, + "text", + "Here we have\n" + + "some left aligned text lines\n" + + "\"align:l, fillc:#3277d3, rot:0\"", + "align:l, fillc:#3277d3, rot:0"}, + + {"TestWatermarkText", + "Walden.pdf", + "TextAlignRight.pdf", + []string{"1-"}, + "text", + "Here we have\n" + + "some right aligned text lines\n" + + "with background color\n" + + "\"align:l, fillc:#3277d3, bgcol:#f7e6c7, rot:0\"", + "align:r, fillc:#3277d3, bgcol:#f7e6c7, rot:0"}, + + {"TestWatermarkText", + "Walden.pdf", + "TextAlignCenter.pdf", + []string{"1-"}, + "text", + "Here we have\n" + + "some centered text lines\n" + + "with background color\n" + + "\"fillc:#3277d3, bgcol:#beded9, rot:0\"", + "fillc:#3277d3, bgcol:#beded9, rot:0"}, + + {"TestWatermarkText", + "Walden.pdf", + "TextAlignJustify.pdf", + []string{"1-"}, + "text", + "Here we have\n" + + "some justified text lines\n" + + "with background color\n" + + "\"al:j, fillc:#3277d3, bgcol:#000000, rot:0\"", + "al:j, fillc:#3277d3, bgcol:#000000, rot:0"}, + + {"TestWatermarkText", + "Walden.pdf", + "TextScaleRel25.pdf", + []string{"1-"}, + "text", + "Relative scale factor: .25\n" + + "scales relative to page dimensions.\n" + + "\"scale:.25 rel, fillc:#3277d3, rot:0\"", + "scale:.25 rel, fillc:#3277d3, rot:0"}, + + {"TestWatermarkText", + "Walden.pdf", + "TextScaleRel50.pdf", + []string{"1-"}, + "text", + "Relative scale factor: .5\n" + + "scales relative to page dimensions.\n" + + "\"scale:.5, fillc:#3277d3, rot:0\"", + "scale:.5, fillc:#3277d3, rot:0"}, + + {"TestWatermarkText", + "Walden.pdf", + "TextScaleRel100.pdf", + []string{"1-"}, + "text", + "Relative scale factor: 1\n" + + "scales relative to page dimensions.\n" + + "\"scale:1, fillc:#3277d3, rot:0\"", + "scale:1, fillc:#3277d3, rot:0"}, + + {"TestWatermarkText", + "Walden.pdf", + "TextScaleAbs50.pdf", + []string{"1-"}, + "text", + "Absolute scale factor: .5\n" + + "scales fontsize\n" + + "(here using the 24 points default)\n" + + "\"scale:.5 abs, font:Courier, rot:0\"", + "scale:.5 abs, font:Courier, rot:0"}, + + {"TestWatermarkText", + "Walden.pdf", + "TextScaleAbs100.pdf", + []string{"1-"}, + "text", + "Absolute scale factor: 1\n" + + "scales fontsize\n" + + "(here using the 24 points default)\n" + + "\"scale:1 abs, font:Courier, rot:0\"", + "scale:1 abs, font:Courier, rot:0"}, + + {"TestWatermarkText", + "Walden.pdf", + "TextScaleAbs150.pdf", + []string{"1-"}, + "text", + "Absolute scale factor: 1.5\n" + + "scales fontsize\n" + + "(here using the 24 points default)\n" + + "\"scale:1.5 abs, font:Courier, rot:0\"", + "scale:1.5 abs, font:Courier, rot:0"}, + + {"TestWatermarkText", + "Walden.pdf", + "TextPosBotLeft.pdf", + []string{"1-"}, + "text", + "Positioning using anchors:\n" + + "bottom left corner with left alignment\n" + + "\"pos:bl, bgcol:#f7e6c7, rot:0\"", + "pos:bl, bgcol:#f7e6c7, rot:0"}, + + {"TestWatermarkText", + "Walden.pdf", + "TextPosBotRightWithOffset.pdf", + []string{"1-"}, + "text", + "Positioning using anchors and offset:\n" + + "bottom right corner with right alignment\n" + + "\"pos:br, off: -10 10, align:r, bgcol:#f7e6c7, rot:0\"", + "pos:br, off: -10 10, align:r, bgcol:#f7e6c7, rot:0"}, + + {"TestWatermarkText", + "Walden.pdf", + "TextOffAndRot.pdf", + []string{"1-"}, + "text", + "Confidential\n\"scale:1 abs, points:20, pos:c, off:0 50, fillc:#000000, rot:20\"", + "scale:1 abs, points:20, pos:c, off:0 50, fillc:#000000, rot:20"}, + + {"TestWatermarkText", + "Walden.pdf", + "TextMargins1Value.pdf", + []string{"1-"}, + "text", + "Set all margins:\n" + + "(needs \"bgcol\")\n" + + "\"margins: 10, fillc:#3277d3, bgcol:#beded9, rot:0\"", + "margins: 10,fillc:#3277d3, bgcol:#beded9, rot:0"}, + + {"TestWatermarkText", + "Walden.pdf", + "TextMargins2Values.pdf", + []string{"1-"}, + "text", + "Set top/bottom and left/right margins:\n" + + "(needs \"bgcol\")\n" + + "\"ma: 5 10, fillc:#3277d3, bgcol:#beded9, rot:0\"", + "ma: 5 10, fillc:#3277d3, bgcol:#beded9, rot:0"}, + + {"TestWatermarkText", + "Walden.pdf", + "TextMargins3Values.pdf", + []string{"1-"}, + "text", + "Set top, left/right and bottom margins:\n" + + "(needs \"bgcol\")\n" + + "\"ma: 5 10 15, fillc:#3277d3, bgcol:#beded9, rot:0\"", + "ma: 5 10 15, fillc:#3277d3, bgcol:#beded9, rot:0"}, + + {"TestWatermarkText", + "Walden.pdf", + "TextMargins4Values.pdf", + []string{"1-"}, + "text", + "Set all margins individually:\n" + + "(needs \"bgcol\")\n" + + "\"ma: 5 10 15 20, fillc:#3277d3, bgcol:#beded9, rot:0\"", + "ma: 5 10 15 20, fillc:#3277d3, bgcol:#beded9, rot:0"}, + + {"TestWatermarkText", + "Walden.pdf", + "TextRoundCornersAndBorder5.pdf", + []string{"1-"}, + "text", + "Set round corners and border:\n" + + "(needs \"bgcol\" and a border)\n" + + "round corner effect depends on border width\n" + + "\"border: 5 round, fillc:#3277d3, bgcol:#beded9, rot:0\"", + "border: 5 round, fillc:#3277d3, bgcol:#beded9, rot:0"}, + + {"TestWatermarkText", + "Walden.pdf", + "TextRoundCornersAndBorder10.pdf", + []string{"1-"}, + "text", + "Set round corners and border:\n" + + "(needs \"bgcol\" and a border)\n" + + "round corner effect depends on border width\n" + + "\"border: 10 round, fillc:#3277d3, bgcol:#beded9, rot:0\"", + "border: 10 round, fillc:#3277d3, bgcol:#beded9, rot:0"}, + + {"TestWatermarkText", + "Walden.pdf", + "TextRoundCornersAndColoredBorder.pdf", + []string{"1-"}, + "text", + "Set round corners and colored border:\n" + + "(needs \"bgcol\")\n" + + "round corner effect depends on border width\n" + + "\"border: 10 round #f7e6c7, fillc:#3277d3, bgcol:#beded9, rot:0\"", + "border: 10 round #f7e6c7, fillc:#3277d3, bgcol:#beded9, rot:0"}, + + {"TestWatermarkText", + "Walden.pdf", + "TextMarginsAndColoredBorder.pdf", + []string{"1-"}, + "text", + "Set margins and colored border:\n" + + "(needs \"bgcol\")\n" + + "\"ma: 10, bo: 5 .3 .7 .7, fillc:#3277d3, bgcol:#beded9, rot:0\"", + "ma: 10, bo: 5 .3 .7 .7, fillc:#3277d3, bgcol:#beded9, rot:0"}, + + {"TestWatermarkText", + "Walden.pdf", + "TextMarginsRoundCornersAndColoredBorder.pdf", + []string{"1-"}, + "text", + "Set margins and round colored border:\n" + + "(needs \"bgcol\")\n" + + "round corner effect depends on border width\n" + + "\"ma: 5, bo: 7 round .3 .7 .7, fillc:#3277d3, bgcol:#beded9, rot:0\"", + "ma: 5, bo: 7 round .3 .7 .7, fillc:#3277d3, bgcol:#beded9, rot:0"}, + + // Add image watermark to inFile starting at page 1 using no rotation. + {"TestWatermarkImage", + "Walden.pdf", + "ImageRotate90.pdf", + []string{"1-"}, + "image", + filepath.Join(resDir, "logoSmall.png"), + "scale:.25, rot:90"}, + + // Add image watermark to inFile for all pages using defaults.. + {"TestWatermarkImage2", + "Walden.pdf", + "ImagePosBottomLeftWithOffset.pdf", + nil, + "image", + filepath.Join(resDir, "logoSmall.png"), + "scale:.1, pos:bl, off:15 20, rot:0"}, + + // Add image stamp to inFile using absolute scaling and a rotation of 45 degrees. + {"TestStampImageAbsScaling", + "Walden.pdf", + "ImageAbsScaling.pdf", + []string{"1-"}, + "image", + filepath.Join(resDir, "logoSmall.png"), + "scale:.33 abs, rot:45"}, + + // Add a PDF stamp to all pages of inFile using the 1st page of pdfFile + // and rotate along the 2nd diagonal running from upper left to lower right corner. + {"TestWatermarkPDF", + "Walden.pdf", + "PdfSingleStampDefault.pdf", + nil, + "pdf", + filepath.Join(inDir, "Walden.pdf:1"), + "d:2"}, + + // Add a PDF multistamp in the top right corner to all pages of inFile. + {"TestWatermarkPDF", + "Walden.pdf", + "PdfMultistampDefault.pdf", + nil, + "pdf", + filepath.Join(inDir, "Walden.pdf"), + "scale:.2, pos:tr, off:-10 -10, rot:0"}, + + // Add a PDF multistamp to all pages of inFile. + // Start by stamping page 3 with page 1. + // You may filter stamping by defining selected Pages. + {"TestWatermarkPDF", + "zineTest.pdf", + "PdfMultistamp13.pdf", + nil, + "pdf", + filepath.Join(inDir, "zineTest.pdf:1:3"), + "scale:.2, pos:tr, off:-10 -10, rot:0"}, + + // Add a PDF multistamp to all pages of inFile. + // Start by stamping page 1 with page 3. + // You may filter stamping by defining selected Pages. + {"TestWatermarkPDF", + "zineTest.pdf", + "PdfMultistamp31.pdf", + nil, + "pdf", + filepath.Join(inDir, "zineTest.pdf:3:1"), + "scale:.2, pos:tr, off:-10 -10, rot:0"}, + + // Add a PDF multistamp to all pages of inFile. + // Start by stamping page 3 with page 3. + // You may filter stamping by defining selected Pages. + {"TestWatermarkPDF", + "zineTest.pdf", + "PdfMultistamp33.pdf", + nil, + "pdf", + filepath.Join(inDir, "zineTest.pdf:3:3"), + "scale:.2, pos:tr, off:-10 -10, rot:0"}, + } { + testAddWatermarks(t, tt.msg, tt.inFile, tt.outFile, tt.selectedPages, tt.mode, tt.modeParm, tt.wmConf, false) + testAddWatermarks(t, tt.msg, tt.inFile, tt.outFile, tt.selectedPages, tt.mode, tt.modeParm, tt.wmConf, true) + } +} + +func TestAddStampWithLink(t *testing.T) { + for _, tt := range []struct { + msg string + inFile, outFile string + selectedPages []string + mode string + modeParm string + wmConf string + }{ + {"TestStampTextWithLink", + "Walden.pdf", + "TextWithLink.pdf", + []string{"1-"}, + "text", + "A simple text watermark with link", + "url:pdfcpu.io"}, + + {"TestStampImageWithLink", + "Walden.pdf", + "ImageWithLink.pdf", + []string{"1-"}, + "image", + filepath.Join(resDir, "logoSmall.png"), + "url:pdfcpu.io, scale:.33 abs, rot:45"}, + } { + // Links supported for stamps only (watermark onTop:true). + testAddWatermarks(t, tt.msg, tt.inFile, tt.outFile, tt.selectedPages, tt.mode, tt.modeParm, tt.wmConf, true) + } + +} + +func TestCropBox(t *testing.T) { + msg := "TestCropBox" + inFile := filepath.Join(inDir, "empty.pdf") + outFile := filepath.Join(samplesDir, "stamp", "pdf", "PdfWithCropBox.pdf") + pdfFile := filepath.Join(inDir, "grid_example.pdf") + + // Create a context. + ctx, err := api.ReadContextFile(inFile) + if err != nil { + t.Fatalf("%s readContext: %v\n", msg, err) + } + + for _, pos := range []string{"tl", "tc", "tr", "l", "c", "r", "bl", "bc", "br"} { + wm, err := api.PDFWatermark(pdfFile+":1", fmt.Sprintf("scale:.25 rel, pos:%s, rot:0", pos), true, false, types.POINTS) + if err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + if err := pdfcpu.AddWatermarks(ctx, nil, wm); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + } + + // Write context to file. + if err := api.WriteContextFile(ctx, outFile); err != nil { + t.Fatalf("%s write: %v\n", msg, err) + } + + if err := api.ValidateFile(outFile, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func hasWatermarks(inFile string, t *testing.T) bool { + t.Helper() + ok, err := api.HasWatermarksFile(inFile, nil) + if err != nil { + t.Fatalf("Checking for watermarks: %s: %v\n", inFile, err) + } + return ok +} + +func TestStampingLifecyle(t *testing.T) { + msg := "TestStampingLifecyle" + inFile := filepath.Join(inDir, "Acroforms2.pdf") + outFile := filepath.Join(outDir, "stampLC.pdf") + onTop := true // we are testing stamps + + // Check for existing stamps. + if ok := hasWatermarks(inFile, t); ok { + t.Fatalf("Watermarks found: %s\n", inFile) + } + + unit := types.POINTS + + // Stamp all pages. + wm, err := api.TextWatermark("Demo", "", onTop, false, unit) + if err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + if err := api.AddWatermarksFile(inFile, outFile, nil, wm, nil); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + // Check for existing stamps. + if ok := hasWatermarks(outFile, t); !ok { + t.Fatalf("No watermarks found: %s\n", outFile) + } + + // // Update stamp on page 1. + wm, err = api.TextWatermark("Confidential", "", onTop, true, unit) + if err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + if err := api.AddWatermarksFile(outFile, "", []string{"1"}, wm, nil); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + // Add another stamp on top for all pages. + // This is a redish transparent footer. + wm, err = api.TextWatermark("Footer", "pos:bc, c:0.8 0 0, op:.6, rot:0", onTop, false, unit) + if err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + if err := api.AddWatermarksFile(outFile, "", nil, wm, nil); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + // Remove stamp on page 1. + if err := api.RemoveWatermarksFile(outFile, "", []string{"1"}, nil); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + // Check for existing stamps. + if ok := hasWatermarks(outFile, t); !ok { + t.Fatalf("No watermarks found: %s\n", outFile) + } + + // Remove all stamps. + if err := api.RemoveWatermarksFile(outFile, "", nil, nil); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + // Validate the result. + if err := api.ValidateFile(outFile, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + // Check for existing stamps. + if ok := hasWatermarks(outFile, t); ok { + t.Fatalf("Watermarks found: %s\n", outFile) + } +} + +func TestRecycleWM(t *testing.T) { + msg := "TestRecycleWM" + inFile := filepath.Join(inDir, "test.pdf") + outFile := filepath.Join(samplesDir, "watermark", "text", "TextRecycled.pdf") + onTop := false // we are testing watermarks + + desc := "pos:tl, points:22, rot:0, scale:1 abs, off:0 -5, opacity:0.3" + wm, err := api.TextWatermark("This is a watermark", desc, onTop, false, types.POINTS) + if err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + if err = api.AddWatermarksFile(inFile, outFile, nil, wm, nil); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + wm.Recycle() + + // Shift down watermark. + wm.Dy = -55 + + if err = api.AddWatermarksFile(outFile, outFile, nil, wm, nil); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } +} diff --git a/pkg/api/test/trim_test.go b/pkg/api/test/trim_test.go new file mode 100644 index 0000000000000000000000000000000000000000..c2f185680a219410e94c50bf414ca99374843db7 --- /dev/null +++ b/pkg/api/test/trim_test.go @@ -0,0 +1,43 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" +) + +func TestTrim(t *testing.T) { + msg := "TestTrim" + fileName := "adobe_errata.pdf" + inFile := filepath.Join(inDir, fileName) + outFile := filepath.Join(outDir, fileName) + + // Create a trimmed version of inFile containing odd page numbers only. + if err := api.TrimFile(inFile, outFile, []string{"odd"}, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + // Create a trimmed version of inFile containing the first two pages only. + // If you want to modify the original file, pass an empty string for outFile. + inFile = outFile + if err := api.TrimFile(inFile, "", []string{"1-2"}, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} diff --git a/pkg/api/test/viewerPreferences_test.go b/pkg/api/test/viewerPreferences_test.go new file mode 100644 index 0000000000000000000000000000000000000000..89c0a16852b493426e16660f4e0fb6d597b1b1a3 --- /dev/null +++ b/pkg/api/test/viewerPreferences_test.go @@ -0,0 +1,80 @@ +/* +Copyright 2023 The pdf Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" +) + +func TestViewerPreferences(t *testing.T) { + msg := "testViewerPreferences" + + fileName := "Hybrid-PDF.pdf" + inFile := filepath.Join(outDir, fileName) + copyFile(t, filepath.Join(inDir, fileName), inFile) + inFileJSON := filepath.Join(inDir, "json", "viewerPreferences.json") + stringJSON := "{\"HideMenuBar\": true, \"CenterWindow\": true}" + + vp, err := api.ViewerPreferencesFile(inFile, false, nil) + if err != nil { + t.Fatalf("%s %s: viewerPref struct: %v\n", msg, inFile, err) + } + if vp == nil { + t.Fatalf("%s %s: missing viewerPref struct\n", msg, inFile) + } + + if err := api.ResetViewerPreferencesFile(inFile, "", nil); err != nil { + t.Fatalf("%s %s: reset: %v\n", msg, inFile, err) + } + + vp, err = api.ViewerPreferencesFile(inFile, false, nil) + if err != nil { + t.Fatalf("%s %s: viewerPref struct: %v\n", msg, inFile, err) + } + if vp != nil { + t.Fatalf("%s %s: unexpected viewerPref struct: %v\n", msg, inFile, vp) + } + + if err := api.SetViewerPreferencesFileFromJSONFile(inFile, "", inFileJSON, nil); err != nil { + t.Fatalf("%s %s: set via JSON file: %v\n", msg, inFile, err) + } + + vp, err = api.ViewerPreferencesFile(inFile, false, nil) + if err != nil { + t.Fatalf("%s %s: viewerPref struct: %v\n", msg, inFile, err) + } + if vp == nil { + t.Fatalf("%s %s: missing viewerPref struct\n", msg, inFile) + } + + vp = &model.ViewerPreferences{} + vp.SetCenterWindow(true) + vp.SetHideMenuBar(true) + vp.SetNumCopies(5) + + if err := api.SetViewerPreferencesFile(inFile, "", *vp, nil); err != nil { + t.Fatalf("%s %s: set: %v\n", msg, inFile, err) + } + + if err := api.SetViewerPreferencesFileFromJSONBytes(inFile, "", []byte(stringJSON), nil); err != nil { + t.Fatalf("%s %s: set via JSON string: %v\n", msg, inFile, err) + } +} diff --git a/pkg/api/test/zoom_test.go b/pkg/api/test/zoom_test.go new file mode 100644 index 0000000000000000000000000000000000000000..a702414122bba51ebe5fefde87452907395f5947 --- /dev/null +++ b/pkg/api/test/zoom_test.go @@ -0,0 +1,122 @@ +/* +Copyright 2024 The pdf Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +func TestZoomInByFactor(t *testing.T) { + msg := "TestZoomInByFactor" + + inFile := filepath.Join(inDir, "test.pdf") + + zoom, err := pdfcpu.ParseZoomConfig("factor:2", types.POINTS) + if err != nil { + t.Fatalf("%s invalid zoom configuration: %v\n", msg, err) + } + outFile := filepath.Join(samplesDir, "zoom", "zoomInByFactor2.pdf") + if err := api.ZoomFile(inFile, outFile, nil, zoom, nil); err != nil { + t.Fatalf("%s zoom: %v\n", msg, err) + } + + zoom, err = pdfcpu.ParseZoomConfig("factor:4", types.POINTS) + if err != nil { + t.Fatalf("%s invalid zoom configuration: %v\n", msg, err) + } + outFile = filepath.Join(samplesDir, "zoom", "zoomInByFactor4.pdf") + if err := api.ZoomFile(inFile, outFile, nil, zoom, nil); err != nil { + t.Fatalf("%s zoom: %v\n", msg, err) + } +} + +func TestZoomOutByFactor(t *testing.T) { + msg := "TestZoomOutByFactor" + + inFile := filepath.Join(inDir, "test.pdf") + + zoom, err := pdfcpu.ParseZoomConfig("factor:.5", types.POINTS) + if err != nil { + t.Fatalf("%s invalid zoom configuration: %v\n", msg, err) + } + outFile := filepath.Join(samplesDir, "zoom", "zoomOutByFactor05.pdf") + if err := api.ZoomFile(inFile, outFile, nil, zoom, nil); err != nil { + t.Fatalf("%s zoom: %v\n", msg, err) + } + + zoom, err = pdfcpu.ParseZoomConfig("factor:.25, border:true", types.POINTS) + if err != nil { + t.Fatalf("%s invalid zoom configuration: %v\n", msg, err) + } + outFile = filepath.Join(samplesDir, "zoom", "zoomOutByFactor025.pdf") + if err := api.ZoomFile(inFile, outFile, nil, zoom, nil); err != nil { + t.Fatalf("%s zoom: %v\n", msg, err) + } +} + +func TestZoomOutByHorizontalMargin(t *testing.T) { + // Zoom out of page content resulting in a preferred horizontal margin. + msg := "TestZoomOutByHMargin" + inFile := filepath.Join(inDir, "test.pdf") + + zoom, err := pdfcpu.ParseZoomConfig("hmargin:149", types.POINTS) + if err != nil { + t.Fatalf("%s invalid zoom configuration: %v\n", msg, err) + } + outFile := filepath.Join(samplesDir, "zoom", "zoomOutByHMarginPoints.pdf") + if err := api.ZoomFile(inFile, outFile, nil, zoom, nil); err != nil { + t.Fatalf("%s zoom: %v\n", msg, err) + } + + zoom, err = pdfcpu.ParseZoomConfig("hmargin:1, border:true, bgcol:lightgray", types.CENTIMETRES) + if err != nil { + t.Fatalf("%s invalid zoom configuration: %v\n", msg, err) + } + outFile = filepath.Join(samplesDir, "zoom", "zoomOutByHMarginCm.pdf") + if err := api.ZoomFile(inFile, outFile, nil, zoom, nil); err != nil { + t.Fatalf("%s zoom: %v\n", msg, err) + } +} + +func TestZoomOutByVerticalMargin(t *testing.T) { + // Zoom out of page content resulting in a preferred vertical margin. + msg := "TestZoomOutByVMargin" + inFile := filepath.Join(inDir, "test.pdf") + + zoom, err := pdfcpu.ParseZoomConfig("vmargin:1", types.INCHES) + if err != nil { + t.Fatalf("%s invalid zoom configuration: %v\n", msg, err) + } + outFile := filepath.Join(samplesDir, "zoom", "zoomOutByVMarginInches.pdf") + if err := api.ZoomFile(inFile, outFile, nil, zoom, nil); err != nil { + t.Fatalf("%s zoom: %v\n", msg, err) + } + + zoom, err = pdfcpu.ParseZoomConfig("vmargin:30, border:false, bgcol:lightgray", types.MILLIMETRES) + if err != nil { + t.Fatalf("%s invalid zoom configuration: %v\n", msg, err) + } + outFile = filepath.Join(samplesDir, "zoom", "zoomOutByVMarginMm.pdf") + if err := api.ZoomFile(inFile, outFile, nil, zoom, nil); err != nil { + t.Fatalf("%s zoom: %v\n", msg, err) + } +} diff --git a/pkg/api/trim.go b/pkg/api/trim.go new file mode 100644 index 0000000000000000000000000000000000000000..ee6b79faf13d5b086a984dcaf37494b7217f37b0 --- /dev/null +++ b/pkg/api/trim.go @@ -0,0 +1,121 @@ +/* + Copyright 2020 The pdfcpu Authors. + + 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 + + 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 api + +import ( + "io" + "os" + "sort" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pkg/errors" +) + +// Trim generates a trimmed version of rs +// containing all selected pages and writes the result to w. +func Trim(rs io.ReadSeeker, w io.Writer, selectedPages []string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: Trim: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.TRIM + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, false, true) + if err != nil { + return err + } + + if len(pages) == 0 { + if log.CLIEnabled() { + log.CLI.Println("aborted: missing page numbers!") + } + return nil + } + + var pageNrs []int + for k, v := range pages { + if v { + pageNrs = append(pageNrs, k) + } + } + sort.Ints(pageNrs) + + ctxDest, err := pdfcpu.ExtractPages(ctx, pageNrs, false) + if err != nil { + return err + } + + if conf.PostProcessValidate { + if err = ValidateContext(ctxDest); err != nil { + return err + } + } + + return WriteContext(ctxDest, w) +} + +// TrimFile generates a trimmed version of inFile +// containing all selected pages and writes the result to outFile. +func TrimFile(inFile, outFile string, selectedPages []string, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + logWritingTo(outFile) + } else { + logWritingTo(inFile) + } + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return Trim(f1, f2, selectedPages, conf) +} diff --git a/pkg/api/validate.go b/pkg/api/validate.go new file mode 100644 index 0000000000000000000000000000000000000000..b09cba2b65f9a067cf2857e2c4966fe9cb2e873b --- /dev/null +++ b/pkg/api/validate.go @@ -0,0 +1,164 @@ +/* + Copyright 2020 The pdfcpu Authors. + + 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 + + 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 api + +import ( + "fmt" + "io" + "os" + "time" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pkg/errors" +) + +// Validate validates a PDF stream read from rs. +func Validate(rs io.ReadSeeker, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: Validate: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.VALIDATE + + from1 := time.Now() + + ctx, err := ReadContext(rs, conf) + if err != nil { + return err + } + + dur1 := time.Since(from1).Seconds() + from2 := time.Now() + + if err = ValidateContext(ctx); err != nil { + s := "" + if conf.ValidationMode == model.ValidationStrict { + s = " (try -mode=relaxed)" + } + err = errors.Wrap(err, fmt.Sprintf("validation error (obj#:%d)%s", ctx.CurObj, s)) + } + + dur2 := time.Since(from2).Seconds() + dur := time.Since(from1).Seconds() + + if log.StatsEnabled() { + log.Stats.Printf("XRefTable:\n%s\n", ctx) + } + + model.ValidationTimingStats(dur1, dur2, dur) + + // at this stage: no binary breakup available! + if ctx.Read.FileSize > 0 { + ctx.Read.LogStats(ctx.Optimized) + } + + return err +} + +// ValidateFile validates inFile. +func ValidateFile(inFile string, conf *model.Configuration) error { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + + log.CLI.Printf("validating(mode=%s) %s ...\n", conf.ValidationModeString(), inFile) + + f, err := os.Open(inFile) + if err != nil { + return err + } + + defer f.Close() + + if err = Validate(f, conf); err != nil { + return err + } + + log.CLI.Println("validation ok") + + return nil +} + +// ValidateFiles validates inFiles. +func ValidateFiles(inFiles []string, conf *model.Configuration) error { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + + for i, fn := range inFiles { + if i > 0 { + log.CLI.Println() + } + if err := ValidateFile(fn, conf); err != nil { + if len(inFiles) == 1 { + return err + } + fmt.Fprintf(os.Stderr, "%s: %v\n", fn, err) + } + } + + return nil +} + +// DumpObject writes an object from rs to stdout. +func DumpObject(rs io.ReadSeeker, mode, objNr int, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: DumpObject: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.DUMP + + ctx, err := ReadContext(rs, conf) + if err != nil { + return err + } + + if err = ValidateContext(ctx); err != nil { + s := "" + if conf.ValidationMode == model.ValidationStrict { + s = " (try -mode=relaxed)" + } + return errors.Wrap(err, fmt.Sprintf("validation error (obj#:%d)%s", ctx.CurObj, s)) + } + + ctx.DumpObject(objNr, mode) + + return err +} + +// DumpObjectFile writes an object from rs to stdout. +func DumpObjectFile(inFile string, mode, objNr int, conf *model.Configuration) error { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + + f, err := os.Open(inFile) + if err != nil { + return err + } + + defer f.Close() + + return DumpObject(f, mode, objNr, conf) +} diff --git a/pkg/api/viewerPreferences.go b/pkg/api/viewerPreferences.go new file mode 100644 index 0000000000000000000000000000000000000000..6355146db3ef36fa39f02c78b18da85876cb6c2f --- /dev/null +++ b/pkg/api/viewerPreferences.go @@ -0,0 +1,408 @@ +/* + Copyright 2023 The pdfcpu Authors. + + 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 + + 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 api + +import ( + "bytes" + "encoding/json" + "io" + "os" + "time" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pkg/errors" +) + +var ErrNoOp = errors.New("pdfcpu: no operation") + +// ViewerPreferences returns rs's viewer preferences. +func ViewerPreferences(rs io.ReadSeeker, conf *model.Configuration) (*model.ViewerPreferences, *model.Version, error) { + if rs == nil { + return nil, nil, errors.New("pdfcpu: ViewerPreferences: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.LISTVIEWERPREFERENCES + + ctx, err := ReadAndValidate(rs, conf) + if err != nil { + return nil, nil, err + } + + v := ctx.Version() + + return ctx.ViewerPref, &v, nil +} + +// ViewerPreferences returns inFile's viewer preferences. +func ViewerPreferencesFile(inFile string, all bool, conf *model.Configuration) (*model.ViewerPreferences, error) { + f, err := os.Open(inFile) + if err != nil { + return nil, err + } + defer f.Close() + + vp, version, err := ViewerPreferences(f, conf) + if err != nil { + return nil, err + } + + if !all { + return vp, nil + } + + return model.ViewerPreferencesWithDefaults(vp, *version) +} + +// ListViewerPreferences returns rs's viewer preferences. +func ListViewerPreferences(rs io.ReadSeeker, all bool, conf *model.Configuration) ([]string, error) { + if rs == nil { + return nil, errors.New("pdfcpu: ListViewerPreferences: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.LISTVIEWERPREFERENCES + + ctx, err := ReadAndValidate(rs, conf) + if err != nil { + return nil, err + } + + if !all { + if ctx.ViewerPref != nil { + return ctx.ViewerPref.List(), nil + } + return []string{"No viewer preferences available."}, nil + } + + vp1, err := model.ViewerPreferencesWithDefaults(ctx.ViewerPref, ctx.Version()) + if err != nil { + return nil, err + } + + return vp1.List(), nil +} + +// ListViewerPreferencesFile lists inFile's viewer preferences in JSON. +func ListViewerPreferencesFileJSON(inFile string, all bool, conf *model.Configuration) ([]string, error) { + f, err := os.Open(inFile) + if err != nil { + return nil, err + } + defer f.Close() + + vp, version, err := ViewerPreferences(f, conf) + if err != nil { + return nil, err + } + + if !all { + if vp == nil { + return []string{"No viewer preferences available."}, nil + } + } else { + vp, err = model.ViewerPreferencesWithDefaults(vp, *version) + if err != nil { + return nil, err + } + } + + s := struct { + Header pdfcpu.Header `json:"header"` + ViewerPref *model.ViewerPreferences `json:"viewerPreferences"` + }{ + Header: pdfcpu.Header{Version: "pdfcpu " + model.VersionStr, Creation: time.Now().Format("2006-01-02 15:04:05 MST")}, + ViewerPref: vp, + } + + bb, err := json.MarshalIndent(s, "", "\t") + if err != nil { + return nil, err + } + + return []string{string(bb)}, nil +} + +// ListViewerPreferencesFile lists inFile's viewer preferences. +func ListViewerPreferencesFile(inFile string, all, json bool, conf *model.Configuration) ([]string, error) { + if json { + return ListViewerPreferencesFileJSON(inFile, all, conf) + } + + f, err := os.Open(inFile) + if err != nil { + return nil, err + } + defer f.Close() + + return ListViewerPreferences(f, all, conf) +} + +// SetViewerPreferences sets rs's viewer preferences and writes the result to w. +func SetViewerPreferences(rs io.ReadSeeker, w io.Writer, vp model.ViewerPreferences, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: SetViewerPreferences: missing rs") + } + + if w == nil { + return errors.New("pdfcpu: SetViewerPreferences: missing w") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.SETVIEWERPREFERENCES + + ctx, err := ReadAndValidate(rs, conf) + if err != nil { + return err + } + + version := ctx.Version() + + if err := vp.Validate(version); err != nil { + return err + } + + if ctx.ViewerPref == nil { + ctx.ViewerPref = &vp + } else { + ctx.ViewerPref.Populate(&vp) + } + + ctx.XRefTable.BindViewerPreferences() + + return Write(ctx, w, conf) +} + +// SetViewerPreferencesFromJSONBytes sets rs's viewer preferences corresponding to jsonBytes and writes the result to w. +func SetViewerPreferencesFromJSONBytes(rs io.ReadSeeker, w io.Writer, jsonBytes []byte, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: SetViewerPreferencesFromJSONBytes: missing rs") + } + + if w == nil { + return errors.New("pdfcpu: SetViewerPreferencesFromJSONBytes: missing w") + } + + if !json.Valid(jsonBytes) { + return ErrInvalidJSON + } + + vp := model.ViewerPreferences{} + + if err := json.Unmarshal(jsonBytes, &vp); err != nil { + return err + } + + return SetViewerPreferences(rs, w, vp, conf) +} + +// SetViewerPreferencesFromJSONReader sets rs's viewer preferences corresponding to rd and writes the result to w. +func SetViewerPreferencesFromJSONReader(rs io.ReadSeeker, w io.Writer, rd io.Reader, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: SetViewerPreferencesFromJSONReader: missing rs") + } + + if w == nil { + return errors.New("pdfcpu: SetViewerPreferencesFromJSONReader: missing w") + } + + if rd == nil { + return errors.New("pdfcpu: SetViewerPreferencesFromJSONReader: missing rd") + } + + var buf bytes.Buffer + if _, err := io.Copy(&buf, rd); err != nil { + return err + } + + return SetViewerPreferencesFromJSONBytes(rs, w, buf.Bytes(), conf) +} + +// SetViewerPreferencesFile sets inFile's viewer preferences and writes the result to outFile. +func SetViewerPreferencesFile(inFile, outFile string, vp model.ViewerPreferences, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + } + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return SetViewerPreferences(f1, f2, vp, conf) +} + +// SetViewerPreferencesFileFromJSONBytes sets inFile's viewer preferences corresponding to jsonBytes and writes the result to outFile. +func SetViewerPreferencesFileFromJSONBytes(inFile, outFile string, jsonBytes []byte, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + } + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return SetViewerPreferencesFromJSONBytes(f1, f2, jsonBytes, conf) +} + +// SetViewerPreferencesFileFromJSONFile sets inFile's viewer preferences corresponding to inFileJSON and writes the result to outFile. +func SetViewerPreferencesFileFromJSONFile(inFilePDF, outFilePDF, inFileJSON string, conf *model.Configuration) error { + if inFileJSON == "" { + return errors.New("pdfcpu: SetViewerPreferencesFileFromJSONFile: missing inFileJSON") + } + + bb, err := os.ReadFile(inFileJSON) + if err != nil { + return err + } + + return SetViewerPreferencesFileFromJSONBytes(inFilePDF, outFilePDF, bb, conf) +} + +// ResetViewerPreferences resets rs's viewer preferences and writes the result to w. +func ResetViewerPreferences(rs io.ReadSeeker, w io.Writer, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: ResetViewerPreferences: missing rs") + } + + if w == nil { + return errors.New("pdfcpu: ResetViewerPreferences: missing w") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.RESETVIEWERPREFERENCES + + ctx, err := ReadAndValidate(rs, conf) + if err != nil { + return err + } + + if ctx.ViewerPref == nil { + return ErrNoOp + } + + delete(ctx.RootDict, "ViewerPreferences") + + return Write(ctx, w, conf) +} + +// ResetViewerPreferencesFile resets inFile's viewer preferences and writes the result to outFile. +func ResetViewerPreferencesFile(inFile, outFile string, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + } + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + if err == ErrNoOp { + err = nil + } + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return ResetViewerPreferences(f1, f2, conf) +} diff --git a/pkg/api/zoom.go b/pkg/api/zoom.go new file mode 100644 index 0000000000000000000000000000000000000000..3ab920c60c5e4cf5474735dc1f2513418c058a45 --- /dev/null +++ b/pkg/api/zoom.go @@ -0,0 +1,108 @@ +/* +Copyright 2024 The pdfcpu Authors. + +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 + + 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 api + +import ( + "io" + "os" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pkg/errors" +) + +// Zoom applies resizeConf for selected pages of rs and writes result to w. +func Zoom(rs io.ReadSeeker, w io.Writer, selectedPages []string, zoom *model.Zoom, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: Zoom: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.ZOOM + + ctx, err := ReadValidateAndOptimize(rs, conf) + if err != nil { + return err + } + + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) + if err != nil { + return err + } + + if err = pdfcpu.Zoom(ctx, pages, zoom); err != nil { + return err + } + + return Write(ctx, w, conf) +} + +// ZoomFile applies zoomConf for selected pages of inFile and writes result to outFile. +func ZoomFile(inFile, outFile string, selectedPages []string, zoom *model.Zoom, conf *model.Configuration) (err error) { + if log.CLIEnabled() { + log.CLI.Printf("zooming %s\n", inFile) + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + logWritingTo(outFile) + } else { + logWritingTo(inFile) + } + + var ( + f1, f2 *os.File + ) + + if f1, err = os.Open(inFile); err != nil { + return err + } + + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + os.Remove(tmpFile) + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.ZOOM + + return Zoom(f1, f2, selectedPages, zoom, conf) +} diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go new file mode 100644 index 0000000000000000000000000000000000000000..5aaa5870128dec03583a3b1bd5e9ececd9a22e43 --- /dev/null +++ b/pkg/cli/cli.go @@ -0,0 +1,424 @@ +/* +Copyright 2019 The pdfcpu Authors. + +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 + + 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 cli provides pdfcpu command line processing. +package cli + +import ( + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" +) + +// Validate inFile against ISO-32000-1:2008. +func Validate(cmd *Command) ([]string, error) { + return nil, api.ValidateFiles(cmd.InFiles, cmd.Conf) +} + +// Optimize inFile and write result to outFile. +func Optimize(cmd *Command) ([]string, error) { + return nil, api.OptimizeFile(*cmd.InFile, *cmd.OutFile, cmd.Conf) +} + +// Encrypt inFile and write result to outFile. +func Encrypt(cmd *Command) ([]string, error) { + return nil, api.EncryptFile(*cmd.InFile, *cmd.OutFile, cmd.Conf) +} + +// Decrypt inFile and write result to outFile. +func Decrypt(cmd *Command) ([]string, error) { + return nil, api.DecryptFile(*cmd.InFile, *cmd.OutFile, cmd.Conf) +} + +// ChangeUserPassword of inFile and write result to outFile. +func ChangeUserPassword(cmd *Command) ([]string, error) { + return nil, api.ChangeUserPasswordFile(*cmd.InFile, *cmd.OutFile, *cmd.PWOld, *cmd.PWNew, cmd.Conf) +} + +// ChangeOwnerPassword of inFile and write result to outFile. +func ChangeOwnerPassword(cmd *Command) ([]string, error) { + return nil, api.ChangeOwnerPasswordFile(*cmd.InFile, *cmd.OutFile, *cmd.PWOld, *cmd.PWNew, cmd.Conf) +} + +// ListPermissions of inFile. +func ListPermissions(cmd *Command) ([]string, error) { + return ListPermissionsFile(cmd.InFiles, cmd.Conf) +} + +// SetPermissions of inFile. +func SetPermissions(cmd *Command) ([]string, error) { + return nil, api.SetPermissionsFile(*cmd.InFile, *cmd.OutFile, cmd.Conf) +} + +// Split inFile into single page PDFs and write result files to outDir. +func Split(cmd *Command) ([]string, error) { + return nil, api.SplitFile(*cmd.InFile, *cmd.OutDir, cmd.IntVal, cmd.Conf) +} + +// Split inFile along pages and write result files to outDir. +func SplitByPageNr(cmd *Command) ([]string, error) { + return nil, api.SplitByPageNrFile(*cmd.InFile, *cmd.OutDir, cmd.IntVals, cmd.Conf) +} + +// Trim inFile and write result to outFile. +func Trim(cmd *Command) ([]string, error) { + return nil, api.TrimFile(*cmd.InFile, *cmd.OutFile, cmd.PageSelection, cmd.Conf) +} + +// Rotate selected pages of inFile and write result to outFile. +func Rotate(cmd *Command) ([]string, error) { + return nil, api.RotateFile(*cmd.InFile, *cmd.OutFile, cmd.IntVal, cmd.PageSelection, cmd.Conf) +} + +// AddWatermarks adds watermarks or stamps to selected pages of inFile and writes the result to outFile. +func AddWatermarks(cmd *Command) ([]string, error) { + return nil, api.AddWatermarksFile(*cmd.InFile, *cmd.OutFile, cmd.PageSelection, cmd.Watermark, cmd.Conf) +} + +// RemoveWatermarks remove watermarks or stamps from selected pages of inFile and writes the result to outFile. +func RemoveWatermarks(cmd *Command) ([]string, error) { + return nil, api.RemoveWatermarksFile(*cmd.InFile, *cmd.OutFile, cmd.PageSelection, cmd.Conf) +} + +// NUp renders selected PDF pages or image files to outFile in n-up fashion. +func NUp(cmd *Command) ([]string, error) { + return nil, api.NUpFile(cmd.InFiles, *cmd.OutFile, cmd.PageSelection, cmd.NUp, cmd.Conf) +} + +// Booklet arranges selected PDF pages to outFile in an order and arrangement that form a small book. +func Booklet(cmd *Command) ([]string, error) { + return nil, api.BookletFile(cmd.InFiles, *cmd.OutFile, cmd.PageSelection, cmd.NUp, cmd.Conf) +} + +// ImportImages appends PDF pages containing images to outFile which will be created if necessary. +// ImportImages turns image files into a page sequence and writes the result to outFile. +// In its simplest form this operation converts an image into a PDF. +func ImportImages(cmd *Command) ([]string, error) { + return nil, api.ImportImagesFile(cmd.InFiles, *cmd.OutFile, cmd.Import, cmd.Conf) +} + +// InsertPages inserts a blank page before or after each selected page. +func InsertPages(cmd *Command) ([]string, error) { + before := true + if cmd.Mode == model.INSERTPAGESAFTER { + before = false + } + return nil, api.InsertPagesFile(*cmd.InFile, *cmd.OutFile, cmd.PageSelection, before, cmd.PageConf, cmd.Conf) +} + +// RemovePages removes selected pages. +func RemovePages(cmd *Command) ([]string, error) { + return nil, api.RemovePagesFile(*cmd.InFile, *cmd.OutFile, cmd.PageSelection, cmd.Conf) +} + +// MergeCreate merges inFiles in the order specified and writes the result to outFile. +func MergeCreate(cmd *Command) ([]string, error) { + return nil, api.MergeCreateFile(cmd.InFiles, *cmd.OutFile, cmd.BoolVal1, cmd.Conf) +} + +// MergeCreateZip zips two inFiles in the order specified and writes the result to outFile. +func MergeCreateZip(cmd *Command) ([]string, error) { + return nil, api.MergeCreateZipFile(cmd.InFiles[0], cmd.InFiles[1], *cmd.OutFile, cmd.Conf) +} + +// MergeAppend merges inFiles in the order specified and writes the result to outFile. +func MergeAppend(cmd *Command) ([]string, error) { + return nil, api.MergeAppendFile(cmd.InFiles, *cmd.OutFile, cmd.BoolVal1, cmd.Conf) +} + +// ExtractImages dumps embedded image resources from inFile into outDir for selected pages. +func ExtractImages(cmd *Command) ([]string, error) { + return nil, api.ExtractImagesFile(*cmd.InFile, *cmd.OutDir, cmd.PageSelection, cmd.Conf) +} + +// ExtractFonts dumps embedded fontfiles from inFile into outDir for selected pages. +func ExtractFonts(cmd *Command) ([]string, error) { + return nil, api.ExtractFontsFile(*cmd.InFile, *cmd.OutDir, cmd.PageSelection, cmd.Conf) +} + +// ExtractPages generates single page PDF files from inFile in outDir for selected pages. +func ExtractPages(cmd *Command) ([]string, error) { + return nil, api.ExtractPagesFile(*cmd.InFile, *cmd.OutDir, cmd.PageSelection, cmd.Conf) +} + +// ExtractContent dumps "PDF source" files from inFile into outDir for selected pages. +func ExtractContent(cmd *Command) ([]string, error) { + return nil, api.ExtractContentFile(*cmd.InFile, *cmd.OutDir, cmd.PageSelection, cmd.Conf) +} + +// ExtractMetadata dumps all metadata dict entries for inFile into outDir. +func ExtractMetadata(cmd *Command) ([]string, error) { + return nil, api.ExtractMetadataFile(*cmd.InFile, *cmd.OutDir, cmd.Conf) +} + +// ListAttachments returns a list of embedded file attachments for inFile. +func ListAttachments(cmd *Command) ([]string, error) { + return ListAttachmentsFile(*cmd.InFile, cmd.Conf) +} + +// AddAttachments embeds inFiles into a PDF context read from inFile and writes the result to outFile. +func AddAttachments(cmd *Command) ([]string, error) { + return nil, api.AddAttachmentsFile(*cmd.InFile, *cmd.OutFile, cmd.InFiles, cmd.Mode == model.ADDATTACHMENTSPORTFOLIO, cmd.Conf) +} + +// RemoveAttachments deletes inFiles from a PDF context read from inFile and writes the result to outFile. +func RemoveAttachments(cmd *Command) ([]string, error) { + return nil, api.RemoveAttachmentsFile(*cmd.InFile, *cmd.OutFile, cmd.InFiles, cmd.Conf) +} + +// ExtractAttachments extracts inFiles from a PDF context read from inFile and writes the result to outFile. +func ExtractAttachments(cmd *Command) ([]string, error) { + return nil, api.ExtractAttachmentsFile(*cmd.InFile, *cmd.OutDir, cmd.InFiles, cmd.Conf) +} + +// ListInfo gathers information about inFile and returns the result as []string. +func ListInfo(cmd *Command) ([]string, error) { + return ListInfoFiles(cmd.InFiles, cmd.PageSelection, cmd.BoolVal1, cmd.Conf) +} + +// CreateCheatSheetsFonts creates single page PDF cheat sheets for user fonts in current dir. +func CreateCheatSheetsFonts(cmd *Command) ([]string, error) { + return nil, api.CreateCheatSheetsUserFonts(cmd.InFiles) +} + +// ListFonts gathers information about supported fonts and returns the result as []string. +func ListFonts(cmd *Command) ([]string, error) { + return api.ListFonts() +} + +// InstallFonts installs True Type fonts into the pdfcpu pconfig dir. +func InstallFonts(cmd *Command) ([]string, error) { + return nil, api.InstallFonts(cmd.InFiles) +} + +// ListKeywords returns a list of keywords for inFile. +func ListKeywords(cmd *Command) ([]string, error) { + return ListKeywordsFile(*cmd.InFile, cmd.Conf) +} + +// AddKeywords adds keywords to inFile's document info dict and writes the result to outFile. +func AddKeywords(cmd *Command) ([]string, error) { + return nil, api.AddKeywordsFile(*cmd.InFile, *cmd.OutFile, cmd.StringVals, cmd.Conf) +} + +// RemoveKeywords deletes keywords from inFile's document info dict and writes the result to outFile. +func RemoveKeywords(cmd *Command) ([]string, error) { + return nil, api.RemoveKeywordsFile(*cmd.InFile, *cmd.OutFile, cmd.StringVals, cmd.Conf) +} + +// ListProperties returns inFile's properties. +func ListProperties(cmd *Command) ([]string, error) { + return ListPropertiesFile(*cmd.InFile, cmd.Conf) +} + +// AddProperties adds properties to inFile's document info dict and writes the result to outFile. +func AddProperties(cmd *Command) ([]string, error) { + return nil, api.AddPropertiesFile(*cmd.InFile, *cmd.OutFile, cmd.StringMap, cmd.Conf) +} + +// RemoveProperties deletes properties from inFile's document info dict and writes the result to outFile. +func RemoveProperties(cmd *Command) ([]string, error) { + return nil, api.RemovePropertiesFile(*cmd.InFile, *cmd.OutFile, cmd.StringVals, cmd.Conf) +} + +// Collect creates a custom page sequence for selected pages of inFile and writes result to outFile. +func Collect(cmd *Command) ([]string, error) { + return nil, api.CollectFile(*cmd.InFile, *cmd.OutFile, cmd.PageSelection, cmd.Conf) +} + +// ListBoxes returns inFile's page boundaries. +func ListBoxes(cmd *Command) ([]string, error) { + return ListBoxesFile(*cmd.InFile, cmd.PageSelection, cmd.PageBoundaries, cmd.Conf) +} + +// AddBoxes adds page boundaries to inFile's page tree and writes the result to outFile. +func AddBoxes(cmd *Command) ([]string, error) { + return nil, api.AddBoxesFile(*cmd.InFile, *cmd.OutFile, cmd.PageSelection, cmd.PageBoundaries, cmd.Conf) +} + +// RemoveBoxes deletes page boundaries from inFile's page tree and writes the result to outFile. +func RemoveBoxes(cmd *Command) ([]string, error) { + return nil, api.RemoveBoxesFile(*cmd.InFile, *cmd.OutFile, cmd.PageSelection, cmd.PageBoundaries, cmd.Conf) +} + +// Crop adds crop boxes for selected pages of inFile and writes result to outFile. +func Crop(cmd *Command) ([]string, error) { + return nil, api.CropFile(*cmd.InFile, *cmd.OutFile, cmd.PageSelection, cmd.Box, cmd.Conf) +} + +// ListAnnotations returns inFile's page annotations. +func ListAnnotations(cmd *Command) ([]string, error) { + _, ss, err := ListAnnotationsFile(*cmd.InFile, cmd.PageSelection, cmd.Conf) + return ss, err +} + +// RemoveAnnotations deletes annotations from inFile's page tree and writes the result to outFile. +func RemoveAnnotations(cmd *Command) ([]string, error) { + incr := false // No incremental writing on cli. + return nil, api.RemoveAnnotationsFile(*cmd.InFile, *cmd.OutFile, cmd.PageSelection, cmd.StringVals, cmd.IntVals, cmd.Conf, incr) +} + +// ListImages returns inFiles embedded images. +func ListImages(cmd *Command) ([]string, error) { + return ListImagesFile(cmd.InFiles, cmd.PageSelection, cmd.Conf) +} + +// Dump known object to stdout. +func Dump(cmd *Command) ([]string, error) { + mode := cmd.IntVals[0] + objNr := cmd.IntVals[1] + return nil, api.DumpObjectFile(*cmd.InFile, mode, objNr, cmd.Conf) +} + +// Create renders page content corresponding to declarations found in inFileJSON and writes the result to outFile. +// If inFile is present, page content will be appended, +func Create(cmd *Command) ([]string, error) { + return nil, api.CreateFile(*cmd.InFile, *cmd.InFileJSON, *cmd.OutFile, cmd.Conf) +} + +// ListFormFields returns inFile's form field ids. +func ListFormFields(cmd *Command) ([]string, error) { + return ListFormFieldsFile(cmd.InFiles, cmd.Conf) +} + +// RemoveFormFields removes some form fields from inFile. +func RemoveFormFields(cmd *Command) ([]string, error) { + return nil, api.RemoveFormFieldsFile(*cmd.InFile, *cmd.OutFile, cmd.StringVals, cmd.Conf) +} + +// LockFormFields makes some or all form fields of inFile read-only. +func LockFormFields(cmd *Command) ([]string, error) { + return nil, api.LockFormFieldsFile(*cmd.InFile, *cmd.OutFile, cmd.StringVals, cmd.Conf) +} + +// UnlockFormFields makes some or all form fields of inFile writeable. +func UnlockFormFields(cmd *Command) ([]string, error) { + return nil, api.UnlockFormFieldsFile(*cmd.InFile, *cmd.OutFile, cmd.StringVals, cmd.Conf) +} + +// ResetFormFields sets some or all form fields of inFile to the corresponding default value. +func ResetFormFields(cmd *Command) ([]string, error) { + return nil, api.ResetFormFieldsFile(*cmd.InFile, *cmd.OutFile, cmd.StringVals, cmd.Conf) +} + +// ExportFormFields returns a representation of inFile's form as outFileJSON. +func ExportFormFields(cmd *Command) ([]string, error) { + return nil, api.ExportFormFile(*cmd.InFile, *cmd.OutFileJSON, cmd.Conf) +} + +// FillFormFields fills out inFile's form using data represented by inFileJSON. +func FillFormFields(cmd *Command) ([]string, error) { + return nil, api.FillFormFile(*cmd.InFile, *cmd.InFileJSON, *cmd.OutFile, cmd.Conf) +} + +// MultiFillFormFields fills out multiple instances of inFile's form using JSON or CSV data. +func MultiFillFormFields(cmd *Command) ([]string, error) { + return nil, api.MultiFillFormFile(*cmd.InFile, *cmd.InFileJSON, *cmd.OutDir, *cmd.OutFile, cmd.BoolVal1, cmd.Conf) +} + +// Resize selected pages and write result to outFile. +func Resize(cmd *Command) ([]string, error) { + return nil, api.ResizeFile(*cmd.InFile, *cmd.OutFile, cmd.PageSelection, cmd.Resize, cmd.Conf) +} + +// Create poster for selected pages and write result PDFs into outDir. +func Poster(cmd *Command) ([]string, error) { + return nil, api.PosterFile(*cmd.InFile, *cmd.OutDir, *cmd.OutFile, cmd.PageSelection, cmd.Cut, cmd.Conf) +} + +// NDown selected pages and write result PDFs into outDir. +func NDown(cmd *Command) ([]string, error) { + return nil, api.NDownFile(*cmd.InFile, *cmd.OutDir, *cmd.OutFile, cmd.PageSelection, cmd.IntVal, cmd.Cut, cmd.Conf) +} + +// Cut selected pages and write result PDFs into outDir. +func Cut(cmd *Command) ([]string, error) { + return nil, api.CutFile(*cmd.InFile, *cmd.OutDir, *cmd.OutFile, cmd.PageSelection, cmd.Cut, cmd.Conf) +} + +// ListBookmarks returns inFile's outlines. +func ListBookmarks(cmd *Command) ([]string, error) { + return ListBookmarksFile(*cmd.InFile, cmd.Conf) +} + +// ExportBookmarks returns a representation of inFile's outlines as outFileJSON. +func ExportBookmarks(cmd *Command) ([]string, error) { + return nil, api.ExportBookmarksFile(*cmd.InFile, *cmd.OutFileJSON, cmd.Conf) +} + +// ImportBookmarks creates/replaces outlines of inFile corresponding to declarations found in inJSONFile and writes the result to outFile. +func ImportBookmarks(cmd *Command) ([]string, error) { + return nil, api.ImportBookmarksFile(*cmd.InFile, *cmd.InFileJSON, *cmd.OutFile, cmd.BoolVal1, cmd.Conf) +} + +// RemoveBookmarks erases outlines of inFile. +func RemoveBookmarks(cmd *Command) ([]string, error) { + return nil, api.RemoveBookmarksFile(*cmd.InFile, *cmd.OutFile, cmd.Conf) +} + +// ListPageLayout returns inFile's page layout. +func ListPageLayout(cmd *Command) ([]string, error) { + return api.ListPageLayoutFile(*cmd.InFile, cmd.Conf) +} + +// SetPageLayout sets inFile's page layout. +func SetPageLayout(cmd *Command) ([]string, error) { + pageLayout := model.PageLayoutFor(cmd.StringVal) + return nil, api.SetPageLayoutFile(*cmd.InFile, *cmd.OutFile, *pageLayout, cmd.Conf) +} + +// ResetPageLayout resets inFile's page layout. +func ResetPageLayout(cmd *Command) ([]string, error) { + return nil, api.ResetPageLayoutFile(*cmd.InFile, *cmd.OutFile, cmd.Conf) +} + +// ListPageMode returns inFile's page mode. +func ListPageMode(cmd *Command) ([]string, error) { + return api.ListPageModeFile(*cmd.InFile, cmd.Conf) +} + +// SetPageMode sets inFile's page mode. +func SetPageMode(cmd *Command) ([]string, error) { + pageMode := model.PageModeFor(cmd.StringVal) + return nil, api.SetPageModeFile(*cmd.InFile, *cmd.OutFile, *pageMode, cmd.Conf) +} + +// ResetPageMode resets inFile's page mode. +func ResetPageMode(cmd *Command) ([]string, error) { + return nil, api.ResetPageModeFile(*cmd.InFile, *cmd.OutFile, cmd.Conf) +} + +// ListViewerPreferences returns inFile's viewer preferences. +func ListViewerPreferences(cmd *Command) ([]string, error) { + return api.ListViewerPreferencesFile(*cmd.InFile, cmd.BoolVal1, cmd.BoolVal2, cmd.Conf) +} + +// SetViewerPreferences sets inFile's viewer preferences. +func SetViewerPreferences(cmd *Command) ([]string, error) { + if *cmd.InFileJSON != "" { + return nil, api.SetViewerPreferencesFileFromJSONFile(*cmd.InFile, *cmd.OutFile, *cmd.InFileJSON, cmd.Conf) + } + return nil, api.SetViewerPreferencesFileFromJSONBytes(*cmd.InFile, *cmd.OutFile, []byte(cmd.StringVal), cmd.Conf) +} + +// ResetViewerPreferences resets inFile's viewer preferences. +func ResetViewerPreferences(cmd *Command) ([]string, error) { + return nil, api.ResetViewerPreferencesFile(*cmd.InFile, *cmd.OutFile, cmd.Conf) +} + +// Zoom in/out of selected pages either by zoom factor or corresponding margin. +func Zoom(cmd *Command) ([]string, error) { + return nil, api.ZoomFile(*cmd.InFile, *cmd.OutFile, cmd.PageSelection, cmd.Zoom, cmd.Conf) +} diff --git a/pkg/cli/cmd.go b/pkg/cli/cmd.go new file mode 100644 index 0000000000000000000000000000000000000000..944625d28aa01895d9e0f69168001a0601ba475d --- /dev/null +++ b/pkg/cli/cmd.go @@ -0,0 +1,1230 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 cli + +import ( + "io" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" +) + +// Command represents an execution context. +type Command struct { + Mode model.CommandMode + InFile *string + InFileJSON *string + InFiles []string + InDir *string + OutFile *string + OutFileJSON *string + OutDir *string + PageSelection []string + PWOld *string + PWNew *string + StringVal string + IntVal int + BoolVal1 bool + BoolVal2 bool + IntVals []int + StringVals []string + StringMap map[string]string + Input io.ReadSeeker + Inputs []io.ReadSeeker + Output io.Writer + Box *model.Box + Import *pdfcpu.Import + NUp *model.NUp + Cut *model.Cut + PageBoundaries *model.PageBoundaries + Resize *model.Resize + Zoom *model.Zoom + Watermark *model.Watermark + ViewerPreferences *model.ViewerPreferences + PageConf *pdfcpu.PageConfiguration + Conf *model.Configuration +} + +var cmdMap = map[model.CommandMode]func(cmd *Command) ([]string, error){ + model.VALIDATE: Validate, + model.OPTIMIZE: Optimize, + model.SPLIT: Split, + model.SPLITBYPAGENR: SplitByPageNr, + model.MERGECREATE: MergeCreate, + model.MERGECREATEZIP: MergeCreateZip, + model.MERGEAPPEND: MergeAppend, + model.EXTRACTIMAGES: ExtractImages, + model.EXTRACTFONTS: ExtractFonts, + model.EXTRACTPAGES: ExtractPages, + model.EXTRACTCONTENT: ExtractContent, + model.EXTRACTMETADATA: ExtractMetadata, + model.TRIM: Trim, + model.ADDWATERMARKS: AddWatermarks, + model.REMOVEWATERMARKS: RemoveWatermarks, + model.LISTATTACHMENTS: processAttachments, + model.ADDATTACHMENTS: processAttachments, + model.ADDATTACHMENTSPORTFOLIO: processAttachments, + model.REMOVEATTACHMENTS: processAttachments, + model.EXTRACTATTACHMENTS: processAttachments, + model.ENCRYPT: processEncryption, + model.DECRYPT: processEncryption, + model.CHANGEUPW: processEncryption, + model.CHANGEOPW: processEncryption, + model.LISTPERMISSIONS: processPermissions, + model.SETPERMISSIONS: processPermissions, + model.IMPORTIMAGES: ImportImages, + model.INSERTPAGESBEFORE: processPages, + model.INSERTPAGESAFTER: processPages, + model.REMOVEPAGES: processPages, + model.ROTATE: Rotate, + model.NUP: NUp, + model.BOOKLET: Booklet, + model.LISTINFO: ListInfo, + model.CHEATSHEETSFONTS: CreateCheatSheetsFonts, + model.INSTALLFONTS: InstallFonts, + model.LISTFONTS: ListFonts, + model.LISTKEYWORDS: processKeywords, + model.ADDKEYWORDS: processKeywords, + model.REMOVEKEYWORDS: processKeywords, + model.LISTPROPERTIES: processProperties, + model.ADDPROPERTIES: processProperties, + model.REMOVEPROPERTIES: processProperties, + model.COLLECT: Collect, + model.LISTBOXES: processPageBoundaries, + model.ADDBOXES: processPageBoundaries, + model.REMOVEBOXES: processPageBoundaries, + model.CROP: processPageBoundaries, + model.LISTANNOTATIONS: processPageAnnotations, + model.REMOVEANNOTATIONS: processPageAnnotations, + model.LISTIMAGES: processImages, + model.DUMP: Dump, + model.CREATE: Create, + model.LISTFORMFIELDS: processForm, + model.REMOVEFORMFIELDS: processForm, + model.LOCKFORMFIELDS: processForm, + model.UNLOCKFORMFIELDS: processForm, + model.RESETFORMFIELDS: processForm, + model.EXPORTFORMFIELDS: processForm, + model.FILLFORMFIELDS: processForm, + model.MULTIFILLFORMFIELDS: processForm, + model.RESIZE: Resize, + model.POSTER: Poster, + model.NDOWN: NDown, + model.CUT: Cut, + model.LISTBOOKMARKS: processBookmarks, + model.EXPORTBOOKMARKS: processBookmarks, + model.IMPORTBOOKMARKS: processBookmarks, + model.REMOVEBOOKMARKS: processBookmarks, + model.LISTPAGEMODE: processPageMode, + model.SETPAGEMODE: processPageMode, + model.RESETPAGEMODE: processPageMode, + model.LISTPAGELAYOUT: processPageLayout, + model.SETPAGELAYOUT: processPageLayout, + model.RESETPAGELAYOUT: processPageLayout, + model.LISTVIEWERPREFERENCES: processViewerPreferences, + model.SETVIEWERPREFERENCES: processViewerPreferences, + model.RESETVIEWERPREFERENCES: processViewerPreferences, + model.ZOOM: Zoom, +} + +// ValidateCommand creates a new command to validate a file. +func ValidateCommand(inFiles []string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.VALIDATE + return &Command{ + Mode: model.VALIDATE, + InFiles: inFiles, + Conf: conf} +} + +// OptimizeCommand creates a new command to optimize a file. +func OptimizeCommand(inFile, outFile string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.OPTIMIZE + return &Command{ + Mode: model.OPTIMIZE, + InFile: &inFile, + OutFile: &outFile, + Conf: conf} +} + +// SplitCommand creates a new command to split a file according to span or along bookmarks.. +func SplitCommand(inFile, dirNameOut string, span int, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.SPLIT + return &Command{ + Mode: model.SPLIT, + InFile: &inFile, + OutDir: &dirNameOut, + IntVal: span, + Conf: conf} +} + +// SplitByPageNrCommand creates a new command to split a file into files along given pages. +func SplitByPageNrCommand(inFile, dirNameOut string, pageNrs []int, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.SPLITBYPAGENR + return &Command{ + Mode: model.SPLITBYPAGENR, + InFile: &inFile, + OutDir: &dirNameOut, + IntVals: pageNrs, + Conf: conf} +} + +// MergeCreateCommand creates a new command to merge files. +// Outfile will be created. An existing outFile will be overwritten. +func MergeCreateCommand(inFiles []string, outFile string, dividerPage bool, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.MERGECREATE + return &Command{ + Mode: model.MERGECREATE, + InFiles: inFiles, + OutFile: &outFile, + BoolVal1: dividerPage, + Conf: conf} +} + +// MergeCreateZipCommand creates a new command to zip merge 2 files. +// Outfile will be created. An existing outFile will be overwritten. +func MergeCreateZipCommand(inFiles []string, outFile string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.MERGECREATEZIP + return &Command{ + Mode: model.MERGECREATEZIP, + InFiles: inFiles, + OutFile: &outFile, + Conf: conf} +} + +// MergeAppendCommand creates a new command to merge files. +// Any existing outFile PDF content will be preserved and serves as the beginning of the merge result. +func MergeAppendCommand(inFiles []string, outFile string, dividerPage bool, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.MERGEAPPEND + return &Command{ + Mode: model.MERGEAPPEND, + InFiles: inFiles, + OutFile: &outFile, + BoolVal1: dividerPage, + Conf: conf} +} + +// ExtractImagesCommand creates a new command to extract embedded images. +// (experimental) +func ExtractImagesCommand(inFile string, outDir string, pageSelection []string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.EXTRACTIMAGES + return &Command{ + Mode: model.EXTRACTIMAGES, + InFile: &inFile, + OutDir: &outDir, + PageSelection: pageSelection, + Conf: conf} +} + +// ExtractFontsCommand creates a new command to extract embedded fonts. +// (experimental) +func ExtractFontsCommand(inFile string, outDir string, pageSelection []string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.EXTRACTFONTS + return &Command{ + Mode: model.EXTRACTFONTS, + InFile: &inFile, + OutDir: &outDir, + PageSelection: pageSelection, + Conf: conf} +} + +// ExtractPagesCommand creates a new command to extract specific pages of a file. +func ExtractPagesCommand(inFile string, outDir string, pageSelection []string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.EXTRACTPAGES + return &Command{ + Mode: model.EXTRACTPAGES, + InFile: &inFile, + OutDir: &outDir, + PageSelection: pageSelection, + Conf: conf} +} + +// ExtractContentCommand creates a new command to extract page content streams. +func ExtractContentCommand(inFile string, outDir string, pageSelection []string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.EXTRACTCONTENT + return &Command{ + Mode: model.EXTRACTCONTENT, + InFile: &inFile, + OutDir: &outDir, + PageSelection: pageSelection, + Conf: conf} +} + +// ExtractMetadataCommand creates a new command to extract metadata streams. +func ExtractMetadataCommand(inFile string, outDir string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.EXTRACTMETADATA + return &Command{ + Mode: model.EXTRACTMETADATA, + InFile: &inFile, + OutDir: &outDir, + Conf: conf} +} + +// TrimCommand creates a new command to trim the pages of a file. +func TrimCommand(inFile, outFile string, pageSelection []string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.TRIM + return &Command{ + Mode: model.TRIM, + InFile: &inFile, + OutFile: &outFile, + PageSelection: pageSelection, + Conf: conf} +} + +// ListAttachmentsCommand create a new command to list attachments. +func ListAttachmentsCommand(inFile string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTATTACHMENTS + return &Command{ + Mode: model.LISTATTACHMENTS, + InFile: &inFile, + Conf: conf} +} + +// AddAttachmentsCommand creates a new command to add attachments. +func AddAttachmentsCommand(inFile, outFile string, fileNames []string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.ADDATTACHMENTS + return &Command{ + Mode: model.ADDATTACHMENTS, + InFile: &inFile, + OutFile: &outFile, + InFiles: fileNames, + Conf: conf} +} + +// AddAttachmentsPortfolioCommand creates a new command to add attachments to a portfolio. +func AddAttachmentsPortfolioCommand(inFile, outFile string, fileNames []string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.ADDATTACHMENTSPORTFOLIO + return &Command{ + Mode: model.ADDATTACHMENTSPORTFOLIO, + InFile: &inFile, + OutFile: &outFile, + InFiles: fileNames, + Conf: conf} +} + +// RemoveAttachmentsCommand creates a new command to remove attachments. +func RemoveAttachmentsCommand(inFile, outFile string, fileNames []string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.REMOVEATTACHMENTS + return &Command{ + Mode: model.REMOVEATTACHMENTS, + InFile: &inFile, + OutFile: &outFile, + InFiles: fileNames, + Conf: conf} +} + +// ExtractAttachmentsCommand creates a new command to extract attachments. +func ExtractAttachmentsCommand(inFile string, outDir string, fileNames []string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.EXTRACTATTACHMENTS + return &Command{ + Mode: model.EXTRACTATTACHMENTS, + InFile: &inFile, + OutDir: &outDir, + InFiles: fileNames, + Conf: conf} +} + +// EncryptCommand creates a new command to encrypt a file. +func EncryptCommand(inFile, outFile string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.ENCRYPT + return &Command{ + Mode: model.ENCRYPT, + InFile: &inFile, + OutFile: &outFile, + Conf: conf} +} + +// DecryptCommand creates a new command to decrypt a file. +func DecryptCommand(inFile, outFile string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.DECRYPT + return &Command{ + Mode: model.DECRYPT, + InFile: &inFile, + OutFile: &outFile, + Conf: conf} +} + +// ChangeUserPWCommand creates a new command to change the user password. +func ChangeUserPWCommand(inFile, outFile string, pwOld, pwNew *string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.CHANGEUPW + return &Command{ + Mode: model.CHANGEUPW, + InFile: &inFile, + OutFile: &outFile, + PWOld: pwOld, + PWNew: pwNew, + Conf: conf} +} + +// ChangeOwnerPWCommand creates a new command to change the owner password. +func ChangeOwnerPWCommand(inFile, outFile string, pwOld, pwNew *string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.CHANGEOPW + return &Command{ + Mode: model.CHANGEOPW, + InFile: &inFile, + OutFile: &outFile, + PWOld: pwOld, + PWNew: pwNew, + Conf: conf} +} + +// ListPermissionsCommand create a new command to list permissions. +func ListPermissionsCommand(inFiles []string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTPERMISSIONS + return &Command{ + Mode: model.LISTPERMISSIONS, + InFiles: inFiles, + Conf: conf} +} + +// SetPermissionsCommand creates a new command to add permissions. +func SetPermissionsCommand(inFile, outFile string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.SETPERMISSIONS + return &Command{ + Mode: model.SETPERMISSIONS, + InFile: &inFile, + OutFile: &outFile, + Conf: conf} +} + +// AddWatermarksCommand creates a new command to add Watermarks to a file. +func AddWatermarksCommand(inFile, outFile string, pageSelection []string, wm *model.Watermark, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.ADDWATERMARKS + return &Command{ + Mode: model.ADDWATERMARKS, + InFile: &inFile, + OutFile: &outFile, + PageSelection: pageSelection, + Watermark: wm, + Conf: conf} +} + +// RemoveWatermarksCommand creates a new command to remove Watermarks from a file. +func RemoveWatermarksCommand(inFile, outFile string, pageSelection []string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.REMOVEWATERMARKS + return &Command{ + Mode: model.REMOVEWATERMARKS, + InFile: &inFile, + OutFile: &outFile, + PageSelection: pageSelection, + Conf: conf} +} + +// ImportImagesCommand creates a new command to import images. +func ImportImagesCommand(imageFiles []string, outFile string, imp *pdfcpu.Import, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.IMPORTIMAGES + return &Command{ + Mode: model.IMPORTIMAGES, + InFiles: imageFiles, + OutFile: &outFile, + Import: imp, + Conf: conf} +} + +// InsertPagesCommand creates a new command to insert a blank page before or after selected pages. +func InsertPagesCommand(inFile, outFile string, pageSelection []string, conf *model.Configuration, mode string, pageConf *pdfcpu.PageConfiguration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + cmdMode := model.INSERTPAGESBEFORE + if mode == "after" { + cmdMode = model.INSERTPAGESAFTER + } + conf.Cmd = cmdMode + return &Command{ + Mode: cmdMode, + InFile: &inFile, + OutFile: &outFile, + PageSelection: pageSelection, + PageConf: pageConf, + Conf: conf} +} + +// RemovePagesCommand creates a new command to remove selected pages. +func RemovePagesCommand(inFile, outFile string, pageSelection []string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.REMOVEPAGES + return &Command{ + Mode: model.REMOVEPAGES, + InFile: &inFile, + OutFile: &outFile, + PageSelection: pageSelection, + Conf: conf} +} + +// RotateCommand creates a new command to rotate pages. +func RotateCommand(inFile, outFile string, rotation int, pageSelection []string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.ROTATE + return &Command{ + Mode: model.ROTATE, + InFile: &inFile, + OutFile: &outFile, + PageSelection: pageSelection, + IntVal: rotation, + Conf: conf} +} + +// NUpCommand creates a new command to render PDFs or image files in n-up fashion. +func NUpCommand(inFiles []string, outFile string, pageSelection []string, nUp *model.NUp, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.NUP + return &Command{ + Mode: model.NUP, + InFiles: inFiles, + OutFile: &outFile, + PageSelection: pageSelection, + NUp: nUp, + Conf: conf} +} + +// BookletCommand creates a new command to render PDFs or image files in booklet fashion. +func BookletCommand(inFiles []string, outFile string, pageSelection []string, nup *model.NUp, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.BOOKLET + return &Command{ + Mode: model.BOOKLET, + InFiles: inFiles, + OutFile: &outFile, + PageSelection: pageSelection, + NUp: nup, + Conf: conf} +} + +// InfoCommand creates a new command to output information about inFile. +func InfoCommand(inFiles []string, pageSelection []string, json bool, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTINFO + return &Command{ + Mode: model.LISTINFO, + InFiles: inFiles, + PageSelection: pageSelection, + BoolVal1: json, + Conf: conf} +} + +// ListFontsCommand returns a list of supported fonts. +func ListFontsCommand(conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTFONTS + return &Command{ + Mode: model.LISTFONTS, + Conf: conf} +} + +// InstallFontsCommand installs true type fonts for embedding. +func InstallFontsCommand(fontFiles []string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.INSTALLFONTS + return &Command{ + Mode: model.INSTALLFONTS, + InFiles: fontFiles, + Conf: conf} +} + +// CreateCheatSheetsFontsCommand creates single page PDF cheat sheets in current dir. +func CreateCheatSheetsFontsCommand(fontFiles []string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.CHEATSHEETSFONTS + return &Command{ + Mode: model.CHEATSHEETSFONTS, + InFiles: fontFiles, + Conf: conf} +} + +// ListKeywordsCommand create a new command to list keywords. +func ListKeywordsCommand(inFile string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTKEYWORDS + return &Command{ + Mode: model.LISTKEYWORDS, + InFile: &inFile, + Conf: conf} +} + +// AddKeywordsCommand creates a new command to add keywords. +func AddKeywordsCommand(inFile, outFile string, keywords []string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.ADDKEYWORDS + return &Command{ + Mode: model.ADDKEYWORDS, + InFile: &inFile, + OutFile: &outFile, + StringVals: keywords, + Conf: conf} +} + +// RemoveKeywordsCommand creates a new command to remove keywords. +func RemoveKeywordsCommand(inFile, outFile string, keywords []string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.REMOVEKEYWORDS + return &Command{ + Mode: model.REMOVEKEYWORDS, + InFile: &inFile, + OutFile: &outFile, + StringVals: keywords, + Conf: conf} +} + +// ListPropertiesCommand creates a new command to list document properties. +func ListPropertiesCommand(inFile string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTPROPERTIES + return &Command{ + Mode: model.LISTPROPERTIES, + InFile: &inFile, + Conf: conf} +} + +// AddPropertiesCommand creates a new command to add document properties. +func AddPropertiesCommand(inFile, outFile string, properties map[string]string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.ADDPROPERTIES + return &Command{ + Mode: model.ADDPROPERTIES, + InFile: &inFile, + OutFile: &outFile, + StringMap: properties, + Conf: conf} +} + +// RemovePropertiesCommand creates a new command to remove document properties. +func RemovePropertiesCommand(inFile, outFile string, propKeys []string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.REMOVEPROPERTIES + return &Command{ + Mode: model.REMOVEPROPERTIES, + InFile: &inFile, + OutFile: &outFile, + StringVals: propKeys, + Conf: conf} +} + +// CollectCommand creates a new command to create a custom PDF page sequence. +func CollectCommand(inFile, outFile string, pageSelection []string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.COLLECT + return &Command{ + Mode: model.COLLECT, + InFile: &inFile, + OutFile: &outFile, + PageSelection: pageSelection, + Conf: conf} +} + +// ListBoxesCommand creates a new command to list page boundaries for selected pages. +func ListBoxesCommand(inFile string, pageSelection []string, pb *model.PageBoundaries, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTBOXES + return &Command{ + Mode: model.LISTBOXES, + InFile: &inFile, + PageSelection: pageSelection, + PageBoundaries: pb, + Conf: conf} +} + +// AddBoxesCommand creates a new command to add page boundaries for selected pages. +func AddBoxesCommand(inFile, outFile string, pageSelection []string, pb *model.PageBoundaries, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.ADDBOXES + return &Command{ + Mode: model.ADDBOXES, + InFile: &inFile, + OutFile: &outFile, + PageSelection: pageSelection, + PageBoundaries: pb, + Conf: conf} +} + +// RemoveBoxesCommand creates a new command to remove page boundaries for selected pages. +func RemoveBoxesCommand(inFile, outFile string, pageSelection []string, pb *model.PageBoundaries, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.REMOVEBOXES + return &Command{ + Mode: model.REMOVEBOXES, + InFile: &inFile, + OutFile: &outFile, + PageSelection: pageSelection, + PageBoundaries: pb, + Conf: conf} +} + +// CropCommand creates a new command to apply a cropBox to selected pages. +func CropCommand(inFile, outFile string, pageSelection []string, box *model.Box, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.CROP + return &Command{ + Mode: model.CROP, + InFile: &inFile, + OutFile: &outFile, + PageSelection: pageSelection, + Box: box, + Conf: conf} +} + +// ListAnnotationsCommand creates a new command to list annotations for selected pages. +func ListAnnotationsCommand(inFile string, pageSelection []string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTANNOTATIONS + return &Command{ + Mode: model.LISTANNOTATIONS, + InFile: &inFile, + PageSelection: pageSelection, + Conf: conf} +} + +// RemoveAnnotationsCommand creates a new command to remove annotations for selected pages. +func RemoveAnnotationsCommand(inFile, outFile string, pageSelection []string, idsAndTypes []string, objNrs []int, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.REMOVEANNOTATIONS + return &Command{ + Mode: model.REMOVEANNOTATIONS, + InFile: &inFile, + OutFile: &outFile, + PageSelection: pageSelection, + StringVals: idsAndTypes, + IntVals: objNrs, + Conf: conf} +} + +// ListImagesCommand creates a new command to list annotations for selected pages. +func ListImagesCommand(inFiles []string, pageSelection []string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTIMAGES + return &Command{ + Mode: model.LISTIMAGES, + InFiles: inFiles, + PageSelection: pageSelection, + Conf: conf} +} + +// DumpCommand creates a new command to dump objects on stdout. +func DumpCommand(inFilePDF string, vals []int, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.DUMP + return &Command{ + Mode: model.DUMP, + InFile: &inFilePDF, + IntVals: vals, + Conf: conf} +} + +// CreateCommand creates a new command to create a PDF file. +func CreateCommand(inFilePDF, inFileJSON, outFilePDF string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.CREATE + return &Command{ + Mode: model.CREATE, + InFile: &inFilePDF, + InFileJSON: &inFileJSON, + OutFile: &outFilePDF, + Conf: conf} +} + +// ListFormFieldsCommand creates a new command to list the field ids from a PDF form. +func ListFormFieldsCommand(inFiles []string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTFORMFIELDS + return &Command{ + Mode: model.LISTFORMFIELDS, + InFiles: inFiles, + Conf: conf} +} + +// RemoveFormFieldsCommand creates a new command to remove fields from a PDF form. +func RemoveFormFieldsCommand(inFile, outFile string, fieldIDs []string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.REMOVEFORMFIELDS + return &Command{ + Mode: model.REMOVEFORMFIELDS, + InFile: &inFile, + OutFile: &outFile, + StringVals: fieldIDs, + Conf: conf} +} + +// LockFormCommand creates a new command to lock PDF form fields. +func LockFormCommand(inFile, outFile string, fieldIDs []string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LOCKFORMFIELDS + return &Command{ + Mode: model.LOCKFORMFIELDS, + InFile: &inFile, + OutFile: &outFile, + StringVals: fieldIDs, + Conf: conf} +} + +// UnlockFormCommand creates a new command to unlock PDF form fields. +func UnlockFormCommand(inFile, outFile string, fieldIDs []string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.UNLOCKFORMFIELDS + return &Command{ + Mode: model.UNLOCKFORMFIELDS, + InFile: &inFile, + OutFile: &outFile, + StringVals: fieldIDs, + Conf: conf} +} + +// ResetFormCommand creates a new command to lock PDF form fields. +func ResetFormCommand(inFile, outFile string, fieldIDs []string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.RESETFORMFIELDS + return &Command{ + Mode: model.RESETFORMFIELDS, + InFile: &inFile, + OutFile: &outFile, + StringVals: fieldIDs, + Conf: conf} +} + +// ExportFormCommand creates a new command to export a PDF form. +func ExportFormCommand(inFilePDF, outFileJSON string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.EXPORTFORMFIELDS + return &Command{ + Mode: model.EXPORTFORMFIELDS, + InFile: &inFilePDF, + OutFileJSON: &outFileJSON, + Conf: conf} +} + +// FillFormCommand creates a new command to fill a PDF form with data. +func FillFormCommand(inFilePDF, inFileJSON, outFilePDF string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.FILLFORMFIELDS + return &Command{ + Mode: model.FILLFORMFIELDS, + InFile: &inFilePDF, + InFileJSON: &inFileJSON, + OutFile: &outFilePDF, + Conf: conf} +} + +// MultiFillFormCommand creates a new command to fill multiple PDF forms with JSON or CSV data. +func MultiFillFormCommand(inFilePDF, inFileData, outDir, outFilePDF string, merge bool, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.MULTIFILLFORMFIELDS + return &Command{ + Mode: model.MULTIFILLFORMFIELDS, + InFile: &inFilePDF, + InFileJSON: &inFileData, // TODO Fix name clash. + OutDir: &outDir, + OutFile: &outFilePDF, + BoolVal1: merge, + Conf: conf} +} + +// ResizeCommand creates a new command to scale selected pages. +func ResizeCommand(inFile, outFile string, pageSelection []string, resize *model.Resize, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.RESIZE + return &Command{ + Mode: model.RESIZE, + InFile: &inFile, + OutFile: &outFile, + PageSelection: pageSelection, + Resize: resize, + Conf: conf} +} + +// PosterCommand creates a new command to cut and slice pages horizontally or vertically. +func PosterCommand(inFile, outDir, outFile string, pageSelection []string, cut *model.Cut, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.POSTER + return &Command{ + Mode: model.POSTER, + InFile: &inFile, + OutDir: &outDir, + OutFile: &outFile, + PageSelection: pageSelection, + Cut: cut, + Conf: conf} +} + +// NDownCommand creates a new command to cut and slice pages horizontally or vertically. +func NDownCommand(inFile, outDir, outFile string, pageSelection []string, n int, cut *model.Cut, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.NDOWN + return &Command{ + Mode: model.NDOWN, + InFile: &inFile, + OutDir: &outDir, + OutFile: &outFile, + PageSelection: pageSelection, + IntVal: n, + Cut: cut, + Conf: conf} +} + +// CutCommand creates a new command to cut and slice pages horizontally or vertically. +func CutCommand(inFile, outDir, outFile string, pageSelection []string, cut *model.Cut, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.CUT + return &Command{ + Mode: model.CUT, + InFile: &inFile, + OutDir: &outDir, + OutFile: &outFile, + PageSelection: pageSelection, + Cut: cut, + Conf: conf} +} + +// ListBookmarksCommand creates a new command to list bookmarks of inFile. +func ListBookmarksCommand(inFile string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTBOOKMARKS + return &Command{ + Mode: model.LISTBOOKMARKS, + InFile: &inFile, + Conf: conf} +} + +// ExportBookmarksCommand creates a new command to export bookmarks of inFile. +func ExportBookmarksCommand(inFile, outFileJSON string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.EXPORTBOOKMARKS + return &Command{ + Mode: model.EXPORTBOOKMARKS, + InFile: &inFile, + OutFileJSON: &outFileJSON, + Conf: conf} +} + +// ImportBookmarksCommand creates a new command to import bookmarks to inFile. +func ImportBookmarksCommand(inFile, inFileJSON, outFile string, replace bool, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.IMPORTBOOKMARKS + return &Command{ + Mode: model.IMPORTBOOKMARKS, + BoolVal1: replace, + InFile: &inFile, + InFileJSON: &inFileJSON, + OutFile: &outFile, + Conf: conf} +} + +// RemoveBookmarksCommand creates a new command to remove all bookmarks from inFile. +func RemoveBookmarksCommand(inFile, outFile string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.REMOVEBOOKMARKS + return &Command{ + Mode: model.REMOVEBOOKMARKS, + InFile: &inFile, + OutFile: &outFile, + Conf: conf} +} + +// ListPageLayoutCommand creates a new command to list the document page layout. +func ListPageLayoutCommand(inFile string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTPAGELAYOUT + return &Command{ + Mode: model.LISTPAGELAYOUT, + InFile: &inFile, + Conf: conf} +} + +// SetPageLayoutCommand creates a new command to set the document page layout. +func SetPageLayoutCommand(inFile, outFile, value string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.SETPAGELAYOUT + return &Command{ + Mode: model.SETPAGELAYOUT, + InFile: &inFile, + OutFile: &outFile, + StringVal: value, + Conf: conf} +} + +// ResetPageLayoutCommand creates a new command to reset the document page layout. +func ResetPageLayoutCommand(inFile, outFile string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.RESETPAGELAYOUT + return &Command{ + Mode: model.RESETPAGELAYOUT, + InFile: &inFile, + OutFile: &outFile, + Conf: conf} +} + +// ListPageModeCommand creates a new command to list the document page mode. +func ListPageModeCommand(inFile string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTPAGEMODE + return &Command{ + Mode: model.LISTPAGEMODE, + InFile: &inFile, + Conf: conf} +} + +// SetPageModeCommand creates a new command to set the document page mode. +func SetPageModeCommand(inFile, outFile, value string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.SETPAGEMODE + return &Command{ + Mode: model.SETPAGEMODE, + InFile: &inFile, + OutFile: &outFile, + StringVal: value, + Conf: conf} +} + +// ResetPageModeCommand creates a new command to reset the document page mode. +func ResetPageModeCommand(inFile, outFile string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.RESETPAGEMODE + return &Command{ + Mode: model.RESETPAGEMODE, + InFile: &inFile, + OutFile: &outFile, + Conf: conf} +} + +// ListViewerPreferencesCommand creates a new command to list the viewer preferences. +func ListViewerPreferencesCommand(inFile string, all, json bool, conf *model.Configuration) *Command { + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTVIEWERPREFERENCES + return &Command{ + Mode: model.LISTVIEWERPREFERENCES, + InFile: &inFile, + BoolVal1: all, + BoolVal2: json, + Conf: conf} +} + +// SetViewerPreferencesCommand creates a new command to set the viewer preferences. +func SetViewerPreferencesCommand(inFilePDF, inFileJSON, outFilePDF, stringJSON string, conf *model.Configuration) *Command { + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.SETVIEWERPREFERENCES + return &Command{ + Mode: model.SETVIEWERPREFERENCES, + InFile: &inFilePDF, + InFileJSON: &inFileJSON, + OutFile: &outFilePDF, + StringVal: stringJSON, + Conf: conf} +} + +// ResetViewerPreferencesCommand creates a new command to reset the viewer preferences. +func ResetViewerPreferencesCommand(inFile, outFile string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.RESETVIEWERPREFERENCES + return &Command{ + Mode: model.RESETVIEWERPREFERENCES, + InFile: &inFile, + OutFile: &outFile, + Conf: conf} +} + +// ZoomCommand creates a new command to zoom in/out of selected pages. +func ZoomCommand(inFile, outFile string, pageSelection []string, zoom *model.Zoom, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.ZOOM + return &Command{ + Mode: model.ZOOM, + InFile: &inFile, + OutFile: &outFile, + PageSelection: pageSelection, + Zoom: zoom, + Conf: conf} +} diff --git a/pkg/cli/list.go b/pkg/cli/list.go new file mode 100644 index 0000000000000000000000000000000000000000..6dff5ed8146749bbef6a525a512a5b3777e061c2 --- /dev/null +++ b/pkg/cli/list.go @@ -0,0 +1,543 @@ +/* +Copyright 2023 The pdfcpu Authors. + +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 + + 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 cli provides pdfcpu command line processing. +package cli + +import ( + "encoding/json" + "fmt" + "io" + "math" + "os" + "sort" + "strconv" + "time" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/form" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +func listAttachments(rs io.ReadSeeker, conf *model.Configuration, withDesc, sorted bool) ([]string, error) { + if rs == nil { + return nil, errors.New("pdfcpu: listAttachments: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTATTACHMENTS + + ctx, err := api.ReadAndValidate(rs, conf) + if err != nil { + return nil, err + } + + aa, err := ctx.ListAttachments() + if err != nil { + return nil, err + } + + var ss []string + for _, a := range aa { + s := a.FileName + if withDesc && a.Desc != "" { + s = fmt.Sprintf("%s (%s)", s, a.Desc) + } + ss = append(ss, s) + } + if sorted { + sort.Strings(ss) + } + + return ss, nil +} + +// ListAttachmentsFile returns a list of embedded file attachments of inFile with optional description. +func ListAttachmentsFile(inFile string, conf *model.Configuration) ([]string, error) { + f, err := os.Open(inFile) + if err != nil { + return nil, err + } + defer f.Close() + + return listAttachments(f, conf, true, true) +} + +// ListAttachmentsCompactFile returns a list of embedded file attachments of inFile w/o optional description. +func ListAttachmentsCompactFile(inFile string, conf *model.Configuration) ([]string, error) { + f, err := os.Open(inFile) + if err != nil { + return nil, err + } + defer f.Close() + + return listAttachments(f, conf, false, false) +} + +func listAnnotations(rs io.ReadSeeker, selectedPages []string, conf *model.Configuration) (int, []string, error) { + annots, err := api.Annotations(rs, selectedPages, conf) + if err != nil { + return 0, nil, err + } + + return pdfcpu.ListAnnotations(annots) +} + +// ListAnnotationsFile returns a list of page annotations of inFile. +func ListAnnotationsFile(inFile string, selectedPages []string, conf *model.Configuration) (int, []string, error) { + f, err := os.Open(inFile) + if err != nil { + return 0, nil, err + } + defer f.Close() + + return listAnnotations(f, selectedPages, conf) +} + +func listBoxes(rs io.ReadSeeker, selectedPages []string, pb *model.PageBoundaries, conf *model.Configuration) ([]string, error) { + if rs == nil { + return nil, errors.New("pdfcpu: listBoxes: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTBOXES + + ctx, err := api.ReadAndValidate(rs, conf) + if err != nil { + return nil, err + } + + pages, err := api.PagesForPageSelection(ctx.PageCount, selectedPages, true, true) + if err != nil { + return nil, err + } + + return ctx.ListPageBoundaries(pages, pb) +} + +// ListBoxesFile returns a list of page boundaries for selected pages of inFile. +func ListBoxesFile(inFile string, selectedPages []string, pb *model.PageBoundaries, conf *model.Configuration) ([]string, error) { + f, err := os.Open(inFile) + if err != nil { + return nil, err + } + defer f.Close() + + if pb == nil { + pb = &model.PageBoundaries{} + pb.SelectAll() + } + log.CLI.Printf("listing %s for %s\n", pb, inFile) + + return listBoxes(f, selectedPages, pb, conf) +} + +func listFormFields(rs io.ReadSeeker, conf *model.Configuration) ([]string, error) { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTFORMFIELDS + + ctx, err := api.ReadAndValidate(rs, conf) + if err != nil { + return nil, err + } + + return form.ListFormFields(ctx) +} + +// ListFormFieldsFile returns a list of form field ids in inFile. +func ListFormFieldsFile(inFiles []string, conf *model.Configuration) ([]string, error) { + log.SetCLILogger(nil) + + ss := []string{} + + for _, fn := range inFiles { + + f, err := os.Open(fn) + if err != nil { + if len(inFiles) > 1 { + ss = append(ss, fmt.Sprintf("\ncan't open %s: %v", fn, err)) + continue + } + return nil, err + } + defer f.Close() + + output, err := listFormFields(f, conf) + if err != nil { + if len(inFiles) > 1 { + ss = append(ss, fmt.Sprintf("\n%s:\n%v", fn, err)) + continue + } + return nil, err + } + + ss = append(ss, "\n"+fn+":\n") + ss = append(ss, output...) + } + + return ss, nil +} + +func listImages(rs io.ReadSeeker, selectedPages []string, conf *model.Configuration) ([]string, error) { + if rs == nil { + return nil, errors.New("pdfcpu: listImages: Please provide rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTIMAGES + + ctx, err := api.ReadValidateAndOptimize(rs, conf) + if err != nil { + return nil, err + } + + pages, err := api.PagesForPageSelection(ctx.PageCount, selectedPages, true, true) + if err != nil { + return nil, err + } + + return pdfcpu.ListImages(ctx, pages) +} + +// ListImagesFile returns a formatted list of embedded images of inFile. +func ListImagesFile(inFiles []string, selectedPages []string, conf *model.Configuration) ([]string, error) { + if len(selectedPages) == 0 { + log.CLI.Printf("pages: all\n") + } + + log.SetCLILogger(nil) + + ss := []string{} + + for _, fn := range inFiles { + f, err := os.Open(fn) + if err != nil { + if len(inFiles) > 1 { + ss = append(ss, fmt.Sprintf("\ncan't open %s: %v", fn, err)) + continue + } + return nil, err + } + defer f.Close() + output, err := listImages(f, selectedPages, conf) + if err != nil { + if len(inFiles) > 1 { + ss = append(ss, fmt.Sprintf("\n%s: %v", fn, err)) + continue + } + return nil, err + } + ss = append(ss, "\n"+fn+":") + ss = append(ss, output...) + } + + return ss, nil +} + +// ListInfoFile returns formatted information about inFile. +func ListInfoFile(inFile string, selectedPages []string, conf *model.Configuration) ([]string, error) { + f, err := os.Open(inFile) + if err != nil { + return nil, err + } + defer f.Close() + + info, err := api.PDFInfo(f, inFile, selectedPages, conf) + if err != nil { + return nil, err + } + + pages, err := api.PagesForPageSelection(info.PageCount, selectedPages, false, false) + if err != nil { + return nil, err + } + + ss, err := pdfcpu.ListInfo(info, pages) + if err != nil { + return nil, err + } + + return append([]string{inFile + ":"}, ss...), err +} + +func jsonInfo(info *pdfcpu.PDFInfo, pages types.IntSet) (map[string]model.PageBoundaries, []types.Dim) { + if len(pages) > 0 { + pbs := map[string]model.PageBoundaries{} + for i, pb := range info.PageBoundaries { + if _, found := pages[i+1]; !found { + continue + } + d := pb.CropBox().Dimensions() + if pb.Rot%180 != 0 { + d.Width, d.Height = d.Height, d.Width + } + pb.Orientation = "portrait" + if d.Landscape() { + pb.Orientation = "landscape" + } + if pb.Media != nil { + pb.Media.Rect = pb.Media.Rect.ConvertToUnit(info.Unit) + pb.Media.Rect.LL.X = math.Round(pb.Media.Rect.LL.X*100) / 100 + pb.Media.Rect.LL.Y = math.Round(pb.Media.Rect.LL.Y*100) / 100 + pb.Media.Rect.UR.X = math.Round(pb.Media.Rect.UR.X*100) / 100 + pb.Media.Rect.UR.Y = math.Round(pb.Media.Rect.UR.Y*100) / 100 + } + if pb.Crop != nil { + pb.Crop.Rect = pb.Crop.Rect.ConvertToUnit(info.Unit) + pb.Crop.Rect.LL.X = math.Round(pb.Crop.Rect.LL.X*100) / 100 + pb.Crop.Rect.LL.Y = math.Round(pb.Crop.Rect.LL.Y*100) / 100 + pb.Crop.Rect.UR.X = math.Round(pb.Crop.Rect.UR.X*100) / 100 + pb.Crop.Rect.UR.Y = math.Round(pb.Crop.Rect.UR.Y*100) / 100 + } + if pb.Trim != nil { + pb.Trim.Rect = pb.Trim.Rect.ConvertToUnit(info.Unit) + pb.Trim.Rect.LL.X = math.Round(pb.Trim.Rect.LL.X*100) / 100 + pb.Trim.Rect.LL.Y = math.Round(pb.Trim.Rect.LL.Y*100) / 100 + pb.Trim.Rect.UR.X = math.Round(pb.Trim.Rect.UR.X*100) / 100 + pb.Trim.Rect.UR.Y = math.Round(pb.Trim.Rect.UR.Y*100) / 100 + } + if pb.Bleed != nil { + pb.Bleed.Rect = pb.Bleed.Rect.ConvertToUnit(info.Unit) + pb.Bleed.Rect.LL.X = math.Round(pb.Bleed.Rect.LL.X*100) / 100 + pb.Bleed.Rect.LL.Y = math.Round(pb.Bleed.Rect.LL.Y*100) / 100 + pb.Bleed.Rect.UR.X = math.Round(pb.Bleed.Rect.UR.X*100) / 100 + pb.Bleed.Rect.UR.Y = math.Round(pb.Bleed.Rect.UR.Y*100) / 100 + } + if pb.Art != nil { + pb.Art.Rect = pb.Art.Rect.ConvertToUnit(info.Unit) + pb.Art.Rect.LL.X = math.Round(pb.Art.Rect.LL.X*100) / 100 + pb.Art.Rect.LL.Y = math.Round(pb.Art.Rect.LL.Y*100) / 100 + pb.Art.Rect.UR.X = math.Round(pb.Art.Rect.UR.X*100) / 100 + pb.Art.Rect.UR.Y = math.Round(pb.Art.Rect.UR.Y*100) / 100 + } + pbs[strconv.Itoa(i+1)] = pb + } + return pbs, nil + } + + var dims []types.Dim + for k, v := range info.PageDimensions { + if v { + dc := k.ConvertToUnit(info.Unit) + dc.Width = math.Round(dc.Width*100) / 100 + dc.Height = math.Round(dc.Height*100) / 100 + dims = append(dims, dc) + } + } + return nil, dims +} + +func listInfoFilesJSON(inFiles []string, selectedPages []string, conf *model.Configuration) ([]string, error) { + var infos []*pdfcpu.PDFInfo + + for _, fn := range inFiles { + + f, err := os.Open(fn) + if err != nil { + return nil, err + } + defer f.Close() + + info, err := api.PDFInfo(f, fn, selectedPages, conf) + if err != nil { + return nil, err + } + + pages, err := api.PagesForPageSelection(info.PageCount, selectedPages, false, false) + if err != nil { + return nil, err + } + + info.Boundaries, info.Dimensions = jsonInfo(info, pages) + + infos = append(infos, info) + } + + s := struct { + Header pdfcpu.Header `json:"header"` + Infos []*pdfcpu.PDFInfo `json:"infos"` + }{ + Header: pdfcpu.Header{Version: "pdfcpu " + model.VersionStr, Creation: time.Now().Format("2006-01-02 15:04:05 MST")}, + Infos: infos, + } + + bb, err := json.MarshalIndent(s, "", "\t") + if err != nil { + return nil, err + } + + return []string{string(bb)}, nil +} + +// ListInfoFiles returns formatted information about inFiles. +func ListInfoFiles(inFiles []string, selectedPages []string, json bool, conf *model.Configuration) ([]string, error) { + + if json { + return listInfoFilesJSON(inFiles, selectedPages, conf) + } + + var ss []string + + for i, fn := range inFiles { + if i > 0 { + ss = append(ss, "") + } + ssx, err := ListInfoFile(fn, selectedPages, conf) + if err != nil { + if len(inFiles) == 1 { + return nil, err + } + fmt.Fprintf(os.Stderr, "%s: %v\n", fn, err) + } + ss = append(ss, ssx...) + } + + return ss, nil +} + +// ListKeywordsFile returns the keyword list of inFile. +func ListKeywordsFile(inFile string, conf *model.Configuration) ([]string, error) { + f, err := os.Open(inFile) + if err != nil { + return nil, err + } + defer f.Close() + + return api.Keywords(f, conf) +} + +func listPermissions(rs io.ReadSeeker, conf *model.Configuration) ([]string, error) { + if rs == nil { + return nil, errors.New("pdfcpu: listPermissions: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTPERMISSIONS + + ctx, err := api.ReadAndValidate(rs, conf) + if err != nil { + return nil, err + } + + return pdfcpu.Permissions(ctx), nil +} + +// ListPermissionsFile returns a list of user access permissions for inFile. +func ListPermissionsFile(inFiles []string, conf *model.Configuration) ([]string, error) { + log.SetCLILogger(nil) + + var ss []string + + for i, fn := range inFiles { + if i > 0 { + ss = append(ss, "") + } + f, err := os.Open(fn) + if err != nil { + return nil, err + } + defer func() { + f.Close() + }() + ssx, err := listPermissions(f, conf) + if err != nil { + if len(inFiles) == 1 { + return nil, err + } + fmt.Fprintf(os.Stderr, "%s: %v\n", fn, err) + } + ss = append(ss, fn+":") + ss = append(ss, ssx...) + } + + return ss, nil +} + +func listProperties(rs io.ReadSeeker, conf *model.Configuration) ([]string, error) { + if rs == nil { + return nil, errors.New("pdfcpu: listProperties: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.LISTPROPERTIES + + ctx, err := api.ReadAndValidate(rs, conf) + if err != nil { + return nil, err + } + + return pdfcpu.PropertiesList(ctx) +} + +// ListPropertiesFile returns the property list of inFile. +func ListPropertiesFile(inFile string, conf *model.Configuration) ([]string, error) { + f, err := os.Open(inFile) + if err != nil { + return nil, err + } + defer f.Close() + + return listProperties(f, conf) +} + +func listBookmarks(rs io.ReadSeeker, conf *model.Configuration) ([]string, error) { + if rs == nil { + return nil, errors.New("pdfcpu: listBookmarks: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.LISTBOOKMARKS + + ctx, err := api.ReadAndValidate(rs, conf) + if err != nil { + return nil, err + } + + return pdfcpu.BookmarkList(ctx) +} + +// ListBookmarksFile returns the bookmarks of inFile. +func ListBookmarksFile(inFile string, conf *model.Configuration) ([]string, error) { + f, err := os.Open(inFile) + if err != nil { + return nil, err + } + defer f.Close() + + return listBookmarks(f, conf) +} diff --git a/pkg/cli/process.go b/pkg/cli/process.go new file mode 100644 index 0000000000000000000000000000000000000000..2248c9d63e819d5256f3c60cb8d3c65672afd887 --- /dev/null +++ b/pkg/cli/process.go @@ -0,0 +1,277 @@ +/* +Copyright 2019 The pdfcpu Authors. + +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 + + 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 cli + +import ( + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pkg/errors" +) + +// Process executes a pdfcpu command. +func Process(cmd *Command) (out []string, err error) { + defer func() { + if r := recover(); r != nil { + err = errors.Errorf("unexpected panic attack: %v\n", r) + } + }() + + cmd.Conf.Cmd = cmd.Mode + + if f, ok := cmdMap[cmd.Mode]; ok { + return f(cmd) + } + + return nil, errors.Errorf("pdfcpu: process: Unknown command mode %d\n", cmd.Mode) +} + +func processPageAnnotations(cmd *Command) (out []string, err error) { + switch cmd.Mode { + + case model.LISTANNOTATIONS: + out, err = ListAnnotations(cmd) + + case model.REMOVEANNOTATIONS: + out, err = RemoveAnnotations(cmd) + } + + return out, err +} + +func processAttachments(cmd *Command) (out []string, err error) { + switch cmd.Mode { + + case model.LISTATTACHMENTS: + out, err = ListAttachments(cmd) + + case model.ADDATTACHMENTS, model.ADDATTACHMENTSPORTFOLIO: + out, err = AddAttachments(cmd) + + case model.REMOVEATTACHMENTS: + out, err = RemoveAttachments(cmd) + + case model.EXTRACTATTACHMENTS: + out, err = ExtractAttachments(cmd) + } + + return out, err +} + +func processKeywords(cmd *Command) (out []string, err error) { + switch cmd.Mode { + + case model.LISTKEYWORDS: + out, err = ListKeywords(cmd) + + case model.ADDKEYWORDS: + out, err = AddKeywords(cmd) + + case model.REMOVEKEYWORDS: + out, err = RemoveKeywords(cmd) + + } + + return out, err +} + +func processProperties(cmd *Command) (out []string, err error) { + switch cmd.Mode { + + case model.LISTPROPERTIES: + out, err = ListProperties(cmd) + + case model.ADDPROPERTIES: + out, err = AddProperties(cmd) + + case model.REMOVEPROPERTIES: + out, err = RemoveProperties(cmd) + + } + + return out, err +} + +func processEncryption(cmd *Command) (out []string, err error) { + switch cmd.Mode { + + case model.ENCRYPT: + return Encrypt(cmd) + + case model.DECRYPT: + return Decrypt(cmd) + + case model.CHANGEUPW: + return ChangeUserPassword(cmd) + + case model.CHANGEOPW: + return ChangeOwnerPassword(cmd) + } + + return nil, nil +} + +func processPermissions(cmd *Command) (out []string, err error) { + switch cmd.Mode { + + case model.LISTPERMISSIONS: + return ListPermissions(cmd) + + case model.SETPERMISSIONS: + return SetPermissions(cmd) + } + + return nil, nil +} + +func processPages(cmd *Command) (out []string, err error) { + switch cmd.Mode { + + case model.INSERTPAGESBEFORE, model.INSERTPAGESAFTER: + return InsertPages(cmd) + + case model.REMOVEPAGES: + return RemovePages(cmd) + } + + return nil, nil +} + +func processPageBoundaries(cmd *Command) (out []string, err error) { + switch cmd.Mode { + + case model.LISTBOXES: + return ListBoxes(cmd) + + case model.ADDBOXES: + return AddBoxes(cmd) + + case model.REMOVEBOXES: + return RemoveBoxes(cmd) + + case model.CROP: + return Crop(cmd) + } + + return nil, nil +} + +func processImages(cmd *Command) (out []string, err error) { + switch cmd.Mode { + + case model.LISTIMAGES: + return ListImages(cmd) + } + + return nil, nil +} + +func processForm(cmd *Command) (out []string, err error) { + switch cmd.Mode { + + case model.LISTFORMFIELDS: + return ListFormFields(cmd) + + case model.REMOVEFORMFIELDS: + return RemoveFormFields(cmd) + + case model.LOCKFORMFIELDS: + return LockFormFields(cmd) + + case model.UNLOCKFORMFIELDS: + return UnlockFormFields(cmd) + + case model.RESETFORMFIELDS: + return ResetFormFields(cmd) + + case model.EXPORTFORMFIELDS: + return ExportFormFields(cmd) + + case model.FILLFORMFIELDS: + return FillFormFields(cmd) + + case model.MULTIFILLFORMFIELDS: + return MultiFillFormFields(cmd) + } + + return nil, nil +} + +func processBookmarks(cmd *Command) (out []string, err error) { + switch cmd.Mode { + + case model.LISTBOOKMARKS: + return ListBookmarks(cmd) + + case model.EXPORTBOOKMARKS: + return ExportBookmarks(cmd) + + case model.IMPORTBOOKMARKS: + return ImportBookmarks(cmd) + + case model.REMOVEBOOKMARKS: + return RemoveBookmarks(cmd) + } + + return nil, nil +} + +func processPageLayout(cmd *Command) (out []string, err error) { + switch cmd.Mode { + + case model.LISTPAGELAYOUT: + return ListPageLayout(cmd) + + case model.SETPAGELAYOUT: + return SetPageLayout(cmd) + + case model.RESETPAGELAYOUT: + return ResetPageLayout(cmd) + } + + return nil, nil +} + +func processPageMode(cmd *Command) (out []string, err error) { + switch cmd.Mode { + + case model.LISTPAGEMODE: + return ListPageMode(cmd) + + case model.SETPAGEMODE: + return SetPageMode(cmd) + + case model.RESETPAGEMODE: + return ResetPageMode(cmd) + } + + return nil, nil +} + +func processViewerPreferences(cmd *Command) (out []string, err error) { + switch cmd.Mode { + + case model.LISTVIEWERPREFERENCES: + return ListViewerPreferences(cmd) + + case model.SETVIEWERPREFERENCES: + return SetViewerPreferences(cmd) + + case model.RESETVIEWERPREFERENCES: + return ResetViewerPreferences(cmd) + } + + return nil, nil +} diff --git a/pkg/cli/test/annotation_test.go b/pkg/cli/test/annotation_test.go new file mode 100644 index 0000000000000000000000000000000000000000..15be29d9ac3fa4d088d661924668aacc805a3cab --- /dev/null +++ b/pkg/cli/test/annotation_test.go @@ -0,0 +1,59 @@ +/* +Copyright 2021 The pdfcpu Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/cli" +) + +func TestListAndRemoveAnnotations(t *testing.T) { + msg := "TestListAndRemoveAnnotations" + + fn := "adobe_errata.pdf" + copyFile(t, filepath.Join(inDir, fn), filepath.Join(outDir, fn)) + inFile := filepath.Join(outDir, fn) + + // See also api/annotations_test.go for page annotation manipulation + // including adding annotations. + + cmd := cli.ListAnnotationsCommand(inFile, nil, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + // Remove page annotation using obj# 34 + cmd = cli.RemoveAnnotationsCommand(inFile, "", nil, nil, []int{34}, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + // Remove all page annotations from page 14 + cmd = cli.RemoveAnnotationsCommand(inFile, "", []string{"14"}, nil, nil, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + // Remove all page annotations + cmd = cli.RemoveAnnotationsCommand(inFile, "", nil, nil, nil, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + +} diff --git a/pkg/cli/test/attachment_test.go b/pkg/cli/test/attachment_test.go new file mode 100644 index 0000000000000000000000000000000000000000..885d64381cc1e6984948f79ebe6355effbb2770d --- /dev/null +++ b/pkg/cli/test/attachment_test.go @@ -0,0 +1,111 @@ +/* +Copyright 2019 The pdf Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/cli" +) + +func prepareForAttachmentTest(t *testing.T) error { + t.Helper() + for _, fileName := range []string{"go.pdf", "golang.pdf", "T4.pdf", "go-lecture.pdf"} { + inFile := filepath.Join(inDir, fileName) + outFile := filepath.Join(outDir, fileName) + if err := copyFile(t, inFile, outFile); err != nil { + return err + } + } + return copyFile(t, filepath.Join(resDir, "test.wav"), filepath.Join(outDir, "test.wav")) +} + +func listAttachments(t *testing.T, msg, fileName string, want int) []string { + t.Helper() + cmd := cli.ListAttachmentsCommand(fileName, conf) + list, err := cli.Process(cmd) + if err != nil { + t.Fatalf("%s list attachments: %v\n", msg, err) + } + // # of attachments must be want + if len(list) != want { + t.Fatalf("%s: list attachments %s: want %d got %d\n", msg, fileName, want, len(list)) + } + return list +} + +func TestAttachments(t *testing.T) { + msg := "testAttachments" + + if err := prepareForAttachmentTest(t); err != nil { + t.Fatalf("%s prepare for attachments: %v\n", msg, err) + } + + fileName := filepath.Join(outDir, "go.pdf") + + // # of attachments must be 0 + listAttachments(t, msg, fileName, 0) + + // attach add 4 files + files := []string{ + filepath.Join(outDir, "golang.pdf"), + filepath.Join(outDir, "T4.pdf"), + filepath.Join(outDir, "go-lecture.pdf"), + filepath.Join(outDir, "test.wav"), + } + + cmd := cli.AddAttachmentsCommand(fileName, "", files, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s add attachments: %v\n", msg, err) + } + list := listAttachments(t, msg, fileName, 4) + for _, s := range list { + t.Log(s) + } + + // Extract all attachments. + cmd = cli.ExtractAttachmentsCommand(fileName, outDir, nil, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s extract all attachments: %v\n", msg, err) + } + + // Extract 1 attachment. + cmd = cli.ExtractAttachmentsCommand(fileName, outDir, []string{"golang.pdf"}, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s extract one attachment: %v\n", msg, err) + } + + // Remove 1 attachment. + cmd = cli.RemoveAttachmentsCommand(fileName, "", []string{"golang.pdf"}, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s remove one attachment: %v\n", msg, err) + } + listAttachments(t, msg, fileName, 3) + + // Remove all attachments. + cmd = cli.RemoveAttachmentsCommand(fileName, "", nil, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s remove all attachments: %v\n", msg, err) + } + listAttachments(t, msg, fileName, 0) + + // Validate the processed file. + if err := validateFile(t, fileName, conf); err != nil { + t.Fatalf("%s: validate: %v\n", msg, err) + } +} diff --git a/pkg/cli/test/booklet_test.go b/pkg/cli/test/booklet_test.go new file mode 100644 index 0000000000000000000000000000000000000000..ec1f45622b2825d9e96941f90ac03ae43c62aac5 --- /dev/null +++ b/pkg/cli/test/booklet_test.go @@ -0,0 +1,175 @@ +/* + Copyright 2021 The pdfcpu Authors. + + 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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/cli" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" +) + +func testBooklet(t *testing.T, msg string, inFiles []string, outFile string, selectedPages []string, desc string, n int, isImg bool, conf *model.Configuration) { + t.Helper() + + var ( + booklet *model.NUp + err error + ) + + if isImg { + if booklet, err = api.ImageBookletConfig(n, desc, conf); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + } else { + if booklet, err = api.PDFBookletConfig(n, desc, conf); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + } + + cmd := cli.BookletCommand(inFiles, outFile, selectedPages, booklet, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + if err := validateFile(t, outFile, conf); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestBookletCommand(t *testing.T) { + for _, tt := range []struct { + msg string + inFiles []string + outFile string + selectedPages []string + desc string + unit string + n int + isImg bool + }{ + + // 2-up booklet from images on A4 + {"TestBookletFromImagesA42Up", + imageFileNames(t, resDir), + filepath.Join(outDir, "BookletFromImagesA4_2Up.pdf"), + nil, + "p:A4, border:false, g:on, ma:25, bgcol:#beded9", + "points", + 2, + true, + }, + + // 4-up booklet from images on A4 + {"TestBookletFromImagesA44Up", + imageFileNames(t, resDir), + filepath.Join(outDir, "BookletFromImagesA4_4Up.pdf"), + nil, + "p:A4, border:false, g:on, ma:25, bgcol:#beded9", + "points", + 4, + true, + }, + + // 2-up booklet from PDF on A4 + {"TestBookletFromPDF2UpA4", + []string{filepath.Join(inDir, "zineTest.pdf")}, + filepath.Join(outDir, "BookletFromPDFA4_2Up.pdf"), + nil, // all pages + "p:A4, border:false, g:on, ma:10, bgcol:#beded9", + "points", + 2, + false, + }, + + // 4-up booklet from PDF on A4 + {"TestBookletFromPDF4UpA4", + []string{filepath.Join(inDir, "zineTest.pdf")}, + filepath.Join(outDir, "BookletFromPDFA4_4Up.pdf"), + []string{"1-"}, // all pages + "p:A4, border:off, guides:on, ma:10, bgcol:#beded9", + "points", + 4, + false, + }, + + // 4-up booklet from PDF on Ledger + {"TestBookletFromPDF4UpLedger", + []string{filepath.Join(inDir, "bookletTest.pdf")}, + filepath.Join(outDir, "BookletFromPDFLedger_4Up.pdf"), + []string{"1-24"}, + "p:LedgerP, g:on, ma:10, bgcol:#f7e6c7", + "points", + 4, + false, + }, + + // 4-up booklet from PDF on Ledger where the number of pages don't fill the whole sheet + {"TestBookletFromPDF4UpLedgerWithTrailingBlankPages", + []string{filepath.Join(inDir, "bookletTest.pdf")}, + filepath.Join(outDir, "BookletFromPDFLedger_4UpWithTrailingBlankPages.pdf"), + []string{"1-21"}, + "p:LedgerP, g:on, ma:10, bgcol:#f7e6c7", + "points", + 4, + false, + }, + + // 2-up booklet from PDF on Letter + {"TestBookletFromPDF2UpLetter", + []string{filepath.Join(inDir, "bookletTest.pdf")}, + filepath.Join(outDir, "BookletFromPDFLetter_2Up.pdf"), + []string{"1-16"}, + "p:LetterP, g:on, ma:10, bgcol:#f7e6c7", + "points", + 2, + false, + }, + + // 2-up booklet from PDF on Letter where the number of pages don't fill the whole sheet + {"TestBookletFromPDF2UpLetterWithTrailingBlankPages", + []string{filepath.Join(inDir, "bookletTest.pdf")}, + filepath.Join(outDir, "BookletFromPDFLetter_2UpWithTrailingBlankPages.pdf"), + []string{"1-14"}, + "p:LetterP, g:on, ma:10, bgcol:#f7e6c7", + "points", + 2, + false, + }, + + // 2-up multi folio booklet from PDF on A4 using 8 sheets per folio + // using the default foliosize:8 + // Here we print 2 complete folios (2 x 8 sheets) + 1 partial folio + // multi folio only makes sense for n = 2 + // See also https://www.instructables.com/How-to-bind-your-own-Hardback-Book/ + {"TestHardbackBookFromPDF", + []string{filepath.Join(inDir, "WaldenFull.pdf")}, + filepath.Join(outDir, "HardbackBookFromPDF.pdf"), + []string{"1-70"}, + "p:A4, multifolio:on, border:off, g:on, ma:10, bgcol:#beded9", + "points", + 2, + false, + }, + } { + conf := model.NewDefaultConfiguration() + conf.SetUnit(tt.unit) + testBooklet(t, tt.msg, tt.inFiles, tt.outFile, tt.selectedPages, tt.desc, tt.n, tt.isImg, conf) + } +} diff --git a/pkg/cli/test/bookmark_test.go b/pkg/cli/test/bookmark_test.go new file mode 100644 index 0000000000000000000000000000000000000000..aafb87c5745ab8bfb09216e9ad0cd5ecef83a9c4 --- /dev/null +++ b/pkg/cli/test/bookmark_test.go @@ -0,0 +1,86 @@ +/* + Copyright 2023 The pdfcpu Authors. + + 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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/cli" +) + +func TestListBookmarks(t *testing.T) { + msg := "TestListBookmarks" + inDir := filepath.Join("..", "..", "samples", "bookmarks") + inFile := filepath.Join(inDir, "bookmarkTree.pdf") + + cmd := cli.ListBookmarksCommand(inFile, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestExportBookmarks(t *testing.T) { + msg := "TestExportBookmarks" + inDir := filepath.Join("..", "..", "samples", "bookmarks") + inFile := filepath.Join(inDir, "bookmarkTree.pdf") + outFile := filepath.Join(outDir, "bookmarkTree.json") + + cmd := cli.ExportBookmarksCommand(inFile, outFile, nil) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestImportBookmarks(t *testing.T) { + msg := "TestImportBookmarks" + inDir := filepath.Join("..", "..", "samples", "bookmarks") + inFile := filepath.Join(inDir, "bookmarkTree.pdf") + inFileJSON := filepath.Join(inDir, "bookmarkTree.json") + outFile := filepath.Join(outDir, "bookmarkTreeImported.pdf") + + replace := true + cmd := cli.ImportBookmarksCommand(inFile, inFileJSON, outFile, replace, nil) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + if err := api.ImportBookmarksFile(inFile, inFileJSON, outFile, replace, nil); err != nil { + t.Fatalf("%s importBookmarks: %v\n", msg, err) + } + + if err := validateFile(t, outFile, conf); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestRemoveBookmarks(t *testing.T) { + msg := "TestRemoveBookmarks" + inDir := filepath.Join("..", "..", "samples", "bookmarks") + inFile := filepath.Join(inDir, "bookmarkTree.pdf") + outFile := filepath.Join(outDir, "bookmarkTreeNoBookmarks.pdf") + + cmd := cli.RemoveBookmarksCommand(inFile, outFile, nil) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + if err := validateFile(t, outFile, conf); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} diff --git a/pkg/cli/test/box_test.go b/pkg/cli/test/box_test.go new file mode 100644 index 0000000000000000000000000000000000000000..bd52200531e1fce581dd8128ce9552a8e325aac6 --- /dev/null +++ b/pkg/cli/test/box_test.go @@ -0,0 +1,132 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/cli" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +func TestListBoxesCommand(t *testing.T) { + msg := "TestListBoxesCommand" + inFile := filepath.Join(inDir, "5116.DCT_Filter.pdf") + + // List all page boundaries for all pages. + cmd := cli.ListBoxesCommand(inFile, nil, nil, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + // List crop box for all pages. + pb, err := api.PageBoundariesFromBoxList("crop") + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + cmd.PageBoundaries = pb + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestCropCommand(t *testing.T) { + msg := "TestCropCommand" + inFile := filepath.Join(inDir, "test.pdf") + outFile := filepath.Join(outDir, "out.pdf") + + for _, tt := range []struct { + s string + u types.DisplayUnit + }{ + {"0 0 10 0", types.POINTS}, + {"[0 0 5 5]", types.CENTIMETRES}, + {"100", types.POINTS}, + {"20% 40%", types.POINTS}, + {"dim:30 30", types.POINTS}, + {"dim:50% 50%", types.POINTS}, + {"pos:bl, dim:50% 50%", types.POINTS}, + {"pos:tl, off: 10 -10, dim:50% 50%", types.POINTS}, + {"-1", types.INCHES}, + {"-25%", types.POINTS}, + } { + box, err := api.Box(tt.s, tt.u) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + cmd := cli.CropCommand(inFile, outFile, nil, box, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + } +} + +func TestAddBoxesCommand(t *testing.T) { + msg := "TestAddBoxesCommand" + inFile := filepath.Join(inDir, "test.pdf") + outFile := filepath.Join(outDir, "out.pdf") + + for _, tt := range []struct { + s string + u types.DisplayUnit + }{ + {"crop:[0 0 5 5]", types.CENTIMETRES}, // Crop 5 x 5 cm at bottom left corner + {"crop:10", types.POINTS}, + {"crop:-10", types.POINTS}, + {"crop:10 20, trim:crop, art:bleed, bleed:art", types.POINTS}, + {"crop:10 20, trim:crop, art:bleed, bleed:media", types.POINTS}, + {"c:10 20, t:c, a:b, b:m", types.POINTS}, + {"crop:10, trim:20, art:trim", types.POINTS}, + } { + pb, err := api.PageBoundaries(tt.s, tt.u) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + cmd := cli.AddBoxesCommand(inFile, outFile, nil, pb, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + } +} + +func TestAddRemoveBoxesCommand(t *testing.T) { + msg := "TestAddRemoveBoxesCommand" + inFile := filepath.Join(inDir, "test.pdf") + outFile := filepath.Join(outDir, "out.pdf") + + pb, err := api.PageBoundaries("crop:[0 0 100 100]", types.POINTS) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + cmd := cli.AddBoxesCommand(inFile, outFile, nil, pb, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + pb, err = api.PageBoundariesFromBoxList("crop") + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + cmd = cli.RemoveBoxesCommand(inFile, outFile, nil, pb, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} diff --git a/pkg/cli/test/cli_test.go b/pkg/cli/test/cli_test.go new file mode 100644 index 0000000000000000000000000000000000000000..f8c246d90c4c03147546e89ac994be8d816292c7 --- /dev/null +++ b/pkg/cli/test/cli_test.go @@ -0,0 +1,195 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 test + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/cli" + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +var inDir, outDir, resDir, fontDir, samplesDir string +var conf *model.Configuration + +func isTrueType(filename string) bool { + s := strings.ToLower(filename) + return strings.HasSuffix(s, ".ttf") || strings.HasSuffix(s, ".ttc") +} + +func userFonts(dir string) ([]string, error) { + files, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + ff := []string(nil) + for _, f := range files { + if isTrueType(f.Name()) { + fn := filepath.Join(dir, f.Name()) + ff = append(ff, fn) + } + } + return ff, nil +} + +func TestMain(m *testing.M) { + inDir = filepath.Join("..", "..", "testdata") + fontDir = filepath.Join(inDir, "fonts") + resDir = filepath.Join(inDir, "resources") + samplesDir = filepath.Join("..", "..", "samples") + + conf = api.LoadConfiguration() + + // Install test user fonts from pkg/testdata/fonts. + fonts, err := userFonts(filepath.Join(inDir, "fonts")) + if err != nil { + fmt.Printf("%v", err) + os.Exit(1) + } + + if err := api.InstallFonts(fonts); err != nil { + fmt.Printf("%v", err) + os.Exit(1) + } + + if outDir, err = os.MkdirTemp("", "pdfcpu_cli_tests"); err != nil { + fmt.Printf("%v", err) + os.Exit(1) + } + //fmt.Printf("outDir = %s\n", outDir) + + exitCode := m.Run() + + if err = os.RemoveAll(outDir); err != nil { + fmt.Printf("%v", err) + os.Exit(1) + } + + os.Exit(exitCode) +} + +func copyFile(t *testing.T, srcFileName, destFileName string) error { + t.Helper() + from, err := os.Open(srcFileName) + if err != nil { + return err + } + defer from.Close() + to, err := os.Create(destFileName) + if err != nil { + return err + } + defer to.Close() + _, err = io.Copy(to, from) + return err +} + +func imageFileNames(t *testing.T, dir string) []string { + t.Helper() + fn, err := model.ImageFileNames(dir, types.MB) + if err != nil { + t.Fatal(err) + } + return fn +} + +func isPDF(filename string) bool { + return strings.HasSuffix(strings.ToLower(filename), ".pdf") +} + +func allPDFs(t *testing.T, dir string) []string { + t.Helper() + files, err := os.ReadDir(dir) + if err != nil { + t.Fatalf("pdfFiles from %s: %v\n", dir, err) + } + ff := []string(nil) + for _, f := range files { + if isPDF(f.Name()) { + ff = append(ff, f.Name()) + } + } + return ff +} + +func validateFile(t *testing.T, fileName string, conf *model.Configuration) error { + t.Helper() + _, err := cli.Process(cli.ValidateCommand([]string{fileName}, conf)) + return err +} + +func TestValidate(t *testing.T) { + msg := "TestValidateCommand" + for _, f := range allPDFs(t, inDir) { + inFile := filepath.Join(inDir, f) + if err := validateFile(t, inFile, conf); err != nil { + t.Fatalf("%s: %s: %v\n", msg, inFile, err) + } + } +} + +func TestInfoCommand(t *testing.T) { + msg := "TestInfoCommand" + inFile := filepath.Join(inDir, "5116.DCT_Filter.pdf") + + cmd := cli.InfoCommand([]string{inFile}, nil, true, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestUnknownCommand(t *testing.T) { + msg := "TestUnknownCommand" + inFile := filepath.Join(outDir, "go.pdf") + + cmd := &cli.Command{ + Mode: 99, + InFile: &inFile, + Conf: conf} + + if _, err := cli.Process(cmd); err == nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +// Enable this test for debugging of a specific file. +func XTestSomeCommand(t *testing.T) { + msg := "TestSomeCommand" + + log.SetDefaultTraceLogger() + //log.SetDefaultParseLogger() + log.SetDefaultReadLogger() + log.SetDefaultValidateLogger() + log.SetDefaultOptimizeLogger() + log.SetDefaultWriteLogger() + //log.SetDefaultStatsLogger() + + inFile := filepath.Join(inDir, "test.pdf") + + cmd := cli.ValidateCommand([]string{inFile}, conf) + + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, inFile, err) + } +} diff --git a/pkg/cli/test/collect_test.go b/pkg/cli/test/collect_test.go new file mode 100644 index 0000000000000000000000000000000000000000..7d569e97fc46590b25c8939b410779a0814dac01 --- /dev/null +++ b/pkg/cli/test/collect_test.go @@ -0,0 +1,43 @@ +/* +Copyright 2020 The pdf Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/cli" +) + +// Create a custom page sequence. +func TestCollectCommand(t *testing.T) { + msg := "TestCollectCommand" + + inFile := filepath.Join(inDir, "pike-stanford.pdf") + outFile := filepath.Join(outDir, "myPageSequence.pdf") + + // Start with all odd pages but page 1, then append pages 8-11 and the last page. + cmd := cli.CollectCommand(inFile, outFile, []string{"odd", "!1", "8-11", "l"}, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + if err := validateFile(t, outFile, conf); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + +} diff --git a/pkg/cli/test/createFromJSON_test.go b/pkg/cli/test/createFromJSON_test.go new file mode 100644 index 0000000000000000000000000000000000000000..00e14111e91126da6114927e12599ff7a39b2db9 --- /dev/null +++ b/pkg/cli/test/createFromJSON_test.go @@ -0,0 +1,81 @@ +/* +Copyright 2023 The pdf Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/cli" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" +) + +/************************************************************** + * All form related processing is optimized for Adobe Reader! * + **************************************************************/ + +func createPDF(t *testing.T, msg, inFile, inFileJSON, outFile string, conf *model.Configuration) { + + t.Helper() + + // inFile inFileJSON outFile action + // --------------------------------------------------------------- + // "" jsonFile outfile write outFile + // inFile jsonFile "" update (read and write inFile) + // inFile jsonFile outFile read inFile and write outFile + + if outFile == "" { + outFile = inFile + } + + cmd := cli.CreateCommand(inFile, inFileJSON, outFile, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + if err := validateFile(t, outFile, conf); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + +} + +func TestCreateSinglePageDemoFormsViaJson(t *testing.T) { + + // Render single page demo forms for export, reset, lock, unlock and fill tests. + + inDirFormDemo := filepath.Join(inDir, "json", "form", "demoSinglePage") + outDirFormDemo := outDir + + for _, tt := range []struct { + msg string + inFileJSON string + outFile string + }{ + + {"TestFormDemoEN", "english.json", "english.pdf"}, // Core font (Helvetica) + {"TestFormDemoUK", "ukrainian.json", "ukrainian.pdf"}, // User font (Roboto-Regular) + {"TestFormDemoAR", "arabic.json", "arabic.pdf"}, // User font RTL (Roboto-Regular) + {"TestFormDemoSC", "chineseSimple.json", "chineseSimple.pdf"}, // User font CJK (UnifontMedium) + {"TestPersonFormDemo", "person.json", "person.pdf"}, // Person Form + } { + inFileJSON := filepath.Join(inDirFormDemo, tt.inFileJSON) + outFile := filepath.Join(outDirFormDemo, tt.outFile) + createPDF(t, tt.msg, "", inFileJSON, outFile, conf) + } + + // For more comprehensive PDF creation tests please refer to api/test/createFromJSON_test.go +} diff --git a/pkg/cli/test/cut_test.go b/pkg/cli/test/cut_test.go new file mode 100644 index 0000000000000000000000000000000000000000..22ef494e4cec1b066421524e6769e18564c48db4 --- /dev/null +++ b/pkg/cli/test/cut_test.go @@ -0,0 +1,198 @@ +/* +Copyright 2023 The pdfcpu Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/cli" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +func testCut(t *testing.T, msg, inFile, outFile string, unit types.DisplayUnit, cutConf string) { + t.Helper() + + cut, err := pdfcpu.ParseCutConfig(cutConf, unit) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + inFile = filepath.Join(inDir, inFile) + + cmd := cli.CutCommand(inFile, outDir, outFile, nil, cut, nil) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } +} + +func TestCut(t *testing.T) { + for _, tt := range []struct { + msg string + inFile, outFile string + unit types.DisplayUnit + cutConf string + }{ + {"TestRotatedCutHor", + "testRot.pdf", + "cutHorRot", + types.CENTIMETRES, + "hor:.5, margin:1, border:on"}, + + {"TestCutHor", + "test.pdf", + "cutHor", + types.CENTIMETRES, + "hor:.5, margin:1, bgcol:#E9967A, border:on"}, + + {"TestCutVer", + "test.pdf", + "cutVer", + types.CENTIMETRES, + "ver:.5, margin:1, bgcol:#E9967A"}, + + {"TestCutHorAndVerQuarter", + "test.pdf", + "cutHorAndVerQuarter", + types.POINTS, + "h:.5, v:.5"}, + + {"TestCutHorAndVerThird", + "test.pdf", + "cutHorAndVerThird", + types.POINTS, + "h:.33333, h:.66666, v:.33333, v:.66666"}, + + {"Test", + "test.pdf", + "cutCustom", + types.POINTS, + "h:.25, v:.5"}, + } { + testCut(t, tt.msg, tt.inFile, tt.outFile, tt.unit, tt.cutConf) + } +} + +func testNDown(t *testing.T, msg, inFile, outFile string, n int, unit types.DisplayUnit, cutConf string) { + t.Helper() + + cut, err := pdfcpu.ParseCutConfigForN(n, cutConf, unit) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + inFile = filepath.Join(inDir, inFile) + + cmd := cli.NDownCommand(inFile, outDir, outFile, nil, n, cut, nil) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } +} + +func TestNDown(t *testing.T) { + for _, tt := range []struct { + msg string + inFile string + outFile string + n int + unit types.DisplayUnit + cutConf string + }{ + {"TestNDownRot2", + "testRot.pdf", + "ndownRot2", + 2, + types.CENTIMETRES, + "margin:1, bgcol:#E9967A"}, + + {"TestNDown2", + "test.pdf", + "ndown2", + 2, + types.CENTIMETRES, + "margin:1, border:on"}, + + {"TestNDown9", + "test.pdf", + "ndown9", + 9, + types.CENTIMETRES, + "margin:1, bgcol:#E9967A, border:on"}, + + {"TestNDown16", + "test.pdf", + "ndown16", + 16, + types.CENTIMETRES, + ""}, // optional border, margin, bgcolor + } { + testNDown(t, tt.msg, tt.inFile, tt.outFile, tt.n, tt.unit, tt.cutConf) + } +} + +func testPoster(t *testing.T, msg, inFile, outFile string, unit types.DisplayUnit, cutConf string) { + t.Helper() + + cut, err := pdfcpu.ParseCutConfigForPoster(cutConf, unit) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + inFile = filepath.Join(inDir, inFile) + + cmd := cli.PosterCommand(inFile, outDir, outFile, nil, cut, nil) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } +} + +func TestPoster(t *testing.T) { + for _, tt := range []struct { + msg string + inFile string + outFile string + unit types.DisplayUnit + cutConf string + }{ + {"TestPoster", // 2x2 grid of A6 => A4 + "test.pdf", // A4 + "poster", + types.POINTS, + "f:A6"}, + + {"TestPosterScaled", // 4x4 grid of A6 => A2 + "test.pdf", // A4 + "posterScaled", + types.CENTIMETRES, + "f:A6, scale:2.0, margin:1, bgcol:#E9967A"}, + + {"TestPosterDim", // grid made up of 15x10 cm tiles => A4 + "test.pdf", // A4 + "posterDim", + types.CENTIMETRES, + "dim:15 10, margin:1, border:on"}, + + {"TestPosterDimScaled", // grid made up of 15x10 cm tiles => A2 + "test.pdf", // A4 + "posterDimScaled", + types.CENTIMETRES, + "dim:15 10, scale:2.0, margin:1, bgcol:#E9967A, border:on"}, + } { + testPoster(t, tt.msg, tt.inFile, tt.outFile, tt.unit, tt.cutConf) + } +} diff --git a/pkg/cli/test/encryption_test.go b/pkg/cli/test/encryption_test.go new file mode 100644 index 0000000000000000000000000000000000000000..71481acc52c2e1b6374fda5e1b3d79d9f6326d20 --- /dev/null +++ b/pkg/cli/test/encryption_test.go @@ -0,0 +1,612 @@ +/* +Copyright 2019 The pdfcpu Authors. + +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 + + 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 test + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/cli" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" +) + +func confForAlgorithm(aes bool, keyLength int) *model.Configuration { + c := model.NewDefaultConfiguration() + c.EncryptUsingAES = aes + c.EncryptKeyLength = keyLength + return c +} + +func testEncryptDecryptUseCase1(t *testing.T, fileName string, aes bool, keyLength int) { + t.Helper() + msg := "testEncryptDecryptUseCase1" + + inFile := filepath.Join(inDir, fileName) + outFile := filepath.Join(outDir, "test.pdf") + t.Log(inFile) + + // Encrypt opw and upw + t.Log("Encrypt") + conf := confForAlgorithm(aes, keyLength) + conf.UserPW = "upw" + conf.OwnerPW = "opw" + + cmd := cli.EncryptCommand(inFile, outFile, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: encrypt to %s: %v\n", msg, outFile, err) + } + + // Validate wrong opw + t.Log("Validate wrong opw fails") + conf = confForAlgorithm(aes, keyLength) + conf.OwnerPW = "opwWrong" + if err := validateFile(t, outFile, conf); err == nil { + t.Fatalf("%s: validate %s using wrong opw should fail!\n", msg, outFile) + } + + // Validate opw + t.Log("Validate opw") + conf = confForAlgorithm(aes, keyLength) + conf.OwnerPW = "opw" + if err := validateFile(t, outFile, conf); err != nil { + t.Fatalf("%s: validate %s using opw: %v\n", msg, outFile, err) + } + + // Validate wrong upw + t.Log("Validate wrong upw fails") + conf = confForAlgorithm(aes, keyLength) + conf.UserPW = "upwWrong" + if err := validateFile(t, outFile, conf); err == nil { + t.Fatalf("%s: validate %s using wrong upw should fail!\n", msg, outFile) + } + + // Validate upw + t.Log("Validate upw") + conf = confForAlgorithm(aes, keyLength) + conf.UserPW = "upw" + if err := validateFile(t, outFile, conf); err != nil { + t.Fatalf("%s: validate %s using upw: %v\n", msg, outFile, err) + } + + // Change upw to "" = remove document open password. + t.Log("ChangeUserPW to \"\"") + conf = confForAlgorithm(aes, keyLength) + conf.OwnerPW = "opw" + pwOld := "upw" + pwNew := "" + cmd = cli.ChangeUserPWCommand(outFile, "", &pwOld, &pwNew, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: %s change userPW to \"\": %v\n", msg, outFile, err) + } + + // Validate upw + t.Log("Validate upw") + conf = confForAlgorithm(aes, keyLength) + conf.UserPW = "" + if err := validateFile(t, outFile, conf); err != nil { + t.Fatalf("%s: validate %s using upw: %v\n", msg, outFile, err) + } + + // Validate no pw + t.Log("Validate upw") + conf = confForAlgorithm(aes, keyLength) + if err := validateFile(t, outFile, conf); err != nil { + t.Fatalf("%s: validate %s: %v\n", msg, outFile, err) + } + + // Change opw + t.Log("ChangeOwnerPW") + conf = confForAlgorithm(aes, keyLength) + conf.UserPW = "" + pwOld = "opw" + pwNew = "opwNew" + cmd = cli.ChangeOwnerPWCommand(outFile, "", &pwOld, &pwNew, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: %s change opw: %v\n", msg, outFile, err) + } + + // Decrypt wrong upw + t.Log("Decrypt wrong upw fails") + conf = confForAlgorithm(aes, keyLength) + conf.UserPW = "upwWrong" + cmd = cli.DecryptCommand(outFile, "", conf) + if _, err := cli.Process(cmd); err == nil { + t.Fatalf("%s: %s decrypt using wrong upw should fail\n", msg, outFile) + } + + // Decrypt wrong opw succeeds on empty upw + t.Log("Decrypt wrong opw succeeds on empty upw") + conf = confForAlgorithm(aes, keyLength) + conf.OwnerPW = "opwWrong" + cmd = cli.DecryptCommand(outFile, "", conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: %s decrypt wrong opw, empty upw: %v\n", msg, outFile, err) + } +} + +func ensurePermissionsNone(t *testing.T, listPermOutput []string) { + t.Helper() + if len(listPermOutput) == 0 || !strings.HasPrefix(listPermOutput[1], "permission bits: 000000000000") { + t.Fail() + } +} + +func ensurePermissionsAll(t *testing.T, listPermOutput []string) { + t.Helper() + if len(listPermOutput) == 0 || !strings.HasPrefix(listPermOutput[1], "permission bits: 111100111100") { + t.Fail() + } +} + +func testEncryptDecryptUseCase2(t *testing.T, fileName string, aes bool, keyLength int) { + t.Helper() + msg := "testEncryptDecryptUseCase2" + + inFile := filepath.Join(inDir, fileName) + outFile := filepath.Join(outDir, "test.pdf") + t.Log(inFile) + + // Encrypt + t.Log("Encrypt") + conf := confForAlgorithm(aes, keyLength) + conf.UserPW = "upw" + conf.OwnerPW = "opw" + cmd := cli.EncryptCommand(inFile, outFile, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: encrypt to %s: %v\n", msg, outFile, err) + } + + // Encrypt already encrypted + t.Log("Encrypt already encrypted") + conf = confForAlgorithm(aes, keyLength) + conf.UserPW = "upw" + conf.OwnerPW = "opw" + cmd = cli.EncryptCommand(outFile, "", conf) + if _, err := cli.Process(cmd); err == nil { + t.Fatalf("%s encrypt encrypted %s\n", msg, outFile) + } + + // Validate using wrong owner pw + t.Log("Validate wrong ownerPW") + conf = confForAlgorithm(aes, keyLength) + conf.UserPW = "upw" + conf.OwnerPW = "opwWrong" + if err := validateFile(t, outFile, conf); err != nil { + t.Fatalf("%s: validate %s using wrong ownerPW: %v\n", msg, outFile, err) + } + + // Optimize using wrong owner pw + t.Log("Optimize wrong ownerPW") + conf = confForAlgorithm(aes, keyLength) + conf.UserPW = "upw" + conf.OwnerPW = "opwWrong" + if err := optimizeFile(t, outFile, conf); err != nil { + t.Fatalf("%s: optimize %s using wrong ownerPW: %v\n", msg, outFile, err) + } + + // Trim using wrong owner pw, falls back to upw and fails with insufficient permissions. + t.Log("Trim wrong ownerPW, fallback to upw and fail with insufficient permissions.") + conf = confForAlgorithm(aes, keyLength) + conf.UserPW = "upw" + conf.OwnerPW = "opwWrong" + selectedPages := []string(nil) // writes w/o trimming anything, but sufficient for testing. + cmd = cli.TrimCommand(outFile, "", selectedPages, conf) + if _, err := cli.Process(cmd); err == nil { + t.Fatalf("%s: trim %s using wrong ownerPW should fail: \n", msg, outFile) + } + + // Set permissions + t.Log("Add user access permissions") + conf = confForAlgorithm(aes, keyLength) + conf.UserPW = "upw" + conf.OwnerPW = "opw" + conf.Permissions = model.PermissionsAll + cmd = cli.SetPermissionsCommand(outFile, "", conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: %s add permissions: %v\n", msg, outFile, err) + } + + // List permissions + conf = model.NewDefaultConfiguration() + conf.OwnerPW = "opw" + cmd = cli.ListPermissionsCommand([]string{outFile}, conf) + list, err := cli.Process(cmd) + if err != nil { + t.Fatalf("%s: list permissions for %s: %v\n", msg, outFile, err) + } + ensurePermissionsAll(t, list) + + // Split using wrong owner pw, falls back to upw + t.Log("Split wrong ownerPW") + conf = confForAlgorithm(aes, keyLength) + conf.UserPW = "upw" + conf.OwnerPW = "opwWrong" + cmd = cli.SplitCommand(outFile, outDir, 1, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: trim %s using wrong ownerPW falls back to upw: \n", msg, outFile) + } + + // Validate + t.Log("Validate") + conf = confForAlgorithm(aes, keyLength) + conf.UserPW = "upw" + conf.OwnerPW = "opw" + if err := validateFile(t, outFile, conf); err != nil { + t.Fatalf("%s: validate %s: %v\n", msg, outFile, err) + } + + // ChangeUserPW using wrong userpw + t.Log("ChangeUserPW wrong userpw") + conf = confForAlgorithm(aes, keyLength) + conf.OwnerPW = "opw" + pwOld := "upwWrong" + pwNew := "upwNew" + cmd = cli.ChangeUserPWCommand(outFile, "", &pwOld, &pwNew, conf) + if _, err := cli.Process(cmd); err == nil { + t.Fatalf("%s: %s change userPW using wrong userPW should fail:\n", msg, outFile) + } + + // ChangeUserPW + t.Log("ChangeUserPW") + conf = confForAlgorithm(aes, keyLength) + conf.OwnerPW = "opw" + pwOld = "upw" + pwNew = "upwNew" + cmd = cli.ChangeUserPWCommand(outFile, "", &pwOld, &pwNew, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: %s change upw: %v\n", msg, outFile, err) + } + + // ChangeOwnerPW + t.Log("ChangeOwnerPW") + conf = confForAlgorithm(aes, keyLength) + conf.UserPW = "upwNew" + pwOld = "opw" + pwNew = "opwNew" + cmd = cli.ChangeOwnerPWCommand(outFile, "", &pwOld, &pwNew, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: %s change opw: %v\n", msg, outFile, err) + } + + // Decrypt using wrong pw + t.Log("\nDecrypt using wrong pw") + conf = confForAlgorithm(aes, keyLength) + conf.UserPW = "upwWrong" + conf.OwnerPW = "opwWrong" + cmd = cli.DecryptCommand(outFile, "", conf) + if _, err := cli.Process(cmd); err == nil { + t.Fatalf("%s: decrypt using wrong pw %s\n", msg, outFile) + } + + // Decrypt + t.Log("\nDecrypt") + conf = confForAlgorithm(aes, keyLength) + conf.UserPW = "upwNew" + conf.OwnerPW = "opwNew" + cmd = cli.DecryptCommand(outFile, "", conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: decrypt %s: %v\n", msg, outFile, err) + } +} + +func testEncryptDecryptUseCase3(t *testing.T, fileName string, aes bool, keyLength int) { + // Test for setting only the owner password. + t.Helper() + msg := "testEncryptDecryptUseCase3" + + inFile := filepath.Join(inDir, fileName) + outFile := filepath.Join(outDir, "test.pdf") + t.Log(inFile) + + // Encrypt opw only + t.Log("Encrypt opw only") + conf := confForAlgorithm(aes, keyLength) + conf.OwnerPW = "opw" + cmd := cli.EncryptCommand(inFile, outFile, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: encrypt with opw only to to %s: %v\n", msg, outFile, err) + } + + // Validate wrong opw succeeds with fallback to empty upw + t.Log("Validate wrong opw succeeds with fallback to empty upw") + conf = confForAlgorithm(aes, keyLength) + conf.OwnerPW = "opwWrong" + if err := validateFile(t, outFile, conf); err != nil { + t.Fatalf("%s: validate %s using wrong opw succeeds falling back to empty upw: %v\n", msg, outFile, err) + } + + // Validate opw + t.Log("Validate opw") + conf = confForAlgorithm(aes, keyLength) + conf.OwnerPW = "opw" + if err := validateFile(t, outFile, conf); err != nil { + t.Fatalf("%s: validate %s using opw: %v\n", msg, outFile, err) + } + + // Validate wrong upw + t.Log("Validate wrong upw fails") + conf = confForAlgorithm(aes, keyLength) + conf.UserPW = "upwWrong" + if err := validateFile(t, outFile, conf); err == nil { + t.Fatalf("%s: validate %s using wrong upw should fail!\n", msg, outFile) + } + + // Validate no pw using empty upw + t.Log("Validate no pw using empty upw") + conf = confForAlgorithm(aes, keyLength) + if err := validateFile(t, outFile, conf); err != nil { + t.Fatalf("%s validate %s no pw using empty upw: %v\n", msg, outFile, err) + } + + // Optimize wrong opw, succeeds with fallback to empty upw + t.Log("Optimize wrong opw succeeds with fallback to empty upw") + conf = confForAlgorithm(aes, keyLength) + conf.OwnerPW = "opwWrong" + if err := optimizeFile(t, outFile, conf); err != nil { + t.Fatalf("%s: optimize %s using wrong opw succeeds falling back to empty upw: %v\n", msg, outFile, err) + } + + // Optimize opw + t.Log("Optimize opw") + conf = confForAlgorithm(aes, keyLength) + conf.OwnerPW = "opw" + if err := optimizeFile(t, outFile, conf); err != nil { + t.Fatalf("%s: optimize %s using opw: %v\n", msg, outFile, err) + } + + // Optimize wrong upw + t.Log("Optimize wrong upw fails") + conf = confForAlgorithm(aes, keyLength) + conf.UserPW = "upwWrong" + if err := optimizeFile(t, outFile, conf); err == nil { + t.Fatalf("%s: optimize %s using wrong upw should fail!\n", msg, outFile) + } + + // Optimize empty upw + t.Log("Optimize empty upw") + conf = confForAlgorithm(aes, keyLength) + conf.UserPW = "" + if err := optimizeFile(t, outFile, conf); err != nil { + t.Fatalf("TestEncrypt%s: optimize %s using upw: %v\n", msg, outFile, err) + } + + // Change opw wrong upw + t.Log("ChangeOwnerPW wrong upw fails") + conf = confForAlgorithm(aes, keyLength) + conf.UserPW = "upw" + pwOld := "opw" + pwNew := "opwNew" + cmd = cli.ChangeOwnerPWCommand(outFile, "", &pwOld, &pwNew, conf) + if _, err := cli.Process(cmd); err == nil { + t.Fatalf("%s: %s change opw using wrong upw should fail\n", msg, outFile) + } + + // Change opw wrong opwOld + t.Log("ChangeOwnerPW wrong opwOld fails") + conf = confForAlgorithm(aes, keyLength) + conf.UserPW = "" + pwOld = "opwOldWrong" + pwNew = "opwNew" + cmd = cli.ChangeOwnerPWCommand(outFile, "", &pwOld, &pwNew, conf) + if _, err := cli.Process(cmd); err == nil { + t.Fatalf("%s: %s change opw using wrong upwOld should fail\n", msg, outFile) + } + + // Change opw + t.Log("ChangeOwnerPW") + conf = confForAlgorithm(aes, keyLength) + conf.UserPW = "" + pwOld = "opw" + pwNew = "opwNew" + cmd = cli.ChangeOwnerPWCommand(outFile, "", &pwOld, &pwNew, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: %s change opw: %v\n", msg, outFile, err) + } + + // Decrypt wrong upw + t.Log("Decrypt wrong upw fails") + conf = confForAlgorithm(aes, keyLength) + conf.UserPW = "upwWrong" + cmd = cli.DecryptCommand(outFile, "", conf) + if _, err := cli.Process(cmd); err == nil { + t.Fatalf("%s: %s decrypt using wrong upw should fail \n", msg, outFile) + } + + // Decrypt wrong opw succeeds because of fallback to empty upw. + t.Log("Decrypt wrong opw succeeds because of fallback to empty upw") + conf = confForAlgorithm(aes, keyLength) + conf.OwnerPW = "opw" + cmd = cli.DecryptCommand(outFile, outFile, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: %s decrypt using opw: %v\n", msg, outFile, err) + } +} + +func testPermissionsOPWOnly(t *testing.T, fileName string, aes bool, keyLength int) { + t.Helper() + msg := "TestPermissionsOPWOnly" + + inFile := filepath.Join(inDir, fileName) + outFile := filepath.Join(outDir, "test.pdf") + t.Log(inFile) + + cmd := cli.ListPermissionsCommand([]string{inFile}, nil) + list, err := cli.Process(cmd) + if err != nil { + t.Fatalf("%s: list permissions %s: %v\n", msg, inFile, err) + } + if len(list) == 0 || list[1] != "Full access" { + t.Fail() + } + + conf := confForAlgorithm(aes, keyLength) + conf.OwnerPW = "opw" + cmd = cli.EncryptCommand(inFile, outFile, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: encrypt %s: %v\n", msg, outFile, err) + } + + cmd = cli.ListPermissionsCommand([]string{outFile}, nil) + if list, err = cli.Process(cmd); err != nil { + t.Fatalf("%s: list permissions %s: %v\n", msg, outFile, err) + } + ensurePermissionsNone(t, list) + + conf = confForAlgorithm(aes, keyLength) + conf.OwnerPW = "opw" + conf.Permissions = model.PermissionsAll + cmd = cli.SetPermissionsCommand(outFile, "", conf) + if _, err = cli.Process(cmd); err != nil { + t.Fatalf("%s: set all permissions for %s: %v\n", msg, outFile, err) + } + + cmd = cli.ListPermissionsCommand([]string{outFile}, nil) + if list, err = cli.Process(cmd); err != nil { + t.Fatalf("%s: list permissions for %s: %v\n", msg, outFile, err) + } + ensurePermissionsAll(t, list) + + conf = confForAlgorithm(aes, keyLength) + conf.Permissions = model.PermissionsNone + cmd = cli.SetPermissionsCommand(outFile, "", conf) + if _, err = cli.Process(cmd); err == nil { + t.Fatalf("%s: clear all permissions w/o opw for %s\n", msg, outFile) + } + + conf = confForAlgorithm(aes, keyLength) + conf.OwnerPW = "opw" + conf.Permissions = model.PermissionsNone + cmd = cli.SetPermissionsCommand(outFile, "", conf) + if _, err = cli.Process(cmd); err != nil { + t.Fatalf("%s: clear all permissions for %s: %v\n", msg, outFile, err) + } +} + +func testPermissions(t *testing.T, fileName string, aes bool, keyLength int) { + t.Helper() + msg := "TestPermissions" + + inFile := filepath.Join(inDir, fileName) + outFile := filepath.Join(outDir, "test.pdf") + t.Log(inFile) + + cmd := cli.ListPermissionsCommand([]string{inFile}, nil) + list, err := cli.Process(cmd) + if err != nil { + t.Fatalf("%s: list permissions %s: %v\n", msg, inFile, err) + } + if len(list) == 0 || list[1] != "Full access" { + t.Fail() + } + + conf := confForAlgorithm(aes, keyLength) + conf.UserPW = "upw" + conf.OwnerPW = "opw" + cmd = cli.EncryptCommand(inFile, outFile, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: encrypt %s: %v\n", msg, outFile, err) + } + + cmd = cli.ListPermissionsCommand([]string{outFile}, nil) + if _, err = cli.Process(cmd); err == nil { + t.Fatalf("%s: list permissions w/o pw %s\n", msg, outFile) + } + + conf = confForAlgorithm(aes, keyLength) + conf.UserPW = "upw" + cmd = cli.ListPermissionsCommand([]string{outFile}, conf) + if list, err = cli.Process(cmd); err != nil { + t.Fatalf("%s: list permissions %s: %v\n", msg, outFile, err) + } + ensurePermissionsNone(t, list) + + conf = model.NewDefaultConfiguration() + conf.OwnerPW = "opw" + cmd = cli.ListPermissionsCommand([]string{outFile}, conf) + if list, err = cli.Process(cmd); err != nil { + t.Fatalf("%s: list permissions %s: %v\n", msg, outFile, err) + } + ensurePermissionsNone(t, list) + + conf = confForAlgorithm(aes, keyLength) + conf.Permissions = model.PermissionsAll + cmd = cli.SetPermissionsCommand(outFile, "", conf) + if _, err = cli.Process(cmd); err == nil { + t.Fatalf("%s: set all permissions w/o pw for %s\n", msg, outFile) + } + + conf = confForAlgorithm(aes, keyLength) + conf.UserPW = "upw" + conf.Permissions = model.PermissionsAll + cmd = cli.SetPermissionsCommand(outFile, "", conf) + if _, err = cli.Process(cmd); err == nil { + t.Fatalf("%s: set all permissions w/o opw for %s\n", msg, outFile) + } + + conf = confForAlgorithm(aes, keyLength) + conf.OwnerPW = "opw" + conf.Permissions = model.PermissionsAll + cmd = cli.SetPermissionsCommand(outFile, "", conf) + if _, err = cli.Process(cmd); err == nil { + t.Fatalf("%s: set all permissions w/o both pws for %s\n", msg, outFile) + } + + conf = confForAlgorithm(aes, keyLength) + conf.OwnerPW = "opw" + conf.UserPW = "upw" + conf.Permissions = model.PermissionsAll + cmd = cli.SetPermissionsCommand(outFile, "", conf) + if _, err = cli.Process(cmd); err != nil { + t.Fatalf("%s: set all permissions for %s: %v\n", msg, outFile, err) + } + + cmd = cli.ListPermissionsCommand([]string{outFile}, nil) + if _, err = cli.Process(cmd); err == nil { + t.Fatalf("%s: list permissions w/o pw %s\n", msg, outFile) + } + + conf = confForAlgorithm(aes, keyLength) + conf.OwnerPW = "opw" + cmd = cli.ListPermissionsCommand([]string{outFile}, conf) + if list, err = cli.Process(cmd); err != nil { + t.Fatalf("%s: list permissions for %s: %v\n", msg, outFile, err) + } + ensurePermissionsAll(t, list) +} + +func testEncryptDecryptFile(t *testing.T, fileName string, mode string, keyLength int) { + t.Helper() + testEncryptDecryptUseCase1(t, fileName, mode == "aes", keyLength) + testEncryptDecryptUseCase2(t, fileName, mode == "aes", keyLength) + testEncryptDecryptUseCase3(t, fileName, mode == "aes", keyLength) + testPermissionsOPWOnly(t, fileName, mode == "aes", keyLength) + testPermissions(t, fileName, mode == "aes", keyLength) +} + +func TestEncryptDecrypt(t *testing.T) { + for _, fileName := range []string{ + "5116.DCT_Filter.pdf", + "adobe_errata.pdf", + } { + testEncryptDecryptFile(t, fileName, "rc4", 40) + testEncryptDecryptFile(t, fileName, "rc4", 128) + testEncryptDecryptFile(t, fileName, "aes", 40) + testEncryptDecryptFile(t, fileName, "aes", 128) + testEncryptDecryptFile(t, fileName, "aes", 256) + } +} diff --git a/pkg/cli/test/extract_test.go b/pkg/cli/test/extract_test.go new file mode 100644 index 0000000000000000000000000000000000000000..483ca1081b0d1b31945867ce91671e9081b7cd51 --- /dev/null +++ b/pkg/cli/test/extract_test.go @@ -0,0 +1,98 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/cli" +) + +func TestExtractImagesCommand(t *testing.T) { + msg := "TestExtractImagesCommand" + + // Extract all images for each PDF file into outDir. + cmd := cli.ExtractImagesCommand("", outDir, nil, conf) + //for _, f := range allPDFs(t, inDir) { + for _, f := range []string{"5116.DCT_Filter.pdf", "testImage.pdf", "go.pdf"} { + inFile := filepath.Join(inDir, f) + cmd.InFile = &inFile + // Extract all images. + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, inFile, err) + } + } + + // Extract all images for inFile starting with page 1 into outDir. + inFile := filepath.Join(inDir, "testImage.pdf") + cmd = cli.ExtractImagesCommand(inFile, outDir, []string{"1-"}, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, inFile, err) + } +} + +func TestExtractFontsCommand(t *testing.T) { + msg := "TestExtractFontsCommand" + + // Extract fonts for all pages for the following 3 PDF files into outDir. + cmd := cli.ExtractFontsCommand("", outDir, nil, conf) + for _, fn := range []string{"5116.DCT_Filter.pdf", "testImage.pdf", "go.pdf"} { + fn = filepath.Join(inDir, fn) + cmd.InFile = &fn + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, fn, err) + } + } + + // Extract fonts for pages 1-3 into outDir. + inFile := filepath.Join(inDir, "go.pdf") + cmd = cli.ExtractFontsCommand(inFile, outDir, []string{"1-3"}, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, inFile, err) + } +} + +func TestExtractPagesCommand(t *testing.T) { + msg := "TestExtractPagesCommand" + // Extract page #1 into outDir. + inFile := filepath.Join(inDir, "TheGoProgrammingLanguageCh1.pdf") + cmd := cli.ExtractPagesCommand(inFile, outDir, []string{"1"}, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, inFile, err) + } +} + +func TestExtractContentCommand(t *testing.T) { + msg := "TestExtractContentCommand" + // Extract content of all pages into outDir. + inFile := filepath.Join(inDir, "5116.DCT_Filter.pdf") + cmd := cli.ExtractContentCommand(inFile, outDir, nil, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, inFile, err) + } +} + +func TestExtractMetadataCommand(t *testing.T) { + msg := "TestExtractMetadataCommand" + // Extract metadata into outDir. + inFile := filepath.Join(inDir, "TheGoProgrammingLanguageCh1.pdf") + cmd := cli.ExtractMetadataCommand(inFile, outDir, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, inFile, err) + } +} diff --git a/pkg/cli/test/font_test.go b/pkg/cli/test/font_test.go new file mode 100644 index 0000000000000000000000000000000000000000..e5a2f8a772f5f339a145ca11bea370e23f39f8ca --- /dev/null +++ b/pkg/cli/test/font_test.go @@ -0,0 +1,59 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/cli" +) + +func TestInstallFontsCommand(t *testing.T) { + msg := "TestInstallFontsCommand" + userFontName := filepath.Join(fontDir, "Roboto-Regular.ttf") + cmd := cli.InstallFontsCommand([]string{userFontName}, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s install fonts: %v\n", msg, err) + } +} + +func TestInstallTTCFontsCommand(t *testing.T) { + msg := "TestInstallTTCFontsCommand" + userFontName := filepath.Join(fontDir, "Songti.ttc") + cmd := cli.InstallFontsCommand([]string{userFontName}, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s install fonts: %v\n", msg, err) + } +} + +func TestListFontsCommand(t *testing.T) { + msg := "TestListFontsCommand" + cmd := cli.ListFontsCommand(conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s list fonts: %v\n", msg, err) + } +} + +func TestCreateCheatSheetsFontsCommand(t *testing.T) { + msg := "TestCreateCheatSheetsFontsCommand" + userFontName := filepath.Join(fontDir, "Songti.ttc") + cmd := cli.CreateCheatSheetsFontsCommand([]string{userFontName}, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s create cheat sheets fonts: %v\n", msg, err) + } +} diff --git a/pkg/cli/test/form_test.go b/pkg/cli/test/form_test.go new file mode 100644 index 0000000000000000000000000000000000000000..5fafd5f7360382bdfc210109406ab220232645fe --- /dev/null +++ b/pkg/cli/test/form_test.go @@ -0,0 +1,268 @@ +/* +Copyright 2023 The pdf Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/cli" +) + +/************************************************************** + * All form related processing is optimized for Adobe Reader! * + **************************************************************/ + +func TestListFormFields(t *testing.T) { + + msg := "TestListFormFields" + inFile := filepath.Join(samplesDir, "form", "demo", "english.pdf") + + cmd := cli.ListFormFieldsCommand([]string{inFile}, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, inFile, err) + } +} + +func TestRemoveFormFields(t *testing.T) { + + msg := "TestRemoveFormFields" + inFile := filepath.Join(samplesDir, "form", "demo", "english.pdf") + outFile := filepath.Join(outDir, "removedField.pdf") + + cmd := cli.RemoveFormFieldsCommand(inFile, outFile, []string{"dob1"}, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, inFile, err) + } +} + +func TestResetFormFields(t *testing.T) { + + for _, tt := range []struct { + msg string + inFile string + outFile string + }{ + {"TestResetFormCorefont", "english.pdf", "english-reset.pdf"}, // Core font (Helvetica) + {"TestResetFormUserfont", "ukrainian.pdf", "ukrainian-reset.pdf"}, // User font (Roboto-Regular) + {"TestFormRTL", "arabic.pdf", "arabic-reset.pdf"}, // User font RTL (Roboto-Regular) + {"TestResetFormCJK", "chineseSimple.pdf", "chineseSimple-reset.pdf"}, // User font CJK (UnifontMedium) + {"TestResetPersonForm", "person.pdf", "person-reset.pdf"}, // Person Form + } { + inFile := filepath.Join(samplesDir, "form", "demoSinglePage", tt.inFile) + outFile := filepath.Join(outDir, tt.outFile) + + cmd := cli.ResetFormCommand(inFile, outFile, nil, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", tt.msg, inFile, err) + } + } + +} + +func TestLockFormFields(t *testing.T) { + + for _, tt := range []struct { + msg string + inFile string + outFile string + }{ + {"TestLockFormEN", "english.pdf", "english-locked.pdf"}, // Core font (Helvetica) + {"TestLockFormUK", "ukrainian.pdf", "ukrainian-locked.pdf"}, // User font (Roboto-Regular) + {"TestLockFormRTL", "arabic.pdf", "arabic-locked.pdf"}, // User font RTL (Roboto-Regular) + {"TestLockFormCJK", "chineseSimple.pdf", "chineseSimple-locked.pdf"}, // User font CJK (UnifontMedium) + {"TestLockPersonForm", "person.pdf", "person-locked.pdf"}, // Person Form + } { + inFile := filepath.Join(samplesDir, "form", "demoSinglePage", tt.inFile) + outFile := filepath.Join(outDir, tt.outFile) + + cmd := cli.LockFormCommand(inFile, outFile, nil, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", tt.msg, inFile, err) + } + } +} + +func TestUnlockFormFields(t *testing.T) { + + for _, tt := range []struct { + msg string + inFile string + outFile string + }{ + {"TestUnlockFormEN", "english-locked.pdf", "english-unlocked.pdf"}, // Core font (Helvetica) + {"TestUnlockFormUK", "ukrainian-locked.pdf", "ukrainian-unlocked.pdf"}, // User font (Roboto-Regular) + {"TestUnlockFormRTL", "arabic-locked.pdf", "arabic-unlocked.pdf"}, // User font RTL (Roboto-Regular) + {"TestUnlockFormCJK", "chineseSimple-locked.pdf", "chineseSimple-unlocked.pdf"}, // User font CJK (UnifontMedium) + {"TestUnlockPersonForm", "person-locked.pdf", "person-unlocked.pdf"}, // Person Form + } { + inFile := filepath.Join(samplesDir, "form", "lock", tt.inFile) + outFile := filepath.Join(outDir, tt.outFile) + + cmd := cli.UnlockFormCommand(inFile, outFile, nil, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", tt.msg, inFile, err) + } + } +} + +func TestExportForm(t *testing.T) { + + inDir := filepath.Join(samplesDir, "form", "demoSinglePage") + + for _, tt := range []struct { + msg string + inFile string + outFile string + }{ + {"TestExportFormEN", "english.pdf", "english.json"}, // Core font (Helvetica) + {"TestExportFormUK", "ukrainian.pdf", "ukrainian.json"}, // User font (Roboto-Regular) + {"TestExportFormRTL", "arabic.pdf", "arabic.json"}, // User font RTL (Roboto-Regular) + {"TestExportFormCJK", "chineseSimple.pdf", "chineseSimple.json"}, // User font CJK (UnifontMedium) + {"TestExportPersonForm", "person.pdf", "person.json"}, // Person Form + } { + inFile := filepath.Join(inDir, tt.inFile) + outFile := filepath.Join(outDir, tt.outFile) + + cmd := cli.ExportFormCommand(inFile, outFile, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", tt.msg, inFile, err) + } + } +} + +func TestFillForm(t *testing.T) { + + inDir := filepath.Join(samplesDir, "form", "demoSinglePage") + jsonDir := filepath.Join(samplesDir, "form", "fill") + + for _, tt := range []struct { + msg string + inFile string + inFileJSON string + outFile string + }{ + {"TestFillFormEN", "english.pdf", "english.json", "english.pdf"}, // Core font (Helvetica) + {"TestFillFormUK", "ukrainian.pdf", "ukrainian.json", "ukrainian.pdf"}, // User font (Roboto-Regular) + {"TestFillFormRTL", "arabic.pdf", "arabic.json", "arabic.pdf"}, // User font RTL (Roboto-Regular) + {"TestFillFormCJK", "chineseSimple.pdf", "chineseSimple.json", "chineseSimple.pdf"}, // User font CJK (UnifontMedium) + {"TestFillPersonForm", "person.pdf", "person.json", "person.pdf"}, // Person Form + } { + inFile := filepath.Join(inDir, tt.inFile) + inFileJSON := filepath.Join(jsonDir, tt.inFileJSON) + outFile := filepath.Join(outDir, tt.outFile) + + cmd := cli.FillFormCommand(inFile, inFileJSON, outFile, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", tt.msg, inFile, err) + } + } +} + +func TestMultiFillFormJSON(t *testing.T) { + + inDir := filepath.Join(samplesDir, "form", "demoSinglePage") + jsonDir := filepath.Join(samplesDir, "form", "multifill", "json") + + for _, tt := range []struct { + msg string + inFile string + inFileJSON string + }{ + {"TestMultiFillFormJSONEnglish", "english.pdf", "english.json"}, + {"TestMultiFillFormJSONPerson", "person.pdf", "person.json"}, + } { + inFile := filepath.Join(inDir, tt.inFile) + inFileJSON := filepath.Join(jsonDir, tt.inFileJSON) + + cmd := cli.MultiFillFormCommand(inFile, inFileJSON, outDir, tt.inFile, false, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", tt.msg, inFile, err) + } + } +} + +func TestMultiFillFormJSONMerged(t *testing.T) { + + inDir := filepath.Join(samplesDir, "form", "demoSinglePage") + jsonDir := filepath.Join(samplesDir, "form", "multifill", "json") + + for _, tt := range []struct { + msg string + inFile string + inFileJSON string + }{ + {"TestMultiFillFormJSONEnglish", "english.pdf", "english.json"}, + {"TestMultiFillFormJSONPerson", "person.pdf", "person.json"}, + } { + inFile := filepath.Join(inDir, tt.inFile) + inFileJSON := filepath.Join(jsonDir, tt.inFileJSON) + + cmd := cli.MultiFillFormCommand(inFile, inFileJSON, outDir, tt.inFile, true, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", tt.msg, inFile, err) + } + } +} + +func TestMultiFillFormCSV(t *testing.T) { + + inDir := filepath.Join(samplesDir, "form", "demoSinglePage") + csvDir := filepath.Join(samplesDir, "form", "multifill", "csv") + + for _, tt := range []struct { + msg string + inFile string + inFileCSV string + }{ + {"TestMultiFillFormCSVEnglish", "english.pdf", "english.csv"}, + {"TestMultiFillFormCSVPerson", "person.pdf", "person.csv"}, + } { + + inFile := filepath.Join(inDir, tt.inFile) + inFileCSV := filepath.Join(csvDir, tt.inFileCSV) + + cmd := cli.MultiFillFormCommand(inFile, inFileCSV, outDir, tt.inFile, false, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", tt.msg, inFile, err) + } + } +} + +func TestMultiFillFormCSVMerged(t *testing.T) { + + inDir := filepath.Join(samplesDir, "form", "demoSinglePage") + csvDir := filepath.Join(samplesDir, "form", "multifill", "csv") + + for _, tt := range []struct { + msg string + inFile string + inFileCSV string + }{ + {"TestMultiFillFormCSVEnglish", "english.pdf", "english.csv"}, + {"TestMultiFillFormCSVPerson", "person.pdf", "person.csv"}, + } { + + inFile := filepath.Join(inDir, tt.inFile) + inFileCSV := filepath.Join(csvDir, tt.inFileCSV) + + cmd := cli.MultiFillFormCommand(inFile, inFileCSV, outDir, tt.inFile, false, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", tt.msg, inFile, err) + } + } +} diff --git a/pkg/cli/test/grid_test.go b/pkg/cli/test/grid_test.go new file mode 100644 index 0000000000000000000000000000000000000000..1d119da6a044413c07de8966659550a7b9e907e3 --- /dev/null +++ b/pkg/cli/test/grid_test.go @@ -0,0 +1,85 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/cli" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" +) + +func testGrid(t *testing.T, msg string, inFiles []string, outFile string, selectedPages []string, desc string, rows, cols int, isImg bool, conf *model.Configuration) { + t.Helper() + + var ( + nup *model.NUp + err error + ) + + if isImg { + if nup, err = api.ImageGridConfig(rows, cols, desc, conf); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + } else { + if nup, err = api.PDFGridConfig(rows, cols, desc, conf); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + } + + cmd := cli.NUpCommand(inFiles, outFile, selectedPages, nup, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + if err := validateFile(t, outFile, conf); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestGridCommand(t *testing.T) { + for _, tt := range []struct { + msg string + inFiles []string + outFile string + selectedPages []string + desc string + unit string + rows, cols int + isImg bool + }{ + {"TestGridFromPDF", + []string{filepath.Join(inDir, "Acroforms2.pdf")}, + filepath.Join(outDir, "testGridFromPDF.pdf"), + nil, "form:LegalL", "points", 1, 3, false}, + + {"TestGridFromImages", + []string{ + filepath.Join(resDir, "pdfchip3.png"), + filepath.Join(resDir, "demo.png"), + filepath.Join(resDir, "snow.jpg"), + }, + filepath.Join(outDir, "testGridFromImages.pdf"), + nil, "d:500 500, margin:20, border:off", "points", 1, 3, true}, + } { + conf := model.NewDefaultConfiguration() + conf.SetUnit(tt.unit) + testGrid(t, tt.msg, tt.inFiles, tt.outFile, tt.selectedPages, tt.desc, tt.rows, tt.cols, tt.isImg, conf) + } +} diff --git a/pkg/cli/test/importImages_test.go b/pkg/cli/test/importImages_test.go new file mode 100644 index 0000000000000000000000000000000000000000..22f8a40b34a06ebfe9b331ed7b59f97a2b640344 --- /dev/null +++ b/pkg/cli/test/importImages_test.go @@ -0,0 +1,91 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/cli" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +func testImportImages(t *testing.T, msg string, imgFiles []string, outFile, impConf string) { + t.Helper() + var err error + + outFile = filepath.Join(outDir, outFile) + + // The default import conf uses the special pos:full argument + // which overrides all other import conf parms. + imp := pdfcpu.DefaultImportConfig() + if impConf != "" { + if imp, err = api.Import(impConf, types.POINTS); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + } + cmd := cli.ImportImagesCommand(imgFiles, outFile, imp, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + if err := validateFile(t, outFile, conf); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestImportCommand(t *testing.T) { + for _, tt := range []struct { + msg string + imgFiles []string + outFile string + impConf string + }{ + // Render image on an A4 portrait mode page. + {"TestCenteredGraySepia", + []string{filepath.Join(resDir, "mountain.jpg")}, + "CenteredGraySepia.pdf", + "f:A4, pos:c, bgcol:#beded9"}, + + // Import another image as a new page of testfile1 and convert image to gray. + {"TestCenteredGraySepia", + []string{filepath.Join(resDir, "mountain.png")}, + "CenteredGraySepia.pdf", + "f:A4, pos:c, sc:.75, bgcol:#beded9, gray:true"}, + + // Import another image as a new page of testfile1 and apply a sepia filter. + {"TestCenteredGraySepia", + []string{filepath.Join(resDir, "mountain.webp")}, + "CenteredGraySepia.pdf", + "f:A4, pos:c, sc:.9, bgcol:#beded9, sepia:true"}, + + // Import another image as a new page of testfile1. + {"TestCenteredGraySepia", + []string{filepath.Join(resDir, "mountain.tif")}, + "CenteredGraySepia.pdf", + "f:A4, pos:c, sc:1, bgcol:#beded9"}, + + // Page dimensions match image dimensions. + {"TestFull", + imageFileNames(t, filepath.Join(resDir)), + "Full.pdf", + "pos:full"}, + } { + testImportImages(t, tt.msg, tt.imgFiles, tt.outFile, tt.impConf) + } +} diff --git a/pkg/cli/test/keyword_test.go b/pkg/cli/test/keyword_test.go new file mode 100644 index 0000000000000000000000000000000000000000..ad8f4e8f84387996e91b9a7de50954c72ec5cb35 --- /dev/null +++ b/pkg/cli/test/keyword_test.go @@ -0,0 +1,79 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/cli" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +func listKeywords(t *testing.T, msg, fileName string, want []string) []string { + t.Helper() + cmd := cli.ListKeywordsCommand(fileName, conf) + got, err := cli.Process(cmd) + if err != nil { + t.Fatalf("%s list keywords: %v\n", msg, err) + } + if len(got) != len(want) { + t.Fatalf("%s: list keywords %s: want %d got %d\n", msg, fileName, len(want), len(got)) + } + + for _, v := range got { + if !types.MemberOf(v, want) { + t.Fatalf("%s: list keywords %s: want %v got %v\n", msg, fileName, want, got) + } + } + return got +} + +func TestKeywordsCommand(t *testing.T) { + msg := "TestKeywordsCommand" + + fileName := filepath.Join(outDir, "go.pdf") + if err := copyFile(t, filepath.Join(inDir, "go.pdf"), fileName); err != nil { + t.Fatalf("%s: copyFile: %v\n", msg, err) + } + + // # of keywords must be 0 + listKeywords(t, msg, fileName, nil) + + keywords := []string{"keyword1", "keyword2"} + cmd := cli.AddKeywordsCommand(fileName, "", keywords, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s add keywords: %v\n", msg, err) + } + + listKeywords(t, msg, fileName, keywords) + + cmd = cli.RemoveKeywordsCommand(fileName, "", []string{"keyword2"}, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s remove 1 keyword: %v\n", msg, err) + } + + listKeywords(t, msg, fileName, []string{"keyword1"}) + + cmd = cli.RemoveKeywordsCommand(fileName, "", nil, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s remove all keywords: %v\n", msg, err) + } + + // # of keywords must be 0 + listKeywords(t, msg, fileName, nil) +} diff --git a/pkg/cli/test/merge_test.go b/pkg/cli/test/merge_test.go new file mode 100644 index 0000000000000000000000000000000000000000..eb098a40e204d8712dc94bc87eaaa85a213b4f86 --- /dev/null +++ b/pkg/cli/test/merge_test.go @@ -0,0 +1,95 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/cli" +) + +// Merge all PDFs in testdir into out/test.pdf. +func TestMergeCreateCommand(t *testing.T) { + msg := "TestMergeCreateCommand" + + var inFiles []string + for _, f := range allPDFs(t, inDir) { + inFiles = append(inFiles, filepath.Join(inDir, f)) + } + + outFile := filepath.Join(outDir, "test.pdf") + + cmd := cli.MergeCreateCommand(inFiles, outFile, true, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + if err := validateFile(t, outFile, conf); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestMergeCreateZippedCommand(t *testing.T) { + msg := "TestMergeCreateZippedCommand" + + // The actual usecase for this is the recombination of 2 PDF files representing even and odd pages of some PDF source. + // See #716 + inFiles := []string{ + filepath.Join(inDir, "Acroforms2.pdf"), + filepath.Join(inDir, "adobe_errata.pdf"), + } + outFile := filepath.Join(outDir, "out.pdf") + + cmd := cli.MergeCreateZipCommand(inFiles, outFile, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + if err := validateFile(t, outFile, conf); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestMergeAppendCommand(t *testing.T) { + msg := "TestMergeAppendCommand" + + var inFiles []string + for _, f := range allPDFs(t, inDir) { + if f == "test.pdf" { + continue + } + inFiles = append(inFiles, filepath.Join(inDir, f)) + } + + outFile := filepath.Join(outDir, "test.pdf") + + if err := copyFile(t, filepath.Join(inDir, "test.pdf"), outFile); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + // Merge inFiles by concatenation in the order specified and write the result to outFile. + // If outFile already exists its content will be preserved and serves as the beginning of the merge result. + cmd := cli.MergeAppendCommand(inFiles, outFile, false, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + if err := validateFile(t, outFile, conf); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} diff --git a/pkg/cli/test/nup_test.go b/pkg/cli/test/nup_test.go new file mode 100644 index 0000000000000000000000000000000000000000..b11bf35e80f40b852850413f91e23febd11d6ccc --- /dev/null +++ b/pkg/cli/test/nup_test.go @@ -0,0 +1,102 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/cli" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" +) + +func testNUp(t *testing.T, msg string, inFiles []string, outFile string, selectedPages []string, desc string, n int, isImg bool, conf *model.Configuration) { + t.Helper() + + var ( + nup *model.NUp + err error + ) + + if isImg { + if nup, err = api.ImageNUpConfig(n, desc, conf); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + } else { + if nup, err = api.PDFNUpConfig(n, desc, conf); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + } + + cmd := cli.NUpCommand(inFiles, outFile, selectedPages, nup, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + if err := validateFile(t, outFile, conf); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestNUpCommand(t *testing.T) { + for _, tt := range []struct { + msg string + inFiles []string + outFile string + selectedPages []string + desc string + unit string + n int + isImg bool + }{ + {"TestNUpFromPDF", + []string{filepath.Join(inDir, "Acroforms2.pdf")}, + filepath.Join(outDir, "Acroforms2.pdf"), + nil, + "", + "points", + 4, + false}, + + {"TestNUpFromSingleImage", + []string{filepath.Join(resDir, "pdfchip3.png")}, + filepath.Join(outDir, "out.pdf"), + nil, + "form:A3L", + "points", + 9, + true}, + + {"TestNUpFromImages", + []string{ + filepath.Join(resDir, "pdfchip3.png"), + filepath.Join(resDir, "demo.png"), + filepath.Join(resDir, "snow.jpg"), + }, + filepath.Join(outDir, "out1.pdf"), + nil, + "form:Tabloid, bo:off, ma:0, enforce:off", + "points", + 6, + true}, + } { + conf := model.NewDefaultConfiguration() + conf.SetUnit(tt.unit) + testNUp(t, tt.msg, tt.inFiles, tt.outFile, tt.selectedPages, tt.desc, tt.n, tt.isImg, conf) + } +} diff --git a/pkg/cli/test/optimize_test.go b/pkg/cli/test/optimize_test.go new file mode 100644 index 0000000000000000000000000000000000000000..28b3a45de29c2ef413edbd201091d5cca598f89d --- /dev/null +++ b/pkg/cli/test/optimize_test.go @@ -0,0 +1,62 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/cli" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" +) + +func optimizeFile(t *testing.T, fileName string, conf *model.Configuration) error { + t.Helper() + cmd := cli.OptimizeCommand(fileName, "", conf) + _, err := cli.Process(cmd) + return err +} + +func testOptimizeFile(t *testing.T, inFile, outFile string) { + t.Helper() + msg := "testOptimizeFile" + + // Optimize inFile and write result to outFile. + cmd := cli.OptimizeCommand(inFile, outFile, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, inFile, err) + } + + // Optimize outFile and write result to outFile. + cmd = cli.OptimizeCommand(outFile, "", conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + // Optimize outFile and write result to outFile. + if err := optimizeFile(t, outFile, nil); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } +} + +func TestOptimizeCommand(t *testing.T) { + for _, f := range allPDFs(t, inDir) { + inFile := filepath.Join(inDir, f) + outFile := filepath.Join(outDir, f) + testOptimizeFile(t, inFile, outFile) + } +} diff --git a/pkg/cli/test/pageLayout_test.go b/pkg/cli/test/pageLayout_test.go new file mode 100644 index 0000000000000000000000000000000000000000..485eb03b883e7eca866a17d9828f6f9a9ee6e4f8 --- /dev/null +++ b/pkg/cli/test/pageLayout_test.go @@ -0,0 +1,74 @@ +/* +Copyright 2023 The pdfcpu Authors. + +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 + + 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 test + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/cli" +) + +func TestPageLayout(t *testing.T) { + msg := "testPageLayout" + + pageLayout := "TwoColumnLeft" + + inFile := filepath.Join(inDir, "test.pdf") + outFile := filepath.Join(outDir, "test.pdf") + + cmd := cli.ListPageLayoutCommand(inFile, conf) + ss, err := cli.Process(cmd) + if err != nil { + t.Fatalf("%s %s: list pageLayout: %v\n", msg, inFile, err) + } + if len(ss) > 0 && !strings.HasPrefix(ss[0], "No page layout") { + t.Fatalf("%s %s: list pageLayout, unexpected: %s\n", msg, inFile, ss[0]) + } + + cmd = cli.SetPageLayoutCommand(inFile, outFile, pageLayout, nil) + if _, err = cli.Process(cmd); err != nil { + t.Fatalf("%s %s: set pageLayout: %v\n", msg, outFile, err) + } + + cmd = cli.ListPageLayoutCommand(outFile, conf) + ss, err = cli.Process(cmd) + if err != nil { + t.Fatalf("%s %s: list pageLayout: %v\n", msg, outFile, err) + } + if len(ss) == 0 { + t.Fatalf("%s %s: list pageLayout, missing page layout\n", msg, outFile) + } + if ss[0] != pageLayout { + t.Fatalf("%s %s: list pageLayout, want:%s, got:%s\n", msg, outFile, pageLayout, ss[0]) + } + + cmd = cli.ResetPageLayoutCommand(outFile, "", nil) + if _, err = cli.Process(cmd); err != nil { + t.Fatalf("%s %s: reset pageLayout: %v\n", msg, outFile, err) + } + + cmd = cli.ListPageLayoutCommand(outFile, conf) + ss, err = cli.Process(cmd) + if err != nil { + t.Fatalf("%s %s: list pageLayout: %v\n", msg, outFile, err) + } + if len(ss) > 0 && !strings.HasPrefix(ss[0], "No page layout") { + t.Fatalf("%s %s: list pageLayout, unexpected: %s\n", msg, outFile, ss[0]) + } +} diff --git a/pkg/cli/test/pageMode_test.go b/pkg/cli/test/pageMode_test.go new file mode 100644 index 0000000000000000000000000000000000000000..fe67d2d3f94ccde31a6c68ce62b78b7922b5e40c --- /dev/null +++ b/pkg/cli/test/pageMode_test.go @@ -0,0 +1,74 @@ +/* +Copyright 2023 The pdfcpu Authors. + +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 + + 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 test + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/cli" +) + +func TestPageMode(t *testing.T) { + msg := "testPageMode" + + pageMode := "UseOutlines" + + inFile := filepath.Join(inDir, "test.pdf") + outFile := filepath.Join(outDir, "test.pdf") + + cmd := cli.ListPageModeCommand(inFile, conf) + ss, err := cli.Process(cmd) + if err != nil { + t.Fatalf("%s %s: list pageMode: %v\n", msg, inFile, err) + } + if len(ss) > 0 && !strings.HasPrefix(ss[0], "No page mode") { + t.Fatalf("%s %s: list pageMode, unexpected: %s\n", msg, inFile, ss[0]) + } + + cmd = cli.SetPageModeCommand(inFile, outFile, pageMode, nil) + if _, err = cli.Process(cmd); err != nil { + t.Fatalf("%s %s: set pageMode: %v\n", msg, outFile, err) + } + + cmd = cli.ListPageModeCommand(outFile, conf) + ss, err = cli.Process(cmd) + if err != nil { + t.Fatalf("%s %s: list pageMode: %v\n", msg, outFile, err) + } + if len(ss) == 0 { + t.Fatalf("%s %s: list pageMode, missing pageMode\n", msg, outFile) + } + if ss[0] != pageMode { + t.Fatalf("%s %s: list pageMode, want:%s, got:%s\n", msg, outFile, pageMode, ss[0]) + } + + cmd = cli.ResetPageModeCommand(outFile, "", nil) + if _, err = cli.Process(cmd); err != nil { + t.Fatalf("%s %s: reset pageMode: %v\n", msg, outFile, err) + } + + cmd = cli.ListPageModeCommand(outFile, conf) + ss, err = cli.Process(cmd) + if err != nil { + t.Fatalf("%s %s: list pageMode: %v\n", msg, outFile, err) + } + if len(ss) > 0 && !strings.HasPrefix(ss[0], "No page mode") { + t.Fatalf("%s %s: list pageMode, unexpected: %s\n", msg, outFile, ss[0]) + } +} diff --git a/pkg/cli/test/page_test.go b/pkg/cli/test/page_test.go new file mode 100644 index 0000000000000000000000000000000000000000..6520abaa3396fe6c033ef25e3e0059cf93fe152d --- /dev/null +++ b/pkg/cli/test/page_test.go @@ -0,0 +1,70 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/cli" +) + +func TestPagesCommand(t *testing.T) { + msg := "TestPagesCommand" + inFile := filepath.Join(inDir, "Acroforms2.pdf") + outFile := filepath.Join(outDir, "test.pdf") + + n1, err := api.PageCountFile(inFile) + if err != nil { + t.Fatalf("%s %s: %v\n", msg, inFile, err) + } + + // Insert an empty page before pages 1 and 2. + cmd := cli.InsertPagesCommand(inFile, outFile, []string{"-2"}, conf, "before", nil) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + if err := validateFile(t, outFile, conf); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + n2, err := api.PageCountFile(outFile) + if err != nil { + t.Fatalf("%s %s: %v\n", msg, inFile, err) + } + if n2 != n1+2 { + t.Fatalf("%s %s: pageCount want:%d got:%d\n", msg, inFile, n1+2, n2) + } + + // Remove pages 1 and 2. + cmd = cli.RemovePagesCommand(outFile, "", []string{"-2"}, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + if err := validateFile(t, outFile, conf); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + n2, err = api.PageCountFile(outFile) + if err != nil { + t.Fatalf("%s %s: %v\n", msg, inFile, err) + } + if n1 != n2 { + t.Fatalf("%s %s: pageCount want:%d got:%d\n", msg, inFile, n1, n2) + } +} diff --git a/pkg/cli/test/portfolio_test.go b/pkg/cli/test/portfolio_test.go new file mode 100644 index 0000000000000000000000000000000000000000..3047234009ff6eb654e6e3b40782b4f94ba0a1fb --- /dev/null +++ b/pkg/cli/test/portfolio_test.go @@ -0,0 +1,86 @@ +/* +Copyright 2019 The pdf Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/cli" +) + +func TestPortfolioCommand(t *testing.T) { + msg := "TestPortfolioCommand" + + if err := prepareForAttachmentTest(t); err != nil { + t.Fatalf("%s prepare for attachments: %v\n", msg, err) + } + + fileName := filepath.Join(outDir, "go.pdf") + + // # of portfolio entries must be 0 + listAttachments(t, msg, fileName, 0) + + // attach add 4 portfolio entries including descriptions. + files := []string{ + filepath.Join(outDir, "golang.pdf"), + filepath.Join(outDir, "T4.pdf") + ", CCITT spec", + filepath.Join(outDir, "go-lecture.pdf"), + filepath.Join(outDir, "test.wav") + ", test audio file"} + + cmd := cli.AddAttachmentsPortfolioCommand(fileName, "", files, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s add portfolio entries: %v\n", msg, err) + } + + // List portfolio entries. + list := listAttachments(t, msg, fileName, 4) + for _, s := range list { + t.Log(s) + } + + // Extract all portfolio entries. + cmd = cli.ExtractAttachmentsCommand(fileName, outDir, nil, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s extract all portfolio entries: %v\n", msg, err) + } + + // Extract 1 portfolio entry. + cmd = cli.ExtractAttachmentsCommand(fileName, outDir, []string{"golang.pdf"}, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s extract one portfolio entry: %v\n", msg, err) + } + + // Remove 1 portfolio entry. + cmd = cli.RemoveAttachmentsCommand(fileName, "", []string{"golang.pdf"}, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s remove one portfolio entry: %v\n", msg, err) + } + listAttachments(t, msg, fileName, 3) + + // Remove all portfolio entries. + cmd = cli.RemoveAttachmentsCommand(fileName, "", nil, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s remove all portfolio entries: %v\n", msg, err) + } + listAttachments(t, msg, fileName, 0) + + // Validate the processed file. + if err := validateFile(t, fileName, conf); err != nil { + t.Fatalf("%s: validate: %v\n", msg, err) + } +} diff --git a/pkg/cli/test/property_test.go b/pkg/cli/test/property_test.go new file mode 100644 index 0000000000000000000000000000000000000000..a2f7b7a382286d77061aaff5020628ebda9c206d --- /dev/null +++ b/pkg/cli/test/property_test.go @@ -0,0 +1,77 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/cli" +) + +func listProperties(t *testing.T, msg, fileName string, want []string) []string { + t.Helper() + cmd := cli.ListPropertiesCommand(fileName, conf) + got, err := cli.Process(cmd) + if err != nil { + t.Fatalf("%s list properties: %v\n", msg, err) + } + if len(got) != len(want) { + t.Fatalf("%s: list properties %s: want %d got %d\n", msg, fileName, len(want), len(got)) + } + for i, v := range got { + if v != want[i] { + t.Fatalf("%s: list properties %s: want %v got %v\n", msg, fileName, want, got) + } + } + return got +} + +func TestPropertiesCommand(t *testing.T) { + msg := "TestPropertiesCommand" + + fileName := filepath.Join(outDir, "go.pdf") + if err := copyFile(t, filepath.Join(inDir, "go.pdf"), fileName); err != nil { + t.Fatalf("%s: copyFile: %v\n", msg, err) + } + + // # of properties must be 0 + listProperties(t, msg, fileName, nil) + + properties := map[string]string{"name1": "value1", "name2": "value2"} + cmd := cli.AddPropertiesCommand(fileName, "", properties, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s add properties: %v\n", msg, err) + } + + listProperties(t, msg, fileName, []string{"name1 = value1", "name2 = value2"}) + + cmd = cli.RemovePropertiesCommand(fileName, "", []string{"name2"}, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s remove 1 property: %v\n", msg, err) + } + + listProperties(t, msg, fileName, []string{"name1 = value1"}) + + cmd = cli.RemovePropertiesCommand(fileName, "", nil, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s remove all properties: %v\n", msg, err) + } + + // # of properties must be 0 + listProperties(t, msg, fileName, nil) +} diff --git a/pkg/cli/test/resize_test.go b/pkg/cli/test/resize_test.go new file mode 100644 index 0000000000000000000000000000000000000000..12a58205fce3058dd67951614012e39b956f198f --- /dev/null +++ b/pkg/cli/test/resize_test.go @@ -0,0 +1,147 @@ +/* +Copyright 2023 The pdfcpu Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/cli" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +func TestResizeByScaleFactor(t *testing.T) { + msg := "TestResizeByScaleFactor" + inFile := filepath.Join(inDir, "test.pdf") + + // Enlarge by scale factor 2. + res, err := pdfcpu.ParseResizeConfig("sc:2", types.POINTS) + if err != nil { + t.Fatalf("%s invalid resize configuration: %v\n", msg, err) + } + + outFile := filepath.Join(outDir, "enlargeByScaleFactor.pdf") + cmd := cli.ResizeCommand(inFile, outFile, nil, res, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + // Shrink by 50%. + res, err = pdfcpu.ParseResizeConfig("sc:.5", types.POINTS) + if err != nil { + t.Fatalf("%s invalid resize configuration: %v\n", msg, err) + } + + outFile = filepath.Join(outDir, "shrinkByScaleFactor.pdf") + cmd = cli.ResizeCommand(inFile, outFile, nil, res, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } +} + +func TestResizeByWidthOrHeight(t *testing.T) { + msg := "TestResizeByWidthOrHeight" + + inFile := filepath.Join(inDir, "test.pdf") + + // Set width to 200 points. + res, err := pdfcpu.ParseResizeConfig("dim:200 0", types.POINTS) + if err != nil { + t.Fatalf("%s invalid resize configuration: %v\n", msg, err) + } + + outFile := filepath.Join(outDir, "resizeByWidth.pdf") + cmd := cli.ResizeCommand(inFile, outFile, nil, res, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + // Set height to 200 mm. + res, err = pdfcpu.ParseResizeConfig("dim:0 200", types.MILLIMETRES) + if err != nil { + t.Fatalf("%s invalid resize configuration: %v\n", msg, err) + } + + outFile = filepath.Join(outDir, "resizeByHeight.pdf") + cmd = cli.ResizeCommand(inFile, outFile, nil, res, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } +} + +func TestResizeToFormSize(t *testing.T) { + msg := "TestResizeToPaperSize" + + inFile := filepath.Join(inDir, "test.pdf") + + // Resize to A3 and keep orientation. + res, err := pdfcpu.ParseResizeConfig("form:A3", types.POINTS) + if err != nil { + t.Fatalf("%s invalid resize configuration: %v\n", msg, err) + } + + outFile := filepath.Join(outDir, "resizeToA3.pdf") + cmd := cli.ResizeCommand(inFile, outFile, nil, res, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + // Resize to A4 and enforce orientation (here landscape mode). + res, err = pdfcpu.ParseResizeConfig("form:A4L", types.POINTS) + if err != nil { + t.Fatalf("%s invalid resize configuration: %v\n", msg, err) + } + + outFile = filepath.Join(outDir, "resizeToA4L.pdf") + cmd = cli.ResizeCommand(inFile, outFile, nil, res, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } +} + +func TestResizeToDimensions(t *testing.T) { + msg := "TestResizeToDimensions" + + inFile := filepath.Join(inDir, "test.pdf") + + // Resize to 400 x 200 and keep orientation of input file. + // Apply background color to unused space. + res, err := pdfcpu.ParseResizeConfig("dim:400 200, bgcol:#E9967A", types.POINTS) + if err != nil { + t.Fatalf("%s invalid resize configuration: %v\n", msg, err) + } + + outFile := filepath.Join(outDir, "resizeToDimensionsKeep.pdf") + cmd := cli.ResizeCommand(inFile, outFile, nil, res, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + // Resize to 400 x 200 and enforce new orientation. + // Render border of original crop box. + res, err = pdfcpu.ParseResizeConfig("dim:400 200, enforce:true, border:on", types.POINTS) + if err != nil { + t.Fatalf("%s invalid resize configuration: %v\n", msg, err) + } + + outFile = filepath.Join(outDir, "resizeToDimensionsEnforce.pdf") + cmd = cli.ResizeCommand(inFile, outFile, nil, res, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } +} diff --git a/pkg/cli/test/rotate_test.go b/pkg/cli/test/rotate_test.go new file mode 100644 index 0000000000000000000000000000000000000000..831a2710e49450f79060942af5b6aaf6137d6d09 --- /dev/null +++ b/pkg/cli/test/rotate_test.go @@ -0,0 +1,41 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/cli" +) + +// Rotate first 2 pages clockwise by 90 degrees. +func TestRotateCommand(t *testing.T) { + msg := "TestRotateCommand" + inFile := filepath.Join(inDir, "Acroforms2.pdf") + outFile := filepath.Join(outDir, "test.pdf") + rotation := 90 + + cmd := cli.RotateCommand(inFile, outFile, rotation, []string{"-2"}, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + if err := validateFile(t, outFile, conf); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} diff --git a/pkg/cli/test/split_test.go b/pkg/cli/test/split_test.go new file mode 100644 index 0000000000000000000000000000000000000000..5ec23bf227dd3f3dcda11ee8cb340479b45a1693 --- /dev/null +++ b/pkg/cli/test/split_test.go @@ -0,0 +1,83 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/cli" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" +) + +// Split a test PDF file up into single page PDFs (using a split span of 1). +func TestSplitCommand(t *testing.T) { + msg := "TestSplitCommand" + fileName := "Acroforms2.pdf" + inFile := filepath.Join(inDir, fileName) + span := 1 + + conf := model.NewDefaultConfiguration() + + cmd := cli.SplitCommand(inFile, outDir, span, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s span=%d %s: %v\n", msg, span, inFile, err) + } +} + +// Split a test PDF file up into PDFs with 2 pages each (using a split span of 2). +func TestSplitBySpanCommand(t *testing.T) { + msg := "TestSplitBySpanCommand" + fileName := "CenterOfWhy.pdf" + inFile := filepath.Join(inDir, fileName) + span := 2 + + cmd := cli.SplitCommand(inFile, outDir, span, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s span=%d %s: %v\n", msg, span, inFile, err) + } +} + +// Split a PDF along its defined bookmarks on level 1 or 2 +func TestSplitByBookmarkCommand(t *testing.T) { + msg := "TestSplitByBookmarkCommand" + fileName := "5116.DCT_Filter.pdf" + inFile := filepath.Join(inDir, fileName) + + span := 0 // This means we are going to split by bookmarks. + + cmd := cli.SplitCommand(inFile, outDir, span, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, inFile, err) + } +} + +func TestSplitByPageNrCommand(t *testing.T) { + msg := "TestSplitByPageNrCommand" + fileName := "5116.DCT_Filter.pdf" + inFile := filepath.Join(inDir, fileName) + + // Generate page section 1 + // Generate page section 2-9 + // Generate page section 10-49 + // Generate page section 50-last page + + cmd := cli.SplitByPageNrCommand(inFile, outDir, []int{2, 10, 50}, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, inFile, err) + } +} diff --git a/pkg/cli/test/stamp_test.go b/pkg/cli/test/stamp_test.go new file mode 100644 index 0000000000000000000000000000000000000000..831ca964778b9c38cc96b916ff84f3b005a42b3f --- /dev/null +++ b/pkg/cli/test/stamp_test.go @@ -0,0 +1,202 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/cli" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +func testAddWatermarks(t *testing.T, msg, inFile, outFile string, selectedPages []string, mode, modeParm, desc string, onTop bool) { + t.Helper() + inFile = filepath.Join(inDir, inFile) + outFile = filepath.Join(outDir, outFile) + unit := types.POINTS + + var ( + wm *model.Watermark + err error + ) + switch mode { + case "text": + wm, err = pdfcpu.ParseTextWatermarkDetails(modeParm, desc, onTop, unit) + case "image": + wm, err = pdfcpu.ParseImageWatermarkDetails(modeParm, desc, onTop, unit) + case "pdf": + wm, err = pdfcpu.ParsePDFWatermarkDetails(modeParm, desc, onTop, unit) + } + if err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + cmd := cli.AddWatermarksCommand(inFile, outFile, selectedPages, wm, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + if err := validateFile(t, outFile, conf); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestAddWatermarks(t *testing.T) { + for _, tt := range []struct { + msg string + inFile, outFile string + selectedPages []string + onTop bool + mode string + modeParm string + wmConf string + }{ + // Add text watermark to all pages of inFile starting at page 1 using a rotation angle of 20 degrees. + {"TestWatermarkText", + "Acroforms2.pdf", + "testwm.pdf", + []string{"1-"}, + false, + "text", + "Draft", + "scale:0.7, rot:20"}, + + // Add a greenish, slightly transparent stroked and filled text stamp to all odd pages of inFile other than page 1 + // using the default rotation which is aligned along the first diagonal running from lower left to upper right corner. + {"TestStampText", + "pike-stanford.pdf", + "testStampText1.pdf", + []string{"odd", "!1"}, + true, + "text", + "Demo", + "font:Courier, c: 0 .8 0, op:0.8, mode:2"}, + + // Add a red filled text stamp to all odd pages of inFile other than page 1 using a font size of 48 points + // and the default rotation which is aligned along the first diagonal running from lower left to upper right corner. + {"TestStampTextUsingFontsize", + "pike-stanford.pdf", + "testStampText2.pdf", + []string{"odd", "!1"}, + true, + "text", + "Demo", + "font:Courier, c: 1 0 0, op:1, scale:1 abs, points:48"}, + + // Add image watermark to inFile starting at page 1 using no rotation. + {"TestWatermarkImage", + "Acroforms2.pdf", "testWMImageRel.pdf", + []string{"1-"}, + false, + "image", + filepath.Join(resDir, "pdfchip3.png"), + "rot:0"}, + + // Add image stamp to inFile using absolute scaling and a negative rotation of 90 degrees. + {"TestStampImageAbsScaling", + "Acroforms2.pdf", + "testWMImageAbs.pdf", + []string{"1-"}, + true, + "image", + filepath.Join(resDir, "pdfchip3.png"), + "scale:.5 a, rot:-90"}, + + // Add a PDF stamp to all pages of inFile using the 3rd page of pdfFile + // and rotate along the 2nd diagonal running from upper left to lower right corner. + {"TestWatermarkText", + "Acroforms2.pdf", + "testStampPDF.pdf", + nil, + true, + "pdf", + filepath.Join(inDir, "Wonderwall.pdf:3"), + "d:2"}, + + // Add a PDF multistamp to all pages of inFile + // and rotate along the 2nd diagonal running from upper left to lower right corner. + {"TestWatermarkText", + "Acroforms2.pdf", + "testMultistampPDF.pdf", + nil, + true, + "pdf", + filepath.Join(inDir, "Wonderwall.pdf"), + "d:2"}, + } { + testAddWatermarks(t, tt.msg, tt.inFile, tt.outFile, tt.selectedPages, tt.mode, tt.modeParm, tt.wmConf, tt.onTop) + } +} + +func TestStampingLifecyle(t *testing.T) { + msg := "TestStampingLifecyle" + inFile := filepath.Join(inDir, "Acroforms2.pdf") + outFile := filepath.Join(outDir, "stampLC.pdf") + onTop := true // we are testing stamps + unit := types.POINTS + + // Stamp all pages. + wm, err := pdfcpu.ParseTextWatermarkDetails("Demo", "", onTop, unit) + if err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + cmd := cli.AddWatermarksCommand(inFile, outFile, nil, wm, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + // // Update stamp on page 1. + wm, err = pdfcpu.ParseTextWatermarkDetails("Confidential", "", onTop, unit) + if err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + wm.Update = true + cmd = cli.AddWatermarksCommand(outFile, "", []string{"1"}, wm, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + // Add another stamp on top for all pages. + // This is a redish transparent footer. + wm, err = pdfcpu.ParseTextWatermarkDetails("Footer", "pos:bc, c:0.8 0 0, op:.6, rot:0", onTop, unit) + if err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + cmd = cli.AddWatermarksCommand(outFile, "", nil, wm, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + // Remove stamp on page 1. + cmd = cli.RemoveWatermarksCommand(outFile, "", []string{"1"}, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + // Remove all stamps. + cmd = cli.RemoveWatermarksCommand(outFile, "", nil, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + // Validate the result. + if err := validateFile(t, outFile, conf); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} diff --git a/pkg/cli/test/trim_test.go b/pkg/cli/test/trim_test.go new file mode 100644 index 0000000000000000000000000000000000000000..8a4e7461a9659c40acb0b567e06c52c456983d80 --- /dev/null +++ b/pkg/cli/test/trim_test.go @@ -0,0 +1,40 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/cli" +) + +// Trim test PDF file so that only the first two pages are rendered. +func TestTrimCommand(t *testing.T) { + msg := "TestTrimCommand" + inFile := filepath.Join(inDir, "pike-stanford.pdf") + outFile := filepath.Join(outDir, "test.pdf") + + cmd := cli.TrimCommand(inFile, outFile, []string{"-2"}, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + if err := validateFile(t, outFile, conf); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} diff --git a/pkg/cli/test/viewerPreferences_test.go b/pkg/cli/test/viewerPreferences_test.go new file mode 100644 index 0000000000000000000000000000000000000000..2ec2760fe08d63122e0b59c509728b8d1ef543ca --- /dev/null +++ b/pkg/cli/test/viewerPreferences_test.go @@ -0,0 +1,78 @@ +/* +Copyright 2023 The pdfcpu Authors. + +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 + + 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 test + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/cli" +) + +func TestViewerPreferences(t *testing.T) { + msg := "testViewerPreferences" + + fileName := "Hybrid-PDF.pdf" + inFile := filepath.Join(outDir, fileName) + copyFile(t, filepath.Join(inDir, fileName), inFile) + inFileJSON := filepath.Join(inDir, "json", "viewerPreferences.json") + stringJSON := "{\"HideMenuBar\": true, \"CenterWindow\": true}" + + all, json := false, false + cmd := cli.ListViewerPreferencesCommand(inFile, all, json, nil) + ss, err := cli.Process(cmd) + if err != nil { + t.Fatalf("%s %s: list viewer preferences: %v\n", msg, inFile, err) + } + if len(ss) > 0 && strings.HasPrefix(ss[0], "No viewer preferences") { + t.Fatalf("%s %s: missing viewer preferences\n", msg, inFile) + } + + cmd = cli.ResetViewerPreferencesCommand(inFile, "", nil) + if _, err = cli.Process(cmd); err != nil { + t.Fatalf("%s %s: reset viewer preferences: %v\n", msg, inFile, err) + } + + cmd = cli.ListViewerPreferencesCommand(inFile, all, json, nil) + ss, err = cli.Process(cmd) + if err != nil { + t.Fatalf("%s %s: list viewer preferences: %v\n", msg, inFile, err) + } + if len(ss) > 0 && !strings.HasPrefix(ss[0], "No viewer preferences") { + t.Fatalf("%s %s: unexpected viewer preferences\n", msg, inFile) + } + + cmd = cli.SetViewerPreferencesCommand(inFile, inFileJSON, "", "", nil) + if _, err = cli.Process(cmd); err != nil { + t.Fatalf("%s %s: set viewer preferences from JSON file: %v\n", msg, inFile, err) + } + + cmd = cli.ListViewerPreferencesCommand(inFile, all, json, nil) + ss, err = cli.Process(cmd) + if err != nil { + t.Fatalf("%s %s: list viewer preferences: %v\n", msg, inFile, err) + } + if len(ss) > 0 && strings.HasPrefix(ss[0], "No viewer preferences") { + t.Fatalf("%s %s: missing viewer preferences\n", msg, inFile) + } + + cmd = cli.SetViewerPreferencesCommand(inFile, "", "", stringJSON, nil) + if _, err = cli.Process(cmd); err != nil { + t.Fatalf("%s %s: set viewer preferences from JSON string: %v\n", msg, inFile, err) + } +} diff --git a/pkg/cli/test/zoom_test.go b/pkg/cli/test/zoom_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d080a85276d6df163513c56d78b22a4a914c5b82 --- /dev/null +++ b/pkg/cli/test/zoom_test.go @@ -0,0 +1,130 @@ +/* +Copyright 2024 The pdfcpu Authors. + +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 + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/cli" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +func TestZoomInByFactor(t *testing.T) { + msg := "TestZoomInByFactor" + + inFile := filepath.Join(inDir, "test.pdf") + + zoom, err := pdfcpu.ParseZoomConfig("factor:2", types.POINTS) + if err != nil { + t.Fatalf("%s invalid zoom configuration: %v\n", msg, err) + } + outFile := filepath.Join(outDir, "zoomInByFactor2.pdf") + cmd := cli.ZoomCommand(inFile, outFile, nil, zoom, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + zoom, err = pdfcpu.ParseZoomConfig("factor:4", types.POINTS) + if err != nil { + t.Fatalf("%s invalid zoom configuration: %v\n", msg, err) + } + outFile = filepath.Join(outDir, "zoomInByFactor4.pdf") + cmd = cli.ZoomCommand(inFile, outFile, nil, zoom, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } +} + +func TestZoomOutByFactor(t *testing.T) { + msg := "TestZoomOutByFactor" + + inFile := filepath.Join(inDir, "test.pdf") + + zoom, err := pdfcpu.ParseZoomConfig("factor:.5", types.POINTS) + if err != nil { + t.Fatalf("%s invalid zoom configuration: %v\n", msg, err) + } + outFile := filepath.Join(outDir, "zoomOutByFactor05.pdf") + cmd := cli.ZoomCommand(inFile, outFile, nil, zoom, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + zoom, err = pdfcpu.ParseZoomConfig("factor:.25, border:true", types.POINTS) + if err != nil { + t.Fatalf("%s invalid zoom configuration: %v\n", msg, err) + } + outFile = filepath.Join(outDir, "zoomOutByFactor025.pdf") + cmd = cli.ZoomCommand(inFile, outFile, nil, zoom, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } +} + +func TestZoomOutByHorizontalMargin(t *testing.T) { + // Zoom out of page content resulting in a preferred horizontal margin. + msg := "TestZoomOutByHMargin" + inFile := filepath.Join(inDir, "test.pdf") + + zoom, err := pdfcpu.ParseZoomConfig("hmargin:149", types.POINTS) + if err != nil { + t.Fatalf("%s invalid zoom configuration: %v\n", msg, err) + } + outFile := filepath.Join(outDir, "zoomOutByHMarginPoints.pdf") + cmd := cli.ZoomCommand(inFile, outFile, nil, zoom, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + zoom, err = pdfcpu.ParseZoomConfig("hmargin:1, border:true, bgcol:lightgray", types.CENTIMETRES) + if err != nil { + t.Fatalf("%s invalid zoom configuration: %v\n", msg, err) + } + outFile = filepath.Join(outDir, "zoomOutByHMarginCm.pdf") + cmd = cli.ZoomCommand(inFile, outFile, nil, zoom, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } +} + +func TestZoomOutByVerticalMargin(t *testing.T) { + // Zoom out of page content resulting in a preferred vertical margin. + msg := "TestZoomOutByVMargin" + inFile := filepath.Join(inDir, "test.pdf") + + zoom, err := pdfcpu.ParseZoomConfig("vmargin:1", types.INCHES) + if err != nil { + t.Fatalf("%s invalid zoom configuration: %v\n", msg, err) + } + outFile := filepath.Join(outDir, "zoomOutByVMarginInches.pdf") + cmd := cli.ZoomCommand(inFile, outFile, nil, zoom, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } + + zoom, err = pdfcpu.ParseZoomConfig("vmargin:30, border:false, bgcol:lightgray", types.MILLIMETRES) + if err != nil { + t.Fatalf("%s invalid zoom configuration: %v\n", msg, err) + } + outFile = filepath.Join(outDir, "zoomOutByVMarginMm.pdf") + cmd = cli.ZoomCommand(inFile, outFile, nil, zoom, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s %s: %v\n", msg, outFile, err) + } +} diff --git a/pkg/filter/ascii85Decode.go b/pkg/filter/ascii85Decode.go new file mode 100644 index 0000000000000000000000000000000000000000..3f195e07188151d035207d76917f5c930cf09af2 --- /dev/null +++ b/pkg/filter/ascii85Decode.go @@ -0,0 +1,89 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 filter + +import ( + "bytes" + "encoding/ascii85" + "io" + + "github.com/pkg/errors" +) + +type ascii85Decode struct { + baseFilter +} + +const eodASCII85 = "~>" + +// Encode implements encoding for an ASCII85Decode filter. +func (f ascii85Decode) Encode(r io.Reader) (io.Reader, error) { + + b2 := &bytes.Buffer{} + encoder := ascii85.NewEncoder(b2) + if _, err := io.Copy(encoder, r); err != nil { + return nil, err + } + encoder.Close() + + // Add eod sequence + b2.WriteString(eodASCII85) + + return b2, nil +} + +// Decode implements decoding for an ASCII85Decode filter. +func (f ascii85Decode) Decode(r io.Reader) (io.Reader, error) { + return f.DecodeLength(r, -1) +} + +func (f ascii85Decode) DecodeLength(r io.Reader, maxLen int64) (io.Reader, error) { + + bb, err := getReaderBytes(r) + if err != nil { + return nil, err + } + + // fmt.Printf("dump:\n%s", hex.Dump(bb)) + + l := len(bb) + if bb[l-1] == 0x0A || bb[l-1] == 0x0D { + bb = bb[:l-1] + } + + if !bytes.HasSuffix(bb, []byte(eodASCII85)) { + return nil, errors.New("pdfcpu: Decode: missing eod marker") + } + + // Strip eod sequence: "~>" + bb = bb[:len(bb)-2] + + decoder := ascii85.NewDecoder(bytes.NewReader(bb)) + + var b2 bytes.Buffer + if maxLen < 0 { + if _, err := io.Copy(&b2, decoder); err != nil { + return nil, err + } + } else { + if _, err := io.CopyN(&b2, decoder, maxLen); err != nil { + return nil, err + } + } + + return &b2, nil +} diff --git a/pkg/filter/asciiHexDecode.go b/pkg/filter/asciiHexDecode.go new file mode 100644 index 0000000000000000000000000000000000000000..5d0945a210b6c5607723d5bd68092a77f4602c6a --- /dev/null +++ b/pkg/filter/asciiHexDecode.go @@ -0,0 +1,86 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 filter + +import ( + "bytes" + "encoding/hex" + "io" +) + +type asciiHexDecode struct { + baseFilter +} + +const eodHexDecode = '>' + +// Encode implements encoding for an ASCIIHexDecode filter. +func (f asciiHexDecode) Encode(r io.Reader) (io.Reader, error) { + + bb, err := getReaderBytes(r) + if err != nil { + return nil, err + } + + dst := make([]byte, hex.EncodedLen(len(bb))) + hex.Encode(dst, bb) + + // eod marker + dst = append(dst, eodHexDecode) + + return bytes.NewBuffer(dst), nil +} + +// Decode implements decoding for an ASCIIHexDecode filter. +func (f asciiHexDecode) Decode(r io.Reader) (io.Reader, error) { + return f.DecodeLength(r, -1) +} + +func (f asciiHexDecode) DecodeLength(r io.Reader, maxLen int64) (io.Reader, error) { + bb, err := getReaderBytes(r) + if err != nil { + return nil, err + } + + var p []byte + + // Remove any white space and cut off on eod + for i := 0; i < len(bb); i++ { + if bb[i] == eodHexDecode { + break + } + if !bytes.ContainsRune([]byte{0x09, 0x0A, 0x0C, 0x0D, 0x20}, rune(bb[i])) { + p = append(p, bb[i]) + } + } + + // if len == odd add "0" + if len(p)%2 == 1 { + p = append(p, '0') + } + + if maxLen < 0 { + maxLen = int64(hex.DecodedLen(len(p))) + } + dst := make([]byte, maxLen) + + if _, err := hex.Decode(dst, p[:maxLen*2]); err != nil { + return nil, err + } + + return bytes.NewBuffer(dst), nil +} diff --git a/pkg/filter/ccittDecode.go b/pkg/filter/ccittDecode.go new file mode 100644 index 0000000000000000000000000000000000000000..4f93e7597230d39067953314a711f5aafd9c546c --- /dev/null +++ b/pkg/filter/ccittDecode.go @@ -0,0 +1,101 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 filter + +import ( + "bytes" + "io" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pkg/errors" + "golang.org/x/image/ccitt" +) + +type ccittDecode struct { + baseFilter +} + +// Encode implements encoding for a CCITTDecode filter. +func (f ccittDecode) Encode(r io.Reader) (io.Reader, error) { + // TODO + return nil, nil +} + +// Decode implements decoding for a CCITTDecode filter. +func (f ccittDecode) Decode(r io.Reader) (io.Reader, error) { + return f.DecodeLength(r, -1) +} + +func (f ccittDecode) DecodeLength(r io.Reader, maxLen int64) (io.Reader, error) { + if log.TraceEnabled() { + log.Trace.Println("DecodeCCITT begin") + } + + var ok bool + + // <0 : Pure two-dimensional encoding (Group 4) + // =0 : Pure one-dimensional encoding (Group 3, 1-D) + // >0 : Mixed one- and two-dimensional encoding (Group 3, 2-D) + k := 0 + k, ok = f.parms["K"] + if ok && k > 0 { + return nil, errors.New("pdfcpu: filter CCITTFax k > 0 currently unsupported") + } + + cols := 1728 + col, ok := f.parms["Columns"] + if ok { + cols = col + } + + rows, ok := f.parms["Rows"] + if !ok { + return nil, errors.New("pdfcpu: ccitt: missing DecodeParam \"Rows\"") + } + + blackIs1 := false + v, ok := f.parms["BlackIs1"] + if ok && v == 1 { + blackIs1 = true + } + + encodedByteAlign := false + v, ok = f.parms["EncodedByteAlign"] + if ok && v == 1 { + encodedByteAlign = true + } + + opts := &ccitt.Options{Invert: blackIs1, Align: encodedByteAlign} + + mode := ccitt.Group3 + if k < 0 { + mode = ccitt.Group4 + } + rd := ccitt.NewReader(r, ccitt.MSB, mode, cols, rows, opts) + + var b bytes.Buffer + written, err := io.Copy(&b, rd) + if err != nil { + return nil, err + } + + if log.TraceEnabled() { + log.Trace.Printf("DecodeCCITT: decoded %d bytes.\n", written) + } + + return &b, nil +} diff --git a/pkg/filter/dctDecode.go b/pkg/filter/dctDecode.go new file mode 100644 index 0000000000000000000000000000000000000000..593c3cccbbef1f103b0b94cec01f7a321be1351f --- /dev/null +++ b/pkg/filter/dctDecode.go @@ -0,0 +1,56 @@ +/* +Copyright 2021 The pdfcpu Authors. + +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 + + 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 filter + +import ( + "bytes" + "encoding/gob" + "image/jpeg" + "io" +) + +type dctDecode struct { + baseFilter +} + +// Encode implements encoding for a DCTDecode filter. +func (f dctDecode) Encode(r io.Reader) (io.Reader, error) { + + return nil, nil +} + +// Decode implements decoding for a DCTDecode filter. +func (f dctDecode) Decode(r io.Reader) (io.Reader, error) { + return f.DecodeLength(r, -1) +} + +func (f dctDecode) DecodeLength(r io.Reader, maxLen int64) (io.Reader, error) { + im, err := jpeg.Decode(r) + if err != nil { + return nil, err + } + + var b bytes.Buffer + + enc := gob.NewEncoder(&b) + + if err := enc.Encode(im); err != nil { + return nil, err + } + + return &b, nil +} diff --git a/pkg/filter/filter.go b/pkg/filter/filter.go new file mode 100644 index 0000000000000000000000000000000000000000..3cb7dcf6dc3f74cc92213bed4175e9ede7188731 --- /dev/null +++ b/pkg/filter/filter.go @@ -0,0 +1,121 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 filter contains PDF filter implementations. +package filter + +import ( + "bytes" + "io" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pkg/errors" +) + +// PDF defines the following filters. See also 7.4 in the PDF spec. +const ( + ASCII85 = "ASCII85Decode" + ASCIIHex = "ASCIIHexDecode" + RunLength = "RunLengthDecode" + LZW = "LZWDecode" + Flate = "FlateDecode" + CCITTFax = "CCITTFaxDecode" + JBIG2 = "JBIG2Decode" + DCT = "DCTDecode" + JPX = "JPXDecode" +) + +// ErrUnsupportedFilter signals unsupported filter encountered. +var ErrUnsupportedFilter = errors.New("pdfcpu: filter not supported") + +// Filter defines an interface for encoding/decoding PDF object streams. +type Filter interface { + Encode(r io.Reader) (io.Reader, error) + Decode(r io.Reader) (io.Reader, error) + // DecodeLength will decode at least maxLen bytes. For filters where decoding + // parts doesn't make sense (e.g. DCT), the whole stream is decoded. + // If maxLen < 0 is passed, the whole stream is decoded. + DecodeLength(r io.Reader, maxLen int64) (io.Reader, error) +} + +// NewFilter returns a filter for given filterName and an optional parameter dictionary. +func NewFilter(filterName string, parms map[string]int) (filter Filter, err error) { + switch filterName { + + case ASCII85: + filter = ascii85Decode{baseFilter{}} + + case ASCIIHex: + filter = asciiHexDecode{baseFilter{}} + + case RunLength: + filter = runLengthDecode{baseFilter{parms}} + + case LZW: + filter = lzwDecode{baseFilter{parms}} + + case Flate: + filter = flate{baseFilter{parms}} + + case CCITTFax: + filter = ccittDecode{baseFilter{parms}} + + case DCT: + filter = dctDecode{baseFilter{parms}} + + case JBIG2: + // Unsupported + fallthrough + + case JPX: + // Unsupported + if log.InfoEnabled() { + log.Info.Printf("Filter not supported: <%s>", filterName) + } + err = ErrUnsupportedFilter + + default: + err = errors.Errorf("Invalid filter: <%s>", filterName) + } + + return filter, err +} + +// List return the list of all supported PDF filters. +func List() []string { + // Exclude CCITTFax, DCT, JBIG2 & JPX since they only makes sense in the context of image processing. + return []string{ASCII85, ASCIIHex, RunLength, LZW, Flate} +} + +type baseFilter struct { + parms map[string]int +} + +func getReaderBytes(r io.Reader) ([]byte, error) { + var bb []byte + if buf, ok := r.(*bytes.Buffer); ok { + bb = buf.Bytes() + } else { + var buf bytes.Buffer + if _, err := io.Copy(&buf, r); err != nil { + return nil, err + } + + bb = buf.Bytes() + } + + return bb, nil +} diff --git a/pkg/filter/filter_test.go b/pkg/filter/filter_test.go new file mode 100644 index 0000000000000000000000000000000000000000..ae5c585f4a1874eed84d34782a000e7691cf5eef --- /dev/null +++ b/pkg/filter/filter_test.go @@ -0,0 +1,273 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 filter_test + +import ( + "errors" + "io" + "os" + "strings" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/filter" +) + +func TestFilterSupport(t *testing.T) { + var filtersTests = []struct { + filterName string + expected error + }{ + {filter.ASCII85, nil}, + {filter.ASCIIHex, nil}, + {filter.RunLength, nil}, + {filter.LZW, nil}, + {filter.Flate, nil}, + {filter.CCITTFax, nil}, + {filter.DCT, nil}, + {filter.JBIG2, filter.ErrUnsupportedFilter}, + {filter.JPX, filter.ErrUnsupportedFilter}, + {"INVALID_FILTER", errors.New("Invalid filter: ")}, + } + for _, tt := range filtersTests { + _, err := filter.NewFilter(tt.filterName, nil) + if (tt.expected != nil && err != nil && err.Error() != tt.expected.Error()) || + ((err == nil || tt.expected == nil) && err != tt.expected) { + t.Errorf("Problem: '%s' (expected '%s')\n", err.Error(), tt.expected.Error()) + } + } +} + +// Encode a test string with filterName then decode and check if result matches original. +func encodeDecodeString(t *testing.T, filterName string) { + t.Helper() + + filter, err := filter.NewFilter(filterName, nil) + if err != nil { + t.Fatalf("Problem: %v\n", err) + } + + want := "Hello, Gopher!" + t.Logf("encoding using filter %s: len:%d % X <%s>\n", filterName, len(want), want, want) + + b1, err := filter.Encode(strings.NewReader(want)) + if err != nil { + t.Fatalf("Problem encoding 1: %v\n", err) + } + //t.Logf("encoded 1: len:%d % X <%s>\n", b1.Len(), b1.Bytes(), b1.Bytes()) + + b2, err := filter.Encode(b1) + if err != nil { + t.Fatalf("Problem encoding 2: %v\n", err) + } + //t.Logf("encoded 2: len:%d % X <%s>\n", b2.Len(), b2.Bytes(), b2.Bytes()) + + c1, err := filter.Decode(b2) + if err != nil { + t.Fatalf("Problem decoding 2: %v\n", err) + } + //t.Logf("decoded 2: len:%d % X <%s>\n", c1.Len(), c1.Bytes(), c1.Bytes()) + + c2, err := filter.Decode(c1) + if err != nil { + t.Fatalf("Problem decoding 1: %v\n", err) + } + //t.Logf("decoded 1: len:%d % X <%s>\n", c2.Len(), c2.Bytes(), c2.Bytes()) + + bb, err := io.ReadAll(c2) + if err != nil { + t.Fatalf("%v\n", err) + } + got := string(bb) + if got != want { + t.Fatalf("got:%s want:%s\n", got, want) + } +} + +func TestEncodeDecodeString(t *testing.T) { + for _, f := range filter.List() { + encodeDecodeString(t, f) + } +} + +var filenames = []string{ + "testdata/gettysburg.txt", + "testdata/e.txt", + "testdata/pi.txt", + "testdata/Mark.Twain-Tom.Sawyer.txt", +} + +// Encode fileName with filterName then decode and check if result matches original. +func encodeDecode(t *testing.T, fileName, filterName string) { + t.Helper() + + t.Logf("testFile: %s with filter:%s\n", fileName, filterName) + + f, err := filter.NewFilter(filterName, nil) + if err != nil { + t.Errorf("Problem: %v\n", err) + } + + raw, err := os.Open(fileName) + if err != nil { + t.Errorf("%s: %v", fileName, err) + return + } + defer raw.Close() + + enc, err := f.Encode(raw) + if err != nil { + t.Errorf("Problem encoding: %v\n", err) + } + + dec, err := f.Decode(enc) + if err != nil { + t.Errorf("Problem decoding: %v\n", err) + } + + // Compare decoded bytes with original bytes. + golden, err := os.Open(fileName) + if err != nil { + t.Errorf("%s: %v", fileName, err) + return + } + defer golden.Close() + + g, err := io.ReadAll(golden) + if err != nil { + t.Errorf("%s: %v", fileName, err) + return + } + + d, err := io.ReadAll(dec) + if err != nil { + t.Errorf("%s: %v", fileName, err) + return + } + + if len(d) != len(g) { + t.Errorf("%s: length mismatch %d != %d", fileName, len(d), len(g)) + return + } + + for i := 0; i < len(d); i++ { + if d[i] != g[i] { + t.Errorf("%s: mismatch at %d, 0x%02x != 0x%02x\n", fileName, i, d[i], g[i]) + return + } + } + +} + +func TestEncodeDecode(t *testing.T) { + for _, filterName := range filter.List() { + for _, filename := range filenames { + encodeDecode(t, filename, filterName) + } + } +} + +func encode(t *testing.T, r io.Reader, filterName string) io.Reader { + t.Helper() + + f, err := filter.NewFilter(filterName, nil) + if err != nil { + t.Errorf("Problem: %v\n", err) + } + + r, err = f.Encode(r) + if err != nil { + t.Errorf("Problem encoding: %v\n", err) + } + + return r +} + +func decode(t *testing.T, r io.Reader, filterName string) io.Reader { + t.Helper() + + f, err := filter.NewFilter(filterName, nil) + if err != nil { + t.Errorf("Problem: %v\n", err) + } + + r, err = f.Decode(r) + if err != nil { + t.Errorf("Problem decoding: %v\n", err) + } + + return r +} + +// Encode fileName with filter pipeline then decode and check if result matches original. +func encodeDecodeFilterPipeline(t *testing.T, fileName string, fpl []string) { + t.Helper() + + f0, err := os.Open(fileName) + if err != nil { + t.Errorf("%s: %v", fileName, err) + return + } + defer f0.Close() + + r := io.Reader(f0) + + for i := len(fpl) - 1; i >= 0; i-- { + r = encode(t, r, fpl[i]) + } + + for _, f := range fpl { + r = decode(t, r, f) + } + + // Compare decoded bytes with original bytes. + golden, err := os.Open(fileName) + if err != nil { + t.Errorf("%s: %v", fileName, err) + return + } + defer golden.Close() + + g, err := io.ReadAll(golden) + if err != nil { + t.Errorf("%s: %v", fileName, err) + return + } + + d, err := io.ReadAll(r) + if err != nil { + t.Errorf("%s: %v", fileName, err) + return + } + + if len(d) != len(g) { + t.Errorf("%s: length mismatch %d != %d", fileName, len(d), len(g)) + return + } + + for i := 0; i < len(d); i++ { + if d[i] != g[i] { + t.Errorf("%s: mismatch at %d, 0x%02x != 0x%02x\n", fileName, i, d[i], g[i]) + return + } + } +} + +func TestEncodeDecodeFilterPipeline(t *testing.T) { + for _, filename := range filenames { + encodeDecodeFilterPipeline(t, filename, []string{filter.ASCII85, filter.Flate}) + } +} diff --git a/pkg/filter/flateDecode.go b/pkg/filter/flateDecode.go new file mode 100644 index 0000000000000000000000000000000000000000..532c0d1680aca964fd38f39d474bb43cf9abbdef --- /dev/null +++ b/pkg/filter/flateDecode.go @@ -0,0 +1,361 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 filter + +import ( + "bytes" + "compress/zlib" + "io" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pkg/errors" +) + +// Portions of this code are based on ideas of image/png: reader.go:readImagePass +// PNG is documented here: www.w3.org/TR/PNG-Filters.html + +// PDF allows a prediction step prior to compression applying TIFF or PNG prediction. +// Predictor algorithm. +const ( + PredictorNo = 1 // No prediction. + PredictorTIFF = 2 // Use TIFF prediction for all rows. + PredictorNone = 10 // Use PNGNone for all rows. + PredictorSub = 11 // Use PNGSub for all rows. + PredictorUp = 12 // Use PNGUp for all rows. + PredictorAverage = 13 // Use PNGAverage for all rows. + PredictorPaeth = 14 // Use PNGPaeth for all rows. + PredictorOptimum = 15 // Use the optimum PNG prediction for each row. +) + +// For predictor > 2 PNG filters (see RFC 2083) get applied and the first byte of each pixelrow defines +// the prediction algorithm used for all pixels of this row. +const ( + PNGNone = 0x00 + PNGSub = 0x01 + PNGUp = 0x02 + PNGAverage = 0x03 + PNGPaeth = 0x04 +) + +type flate struct { + baseFilter +} + +// Encode implements encoding for a Flate filter. +func (f flate) Encode(r io.Reader) (io.Reader, error) { + if log.TraceEnabled() { + log.Trace.Println("EncodeFlate begin") + } + + // TODO Optional decode parameters may need predictor preprocessing. + + var b bytes.Buffer + w := zlib.NewWriter(&b) + defer w.Close() + + written, err := io.Copy(w, r) + if err != nil { + return nil, err + } + + if log.TraceEnabled() { + log.Trace.Printf("EncodeFlate end: %d bytes written\n", written) + } + + return &b, nil +} + +// Decode implements decoding for a Flate filter. +func (f flate) Decode(r io.Reader) (io.Reader, error) { + return f.DecodeLength(r, -1) +} + +func (f flate) DecodeLength(r io.Reader, maxLen int64) (io.Reader, error) { + if log.TraceEnabled() { + log.Trace.Println("DecodeFlate begin") + } + + rc, err := zlib.NewReader(r) + if err != nil { + return nil, err + } + defer rc.Close() + + // Optional decode parameters need postprocessing. + return f.decodePostProcess(rc, maxLen) +} + +func passThru(rin io.Reader, maxLen int64) (*bytes.Buffer, error) { + var b bytes.Buffer + var err error + if maxLen < 0 { + _, err = io.Copy(&b, rin) + } else { + _, err = io.CopyN(&b, rin, maxLen) + } + if err == io.ErrUnexpectedEOF { + // Workaround for missing support for partial flush in compress/flate. + // See also https://github.com/golang/go/issues/31514 + if log.ReadEnabled() { + log.Read.Println("flateDecode: ignoring unexpected EOF") + } + err = nil + } + return &b, err +} + +func intMemberOf(i int, list []int) bool { + for _, v := range list { + if i == v { + return true + } + } + return false +} + +// Each prediction value implies (a) certain row filter(s). +// func validateRowFilter(f, p int) error { + +// switch p { + +// case PredictorNone: +// if !intMemberOf(f, []int{PNGNone, PNGSub, PNGUp, PNGAverage, PNGPaeth}) { +// return errors.Errorf("pdfcpu: validateRowFilter: PredictorOptimum, unexpected row filter #%02x", f) +// } +// // if f != PNGNone { +// // return errors.Errorf("validateRowFilter: expected row filter #%02x, got: #%02x", PNGNone, f) +// // } + +// case PredictorSub: +// if f != PNGSub { +// return errors.Errorf("pdfcpu: validateRowFilter: expected row filter #%02x, got: #%02x", PNGSub, f) +// } + +// case PredictorUp: +// if f != PNGUp { +// return errors.Errorf("pdfcpu: validateRowFilter: expected row filter #%02x, got: #%02x", PNGUp, f) +// } + +// case PredictorAverage: +// if f != PNGAverage { +// return errors.Errorf("pdfcpu: validateRowFilter: expected row filter #%02x, got: #%02x", PNGAverage, f) +// } + +// case PredictorPaeth: +// if f != PNGPaeth { +// return errors.Errorf("pdfcpu: validateRowFilter: expected row filter #%02x, got: #%02x", PNGPaeth, f) +// } + +// case PredictorOptimum: +// if !intMemberOf(f, []int{PNGNone, PNGSub, PNGUp, PNGAverage, PNGPaeth}) { +// return errors.Errorf("pdfcpu: validateRowFilter: PredictorOptimum, unexpected row filter #%02x", f) +// } + +// default: +// return errors.Errorf("pdfcpu: validateRowFilter: unexpected predictor #%02x", p) + +// } + +// return nil +// } + +func applyHorDiff(row []byte, colors int) ([]byte, error) { + // This works for 8 bits per color only. + for i := 1; i < len(row)/colors; i++ { + for j := 0; j < colors; j++ { + row[i*colors+j] += row[(i-1)*colors+j] + } + } + return row, nil +} + +func processRow(pr, cr []byte, p, colors, bytesPerPixel int) ([]byte, error) { + //fmt.Printf("pr(%v) =\n%s\n", &pr, hex.Dump(pr)) + //fmt.Printf("cr(%v) =\n%s\n", &cr, hex.Dump(cr)) + + if p == PredictorTIFF { + return applyHorDiff(cr, colors) + } + + // Apply the filter. + cdat := cr[1:] + pdat := pr[1:] + + // Get row filter from 1st byte + f := int(cr[0]) + + // The value of Predictor supplied by the decoding filter need not match the value + // used when the data was encoded if they are both greater than or equal to 10. + + switch f { + + case PNGNone: + // No operation. + + case PNGSub: + for i := bytesPerPixel; i < len(cdat); i++ { + cdat[i] += cdat[i-bytesPerPixel] + } + + case PNGUp: + for i, p := range pdat { + cdat[i] += p + } + + case PNGAverage: + // The average of the two neighboring pixels (left and above). + // Raw(x) - floor((Raw(x-bpp)+Prior(x))/2) + for i := 0; i < bytesPerPixel; i++ { + cdat[i] += pdat[i] / 2 + } + for i := bytesPerPixel; i < len(cdat); i++ { + cdat[i] += uint8((int(cdat[i-bytesPerPixel]) + int(pdat[i])) / 2) + } + + case PNGPaeth: + filterPaeth(cdat, pdat, bytesPerPixel) + + } + + return cdat, nil +} + +func (f flate) parameters() (colors, bpc, columns int, err error) { + // Colors, int + // The number of interleaved colour components per sample. + // Valid values are 1 to 4 (PDF 1.0) and 1 or greater (PDF 1.3). Default value: 1. + // Used by PredictorTIFF only. + colors, found := f.parms["Colors"] + if !found { + colors = 1 + } else if colors == 0 { + return 0, 0, 0, errors.Errorf("pdfcpu: filter FlateDecode: \"Colors\" must be > 0") + } + + // BitsPerComponent, int + // The number of bits used to represent each colour component in a sample. + // Valid values are 1, 2, 4, 8, and (PDF 1.5) 16. Default value: 8. + // Used by PredictorTIFF only. + bpc, found = f.parms["BitsPerComponent"] + if !found { + bpc = 8 + } else if !intMemberOf(bpc, []int{1, 2, 4, 8, 16}) { + return 0, 0, 0, errors.Errorf("pdfcpu: filter FlateDecode: Unexpected \"BitsPerComponent\": %d", bpc) + } + + // Columns, int + // The number of samples in each row. Default value: 1. + columns, found = f.parms["Columns"] + if !found { + columns = 1 + } + + return colors, bpc, columns, nil +} + +func checkBufLen(b bytes.Buffer, maxLen int64) bool { + return maxLen < 0 || int64(b.Len()) < maxLen +} + +func process(w io.Writer, pr, cr []byte, predictor, colors, bytesPerPixel int) error { + d, err := processRow(pr, cr, predictor, colors, bytesPerPixel) + if err != nil { + return err + } + + _, err = w.Write(d) + + return err +} + +// decodePostProcess +func (f flate) decodePostProcess(r io.Reader, maxLen int64) (io.Reader, error) { + predictor, found := f.parms["Predictor"] + if !found || predictor == PredictorNo { + return passThru(r, maxLen) + } + + if !intMemberOf( + predictor, + []int{PredictorTIFF, + PredictorNone, + PredictorSub, + PredictorUp, + PredictorAverage, + PredictorPaeth, + PredictorOptimum, + }) { + return nil, errors.Errorf("pdfcpu: filter FlateDecode: undefined \"Predictor\" %d", predictor) + } + + colors, bpc, columns, err := f.parameters() + if err != nil { + return nil, err + } + + bytesPerPixel := (bpc*colors + 7) / 8 + rowSize := (bpc*colors*columns + 7) / 8 + + m := rowSize + if predictor != PredictorTIFF { + // PNG prediction uses a row filter byte prefixing the pixelbytes of a row. + m++ + } + + // cr and pr are the bytes for the current and previous row. + cr := make([]byte, m) + pr := make([]byte, m) + + // Output buffer + var b bytes.Buffer + + for checkBufLen(b, maxLen) { + + // Read decompressed bytes for one pixel row. + n, err := io.ReadFull(r, cr) + if err != nil { + if err != io.EOF { + return nil, err + } + // eof + if n == 0 { + break + } + } + + if n != m { + return nil, errors.Errorf("pdfcpu: filter FlateDecode: read error, expected %d bytes, got: %d", m, n) + } + + if err := process(&b, pr, cr, predictor, colors, bytesPerPixel); err != nil { + return nil, err + } + + if err == io.EOF { + break + } + + pr, cr = cr, pr + } + + if maxLen < 0 && b.Len()%rowSize > 0 { + log.Info.Printf("failed postprocessing: %d %d\n", b.Len(), rowSize) + return nil, errors.New("pdfcpu: filter FlateDecode: postprocessing failed") + } + + return &b, nil +} diff --git a/pkg/filter/lzwDecode.go b/pkg/filter/lzwDecode.go new file mode 100644 index 0000000000000000000000000000000000000000..ac5e869081626811f156b4db69096a8830db3b82 --- /dev/null +++ b/pkg/filter/lzwDecode.go @@ -0,0 +1,100 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 filter + +import ( + "bytes" + "io" + + "github.com/hhrutter/lzw" + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pkg/errors" +) + +type lzwDecode struct { + baseFilter +} + +// Encode implements encoding for an LZWDecode filter. +func (f lzwDecode) Encode(r io.Reader) (io.Reader, error) { + if log.TraceEnabled() { + log.Trace.Println("EncodeLZW begin") + } + + var b bytes.Buffer + + ec, ok := f.parms["EarlyChange"] + if !ok { + ec = 1 + } + + wc := lzw.NewWriter(&b, ec == 1) + defer wc.Close() + + written, err := io.Copy(wc, r) + if err != nil { + return nil, err + } + + if log.TraceEnabled() { + log.Trace.Printf("EncodeLZW end: %d bytes written\n", written) + } + + return &b, nil +} + +// Decode implements decoding for an LZWDecode filter. +func (f lzwDecode) Decode(r io.Reader) (io.Reader, error) { + return f.DecodeLength(r, -1) +} + +func (f lzwDecode) DecodeLength(r io.Reader, maxLen int64) (io.Reader, error) { + if log.TraceEnabled() { + log.Trace.Println("DecodeLZW begin") + } + + p, found := f.parms["Predictor"] + if found && p > 1 { + return nil, errors.Errorf("DecodeLZW: unsupported predictor %d", p) + } + + ec, ok := f.parms["EarlyChange"] + if !ok { + ec = 1 + } + + rc := lzw.NewReader(r, ec == 1) + defer rc.Close() + + var b bytes.Buffer + var written int64 + var err error + if maxLen < 0 { + written, err = io.Copy(&b, rc) + } else { + written, err = io.CopyN(&b, rc, maxLen) + } + if err != nil { + return nil, err + } + + if log.TraceEnabled() { + log.Trace.Printf("DecodeLZW: decoded %d bytes.\n", written) + } + + return &b, nil +} diff --git a/pkg/filter/paeth.go b/pkg/filter/paeth.go new file mode 100644 index 0000000000000000000000000000000000000000..c02233281deb76dbbdae36b152d254e7b5b55c6a --- /dev/null +++ b/pkg/filter/paeth.go @@ -0,0 +1,75 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// The code to compute Paeth is borrowed from image/png in the stdlib because it is internal over there to the png reader. +// The PNG Paeth filter is documented here: www.w3.org/TR/PNG-Filters.html + +package filter + +// intSize is either 32 or 64. +// Disabled intSize 64 for govet. +const intSize = 32 //<< (^uint(0) >> 63) + +func abs(x int) int { + // m := -1 if x < 0. m := 0 otherwise. + m := x >> (intSize - 1) + + // In two's complement representation, the negative number + // of any number (except the smallest one) can be computed + // by flipping all the bits and add 1. This is faster than + // code with a branch. + // See Hacker's Delight, section 2-4. + return (x ^ m) - m +} + +// paeth implements the Paeth filter function, as per the PNG specification. +func paeth(a, b, c uint8) uint8 { + // This is an optimized version of the sample code in the PNG spec. + // For example, the sample code starts with: + // p := int(a) + int(b) - int(c) + // pa := abs(p - int(a)) + // but the optimized form uses fewer arithmetic operations: + // pa := int(b) - int(c) + // pa = abs(pa) + pc := int(c) + pa := int(b) - pc + pb := int(a) - pc + pc = abs(pa + pb) + pa = abs(pa) + pb = abs(pb) + if pa <= pb && pa <= pc { + return a + } else if pb <= pc { + return b + } + return c +} + +// filterPaeth applies the Paeth filter to the cdat slice. +// cdat is the current row's data, pdat is the previous row's data. +func filterPaeth(cdat, pdat []byte, bytesPerPixel int) { + var a, b, c, pa, pb, pc int + for i := 0; i < bytesPerPixel; i++ { + a, c = 0, 0 + for j := i; j < len(cdat); j += bytesPerPixel { + b = int(pdat[j]) + pa = b - c + pb = a - c + pc = abs(pa + pb) + pa = abs(pa) + pb = abs(pb) + if pa <= pb && pa <= pc { + // No-op. + } else if pb <= pc { + a = b + } else { + a = c + } + a += int(cdat[j]) + a &= 0xff + cdat[j] = uint8(a) + c = b + } + } +} diff --git a/pkg/filter/runLengthDecode.go b/pkg/filter/runLengthDecode.go new file mode 100644 index 0000000000000000000000000000000000000000..2cbb0bcf061a33b1452fc84403dcc7f63f212f05 --- /dev/null +++ b/pkg/filter/runLengthDecode.go @@ -0,0 +1,153 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 filter + +import ( + "bytes" + "io" +) + +type runLengthDecode struct { + baseFilter +} + +func (f runLengthDecode) decode(w io.ByteWriter, src []byte, maxLen int64) { + var written int64 + + for i := 0; i < len(src); { + b := src[i] + if b == 0x80 { + // eod + break + } + i++ + if b < 0x80 { + c := int(b) + 1 + for j := 0; j < c; j++ { + if maxLen >= 0 && maxLen == written { + break + } + + w.WriteByte(src[i]) + written++ + i++ + } + continue + } + c := 257 - int(b) + for j := 0; j < c; j++ { + if maxLen >= 0 && maxLen == written { + break + } + + w.WriteByte(src[i]) + written++ + } + i++ + } +} + +func (f runLengthDecode) encode(w io.ByteWriter, src []byte) { + + const maxLen = 0x80 + const eod = 0x80 + + i := 0 + b := src[i] + start := i + + for { + + // Detect constant run eg. 0x1414141414141414 + for i < len(src) && src[i] == b && (i-start < maxLen) { + i++ + } + c := i - start + if c > 1 { + // Write constant run with length=c + w.WriteByte(byte(257 - c)) + w.WriteByte(b) + if i == len(src) { + w.WriteByte(eod) + return + } + b = src[i] + start = i + continue + } + + // Detect variable run eg. 0x20FFD023335BCC12 + for i < len(src) && src[i] != b && (i-start < maxLen) { + b = src[i] + i++ + } + if i == len(src) || i-start == maxLen { + c = i - start + w.WriteByte(byte(c - 1)) + for j := 0; j < c; j++ { + w.WriteByte(src[start+j]) + } + if i == len(src) { + w.WriteByte(eod) + return + } + } else { + c = i - 1 - start + // Write variable run with length=c + w.WriteByte(byte(c - 1)) + for j := 0; j < c; j++ { + w.WriteByte(src[start+j]) + } + i-- + } + b = src[i] + start = i + } + +} + +// Encode implements encoding for a RunLengthDecode filter. +func (f runLengthDecode) Encode(r io.Reader) (io.Reader, error) { + + b1, err := getReaderBytes(r) + if err != nil { + return nil, err + } + + var b2 bytes.Buffer + f.encode(&b2, b1) + + return &b2, nil +} + +// Decode implements decoding for an RunLengthDecode filter. +func (f runLengthDecode) Decode(r io.Reader) (io.Reader, error) { + return f.DecodeLength(r, -1) +} + +func (f runLengthDecode) DecodeLength(r io.Reader, maxLen int64) (io.Reader, error) { + + b1, err := getReaderBytes(r) + if err != nil { + return nil, err + } + + var b2 bytes.Buffer + f.decode(&b2, b1, maxLen) + + return &b2, nil +} diff --git a/pkg/filter/runLengthDecode_test.go b/pkg/filter/runLengthDecode_test.go new file mode 100644 index 0000000000000000000000000000000000000000..1008cecc1e2a965a2797c135fb5791d23b62c70b --- /dev/null +++ b/pkg/filter/runLengthDecode_test.go @@ -0,0 +1,78 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 filter + +import ( + "bytes" + "encoding/hex" + "testing" +) + +func compare(t *testing.T, a, b []byte) { + + if len(a) != len(b) { + t.Errorf("length mismatch %d != %d", len(a), len(b)) + t.Logf("a:\n%s\n", hex.Dump(a)) + t.Logf("b:\n%s\n", hex.Dump(b)) + return + } + + for i := 0; i < len(a); i++ { + if a[i] != b[i] { + t.Errorf("mismatch at %d(0x%02x), 0x%02x != 0x%02x\n", i, i, a[i], b[i]) + t.Logf("a:\n%s\n", hex.Dump(a)) + t.Logf("b:\n%s\n", hex.Dump(b)) + return + } + } + +} + +func TestRunLengthEncoding(t *testing.T) { + + f := runLengthDecode{baseFilter{}} + + for _, tt := range []struct { + raw, enc string + }{ + {"\x01", "\x00\x01\x80"}, + {"\x01\x01", "\xFF\x01\x80"}, + {"\x00\x00\x02\x02", "\xFF\x00\xFF\x02\x80"}, + {"\x00\x00\x00", "\xFE\x00\x80"}, + {"\x00\x00\x00\x01", "\xFE\x00\x00\x01\x80"}, + {"\x00\x00\x00\x00", "\xFD\x00\x80"}, + {"\x00\x00\x00\x00\x00", "\xFC\x00\x80"}, + {"\x00\x00\x01", "\xFF\x00\x00\x01\x80"}, + {"\x00\x01", "\x01\x00\x01\x80"}, + {"\x00\x01\x02", "\x02\x00\x01\x02\x80"}, + {"\x00\x01\x02\x03", "\x03\x00\x01\x02\x03\x80"}, + {"\x00\x01\x02\x03\x02", "\x04\x00\x01\x02\x03\x02\x80"}, + {"\x00\x01", "\x01\x00\x01\x80"}, + {"\x00\x01\x01", "\x00\x00\xFF\x01\x80"}, + {"\x00\x01\x01\x01", "\x00\x00\xFE\x01\x80"}, + {"\x00\x00\x01\x02\x00\x00", "\xFF\x00\x01\x01\x02\xFF\x00\x80"}, + } { + var enc bytes.Buffer + f.encode(&enc, []byte(tt.raw)) + compare(t, enc.Bytes(), []byte(tt.enc)) + + var raw bytes.Buffer + f.decode(&raw, enc.Bytes(), -1) + compare(t, raw.Bytes(), []byte(tt.raw)) + } + +} diff --git a/pkg/filter/testdata/Mark.Twain-Tom.Sawyer.txt b/pkg/filter/testdata/Mark.Twain-Tom.Sawyer.txt new file mode 100644 index 0000000000000000000000000000000000000000..c9106fd522cec80b8de64958de765776a1476488 --- /dev/null +++ b/pkg/filter/testdata/Mark.Twain-Tom.Sawyer.txt @@ -0,0 +1,8465 @@ +Produced by David Widger. The previous edition was updated by Jose +Menendez. + + + + + + THE ADVENTURES OF TOM SAWYER + BY + MARK TWAIN + (Samuel Langhorne Clemens) + + + + + P R E F A C E + +MOST of the adventures recorded in this book really occurred; one or +two were experiences of my own, the rest those of boys who were +schoolmates of mine. Huck Finn is drawn from life; Tom Sawyer also, but +not from an individual--he is a combination of the characteristics of +three boys whom I knew, and therefore belongs to the composite order of +architecture. + +The odd superstitions touched upon were all prevalent among children +and slaves in the West at the period of this story--that is to say, +thirty or forty years ago. + +Although my book is intended mainly for the entertainment of boys and +girls, I hope it will not be shunned by men and women on that account, +for part of my plan has been to try to pleasantly remind adults of what +they once were themselves, and of how they felt and thought and talked, +and what queer enterprises they sometimes engaged in. + + THE AUTHOR. + +HARTFORD, 1876. + + + + T O M S A W Y E R + + + +CHAPTER I + +"TOM!" + +No answer. + +"TOM!" + +No answer. + +"What's gone with that boy, I wonder? You TOM!" + +No answer. + +The old lady pulled her spectacles down and looked over them about the +room; then she put them up and looked out under them. She seldom or +never looked THROUGH them for so small a thing as a boy; they were her +state pair, the pride of her heart, and were built for "style," not +service--she could have seen through a pair of stove-lids just as well. +She looked perplexed for a moment, and then said, not fiercely, but +still loud enough for the furniture to hear: + +"Well, I lay if I get hold of you I'll--" + +She did not finish, for by this time she was bending down and punching +under the bed with the broom, and so she needed breath to punctuate the +punches with. She resurrected nothing but the cat. + +"I never did see the beat of that boy!" + +She went to the open door and stood in it and looked out among the +tomato vines and "jimpson" weeds that constituted the garden. No Tom. +So she lifted up her voice at an angle calculated for distance and +shouted: + +"Y-o-u-u TOM!" + +There was a slight noise behind her and she turned just in time to +seize a small boy by the slack of his roundabout and arrest his flight. + +"There! I might 'a' thought of that closet. What you been doing in +there?" + +"Nothing." + +"Nothing! Look at your hands. And look at your mouth. What IS that +truck?" + +"I don't know, aunt." + +"Well, I know. It's jam--that's what it is. Forty times I've said if +you didn't let that jam alone I'd skin you. Hand me that switch." + +The switch hovered in the air--the peril was desperate-- + +"My! Look behind you, aunt!" + +The old lady whirled round, and snatched her skirts out of danger. The +lad fled on the instant, scrambled up the high board-fence, and +disappeared over it. + +His aunt Polly stood surprised a moment, and then broke into a gentle +laugh. + +"Hang the boy, can't I never learn anything? Ain't he played me tricks +enough like that for me to be looking out for him by this time? But old +fools is the biggest fools there is. Can't learn an old dog new tricks, +as the saying is. But my goodness, he never plays them alike, two days, +and how is a body to know what's coming? He 'pears to know just how +long he can torment me before I get my dander up, and he knows if he +can make out to put me off for a minute or make me laugh, it's all down +again and I can't hit him a lick. I ain't doing my duty by that boy, +and that's the Lord's truth, goodness knows. Spare the rod and spile +the child, as the Good Book says. I'm a laying up sin and suffering for +us both, I know. He's full of the Old Scratch, but laws-a-me! he's my +own dead sister's boy, poor thing, and I ain't got the heart to lash +him, somehow. Every time I let him off, my conscience does hurt me so, +and every time I hit him my old heart most breaks. Well-a-well, man +that is born of woman is of few days and full of trouble, as the +Scripture says, and I reckon it's so. He'll play hookey this evening, * +and [* Southwestern for "afternoon"] I'll just be obleeged to make him +work, to-morrow, to punish him. It's mighty hard to make him work +Saturdays, when all the boys is having holiday, but he hates work more +than he hates anything else, and I've GOT to do some of my duty by him, +or I'll be the ruination of the child." + +Tom did play hookey, and he had a very good time. He got back home +barely in season to help Jim, the small colored boy, saw next-day's +wood and split the kindlings before supper--at least he was there in +time to tell his adventures to Jim while Jim did three-fourths of the +work. Tom's younger brother (or rather half-brother) Sid was already +through with his part of the work (picking up chips), for he was a +quiet boy, and had no adventurous, troublesome ways. + +While Tom was eating his supper, and stealing sugar as opportunity +offered, Aunt Polly asked him questions that were full of guile, and +very deep--for she wanted to trap him into damaging revealments. Like +many other simple-hearted souls, it was her pet vanity to believe she +was endowed with a talent for dark and mysterious diplomacy, and she +loved to contemplate her most transparent devices as marvels of low +cunning. Said she: + +"Tom, it was middling warm in school, warn't it?" + +"Yes'm." + +"Powerful warm, warn't it?" + +"Yes'm." + +"Didn't you want to go in a-swimming, Tom?" + +A bit of a scare shot through Tom--a touch of uncomfortable suspicion. +He searched Aunt Polly's face, but it told him nothing. So he said: + +"No'm--well, not very much." + +The old lady reached out her hand and felt Tom's shirt, and said: + +"But you ain't too warm now, though." And it flattered her to reflect +that she had discovered that the shirt was dry without anybody knowing +that that was what she had in her mind. But in spite of her, Tom knew +where the wind lay, now. So he forestalled what might be the next move: + +"Some of us pumped on our heads--mine's damp yet. See?" + +Aunt Polly was vexed to think she had overlooked that bit of +circumstantial evidence, and missed a trick. Then she had a new +inspiration: + +"Tom, you didn't have to undo your shirt collar where I sewed it, to +pump on your head, did you? Unbutton your jacket!" + +The trouble vanished out of Tom's face. He opened his jacket. His +shirt collar was securely sewed. + +"Bother! Well, go 'long with you. I'd made sure you'd played hookey +and been a-swimming. But I forgive ye, Tom. I reckon you're a kind of a +singed cat, as the saying is--better'n you look. THIS time." + +She was half sorry her sagacity had miscarried, and half glad that Tom +had stumbled into obedient conduct for once. + +But Sidney said: + +"Well, now, if I didn't think you sewed his collar with white thread, +but it's black." + +"Why, I did sew it with white! Tom!" + +But Tom did not wait for the rest. As he went out at the door he said: + +"Siddy, I'll lick you for that." + +In a safe place Tom examined two large needles which were thrust into +the lapels of his jacket, and had thread bound about them--one needle +carried white thread and the other black. He said: + +"She'd never noticed if it hadn't been for Sid. Confound it! sometimes +she sews it with white, and sometimes she sews it with black. I wish to +geeminy she'd stick to one or t'other--I can't keep the run of 'em. But +I bet you I'll lam Sid for that. I'll learn him!" + +He was not the Model Boy of the village. He knew the model boy very +well though--and loathed him. + +Within two minutes, or even less, he had forgotten all his troubles. +Not because his troubles were one whit less heavy and bitter to him +than a man's are to a man, but because a new and powerful interest bore +them down and drove them out of his mind for the time--just as men's +misfortunes are forgotten in the excitement of new enterprises. This +new interest was a valued novelty in whistling, which he had just +acquired from a negro, and he was suffering to practise it undisturbed. +It consisted in a peculiar bird-like turn, a sort of liquid warble, +produced by touching the tongue to the roof of the mouth at short +intervals in the midst of the music--the reader probably remembers how +to do it, if he has ever been a boy. Diligence and attention soon gave +him the knack of it, and he strode down the street with his mouth full +of harmony and his soul full of gratitude. He felt much as an +astronomer feels who has discovered a new planet--no doubt, as far as +strong, deep, unalloyed pleasure is concerned, the advantage was with +the boy, not the astronomer. + +The summer evenings were long. It was not dark, yet. Presently Tom +checked his whistle. A stranger was before him--a boy a shade larger +than himself. A new-comer of any age or either sex was an impressive +curiosity in the poor little shabby village of St. Petersburg. This boy +was well dressed, too--well dressed on a week-day. This was simply +astounding. His cap was a dainty thing, his close-buttoned blue cloth +roundabout was new and natty, and so were his pantaloons. He had shoes +on--and it was only Friday. He even wore a necktie, a bright bit of +ribbon. He had a citified air about him that ate into Tom's vitals. The +more Tom stared at the splendid marvel, the higher he turned up his +nose at his finery and the shabbier and shabbier his own outfit seemed +to him to grow. Neither boy spoke. If one moved, the other moved--but +only sidewise, in a circle; they kept face to face and eye to eye all +the time. Finally Tom said: + +"I can lick you!" + +"I'd like to see you try it." + +"Well, I can do it." + +"No you can't, either." + +"Yes I can." + +"No you can't." + +"I can." + +"You can't." + +"Can!" + +"Can't!" + +An uncomfortable pause. Then Tom said: + +"What's your name?" + +"'Tisn't any of your business, maybe." + +"Well I 'low I'll MAKE it my business." + +"Well why don't you?" + +"If you say much, I will." + +"Much--much--MUCH. There now." + +"Oh, you think you're mighty smart, DON'T you? I could lick you with +one hand tied behind me, if I wanted to." + +"Well why don't you DO it? You SAY you can do it." + +"Well I WILL, if you fool with me." + +"Oh yes--I've seen whole families in the same fix." + +"Smarty! You think you're SOME, now, DON'T you? Oh, what a hat!" + +"You can lump that hat if you don't like it. I dare you to knock it +off--and anybody that'll take a dare will suck eggs." + +"You're a liar!" + +"You're another." + +"You're a fighting liar and dasn't take it up." + +"Aw--take a walk!" + +"Say--if you give me much more of your sass I'll take and bounce a +rock off'n your head." + +"Oh, of COURSE you will." + +"Well I WILL." + +"Well why don't you DO it then? What do you keep SAYING you will for? +Why don't you DO it? It's because you're afraid." + +"I AIN'T afraid." + +"You are." + +"I ain't." + +"You are." + +Another pause, and more eying and sidling around each other. Presently +they were shoulder to shoulder. Tom said: + +"Get away from here!" + +"Go away yourself!" + +"I won't." + +"I won't either." + +So they stood, each with a foot placed at an angle as a brace, and +both shoving with might and main, and glowering at each other with +hate. But neither could get an advantage. After struggling till both +were hot and flushed, each relaxed his strain with watchful caution, +and Tom said: + +"You're a coward and a pup. I'll tell my big brother on you, and he +can thrash you with his little finger, and I'll make him do it, too." + +"What do I care for your big brother? I've got a brother that's bigger +than he is--and what's more, he can throw him over that fence, too." +[Both brothers were imaginary.] + +"That's a lie." + +"YOUR saying so don't make it so." + +Tom drew a line in the dust with his big toe, and said: + +"I dare you to step over that, and I'll lick you till you can't stand +up. Anybody that'll take a dare will steal sheep." + +The new boy stepped over promptly, and said: + +"Now you said you'd do it, now let's see you do it." + +"Don't you crowd me now; you better look out." + +"Well, you SAID you'd do it--why don't you do it?" + +"By jingo! for two cents I WILL do it." + +The new boy took two broad coppers out of his pocket and held them out +with derision. Tom struck them to the ground. In an instant both boys +were rolling and tumbling in the dirt, gripped together like cats; and +for the space of a minute they tugged and tore at each other's hair and +clothes, punched and scratched each other's nose, and covered +themselves with dust and glory. Presently the confusion took form, and +through the fog of battle Tom appeared, seated astride the new boy, and +pounding him with his fists. "Holler 'nuff!" said he. + +The boy only struggled to free himself. He was crying--mainly from rage. + +"Holler 'nuff!"--and the pounding went on. + +At last the stranger got out a smothered "'Nuff!" and Tom let him up +and said: + +"Now that'll learn you. Better look out who you're fooling with next +time." + +The new boy went off brushing the dust from his clothes, sobbing, +snuffling, and occasionally looking back and shaking his head and +threatening what he would do to Tom the "next time he caught him out." +To which Tom responded with jeers, and started off in high feather, and +as soon as his back was turned the new boy snatched up a stone, threw +it and hit him between the shoulders and then turned tail and ran like +an antelope. Tom chased the traitor home, and thus found out where he +lived. He then held a position at the gate for some time, daring the +enemy to come outside, but the enemy only made faces at him through the +window and declined. At last the enemy's mother appeared, and called +Tom a bad, vicious, vulgar child, and ordered him away. So he went +away; but he said he "'lowed" to "lay" for that boy. + +He got home pretty late that night, and when he climbed cautiously in +at the window, he uncovered an ambuscade, in the person of his aunt; +and when she saw the state his clothes were in her resolution to turn +his Saturday holiday into captivity at hard labor became adamantine in +its firmness. + + + +CHAPTER II + +SATURDAY morning was come, and all the summer world was bright and +fresh, and brimming with life. There was a song in every heart; and if +the heart was young the music issued at the lips. There was cheer in +every face and a spring in every step. The locust-trees were in bloom +and the fragrance of the blossoms filled the air. Cardiff Hill, beyond +the village and above it, was green with vegetation and it lay just far +enough away to seem a Delectable Land, dreamy, reposeful, and inviting. + +Tom appeared on the sidewalk with a bucket of whitewash and a +long-handled brush. He surveyed the fence, and all gladness left him and +a deep melancholy settled down upon his spirit. Thirty yards of board +fence nine feet high. Life to him seemed hollow, and existence but a +burden. Sighing, he dipped his brush and passed it along the topmost +plank; repeated the operation; did it again; compared the insignificant +whitewashed streak with the far-reaching continent of unwhitewashed +fence, and sat down on a tree-box discouraged. Jim came skipping out at +the gate with a tin pail, and singing Buffalo Gals. Bringing water from +the town pump had always been hateful work in Tom's eyes, before, but +now it did not strike him so. He remembered that there was company at +the pump. White, mulatto, and negro boys and girls were always there +waiting their turns, resting, trading playthings, quarrelling, +fighting, skylarking. And he remembered that although the pump was only +a hundred and fifty yards off, Jim never got back with a bucket of +water under an hour--and even then somebody generally had to go after +him. Tom said: + +"Say, Jim, I'll fetch the water if you'll whitewash some." + +Jim shook his head and said: + +"Can't, Mars Tom. Ole missis, she tole me I got to go an' git dis +water an' not stop foolin' roun' wid anybody. She say she spec' Mars +Tom gwine to ax me to whitewash, an' so she tole me go 'long an' 'tend +to my own business--she 'lowed SHE'D 'tend to de whitewashin'." + +"Oh, never you mind what she said, Jim. That's the way she always +talks. Gimme the bucket--I won't be gone only a a minute. SHE won't +ever know." + +"Oh, I dasn't, Mars Tom. Ole missis she'd take an' tar de head off'n +me. 'Deed she would." + +"SHE! She never licks anybody--whacks 'em over the head with her +thimble--and who cares for that, I'd like to know. She talks awful, but +talk don't hurt--anyways it don't if she don't cry. Jim, I'll give you +a marvel. I'll give you a white alley!" + +Jim began to waver. + +"White alley, Jim! And it's a bully taw." + +"My! Dat's a mighty gay marvel, I tell you! But Mars Tom I's powerful +'fraid ole missis--" + +"And besides, if you will I'll show you my sore toe." + +Jim was only human--this attraction was too much for him. He put down +his pail, took the white alley, and bent over the toe with absorbing +interest while the bandage was being unwound. In another moment he was +flying down the street with his pail and a tingling rear, Tom was +whitewashing with vigor, and Aunt Polly was retiring from the field +with a slipper in her hand and triumph in her eye. + +But Tom's energy did not last. He began to think of the fun he had +planned for this day, and his sorrows multiplied. Soon the free boys +would come tripping along on all sorts of delicious expeditions, and +they would make a world of fun of him for having to work--the very +thought of it burnt him like fire. He got out his worldly wealth and +examined it--bits of toys, marbles, and trash; enough to buy an +exchange of WORK, maybe, but not half enough to buy so much as half an +hour of pure freedom. So he returned his straitened means to his +pocket, and gave up the idea of trying to buy the boys. At this dark +and hopeless moment an inspiration burst upon him! Nothing less than a +great, magnificent inspiration. + +He took up his brush and went tranquilly to work. Ben Rogers hove in +sight presently--the very boy, of all boys, whose ridicule he had been +dreading. Ben's gait was the hop-skip-and-jump--proof enough that his +heart was light and his anticipations high. He was eating an apple, and +giving a long, melodious whoop, at intervals, followed by a deep-toned +ding-dong-dong, ding-dong-dong, for he was personating a steamboat. As +he drew near, he slackened speed, took the middle of the street, leaned +far over to starboard and rounded to ponderously and with laborious +pomp and circumstance--for he was personating the Big Missouri, and +considered himself to be drawing nine feet of water. He was boat and +captain and engine-bells combined, so he had to imagine himself +standing on his own hurricane-deck giving the orders and executing them: + +"Stop her, sir! Ting-a-ling-ling!" The headway ran almost out, and he +drew up slowly toward the sidewalk. + +"Ship up to back! Ting-a-ling-ling!" His arms straightened and +stiffened down his sides. + +"Set her back on the stabboard! Ting-a-ling-ling! Chow! ch-chow-wow! +Chow!" His right hand, meantime, describing stately circles--for it was +representing a forty-foot wheel. + +"Let her go back on the labboard! Ting-a-lingling! Chow-ch-chow-chow!" +The left hand began to describe circles. + +"Stop the stabboard! Ting-a-ling-ling! Stop the labboard! Come ahead +on the stabboard! Stop her! Let your outside turn over slow! +Ting-a-ling-ling! Chow-ow-ow! Get out that head-line! LIVELY now! +Come--out with your spring-line--what're you about there! Take a turn +round that stump with the bight of it! Stand by that stage, now--let her +go! Done with the engines, sir! Ting-a-ling-ling! SH'T! S'H'T! SH'T!" +(trying the gauge-cocks). + +Tom went on whitewashing--paid no attention to the steamboat. Ben +stared a moment and then said: "Hi-YI! YOU'RE up a stump, ain't you!" + +No answer. Tom surveyed his last touch with the eye of an artist, then +he gave his brush another gentle sweep and surveyed the result, as +before. Ben ranged up alongside of him. Tom's mouth watered for the +apple, but he stuck to his work. Ben said: + +"Hello, old chap, you got to work, hey?" + +Tom wheeled suddenly and said: + +"Why, it's you, Ben! I warn't noticing." + +"Say--I'm going in a-swimming, I am. Don't you wish you could? But of +course you'd druther WORK--wouldn't you? Course you would!" + +Tom contemplated the boy a bit, and said: + +"What do you call work?" + +"Why, ain't THAT work?" + +Tom resumed his whitewashing, and answered carelessly: + +"Well, maybe it is, and maybe it ain't. All I know, is, it suits Tom +Sawyer." + +"Oh come, now, you don't mean to let on that you LIKE it?" + +The brush continued to move. + +"Like it? Well, I don't see why I oughtn't to like it. Does a boy get +a chance to whitewash a fence every day?" + +That put the thing in a new light. Ben stopped nibbling his apple. Tom +swept his brush daintily back and forth--stepped back to note the +effect--added a touch here and there--criticised the effect again--Ben +watching every move and getting more and more interested, more and more +absorbed. Presently he said: + +"Say, Tom, let ME whitewash a little." + +Tom considered, was about to consent; but he altered his mind: + +"No--no--I reckon it wouldn't hardly do, Ben. You see, Aunt Polly's +awful particular about this fence--right here on the street, you know +--but if it was the back fence I wouldn't mind and SHE wouldn't. Yes, +she's awful particular about this fence; it's got to be done very +careful; I reckon there ain't one boy in a thousand, maybe two +thousand, that can do it the way it's got to be done." + +"No--is that so? Oh come, now--lemme just try. Only just a little--I'd +let YOU, if you was me, Tom." + +"Ben, I'd like to, honest injun; but Aunt Polly--well, Jim wanted to +do it, but she wouldn't let him; Sid wanted to do it, and she wouldn't +let Sid. Now don't you see how I'm fixed? If you was to tackle this +fence and anything was to happen to it--" + +"Oh, shucks, I'll be just as careful. Now lemme try. Say--I'll give +you the core of my apple." + +"Well, here--No, Ben, now don't. I'm afeard--" + +"I'll give you ALL of it!" + +Tom gave up the brush with reluctance in his face, but alacrity in his +heart. And while the late steamer Big Missouri worked and sweated in +the sun, the retired artist sat on a barrel in the shade close by, +dangled his legs, munched his apple, and planned the slaughter of more +innocents. There was no lack of material; boys happened along every +little while; they came to jeer, but remained to whitewash. By the time +Ben was fagged out, Tom had traded the next chance to Billy Fisher for +a kite, in good repair; and when he played out, Johnny Miller bought in +for a dead rat and a string to swing it with--and so on, and so on, +hour after hour. And when the middle of the afternoon came, from being +a poor poverty-stricken boy in the morning, Tom was literally rolling +in wealth. He had besides the things before mentioned, twelve marbles, +part of a jews-harp, a piece of blue bottle-glass to look through, a +spool cannon, a key that wouldn't unlock anything, a fragment of chalk, +a glass stopper of a decanter, a tin soldier, a couple of tadpoles, six +fire-crackers, a kitten with only one eye, a brass doorknob, a +dog-collar--but no dog--the handle of a knife, four pieces of +orange-peel, and a dilapidated old window sash. + +He had had a nice, good, idle time all the while--plenty of company +--and the fence had three coats of whitewash on it! If he hadn't run out +of whitewash he would have bankrupted every boy in the village. + +Tom said to himself that it was not such a hollow world, after all. He +had discovered a great law of human action, without knowing it--namely, +that in order to make a man or a boy covet a thing, it is only +necessary to make the thing difficult to attain. If he had been a great +and wise philosopher, like the writer of this book, he would now have +comprehended that Work consists of whatever a body is OBLIGED to do, +and that Play consists of whatever a body is not obliged to do. And +this would help him to understand why constructing artificial flowers +or performing on a tread-mill is work, while rolling ten-pins or +climbing Mont Blanc is only amusement. There are wealthy gentlemen in +England who drive four-horse passenger-coaches twenty or thirty miles +on a daily line, in the summer, because the privilege costs them +considerable money; but if they were offered wages for the service, +that would turn it into work and then they would resign. + +The boy mused awhile over the substantial change which had taken place +in his worldly circumstances, and then wended toward headquarters to +report. + + + +CHAPTER III + +TOM presented himself before Aunt Polly, who was sitting by an open +window in a pleasant rearward apartment, which was bedroom, +breakfast-room, dining-room, and library, combined. The balmy summer +air, the restful quiet, the odor of the flowers, and the drowsing murmur +of the bees had had their effect, and she was nodding over her knitting +--for she had no company but the cat, and it was asleep in her lap. Her +spectacles were propped up on her gray head for safety. She had thought +that of course Tom had deserted long ago, and she wondered at seeing him +place himself in her power again in this intrepid way. He said: "Mayn't +I go and play now, aunt?" + +"What, a'ready? How much have you done?" + +"It's all done, aunt." + +"Tom, don't lie to me--I can't bear it." + +"I ain't, aunt; it IS all done." + +Aunt Polly placed small trust in such evidence. She went out to see +for herself; and she would have been content to find twenty per cent. +of Tom's statement true. When she found the entire fence whitewashed, +and not only whitewashed but elaborately coated and recoated, and even +a streak added to the ground, her astonishment was almost unspeakable. +She said: + +"Well, I never! There's no getting round it, you can work when you're +a mind to, Tom." And then she diluted the compliment by adding, "But +it's powerful seldom you're a mind to, I'm bound to say. Well, go 'long +and play; but mind you get back some time in a week, or I'll tan you." + +She was so overcome by the splendor of his achievement that she took +him into the closet and selected a choice apple and delivered it to +him, along with an improving lecture upon the added value and flavor a +treat took to itself when it came without sin through virtuous effort. +And while she closed with a happy Scriptural flourish, he "hooked" a +doughnut. + +Then he skipped out, and saw Sid just starting up the outside stairway +that led to the back rooms on the second floor. Clods were handy and +the air was full of them in a twinkling. They raged around Sid like a +hail-storm; and before Aunt Polly could collect her surprised faculties +and sally to the rescue, six or seven clods had taken personal effect, +and Tom was over the fence and gone. There was a gate, but as a general +thing he was too crowded for time to make use of it. His soul was at +peace, now that he had settled with Sid for calling attention to his +black thread and getting him into trouble. + +Tom skirted the block, and came round into a muddy alley that led by +the back of his aunt's cow-stable. He presently got safely beyond the +reach of capture and punishment, and hastened toward the public square +of the village, where two "military" companies of boys had met for +conflict, according to previous appointment. Tom was General of one of +these armies, Joe Harper (a bosom friend) General of the other. These +two great commanders did not condescend to fight in person--that being +better suited to the still smaller fry--but sat together on an eminence +and conducted the field operations by orders delivered through +aides-de-camp. Tom's army won a great victory, after a long and +hard-fought battle. Then the dead were counted, prisoners exchanged, +the terms of the next disagreement agreed upon, and the day for the +necessary battle appointed; after which the armies fell into line and +marched away, and Tom turned homeward alone. + +As he was passing by the house where Jeff Thatcher lived, he saw a new +girl in the garden--a lovely little blue-eyed creature with yellow hair +plaited into two long-tails, white summer frock and embroidered +pantalettes. The fresh-crowned hero fell without firing a shot. A +certain Amy Lawrence vanished out of his heart and left not even a +memory of herself behind. He had thought he loved her to distraction; +he had regarded his passion as adoration; and behold it was only a poor +little evanescent partiality. He had been months winning her; she had +confessed hardly a week ago; he had been the happiest and the proudest +boy in the world only seven short days, and here in one instant of time +she had gone out of his heart like a casual stranger whose visit is +done. + +He worshipped this new angel with furtive eye, till he saw that she +had discovered him; then he pretended he did not know she was present, +and began to "show off" in all sorts of absurd boyish ways, in order to +win her admiration. He kept up this grotesque foolishness for some +time; but by-and-by, while he was in the midst of some dangerous +gymnastic performances, he glanced aside and saw that the little girl +was wending her way toward the house. Tom came up to the fence and +leaned on it, grieving, and hoping she would tarry yet awhile longer. +She halted a moment on the steps and then moved toward the door. Tom +heaved a great sigh as she put her foot on the threshold. But his face +lit up, right away, for she tossed a pansy over the fence a moment +before she disappeared. + +The boy ran around and stopped within a foot or two of the flower, and +then shaded his eyes with his hand and began to look down street as if +he had discovered something of interest going on in that direction. +Presently he picked up a straw and began trying to balance it on his +nose, with his head tilted far back; and as he moved from side to side, +in his efforts, he edged nearer and nearer toward the pansy; finally +his bare foot rested upon it, his pliant toes closed upon it, and he +hopped away with the treasure and disappeared round the corner. But +only for a minute--only while he could button the flower inside his +jacket, next his heart--or next his stomach, possibly, for he was not +much posted in anatomy, and not hypercritical, anyway. + +He returned, now, and hung about the fence till nightfall, "showing +off," as before; but the girl never exhibited herself again, though Tom +comforted himself a little with the hope that she had been near some +window, meantime, and been aware of his attentions. Finally he strode +home reluctantly, with his poor head full of visions. + +All through supper his spirits were so high that his aunt wondered +"what had got into the child." He took a good scolding about clodding +Sid, and did not seem to mind it in the least. He tried to steal sugar +under his aunt's very nose, and got his knuckles rapped for it. He said: + +"Aunt, you don't whack Sid when he takes it." + +"Well, Sid don't torment a body the way you do. You'd be always into +that sugar if I warn't watching you." + +Presently she stepped into the kitchen, and Sid, happy in his +immunity, reached for the sugar-bowl--a sort of glorying over Tom which +was wellnigh unbearable. But Sid's fingers slipped and the bowl dropped +and broke. Tom was in ecstasies. In such ecstasies that he even +controlled his tongue and was silent. He said to himself that he would +not speak a word, even when his aunt came in, but would sit perfectly +still till she asked who did the mischief; and then he would tell, and +there would be nothing so good in the world as to see that pet model +"catch it." He was so brimful of exultation that he could hardly hold +himself when the old lady came back and stood above the wreck +discharging lightnings of wrath from over her spectacles. He said to +himself, "Now it's coming!" And the next instant he was sprawling on +the floor! The potent palm was uplifted to strike again when Tom cried +out: + +"Hold on, now, what 'er you belting ME for?--Sid broke it!" + +Aunt Polly paused, perplexed, and Tom looked for healing pity. But +when she got her tongue again, she only said: + +"Umf! Well, you didn't get a lick amiss, I reckon. You been into some +other audacious mischief when I wasn't around, like enough." + +Then her conscience reproached her, and she yearned to say something +kind and loving; but she judged that this would be construed into a +confession that she had been in the wrong, and discipline forbade that. +So she kept silence, and went about her affairs with a troubled heart. +Tom sulked in a corner and exalted his woes. He knew that in her heart +his aunt was on her knees to him, and he was morosely gratified by the +consciousness of it. He would hang out no signals, he would take notice +of none. He knew that a yearning glance fell upon him, now and then, +through a film of tears, but he refused recognition of it. He pictured +himself lying sick unto death and his aunt bending over him beseeching +one little forgiving word, but he would turn his face to the wall, and +die with that word unsaid. Ah, how would she feel then? And he pictured +himself brought home from the river, dead, with his curls all wet, and +his sore heart at rest. How she would throw herself upon him, and how +her tears would fall like rain, and her lips pray God to give her back +her boy and she would never, never abuse him any more! But he would lie +there cold and white and make no sign--a poor little sufferer, whose +griefs were at an end. He so worked upon his feelings with the pathos +of these dreams, that he had to keep swallowing, he was so like to +choke; and his eyes swam in a blur of water, which overflowed when he +winked, and ran down and trickled from the end of his nose. And such a +luxury to him was this petting of his sorrows, that he could not bear +to have any worldly cheeriness or any grating delight intrude upon it; +it was too sacred for such contact; and so, presently, when his cousin +Mary danced in, all alive with the joy of seeing home again after an +age-long visit of one week to the country, he got up and moved in +clouds and darkness out at one door as she brought song and sunshine in +at the other. + +He wandered far from the accustomed haunts of boys, and sought +desolate places that were in harmony with his spirit. A log raft in the +river invited him, and he seated himself on its outer edge and +contemplated the dreary vastness of the stream, wishing, the while, +that he could only be drowned, all at once and unconsciously, without +undergoing the uncomfortable routine devised by nature. Then he thought +of his flower. He got it out, rumpled and wilted, and it mightily +increased his dismal felicity. He wondered if she would pity him if she +knew? Would she cry, and wish that she had a right to put her arms +around his neck and comfort him? Or would she turn coldly away like all +the hollow world? This picture brought such an agony of pleasurable +suffering that he worked it over and over again in his mind and set it +up in new and varied lights, till he wore it threadbare. At last he +rose up sighing and departed in the darkness. + +About half-past nine or ten o'clock he came along the deserted street +to where the Adored Unknown lived; he paused a moment; no sound fell +upon his listening ear; a candle was casting a dull glow upon the +curtain of a second-story window. Was the sacred presence there? He +climbed the fence, threaded his stealthy way through the plants, till +he stood under that window; he looked up at it long, and with emotion; +then he laid him down on the ground under it, disposing himself upon +his back, with his hands clasped upon his breast and holding his poor +wilted flower. And thus he would die--out in the cold world, with no +shelter over his homeless head, no friendly hand to wipe the +death-damps from his brow, no loving face to bend pityingly over him +when the great agony came. And thus SHE would see him when she looked +out upon the glad morning, and oh! would she drop one little tear upon +his poor, lifeless form, would she heave one little sigh to see a bright +young life so rudely blighted, so untimely cut down? + +The window went up, a maid-servant's discordant voice profaned the +holy calm, and a deluge of water drenched the prone martyr's remains! + +The strangling hero sprang up with a relieving snort. There was a whiz +as of a missile in the air, mingled with the murmur of a curse, a sound +as of shivering glass followed, and a small, vague form went over the +fence and shot away in the gloom. + +Not long after, as Tom, all undressed for bed, was surveying his +drenched garments by the light of a tallow dip, Sid woke up; but if he +had any dim idea of making any "references to allusions," he thought +better of it and held his peace, for there was danger in Tom's eye. + +Tom turned in without the added vexation of prayers, and Sid made +mental note of the omission. + + + +CHAPTER IV + +THE sun rose upon a tranquil world, and beamed down upon the peaceful +village like a benediction. Breakfast over, Aunt Polly had family +worship: it began with a prayer built from the ground up of solid +courses of Scriptural quotations, welded together with a thin mortar of +originality; and from the summit of this she delivered a grim chapter +of the Mosaic Law, as from Sinai. + +Then Tom girded up his loins, so to speak, and went to work to "get +his verses." Sid had learned his lesson days before. Tom bent all his +energies to the memorizing of five verses, and he chose part of the +Sermon on the Mount, because he could find no verses that were shorter. +At the end of half an hour Tom had a vague general idea of his lesson, +but no more, for his mind was traversing the whole field of human +thought, and his hands were busy with distracting recreations. Mary +took his book to hear him recite, and he tried to find his way through +the fog: + +"Blessed are the--a--a--" + +"Poor"-- + +"Yes--poor; blessed are the poor--a--a--" + +"In spirit--" + +"In spirit; blessed are the poor in spirit, for they--they--" + +"THEIRS--" + +"For THEIRS. Blessed are the poor in spirit, for theirs is the kingdom +of heaven. Blessed are they that mourn, for they--they--" + +"Sh--" + +"For they--a--" + +"S, H, A--" + +"For they S, H--Oh, I don't know what it is!" + +"SHALL!" + +"Oh, SHALL! for they shall--for they shall--a--a--shall mourn--a--a-- +blessed are they that shall--they that--a--they that shall mourn, for +they shall--a--shall WHAT? Why don't you tell me, Mary?--what do you +want to be so mean for?" + +"Oh, Tom, you poor thick-headed thing, I'm not teasing you. I wouldn't +do that. You must go and learn it again. Don't you be discouraged, Tom, +you'll manage it--and if you do, I'll give you something ever so nice. +There, now, that's a good boy." + +"All right! What is it, Mary, tell me what it is." + +"Never you mind, Tom. You know if I say it's nice, it is nice." + +"You bet you that's so, Mary. All right, I'll tackle it again." + +And he did "tackle it again"--and under the double pressure of +curiosity and prospective gain he did it with such spirit that he +accomplished a shining success. Mary gave him a brand-new "Barlow" +knife worth twelve and a half cents; and the convulsion of delight that +swept his system shook him to his foundations. True, the knife would +not cut anything, but it was a "sure-enough" Barlow, and there was +inconceivable grandeur in that--though where the Western boys ever got +the idea that such a weapon could possibly be counterfeited to its +injury is an imposing mystery and will always remain so, perhaps. Tom +contrived to scarify the cupboard with it, and was arranging to begin +on the bureau, when he was called off to dress for Sunday-school. + +Mary gave him a tin basin of water and a piece of soap, and he went +outside the door and set the basin on a little bench there; then he +dipped the soap in the water and laid it down; turned up his sleeves; +poured out the water on the ground, gently, and then entered the +kitchen and began to wipe his face diligently on the towel behind the +door. But Mary removed the towel and said: + +"Now ain't you ashamed, Tom. You mustn't be so bad. Water won't hurt +you." + +Tom was a trifle disconcerted. The basin was refilled, and this time +he stood over it a little while, gathering resolution; took in a big +breath and began. When he entered the kitchen presently, with both eyes +shut and groping for the towel with his hands, an honorable testimony +of suds and water was dripping from his face. But when he emerged from +the towel, he was not yet satisfactory, for the clean territory stopped +short at his chin and his jaws, like a mask; below and beyond this line +there was a dark expanse of unirrigated soil that spread downward in +front and backward around his neck. Mary took him in hand, and when she +was done with him he was a man and a brother, without distinction of +color, and his saturated hair was neatly brushed, and its short curls +wrought into a dainty and symmetrical general effect. [He privately +smoothed out the curls, with labor and difficulty, and plastered his +hair close down to his head; for he held curls to be effeminate, and +his own filled his life with bitterness.] Then Mary got out a suit of +his clothing that had been used only on Sundays during two years--they +were simply called his "other clothes"--and so by that we know the +size of his wardrobe. The girl "put him to rights" after he had dressed +himself; she buttoned his neat roundabout up to his chin, turned his +vast shirt collar down over his shoulders, brushed him off and crowned +him with his speckled straw hat. He now looked exceedingly improved and +uncomfortable. He was fully as uncomfortable as he looked; for there +was a restraint about whole clothes and cleanliness that galled him. He +hoped that Mary would forget his shoes, but the hope was blighted; she +coated them thoroughly with tallow, as was the custom, and brought them +out. He lost his temper and said he was always being made to do +everything he didn't want to do. But Mary said, persuasively: + +"Please, Tom--that's a good boy." + +So he got into the shoes snarling. Mary was soon ready, and the three +children set out for Sunday-school--a place that Tom hated with his +whole heart; but Sid and Mary were fond of it. + +Sabbath-school hours were from nine to half-past ten; and then church +service. Two of the children always remained for the sermon +voluntarily, and the other always remained too--for stronger reasons. +The church's high-backed, uncushioned pews would seat about three +hundred persons; the edifice was but a small, plain affair, with a sort +of pine board tree-box on top of it for a steeple. At the door Tom +dropped back a step and accosted a Sunday-dressed comrade: + +"Say, Billy, got a yaller ticket?" + +"Yes." + +"What'll you take for her?" + +"What'll you give?" + +"Piece of lickrish and a fish-hook." + +"Less see 'em." + +Tom exhibited. They were satisfactory, and the property changed hands. +Then Tom traded a couple of white alleys for three red tickets, and +some small trifle or other for a couple of blue ones. He waylaid other +boys as they came, and went on buying tickets of various colors ten or +fifteen minutes longer. He entered the church, now, with a swarm of +clean and noisy boys and girls, proceeded to his seat and started a +quarrel with the first boy that came handy. The teacher, a grave, +elderly man, interfered; then turned his back a moment and Tom pulled a +boy's hair in the next bench, and was absorbed in his book when the boy +turned around; stuck a pin in another boy, presently, in order to hear +him say "Ouch!" and got a new reprimand from his teacher. Tom's whole +class were of a pattern--restless, noisy, and troublesome. When they +came to recite their lessons, not one of them knew his verses +perfectly, but had to be prompted all along. However, they worried +through, and each got his reward--in small blue tickets, each with a +passage of Scripture on it; each blue ticket was pay for two verses of +the recitation. Ten blue tickets equalled a red one, and could be +exchanged for it; ten red tickets equalled a yellow one; for ten yellow +tickets the superintendent gave a very plainly bound Bible (worth forty +cents in those easy times) to the pupil. How many of my readers would +have the industry and application to memorize two thousand verses, even +for a Dore Bible? And yet Mary had acquired two Bibles in this way--it +was the patient work of two years--and a boy of German parentage had +won four or five. He once recited three thousand verses without +stopping; but the strain upon his mental faculties was too great, and +he was little better than an idiot from that day forth--a grievous +misfortune for the school, for on great occasions, before company, the +superintendent (as Tom expressed it) had always made this boy come out +and "spread himself." Only the older pupils managed to keep their +tickets and stick to their tedious work long enough to get a Bible, and +so the delivery of one of these prizes was a rare and noteworthy +circumstance; the successful pupil was so great and conspicuous for +that day that on the spot every scholar's heart was fired with a fresh +ambition that often lasted a couple of weeks. It is possible that Tom's +mental stomach had never really hungered for one of those prizes, but +unquestionably his entire being had for many a day longed for the glory +and the eclat that came with it. + +In due course the superintendent stood up in front of the pulpit, with +a closed hymn-book in his hand and his forefinger inserted between its +leaves, and commanded attention. When a Sunday-school superintendent +makes his customary little speech, a hymn-book in the hand is as +necessary as is the inevitable sheet of music in the hand of a singer +who stands forward on the platform and sings a solo at a concert +--though why, is a mystery: for neither the hymn-book nor the sheet of +music is ever referred to by the sufferer. This superintendent was a +slim creature of thirty-five, with a sandy goatee and short sandy hair; +he wore a stiff standing-collar whose upper edge almost reached his +ears and whose sharp points curved forward abreast the corners of his +mouth--a fence that compelled a straight lookout ahead, and a turning +of the whole body when a side view was required; his chin was propped +on a spreading cravat which was as broad and as long as a bank-note, +and had fringed ends; his boot toes were turned sharply up, in the +fashion of the day, like sleigh-runners--an effect patiently and +laboriously produced by the young men by sitting with their toes +pressed against a wall for hours together. Mr. Walters was very earnest +of mien, and very sincere and honest at heart; and he held sacred +things and places in such reverence, and so separated them from worldly +matters, that unconsciously to himself his Sunday-school voice had +acquired a peculiar intonation which was wholly absent on week-days. He +began after this fashion: + +"Now, children, I want you all to sit up just as straight and pretty +as you can and give me all your attention for a minute or two. There +--that is it. That is the way good little boys and girls should do. I see +one little girl who is looking out of the window--I am afraid she +thinks I am out there somewhere--perhaps up in one of the trees making +a speech to the little birds. [Applausive titter.] I want to tell you +how good it makes me feel to see so many bright, clean little faces +assembled in a place like this, learning to do right and be good." And +so forth and so on. It is not necessary to set down the rest of the +oration. It was of a pattern which does not vary, and so it is familiar +to us all. + +The latter third of the speech was marred by the resumption of fights +and other recreations among certain of the bad boys, and by fidgetings +and whisperings that extended far and wide, washing even to the bases +of isolated and incorruptible rocks like Sid and Mary. But now every +sound ceased suddenly, with the subsidence of Mr. Walters' voice, and +the conclusion of the speech was received with a burst of silent +gratitude. + +A good part of the whispering had been occasioned by an event which +was more or less rare--the entrance of visitors: lawyer Thatcher, +accompanied by a very feeble and aged man; a fine, portly, middle-aged +gentleman with iron-gray hair; and a dignified lady who was doubtless +the latter's wife. The lady was leading a child. Tom had been restless +and full of chafings and repinings; conscience-smitten, too--he could +not meet Amy Lawrence's eye, he could not brook her loving gaze. But +when he saw this small new-comer his soul was all ablaze with bliss in +a moment. The next moment he was "showing off" with all his might +--cuffing boys, pulling hair, making faces--in a word, using every art +that seemed likely to fascinate a girl and win her applause. His +exaltation had but one alloy--the memory of his humiliation in this +angel's garden--and that record in sand was fast washing out, under +the waves of happiness that were sweeping over it now. + +The visitors were given the highest seat of honor, and as soon as Mr. +Walters' speech was finished, he introduced them to the school. The +middle-aged man turned out to be a prodigious personage--no less a one +than the county judge--altogether the most august creation these +children had ever looked upon--and they wondered what kind of material +he was made of--and they half wanted to hear him roar, and were half +afraid he might, too. He was from Constantinople, twelve miles away--so +he had travelled, and seen the world--these very eyes had looked upon +the county court-house--which was said to have a tin roof. The awe +which these reflections inspired was attested by the impressive silence +and the ranks of staring eyes. This was the great Judge Thatcher, +brother of their own lawyer. Jeff Thatcher immediately went forward, to +be familiar with the great man and be envied by the school. It would +have been music to his soul to hear the whisperings: + +"Look at him, Jim! He's a going up there. Say--look! he's a going to +shake hands with him--he IS shaking hands with him! By jings, don't you +wish you was Jeff?" + +Mr. Walters fell to "showing off," with all sorts of official +bustlings and activities, giving orders, delivering judgments, +discharging directions here, there, everywhere that he could find a +target. The librarian "showed off"--running hither and thither with his +arms full of books and making a deal of the splutter and fuss that +insect authority delights in. The young lady teachers "showed off" +--bending sweetly over pupils that were lately being boxed, lifting +pretty warning fingers at bad little boys and patting good ones +lovingly. The young gentlemen teachers "showed off" with small +scoldings and other little displays of authority and fine attention to +discipline--and most of the teachers, of both sexes, found business up +at the library, by the pulpit; and it was business that frequently had +to be done over again two or three times (with much seeming vexation). +The little girls "showed off" in various ways, and the little boys +"showed off" with such diligence that the air was thick with paper wads +and the murmur of scufflings. And above it all the great man sat and +beamed a majestic judicial smile upon all the house, and warmed himself +in the sun of his own grandeur--for he was "showing off," too. + +There was only one thing wanting to make Mr. Walters' ecstasy +complete, and that was a chance to deliver a Bible-prize and exhibit a +prodigy. Several pupils had a few yellow tickets, but none had enough +--he had been around among the star pupils inquiring. He would have given +worlds, now, to have that German lad back again with a sound mind. + +And now at this moment, when hope was dead, Tom Sawyer came forward +with nine yellow tickets, nine red tickets, and ten blue ones, and +demanded a Bible. This was a thunderbolt out of a clear sky. Walters +was not expecting an application from this source for the next ten +years. But there was no getting around it--here were the certified +checks, and they were good for their face. Tom was therefore elevated +to a place with the Judge and the other elect, and the great news was +announced from headquarters. It was the most stunning surprise of the +decade, and so profound was the sensation that it lifted the new hero +up to the judicial one's altitude, and the school had two marvels to +gaze upon in place of one. The boys were all eaten up with envy--but +those that suffered the bitterest pangs were those who perceived too +late that they themselves had contributed to this hated splendor by +trading tickets to Tom for the wealth he had amassed in selling +whitewashing privileges. These despised themselves, as being the dupes +of a wily fraud, a guileful snake in the grass. + +The prize was delivered to Tom with as much effusion as the +superintendent could pump up under the circumstances; but it lacked +somewhat of the true gush, for the poor fellow's instinct taught him +that there was a mystery here that could not well bear the light, +perhaps; it was simply preposterous that this boy had warehoused two +thousand sheaves of Scriptural wisdom on his premises--a dozen would +strain his capacity, without a doubt. + +Amy Lawrence was proud and glad, and she tried to make Tom see it in +her face--but he wouldn't look. She wondered; then she was just a grain +troubled; next a dim suspicion came and went--came again; she watched; +a furtive glance told her worlds--and then her heart broke, and she was +jealous, and angry, and the tears came and she hated everybody. Tom +most of all (she thought). + +Tom was introduced to the Judge; but his tongue was tied, his breath +would hardly come, his heart quaked--partly because of the awful +greatness of the man, but mainly because he was her parent. He would +have liked to fall down and worship him, if it were in the dark. The +Judge put his hand on Tom's head and called him a fine little man, and +asked him what his name was. The boy stammered, gasped, and got it out: + +"Tom." + +"Oh, no, not Tom--it is--" + +"Thomas." + +"Ah, that's it. I thought there was more to it, maybe. That's very +well. But you've another one I daresay, and you'll tell it to me, won't +you?" + +"Tell the gentleman your other name, Thomas," said Walters, "and say +sir. You mustn't forget your manners." + +"Thomas Sawyer--sir." + +"That's it! That's a good boy. Fine boy. Fine, manly little fellow. +Two thousand verses is a great many--very, very great many. And you +never can be sorry for the trouble you took to learn them; for +knowledge is worth more than anything there is in the world; it's what +makes great men and good men; you'll be a great man and a good man +yourself, some day, Thomas, and then you'll look back and say, It's all +owing to the precious Sunday-school privileges of my boyhood--it's all +owing to my dear teachers that taught me to learn--it's all owing to +the good superintendent, who encouraged me, and watched over me, and +gave me a beautiful Bible--a splendid elegant Bible--to keep and have +it all for my own, always--it's all owing to right bringing up! That is +what you will say, Thomas--and you wouldn't take any money for those +two thousand verses--no indeed you wouldn't. And now you wouldn't mind +telling me and this lady some of the things you've learned--no, I know +you wouldn't--for we are proud of little boys that learn. Now, no +doubt you know the names of all the twelve disciples. Won't you tell us +the names of the first two that were appointed?" + +Tom was tugging at a button-hole and looking sheepish. He blushed, +now, and his eyes fell. Mr. Walters' heart sank within him. He said to +himself, it is not possible that the boy can answer the simplest +question--why DID the Judge ask him? Yet he felt obliged to speak up +and say: + +"Answer the gentleman, Thomas--don't be afraid." + +Tom still hung fire. + +"Now I know you'll tell me," said the lady. "The names of the first +two disciples were--" + +"DAVID AND GOLIAH!" + +Let us draw the curtain of charity over the rest of the scene. + + + +CHAPTER V + +ABOUT half-past ten the cracked bell of the small church began to +ring, and presently the people began to gather for the morning sermon. +The Sunday-school children distributed themselves about the house and +occupied pews with their parents, so as to be under supervision. Aunt +Polly came, and Tom and Sid and Mary sat with her--Tom being placed +next the aisle, in order that he might be as far away from the open +window and the seductive outside summer scenes as possible. The crowd +filed up the aisles: the aged and needy postmaster, who had seen better +days; the mayor and his wife--for they had a mayor there, among other +unnecessaries; the justice of the peace; the widow Douglass, fair, +smart, and forty, a generous, good-hearted soul and well-to-do, her +hill mansion the only palace in the town, and the most hospitable and +much the most lavish in the matter of festivities that St. Petersburg +could boast; the bent and venerable Major and Mrs. Ward; lawyer +Riverson, the new notable from a distance; next the belle of the +village, followed by a troop of lawn-clad and ribbon-decked young +heart-breakers; then all the young clerks in town in a body--for they +had stood in the vestibule sucking their cane-heads, a circling wall of +oiled and simpering admirers, till the last girl had run their gantlet; +and last of all came the Model Boy, Willie Mufferson, taking as heedful +care of his mother as if she were cut glass. He always brought his +mother to church, and was the pride of all the matrons. The boys all +hated him, he was so good. And besides, he had been "thrown up to them" +so much. His white handkerchief was hanging out of his pocket behind, as +usual on Sundays--accidentally. Tom had no handkerchief, and he looked +upon boys who had as snobs. + +The congregation being fully assembled, now, the bell rang once more, +to warn laggards and stragglers, and then a solemn hush fell upon the +church which was only broken by the tittering and whispering of the +choir in the gallery. The choir always tittered and whispered all +through service. There was once a church choir that was not ill-bred, +but I have forgotten where it was, now. It was a great many years ago, +and I can scarcely remember anything about it, but I think it was in +some foreign country. + +The minister gave out the hymn, and read it through with a relish, in +a peculiar style which was much admired in that part of the country. +His voice began on a medium key and climbed steadily up till it reached +a certain point, where it bore with strong emphasis upon the topmost +word and then plunged down as if from a spring-board: + + Shall I be car-ri-ed toe the skies, on flow'ry BEDS of ease, + + Whilst others fight to win the prize, and sail thro' BLOODY seas? + +He was regarded as a wonderful reader. At church "sociables" he was +always called upon to read poetry; and when he was through, the ladies +would lift up their hands and let them fall helplessly in their laps, +and "wall" their eyes, and shake their heads, as much as to say, "Words +cannot express it; it is too beautiful, TOO beautiful for this mortal +earth." + +After the hymn had been sung, the Rev. Mr. Sprague turned himself into +a bulletin-board, and read off "notices" of meetings and societies and +things till it seemed that the list would stretch out to the crack of +doom--a queer custom which is still kept up in America, even in cities, +away here in this age of abundant newspapers. Often, the less there is +to justify a traditional custom, the harder it is to get rid of it. + +And now the minister prayed. A good, generous prayer it was, and went +into details: it pleaded for the church, and the little children of the +church; for the other churches of the village; for the village itself; +for the county; for the State; for the State officers; for the United +States; for the churches of the United States; for Congress; for the +President; for the officers of the Government; for poor sailors, tossed +by stormy seas; for the oppressed millions groaning under the heel of +European monarchies and Oriental despotisms; for such as have the light +and the good tidings, and yet have not eyes to see nor ears to hear +withal; for the heathen in the far islands of the sea; and closed with +a supplication that the words he was about to speak might find grace +and favor, and be as seed sown in fertile ground, yielding in time a +grateful harvest of good. Amen. + +There was a rustling of dresses, and the standing congregation sat +down. The boy whose history this book relates did not enjoy the prayer, +he only endured it--if he even did that much. He was restive all +through it; he kept tally of the details of the prayer, unconsciously +--for he was not listening, but he knew the ground of old, and the +clergyman's regular route over it--and when a little trifle of new +matter was interlarded, his ear detected it and his whole nature +resented it; he considered additions unfair, and scoundrelly. In the +midst of the prayer a fly had lit on the back of the pew in front of +him and tortured his spirit by calmly rubbing its hands together, +embracing its head with its arms, and polishing it so vigorously that +it seemed to almost part company with the body, and the slender thread +of a neck was exposed to view; scraping its wings with its hind legs +and smoothing them to its body as if they had been coat-tails; going +through its whole toilet as tranquilly as if it knew it was perfectly +safe. As indeed it was; for as sorely as Tom's hands itched to grab for +it they did not dare--he believed his soul would be instantly destroyed +if he did such a thing while the prayer was going on. But with the +closing sentence his hand began to curve and steal forward; and the +instant the "Amen" was out the fly was a prisoner of war. His aunt +detected the act and made him let it go. + +The minister gave out his text and droned along monotonously through +an argument that was so prosy that many a head by and by began to nod +--and yet it was an argument that dealt in limitless fire and brimstone +and thinned the predestined elect down to a company so small as to be +hardly worth the saving. Tom counted the pages of the sermon; after +church he always knew how many pages there had been, but he seldom knew +anything else about the discourse. However, this time he was really +interested for a little while. The minister made a grand and moving +picture of the assembling together of the world's hosts at the +millennium when the lion and the lamb should lie down together and a +little child should lead them. But the pathos, the lesson, the moral of +the great spectacle were lost upon the boy; he only thought of the +conspicuousness of the principal character before the on-looking +nations; his face lit with the thought, and he said to himself that he +wished he could be that child, if it was a tame lion. + +Now he lapsed into suffering again, as the dry argument was resumed. +Presently he bethought him of a treasure he had and got it out. It was +a large black beetle with formidable jaws--a "pinchbug," he called it. +It was in a percussion-cap box. The first thing the beetle did was to +take him by the finger. A natural fillip followed, the beetle went +floundering into the aisle and lit on its back, and the hurt finger +went into the boy's mouth. The beetle lay there working its helpless +legs, unable to turn over. Tom eyed it, and longed for it; but it was +safe out of his reach. Other people uninterested in the sermon found +relief in the beetle, and they eyed it too. Presently a vagrant poodle +dog came idling along, sad at heart, lazy with the summer softness and +the quiet, weary of captivity, sighing for change. He spied the beetle; +the drooping tail lifted and wagged. He surveyed the prize; walked +around it; smelt at it from a safe distance; walked around it again; +grew bolder, and took a closer smell; then lifted his lip and made a +gingerly snatch at it, just missing it; made another, and another; +began to enjoy the diversion; subsided to his stomach with the beetle +between his paws, and continued his experiments; grew weary at last, +and then indifferent and absent-minded. His head nodded, and little by +little his chin descended and touched the enemy, who seized it. There +was a sharp yelp, a flirt of the poodle's head, and the beetle fell a +couple of yards away, and lit on its back once more. The neighboring +spectators shook with a gentle inward joy, several faces went behind +fans and handkerchiefs, and Tom was entirely happy. The dog looked +foolish, and probably felt so; but there was resentment in his heart, +too, and a craving for revenge. So he went to the beetle and began a +wary attack on it again; jumping at it from every point of a circle, +lighting with his fore-paws within an inch of the creature, making even +closer snatches at it with his teeth, and jerking his head till his +ears flapped again. But he grew tired once more, after a while; tried +to amuse himself with a fly but found no relief; followed an ant +around, with his nose close to the floor, and quickly wearied of that; +yawned, sighed, forgot the beetle entirely, and sat down on it. Then +there was a wild yelp of agony and the poodle went sailing up the +aisle; the yelps continued, and so did the dog; he crossed the house in +front of the altar; he flew down the other aisle; he crossed before the +doors; he clamored up the home-stretch; his anguish grew with his +progress, till presently he was but a woolly comet moving in its orbit +with the gleam and the speed of light. At last the frantic sufferer +sheered from its course, and sprang into its master's lap; he flung it +out of the window, and the voice of distress quickly thinned away and +died in the distance. + +By this time the whole church was red-faced and suffocating with +suppressed laughter, and the sermon had come to a dead standstill. The +discourse was resumed presently, but it went lame and halting, all +possibility of impressiveness being at an end; for even the gravest +sentiments were constantly being received with a smothered burst of +unholy mirth, under cover of some remote pew-back, as if the poor +parson had said a rarely facetious thing. It was a genuine relief to +the whole congregation when the ordeal was over and the benediction +pronounced. + +Tom Sawyer went home quite cheerful, thinking to himself that there +was some satisfaction about divine service when there was a bit of +variety in it. He had but one marring thought; he was willing that the +dog should play with his pinchbug, but he did not think it was upright +in him to carry it off. + + + +CHAPTER VI + +MONDAY morning found Tom Sawyer miserable. Monday morning always found +him so--because it began another week's slow suffering in school. He +generally began that day with wishing he had had no intervening +holiday, it made the going into captivity and fetters again so much +more odious. + +Tom lay thinking. Presently it occurred to him that he wished he was +sick; then he could stay home from school. Here was a vague +possibility. He canvassed his system. No ailment was found, and he +investigated again. This time he thought he could detect colicky +symptoms, and he began to encourage them with considerable hope. But +they soon grew feeble, and presently died wholly away. He reflected +further. Suddenly he discovered something. One of his upper front teeth +was loose. This was lucky; he was about to begin to groan, as a +"starter," as he called it, when it occurred to him that if he came +into court with that argument, his aunt would pull it out, and that +would hurt. So he thought he would hold the tooth in reserve for the +present, and seek further. Nothing offered for some little time, and +then he remembered hearing the doctor tell about a certain thing that +laid up a patient for two or three weeks and threatened to make him +lose a finger. So the boy eagerly drew his sore toe from under the +sheet and held it up for inspection. But now he did not know the +necessary symptoms. However, it seemed well worth while to chance it, +so he fell to groaning with considerable spirit. + +But Sid slept on unconscious. + +Tom groaned louder, and fancied that he began to feel pain in the toe. + +No result from Sid. + +Tom was panting with his exertions by this time. He took a rest and +then swelled himself up and fetched a succession of admirable groans. + +Sid snored on. + +Tom was aggravated. He said, "Sid, Sid!" and shook him. This course +worked well, and Tom began to groan again. Sid yawned, stretched, then +brought himself up on his elbow with a snort, and began to stare at +Tom. Tom went on groaning. Sid said: + +"Tom! Say, Tom!" [No response.] "Here, Tom! TOM! What is the matter, +Tom?" And he shook him and looked in his face anxiously. + +Tom moaned out: + +"Oh, don't, Sid. Don't joggle me." + +"Why, what's the matter, Tom? I must call auntie." + +"No--never mind. It'll be over by and by, maybe. Don't call anybody." + +"But I must! DON'T groan so, Tom, it's awful. How long you been this +way?" + +"Hours. Ouch! Oh, don't stir so, Sid, you'll kill me." + +"Tom, why didn't you wake me sooner? Oh, Tom, DON'T! It makes my +flesh crawl to hear you. Tom, what is the matter?" + +"I forgive you everything, Sid. [Groan.] Everything you've ever done +to me. When I'm gone--" + +"Oh, Tom, you ain't dying, are you? Don't, Tom--oh, don't. Maybe--" + +"I forgive everybody, Sid. [Groan.] Tell 'em so, Sid. And Sid, you +give my window-sash and my cat with one eye to that new girl that's +come to town, and tell her--" + +But Sid had snatched his clothes and gone. Tom was suffering in +reality, now, so handsomely was his imagination working, and so his +groans had gathered quite a genuine tone. + +Sid flew down-stairs and said: + +"Oh, Aunt Polly, come! Tom's dying!" + +"Dying!" + +"Yes'm. Don't wait--come quick!" + +"Rubbage! I don't believe it!" + +But she fled up-stairs, nevertheless, with Sid and Mary at her heels. +And her face grew white, too, and her lip trembled. When she reached +the bedside she gasped out: + +"You, Tom! Tom, what's the matter with you?" + +"Oh, auntie, I'm--" + +"What's the matter with you--what is the matter with you, child?" + +"Oh, auntie, my sore toe's mortified!" + +The old lady sank down into a chair and laughed a little, then cried a +little, then did both together. This restored her and she said: + +"Tom, what a turn you did give me. Now you shut up that nonsense and +climb out of this." + +The groans ceased and the pain vanished from the toe. The boy felt a +little foolish, and he said: + +"Aunt Polly, it SEEMED mortified, and it hurt so I never minded my +tooth at all." + +"Your tooth, indeed! What's the matter with your tooth?" + +"One of them's loose, and it aches perfectly awful." + +"There, there, now, don't begin that groaning again. Open your mouth. +Well--your tooth IS loose, but you're not going to die about that. +Mary, get me a silk thread, and a chunk of fire out of the kitchen." + +Tom said: + +"Oh, please, auntie, don't pull it out. It don't hurt any more. I wish +I may never stir if it does. Please don't, auntie. I don't want to stay +home from school." + +"Oh, you don't, don't you? So all this row was because you thought +you'd get to stay home from school and go a-fishing? Tom, Tom, I love +you so, and you seem to try every way you can to break my old heart +with your outrageousness." By this time the dental instruments were +ready. The old lady made one end of the silk thread fast to Tom's tooth +with a loop and tied the other to the bedpost. Then she seized the +chunk of fire and suddenly thrust it almost into the boy's face. The +tooth hung dangling by the bedpost, now. + +But all trials bring their compensations. As Tom wended to school +after breakfast, he was the envy of every boy he met because the gap in +his upper row of teeth enabled him to expectorate in a new and +admirable way. He gathered quite a following of lads interested in the +exhibition; and one that had cut his finger and had been a centre of +fascination and homage up to this time, now found himself suddenly +without an adherent, and shorn of his glory. His heart was heavy, and +he said with a disdain which he did not feel that it wasn't anything to +spit like Tom Sawyer; but another boy said, "Sour grapes!" and he +wandered away a dismantled hero. + +Shortly Tom came upon the juvenile pariah of the village, Huckleberry +Finn, son of the town drunkard. Huckleberry was cordially hated and +dreaded by all the mothers of the town, because he was idle and lawless +and vulgar and bad--and because all their children admired him so, and +delighted in his forbidden society, and wished they dared to be like +him. Tom was like the rest of the respectable boys, in that he envied +Huckleberry his gaudy outcast condition, and was under strict orders +not to play with him. So he played with him every time he got a chance. +Huckleberry was always dressed in the cast-off clothes of full-grown +men, and they were in perennial bloom and fluttering with rags. His hat +was a vast ruin with a wide crescent lopped out of its brim; his coat, +when he wore one, hung nearly to his heels and had the rearward buttons +far down the back; but one suspender supported his trousers; the seat +of the trousers bagged low and contained nothing, the fringed legs +dragged in the dirt when not rolled up. + +Huckleberry came and went, at his own free will. He slept on doorsteps +in fine weather and in empty hogsheads in wet; he did not have to go to +school or to church, or call any being master or obey anybody; he could +go fishing or swimming when and where he chose, and stay as long as it +suited him; nobody forbade him to fight; he could sit up as late as he +pleased; he was always the first boy that went barefoot in the spring +and the last to resume leather in the fall; he never had to wash, nor +put on clean clothes; he could swear wonderfully. In a word, everything +that goes to make life precious that boy had. So thought every +harassed, hampered, respectable boy in St. Petersburg. + +Tom hailed the romantic outcast: + +"Hello, Huckleberry!" + +"Hello yourself, and see how you like it." + +"What's that you got?" + +"Dead cat." + +"Lemme see him, Huck. My, he's pretty stiff. Where'd you get him?" + +"Bought him off'n a boy." + +"What did you give?" + +"I give a blue ticket and a bladder that I got at the slaughter-house." + +"Where'd you get the blue ticket?" + +"Bought it off'n Ben Rogers two weeks ago for a hoop-stick." + +"Say--what is dead cats good for, Huck?" + +"Good for? Cure warts with." + +"No! Is that so? I know something that's better." + +"I bet you don't. What is it?" + +"Why, spunk-water." + +"Spunk-water! I wouldn't give a dern for spunk-water." + +"You wouldn't, wouldn't you? D'you ever try it?" + +"No, I hain't. But Bob Tanner did." + +"Who told you so!" + +"Why, he told Jeff Thatcher, and Jeff told Johnny Baker, and Johnny +told Jim Hollis, and Jim told Ben Rogers, and Ben told a nigger, and +the nigger told me. There now!" + +"Well, what of it? They'll all lie. Leastways all but the nigger. I +don't know HIM. But I never see a nigger that WOULDN'T lie. Shucks! Now +you tell me how Bob Tanner done it, Huck." + +"Why, he took and dipped his hand in a rotten stump where the +rain-water was." + +"In the daytime?" + +"Certainly." + +"With his face to the stump?" + +"Yes. Least I reckon so." + +"Did he say anything?" + +"I don't reckon he did. I don't know." + +"Aha! Talk about trying to cure warts with spunk-water such a blame +fool way as that! Why, that ain't a-going to do any good. You got to go +all by yourself, to the middle of the woods, where you know there's a +spunk-water stump, and just as it's midnight you back up against the +stump and jam your hand in and say: + + 'Barley-corn, barley-corn, injun-meal shorts, + Spunk-water, spunk-water, swaller these warts,' + +and then walk away quick, eleven steps, with your eyes shut, and then +turn around three times and walk home without speaking to anybody. +Because if you speak the charm's busted." + +"Well, that sounds like a good way; but that ain't the way Bob Tanner +done." + +"No, sir, you can bet he didn't, becuz he's the wartiest boy in this +town; and he wouldn't have a wart on him if he'd knowed how to work +spunk-water. I've took off thousands of warts off of my hands that way, +Huck. I play with frogs so much that I've always got considerable many +warts. Sometimes I take 'em off with a bean." + +"Yes, bean's good. I've done that." + +"Have you? What's your way?" + +"You take and split the bean, and cut the wart so as to get some +blood, and then you put the blood on one piece of the bean and take and +dig a hole and bury it 'bout midnight at the crossroads in the dark of +the moon, and then you burn up the rest of the bean. You see that piece +that's got the blood on it will keep drawing and drawing, trying to +fetch the other piece to it, and so that helps the blood to draw the +wart, and pretty soon off she comes." + +"Yes, that's it, Huck--that's it; though when you're burying it if you +say 'Down bean; off wart; come no more to bother me!' it's better. +That's the way Joe Harper does, and he's been nearly to Coonville and +most everywheres. But say--how do you cure 'em with dead cats?" + +"Why, you take your cat and go and get in the graveyard 'long about +midnight when somebody that was wicked has been buried; and when it's +midnight a devil will come, or maybe two or three, but you can't see +'em, you can only hear something like the wind, or maybe hear 'em talk; +and when they're taking that feller away, you heave your cat after 'em +and say, 'Devil follow corpse, cat follow devil, warts follow cat, I'm +done with ye!' That'll fetch ANY wart." + +"Sounds right. D'you ever try it, Huck?" + +"No, but old Mother Hopkins told me." + +"Well, I reckon it's so, then. Becuz they say she's a witch." + +"Say! Why, Tom, I KNOW she is. She witched pap. Pap says so his own +self. He come along one day, and he see she was a-witching him, so he +took up a rock, and if she hadn't dodged, he'd a got her. Well, that +very night he rolled off'n a shed wher' he was a layin drunk, and broke +his arm." + +"Why, that's awful. How did he know she was a-witching him?" + +"Lord, pap can tell, easy. Pap says when they keep looking at you +right stiddy, they're a-witching you. Specially if they mumble. Becuz +when they mumble they're saying the Lord's Prayer backards." + +"Say, Hucky, when you going to try the cat?" + +"To-night. I reckon they'll come after old Hoss Williams to-night." + +"But they buried him Saturday. Didn't they get him Saturday night?" + +"Why, how you talk! How could their charms work till midnight?--and +THEN it's Sunday. Devils don't slosh around much of a Sunday, I don't +reckon." + +"I never thought of that. That's so. Lemme go with you?" + +"Of course--if you ain't afeard." + +"Afeard! 'Tain't likely. Will you meow?" + +"Yes--and you meow back, if you get a chance. Last time, you kep' me +a-meowing around till old Hays went to throwing rocks at me and says +'Dern that cat!' and so I hove a brick through his window--but don't +you tell." + +"I won't. I couldn't meow that night, becuz auntie was watching me, +but I'll meow this time. Say--what's that?" + +"Nothing but a tick." + +"Where'd you get him?" + +"Out in the woods." + +"What'll you take for him?" + +"I don't know. I don't want to sell him." + +"All right. It's a mighty small tick, anyway." + +"Oh, anybody can run a tick down that don't belong to them. I'm +satisfied with it. It's a good enough tick for me." + +"Sho, there's ticks a plenty. I could have a thousand of 'em if I +wanted to." + +"Well, why don't you? Becuz you know mighty well you can't. This is a +pretty early tick, I reckon. It's the first one I've seen this year." + +"Say, Huck--I'll give you my tooth for him." + +"Less see it." + +Tom got out a bit of paper and carefully unrolled it. Huckleberry +viewed it wistfully. The temptation was very strong. At last he said: + +"Is it genuwyne?" + +Tom lifted his lip and showed the vacancy. + +"Well, all right," said Huckleberry, "it's a trade." + +Tom enclosed the tick in the percussion-cap box that had lately been +the pinchbug's prison, and the boys separated, each feeling wealthier +than before. + +When Tom reached the little isolated frame schoolhouse, he strode in +briskly, with the manner of one who had come with all honest speed. +He hung his hat on a peg and flung himself into his seat with +business-like alacrity. The master, throned on high in his great +splint-bottom arm-chair, was dozing, lulled by the drowsy hum of study. +The interruption roused him. + +"Thomas Sawyer!" + +Tom knew that when his name was pronounced in full, it meant trouble. + +"Sir!" + +"Come up here. Now, sir, why are you late again, as usual?" + +Tom was about to take refuge in a lie, when he saw two long tails of +yellow hair hanging down a back that he recognized by the electric +sympathy of love; and by that form was THE ONLY VACANT PLACE on the +girls' side of the schoolhouse. He instantly said: + +"I STOPPED TO TALK WITH HUCKLEBERRY FINN!" + +The master's pulse stood still, and he stared helplessly. The buzz of +study ceased. The pupils wondered if this foolhardy boy had lost his +mind. The master said: + +"You--you did what?" + +"Stopped to talk with Huckleberry Finn." + +There was no mistaking the words. + +"Thomas Sawyer, this is the most astounding confession I have ever +listened to. No mere ferule will answer for this offence. Take off your +jacket." + +The master's arm performed until it was tired and the stock of +switches notably diminished. Then the order followed: + +"Now, sir, go and sit with the girls! And let this be a warning to you." + +The titter that rippled around the room appeared to abash the boy, but +in reality that result was caused rather more by his worshipful awe of +his unknown idol and the dread pleasure that lay in his high good +fortune. He sat down upon the end of the pine bench and the girl +hitched herself away from him with a toss of her head. Nudges and winks +and whispers traversed the room, but Tom sat still, with his arms upon +the long, low desk before him, and seemed to study his book. + +By and by attention ceased from him, and the accustomed school murmur +rose upon the dull air once more. Presently the boy began to steal +furtive glances at the girl. She observed it, "made a mouth" at him and +gave him the back of her head for the space of a minute. When she +cautiously faced around again, a peach lay before her. She thrust it +away. Tom gently put it back. She thrust it away again, but with less +animosity. Tom patiently returned it to its place. Then she let it +remain. Tom scrawled on his slate, "Please take it--I got more." The +girl glanced at the words, but made no sign. Now the boy began to draw +something on the slate, hiding his work with his left hand. For a time +the girl refused to notice; but her human curiosity presently began to +manifest itself by hardly perceptible signs. The boy worked on, +apparently unconscious. The girl made a sort of noncommittal attempt to +see, but the boy did not betray that he was aware of it. At last she +gave in and hesitatingly whispered: + +"Let me see it." + +Tom partly uncovered a dismal caricature of a house with two gable +ends to it and a corkscrew of smoke issuing from the chimney. Then the +girl's interest began to fasten itself upon the work and she forgot +everything else. When it was finished, she gazed a moment, then +whispered: + +"It's nice--make a man." + +The artist erected a man in the front yard, that resembled a derrick. +He could have stepped over the house; but the girl was not +hypercritical; she was satisfied with the monster, and whispered: + +"It's a beautiful man--now make me coming along." + +Tom drew an hour-glass with a full moon and straw limbs to it and +armed the spreading fingers with a portentous fan. The girl said: + +"It's ever so nice--I wish I could draw." + +"It's easy," whispered Tom, "I'll learn you." + +"Oh, will you? When?" + +"At noon. Do you go home to dinner?" + +"I'll stay if you will." + +"Good--that's a whack. What's your name?" + +"Becky Thatcher. What's yours? Oh, I know. It's Thomas Sawyer." + +"That's the name they lick me by. I'm Tom when I'm good. You call me +Tom, will you?" + +"Yes." + +Now Tom began to scrawl something on the slate, hiding the words from +the girl. But she was not backward this time. She begged to see. Tom +said: + +"Oh, it ain't anything." + +"Yes it is." + +"No it ain't. You don't want to see." + +"Yes I do, indeed I do. Please let me." + +"You'll tell." + +"No I won't--deed and deed and double deed won't." + +"You won't tell anybody at all? Ever, as long as you live?" + +"No, I won't ever tell ANYbody. Now let me." + +"Oh, YOU don't want to see!" + +"Now that you treat me so, I WILL see." And she put her small hand +upon his and a little scuffle ensued, Tom pretending to resist in +earnest but letting his hand slip by degrees till these words were +revealed: "I LOVE YOU." + +"Oh, you bad thing!" And she hit his hand a smart rap, but reddened +and looked pleased, nevertheless. + +Just at this juncture the boy felt a slow, fateful grip closing on his +ear, and a steady lifting impulse. In that wise he was borne across the +house and deposited in his own seat, under a peppering fire of giggles +from the whole school. Then the master stood over him during a few +awful moments, and finally moved away to his throne without saying a +word. But although Tom's ear tingled, his heart was jubilant. + +As the school quieted down Tom made an honest effort to study, but the +turmoil within him was too great. In turn he took his place in the +reading class and made a botch of it; then in the geography class and +turned lakes into mountains, mountains into rivers, and rivers into +continents, till chaos was come again; then in the spelling class, and +got "turned down," by a succession of mere baby words, till he brought +up at the foot and yielded up the pewter medal which he had worn with +ostentation for months. + + + +CHAPTER VII + +THE harder Tom tried to fasten his mind on his book, the more his +ideas wandered. So at last, with a sigh and a yawn, he gave it up. It +seemed to him that the noon recess would never come. The air was +utterly dead. There was not a breath stirring. It was the sleepiest of +sleepy days. The drowsing murmur of the five and twenty studying +scholars soothed the soul like the spell that is in the murmur of bees. +Away off in the flaming sunshine, Cardiff Hill lifted its soft green +sides through a shimmering veil of heat, tinted with the purple of +distance; a few birds floated on lazy wing high in the air; no other +living thing was visible but some cows, and they were asleep. Tom's +heart ached to be free, or else to have something of interest to do to +pass the dreary time. His hand wandered into his pocket and his face +lit up with a glow of gratitude that was prayer, though he did not know +it. Then furtively the percussion-cap box came out. He released the +tick and put him on the long flat desk. The creature probably glowed +with a gratitude that amounted to prayer, too, at this moment, but it +was premature: for when he started thankfully to travel off, Tom turned +him aside with a pin and made him take a new direction. + +Tom's bosom friend sat next him, suffering just as Tom had been, and +now he was deeply and gratefully interested in this entertainment in an +instant. This bosom friend was Joe Harper. The two boys were sworn +friends all the week, and embattled enemies on Saturdays. Joe took a +pin out of his lapel and began to assist in exercising the prisoner. +The sport grew in interest momently. Soon Tom said that they were +interfering with each other, and neither getting the fullest benefit of +the tick. So he put Joe's slate on the desk and drew a line down the +middle of it from top to bottom. + +"Now," said he, "as long as he is on your side you can stir him up and +I'll let him alone; but if you let him get away and get on my side, +you're to leave him alone as long as I can keep him from crossing over." + +"All right, go ahead; start him up." + +The tick escaped from Tom, presently, and crossed the equator. Joe +harassed him awhile, and then he got away and crossed back again. This +change of base occurred often. While one boy was worrying the tick with +absorbing interest, the other would look on with interest as strong, +the two heads bowed together over the slate, and the two souls dead to +all things else. At last luck seemed to settle and abide with Joe. The +tick tried this, that, and the other course, and got as excited and as +anxious as the boys themselves, but time and again just as he would +have victory in his very grasp, so to speak, and Tom's fingers would be +twitching to begin, Joe's pin would deftly head him off, and keep +possession. At last Tom could stand it no longer. The temptation was +too strong. So he reached out and lent a hand with his pin. Joe was +angry in a moment. Said he: + +"Tom, you let him alone." + +"I only just want to stir him up a little, Joe." + +"No, sir, it ain't fair; you just let him alone." + +"Blame it, I ain't going to stir him much." + +"Let him alone, I tell you." + +"I won't!" + +"You shall--he's on my side of the line." + +"Look here, Joe Harper, whose is that tick?" + +"I don't care whose tick he is--he's on my side of the line, and you +sha'n't touch him." + +"Well, I'll just bet I will, though. He's my tick and I'll do what I +blame please with him, or die!" + +A tremendous whack came down on Tom's shoulders, and its duplicate on +Joe's; and for the space of two minutes the dust continued to fly from +the two jackets and the whole school to enjoy it. The boys had been too +absorbed to notice the hush that had stolen upon the school awhile +before when the master came tiptoeing down the room and stood over +them. He had contemplated a good part of the performance before he +contributed his bit of variety to it. + +When school broke up at noon, Tom flew to Becky Thatcher, and +whispered in her ear: + +"Put on your bonnet and let on you're going home; and when you get to +the corner, give the rest of 'em the slip, and turn down through the +lane and come back. I'll go the other way and come it over 'em the same +way." + +So the one went off with one group of scholars, and the other with +another. In a little while the two met at the bottom of the lane, and +when they reached the school they had it all to themselves. Then they +sat together, with a slate before them, and Tom gave Becky the pencil +and held her hand in his, guiding it, and so created another surprising +house. When the interest in art began to wane, the two fell to talking. +Tom was swimming in bliss. He said: + +"Do you love rats?" + +"No! I hate them!" + +"Well, I do, too--LIVE ones. But I mean dead ones, to swing round your +head with a string." + +"No, I don't care for rats much, anyway. What I like is chewing-gum." + +"Oh, I should say so! I wish I had some now." + +"Do you? I've got some. I'll let you chew it awhile, but you must give +it back to me." + +That was agreeable, so they chewed it turn about, and dangled their +legs against the bench in excess of contentment. + +"Was you ever at a circus?" said Tom. + +"Yes, and my pa's going to take me again some time, if I'm good." + +"I been to the circus three or four times--lots of times. Church ain't +shucks to a circus. There's things going on at a circus all the time. +I'm going to be a clown in a circus when I grow up." + +"Oh, are you! That will be nice. They're so lovely, all spotted up." + +"Yes, that's so. And they get slathers of money--most a dollar a day, +Ben Rogers says. Say, Becky, was you ever engaged?" + +"What's that?" + +"Why, engaged to be married." + +"No." + +"Would you like to?" + +"I reckon so. I don't know. What is it like?" + +"Like? Why it ain't like anything. You only just tell a boy you won't +ever have anybody but him, ever ever ever, and then you kiss and that's +all. Anybody can do it." + +"Kiss? What do you kiss for?" + +"Why, that, you know, is to--well, they always do that." + +"Everybody?" + +"Why, yes, everybody that's in love with each other. Do you remember +what I wrote on the slate?" + +"Ye--yes." + +"What was it?" + +"I sha'n't tell you." + +"Shall I tell YOU?" + +"Ye--yes--but some other time." + +"No, now." + +"No, not now--to-morrow." + +"Oh, no, NOW. Please, Becky--I'll whisper it, I'll whisper it ever so +easy." + +Becky hesitating, Tom took silence for consent, and passed his arm +about her waist and whispered the tale ever so softly, with his mouth +close to her ear. And then he added: + +"Now you whisper it to me--just the same." + +She resisted, for a while, and then said: + +"You turn your face away so you can't see, and then I will. But you +mustn't ever tell anybody--WILL you, Tom? Now you won't, WILL you?" + +"No, indeed, indeed I won't. Now, Becky." + +He turned his face away. She bent timidly around till her breath +stirred his curls and whispered, "I--love--you!" + +Then she sprang away and ran around and around the desks and benches, +with Tom after her, and took refuge in a corner at last, with her +little white apron to her face. Tom clasped her about her neck and +pleaded: + +"Now, Becky, it's all done--all over but the kiss. Don't you be afraid +of that--it ain't anything at all. Please, Becky." And he tugged at her +apron and the hands. + +By and by she gave up, and let her hands drop; her face, all glowing +with the struggle, came up and submitted. Tom kissed the red lips and +said: + +"Now it's all done, Becky. And always after this, you know, you ain't +ever to love anybody but me, and you ain't ever to marry anybody but +me, ever never and forever. Will you?" + +"No, I'll never love anybody but you, Tom, and I'll never marry +anybody but you--and you ain't to ever marry anybody but me, either." + +"Certainly. Of course. That's PART of it. And always coming to school +or when we're going home, you're to walk with me, when there ain't +anybody looking--and you choose me and I choose you at parties, because +that's the way you do when you're engaged." + +"It's so nice. I never heard of it before." + +"Oh, it's ever so gay! Why, me and Amy Lawrence--" + +The big eyes told Tom his blunder and he stopped, confused. + +"Oh, Tom! Then I ain't the first you've ever been engaged to!" + +The child began to cry. Tom said: + +"Oh, don't cry, Becky, I don't care for her any more." + +"Yes, you do, Tom--you know you do." + +Tom tried to put his arm about her neck, but she pushed him away and +turned her face to the wall, and went on crying. Tom tried again, with +soothing words in his mouth, and was repulsed again. Then his pride was +up, and he strode away and went outside. He stood about, restless and +uneasy, for a while, glancing at the door, every now and then, hoping +she would repent and come to find him. But she did not. Then he began +to feel badly and fear that he was in the wrong. It was a hard struggle +with him to make new advances, now, but he nerved himself to it and +entered. She was still standing back there in the corner, sobbing, with +her face to the wall. Tom's heart smote him. He went to her and stood a +moment, not knowing exactly how to proceed. Then he said hesitatingly: + +"Becky, I--I don't care for anybody but you." + +No reply--but sobs. + +"Becky"--pleadingly. "Becky, won't you say something?" + +More sobs. + +Tom got out his chiefest jewel, a brass knob from the top of an +andiron, and passed it around her so that she could see it, and said: + +"Please, Becky, won't you take it?" + +She struck it to the floor. Then Tom marched out of the house and over +the hills and far away, to return to school no more that day. Presently +Becky began to suspect. She ran to the door; he was not in sight; she +flew around to the play-yard; he was not there. Then she called: + +"Tom! Come back, Tom!" + +She listened intently, but there was no answer. She had no companions +but silence and loneliness. So she sat down to cry again and upbraid +herself; and by this time the scholars began to gather again, and she +had to hide her griefs and still her broken heart and take up the cross +of a long, dreary, aching afternoon, with none among the strangers +about her to exchange sorrows with. + + + +CHAPTER VIII + +TOM dodged hither and thither through lanes until he was well out of +the track of returning scholars, and then fell into a moody jog. He +crossed a small "branch" two or three times, because of a prevailing +juvenile superstition that to cross water baffled pursuit. Half an hour +later he was disappearing behind the Douglas mansion on the summit of +Cardiff Hill, and the schoolhouse was hardly distinguishable away off +in the valley behind him. He entered a dense wood, picked his pathless +way to the centre of it, and sat down on a mossy spot under a spreading +oak. There was not even a zephyr stirring; the dead noonday heat had +even stilled the songs of the birds; nature lay in a trance that was +broken by no sound but the occasional far-off hammering of a +woodpecker, and this seemed to render the pervading silence and sense +of loneliness the more profound. The boy's soul was steeped in +melancholy; his feelings were in happy accord with his surroundings. He +sat long with his elbows on his knees and his chin in his hands, +meditating. It seemed to him that life was but a trouble, at best, and +he more than half envied Jimmy Hodges, so lately released; it must be +very peaceful, he thought, to lie and slumber and dream forever and +ever, with the wind whispering through the trees and caressing the +grass and the flowers over the grave, and nothing to bother and grieve +about, ever any more. If he only had a clean Sunday-school record he +could be willing to go, and be done with it all. Now as to this girl. +What had he done? Nothing. He had meant the best in the world, and been +treated like a dog--like a very dog. She would be sorry some day--maybe +when it was too late. Ah, if he could only die TEMPORARILY! + +But the elastic heart of youth cannot be compressed into one +constrained shape long at a time. Tom presently began to drift +insensibly back into the concerns of this life again. What if he turned +his back, now, and disappeared mysteriously? What if he went away--ever +so far away, into unknown countries beyond the seas--and never came +back any more! How would she feel then! The idea of being a clown +recurred to him now, only to fill him with disgust. For frivolity and +jokes and spotted tights were an offense, when they intruded themselves +upon a spirit that was exalted into the vague august realm of the +romantic. No, he would be a soldier, and return after long years, all +war-worn and illustrious. No--better still, he would join the Indians, +and hunt buffaloes and go on the warpath in the mountain ranges and the +trackless great plains of the Far West, and away in the future come +back a great chief, bristling with feathers, hideous with paint, and +prance into Sunday-school, some drowsy summer morning, with a +bloodcurdling war-whoop, and sear the eyeballs of all his companions +with unappeasable envy. But no, there was something gaudier even than +this. He would be a pirate! That was it! NOW his future lay plain +before him, and glowing with unimaginable splendor. How his name would +fill the world, and make people shudder! How gloriously he would go +plowing the dancing seas, in his long, low, black-hulled racer, the +Spirit of the Storm, with his grisly flag flying at the fore! And at +the zenith of his fame, how he would suddenly appear at the old village +and stalk into church, brown and weather-beaten, in his black velvet +doublet and trunks, his great jack-boots, his crimson sash, his belt +bristling with horse-pistols, his crime-rusted cutlass at his side, his +slouch hat with waving plumes, his black flag unfurled, with the skull +and crossbones on it, and hear with swelling ecstasy the whisperings, +"It's Tom Sawyer the Pirate!--the Black Avenger of the Spanish Main!" + +Yes, it was settled; his career was determined. He would run away from +home and enter upon it. He would start the very next morning. Therefore +he must now begin to get ready. He would collect his resources +together. He went to a rotten log near at hand and began to dig under +one end of it with his Barlow knife. He soon struck wood that sounded +hollow. He put his hand there and uttered this incantation impressively: + +"What hasn't come here, come! What's here, stay here!" + +Then he scraped away the dirt, and exposed a pine shingle. He took it +up and disclosed a shapely little treasure-house whose bottom and sides +were of shingles. In it lay a marble. Tom's astonishment was boundless! +He scratched his head with a perplexed air, and said: + +"Well, that beats anything!" + +Then he tossed the marble away pettishly, and stood cogitating. The +truth was, that a superstition of his had failed, here, which he and +all his comrades had always looked upon as infallible. If you buried a +marble with certain necessary incantations, and left it alone a +fortnight, and then opened the place with the incantation he had just +used, you would find that all the marbles you had ever lost had +gathered themselves together there, meantime, no matter how widely they +had been separated. But now, this thing had actually and unquestionably +failed. Tom's whole structure of faith was shaken to its foundations. +He had many a time heard of this thing succeeding but never of its +failing before. It did not occur to him that he had tried it several +times before, himself, but could never find the hiding-places +afterward. He puzzled over the matter some time, and finally decided +that some witch had interfered and broken the charm. He thought he +would satisfy himself on that point; so he searched around till he +found a small sandy spot with a little funnel-shaped depression in it. +He laid himself down and put his mouth close to this depression and +called-- + +"Doodle-bug, doodle-bug, tell me what I want to know! Doodle-bug, +doodle-bug, tell me what I want to know!" + +The sand began to work, and presently a small black bug appeared for a +second and then darted under again in a fright. + +"He dasn't tell! So it WAS a witch that done it. I just knowed it." + +He well knew the futility of trying to contend against witches, so he +gave up discouraged. But it occurred to him that he might as well have +the marble he had just thrown away, and therefore he went and made a +patient search for it. But he could not find it. Now he went back to +his treasure-house and carefully placed himself just as he had been +standing when he tossed the marble away; then he took another marble +from his pocket and tossed it in the same way, saying: + +"Brother, go find your brother!" + +He watched where it stopped, and went there and looked. But it must +have fallen short or gone too far; so he tried twice more. The last +repetition was successful. The two marbles lay within a foot of each +other. + +Just here the blast of a toy tin trumpet came faintly down the green +aisles of the forest. Tom flung off his jacket and trousers, turned a +suspender into a belt, raked away some brush behind the rotten log, +disclosing a rude bow and arrow, a lath sword and a tin trumpet, and in +a moment had seized these things and bounded away, barelegged, with +fluttering shirt. He presently halted under a great elm, blew an +answering blast, and then began to tiptoe and look warily out, this way +and that. He said cautiously--to an imaginary company: + +"Hold, my merry men! Keep hid till I blow." + +Now appeared Joe Harper, as airily clad and elaborately armed as Tom. +Tom called: + +"Hold! Who comes here into Sherwood Forest without my pass?" + +"Guy of Guisborne wants no man's pass. Who art thou that--that--" + +"Dares to hold such language," said Tom, prompting--for they talked +"by the book," from memory. + +"Who art thou that dares to hold such language?" + +"I, indeed! I am Robin Hood, as thy caitiff carcase soon shall know." + +"Then art thou indeed that famous outlaw? Right gladly will I dispute +with thee the passes of the merry wood. Have at thee!" + +They took their lath swords, dumped their other traps on the ground, +struck a fencing attitude, foot to foot, and began a grave, careful +combat, "two up and two down." Presently Tom said: + +"Now, if you've got the hang, go it lively!" + +So they "went it lively," panting and perspiring with the work. By and +by Tom shouted: + +"Fall! fall! Why don't you fall?" + +"I sha'n't! Why don't you fall yourself? You're getting the worst of +it." + +"Why, that ain't anything. I can't fall; that ain't the way it is in +the book. The book says, 'Then with one back-handed stroke he slew poor +Guy of Guisborne.' You're to turn around and let me hit you in the +back." + +There was no getting around the authorities, so Joe turned, received +the whack and fell. + +"Now," said Joe, getting up, "you got to let me kill YOU. That's fair." + +"Why, I can't do that, it ain't in the book." + +"Well, it's blamed mean--that's all." + +"Well, say, Joe, you can be Friar Tuck or Much the miller's son, and +lam me with a quarter-staff; or I'll be the Sheriff of Nottingham and +you be Robin Hood a little while and kill me." + +This was satisfactory, and so these adventures were carried out. Then +Tom became Robin Hood again, and was allowed by the treacherous nun to +bleed his strength away through his neglected wound. And at last Joe, +representing a whole tribe of weeping outlaws, dragged him sadly forth, +gave his bow into his feeble hands, and Tom said, "Where this arrow +falls, there bury poor Robin Hood under the greenwood tree." Then he +shot the arrow and fell back and would have died, but he lit on a +nettle and sprang up too gaily for a corpse. + +The boys dressed themselves, hid their accoutrements, and went off +grieving that there were no outlaws any more, and wondering what modern +civilization could claim to have done to compensate for their loss. +They said they would rather be outlaws a year in Sherwood Forest than +President of the United States forever. + + + +CHAPTER IX + +AT half-past nine, that night, Tom and Sid were sent to bed, as usual. +They said their prayers, and Sid was soon asleep. Tom lay awake and +waited, in restless impatience. When it seemed to him that it must be +nearly daylight, he heard the clock strike ten! This was despair. He +would have tossed and fidgeted, as his nerves demanded, but he was +afraid he might wake Sid. So he lay still, and stared up into the dark. +Everything was dismally still. By and by, out of the stillness, little, +scarcely perceptible noises began to emphasize themselves. The ticking +of the clock began to bring itself into notice. Old beams began to +crack mysteriously. The stairs creaked faintly. Evidently spirits were +abroad. A measured, muffled snore issued from Aunt Polly's chamber. And +now the tiresome chirping of a cricket that no human ingenuity could +locate, began. Next the ghastly ticking of a deathwatch in the wall at +the bed's head made Tom shudder--it meant that somebody's days were +numbered. Then the howl of a far-off dog rose on the night air, and was +answered by a fainter howl from a remoter distance. Tom was in an +agony. At last he was satisfied that time had ceased and eternity +begun; he began to doze, in spite of himself; the clock chimed eleven, +but he did not hear it. And then there came, mingling with his +half-formed dreams, a most melancholy caterwauling. The raising of a +neighboring window disturbed him. A cry of "Scat! you devil!" and the +crash of an empty bottle against the back of his aunt's woodshed +brought him wide awake, and a single minute later he was dressed and +out of the window and creeping along the roof of the "ell" on all +fours. He "meow'd" with caution once or twice, as he went; then jumped +to the roof of the woodshed and thence to the ground. Huckleberry Finn +was there, with his dead cat. The boys moved off and disappeared in the +gloom. At the end of half an hour they were wading through the tall +grass of the graveyard. + +It was a graveyard of the old-fashioned Western kind. It was on a +hill, about a mile and a half from the village. It had a crazy board +fence around it, which leaned inward in places, and outward the rest of +the time, but stood upright nowhere. Grass and weeds grew rank over the +whole cemetery. All the old graves were sunken in, there was not a +tombstone on the place; round-topped, worm-eaten boards staggered over +the graves, leaning for support and finding none. "Sacred to the memory +of" So-and-So had been painted on them once, but it could no longer +have been read, on the most of them, now, even if there had been light. + +A faint wind moaned through the trees, and Tom feared it might be the +spirits of the dead, complaining at being disturbed. The boys talked +little, and only under their breath, for the time and the place and the +pervading solemnity and silence oppressed their spirits. They found the +sharp new heap they were seeking, and ensconced themselves within the +protection of three great elms that grew in a bunch within a few feet +of the grave. + +Then they waited in silence for what seemed a long time. The hooting +of a distant owl was all the sound that troubled the dead stillness. +Tom's reflections grew oppressive. He must force some talk. So he said +in a whisper: + +"Hucky, do you believe the dead people like it for us to be here?" + +Huckleberry whispered: + +"I wisht I knowed. It's awful solemn like, AIN'T it?" + +"I bet it is." + +There was a considerable pause, while the boys canvassed this matter +inwardly. Then Tom whispered: + +"Say, Hucky--do you reckon Hoss Williams hears us talking?" + +"O' course he does. Least his sperrit does." + +Tom, after a pause: + +"I wish I'd said Mister Williams. But I never meant any harm. +Everybody calls him Hoss." + +"A body can't be too partic'lar how they talk 'bout these-yer dead +people, Tom." + +This was a damper, and conversation died again. + +Presently Tom seized his comrade's arm and said: + +"Sh!" + +"What is it, Tom?" And the two clung together with beating hearts. + +"Sh! There 'tis again! Didn't you hear it?" + +"I--" + +"There! Now you hear it." + +"Lord, Tom, they're coming! They're coming, sure. What'll we do?" + +"I dono. Think they'll see us?" + +"Oh, Tom, they can see in the dark, same as cats. I wisht I hadn't +come." + +"Oh, don't be afeard. I don't believe they'll bother us. We ain't +doing any harm. If we keep perfectly still, maybe they won't notice us +at all." + +"I'll try to, Tom, but, Lord, I'm all of a shiver." + +"Listen!" + +The boys bent their heads together and scarcely breathed. A muffled +sound of voices floated up from the far end of the graveyard. + +"Look! See there!" whispered Tom. "What is it?" + +"It's devil-fire. Oh, Tom, this is awful." + +Some vague figures approached through the gloom, swinging an +old-fashioned tin lantern that freckled the ground with innumerable +little spangles of light. Presently Huckleberry whispered with a +shudder: + +"It's the devils sure enough. Three of 'em! Lordy, Tom, we're goners! +Can you pray?" + +"I'll try, but don't you be afeard. They ain't going to hurt us. 'Now +I lay me down to sleep, I--'" + +"Sh!" + +"What is it, Huck?" + +"They're HUMANS! One of 'em is, anyway. One of 'em's old Muff Potter's +voice." + +"No--'tain't so, is it?" + +"I bet I know it. Don't you stir nor budge. He ain't sharp enough to +notice us. Drunk, the same as usual, likely--blamed old rip!" + +"All right, I'll keep still. Now they're stuck. Can't find it. Here +they come again. Now they're hot. Cold again. Hot again. Red hot! +They're p'inted right, this time. Say, Huck, I know another o' them +voices; it's Injun Joe." + +"That's so--that murderin' half-breed! I'd druther they was devils a +dern sight. What kin they be up to?" + +The whisper died wholly out, now, for the three men had reached the +grave and stood within a few feet of the boys' hiding-place. + +"Here it is," said the third voice; and the owner of it held the +lantern up and revealed the face of young Doctor Robinson. + +Potter and Injun Joe were carrying a handbarrow with a rope and a +couple of shovels on it. They cast down their load and began to open +the grave. The doctor put the lantern at the head of the grave and came +and sat down with his back against one of the elm trees. He was so +close the boys could have touched him. + +"Hurry, men!" he said, in a low voice; "the moon might come out at any +moment." + +They growled a response and went on digging. For some time there was +no noise but the grating sound of the spades discharging their freight +of mould and gravel. It was very monotonous. Finally a spade struck +upon the coffin with a dull woody accent, and within another minute or +two the men had hoisted it out on the ground. They pried off the lid +with their shovels, got out the body and dumped it rudely on the +ground. The moon drifted from behind the clouds and exposed the pallid +face. The barrow was got ready and the corpse placed on it, covered +with a blanket, and bound to its place with the rope. Potter took out a +large spring-knife and cut off the dangling end of the rope and then +said: + +"Now the cussed thing's ready, Sawbones, and you'll just out with +another five, or here she stays." + +"That's the talk!" said Injun Joe. + +"Look here, what does this mean?" said the doctor. "You required your +pay in advance, and I've paid you." + +"Yes, and you done more than that," said Injun Joe, approaching the +doctor, who was now standing. "Five years ago you drove me away from +your father's kitchen one night, when I come to ask for something to +eat, and you said I warn't there for any good; and when I swore I'd get +even with you if it took a hundred years, your father had me jailed for +a vagrant. Did you think I'd forget? The Injun blood ain't in me for +nothing. And now I've GOT you, and you got to SETTLE, you know!" + +He was threatening the doctor, with his fist in his face, by this +time. The doctor struck out suddenly and stretched the ruffian on the +ground. Potter dropped his knife, and exclaimed: + +"Here, now, don't you hit my pard!" and the next moment he had +grappled with the doctor and the two were struggling with might and +main, trampling the grass and tearing the ground with their heels. +Injun Joe sprang to his feet, his eyes flaming with passion, snatched +up Potter's knife, and went creeping, catlike and stooping, round and +round about the combatants, seeking an opportunity. All at once the +doctor flung himself free, seized the heavy headboard of Williams' +grave and felled Potter to the earth with it--and in the same instant +the half-breed saw his chance and drove the knife to the hilt in the +young man's breast. He reeled and fell partly upon Potter, flooding him +with his blood, and in the same moment the clouds blotted out the +dreadful spectacle and the two frightened boys went speeding away in +the dark. + +Presently, when the moon emerged again, Injun Joe was standing over +the two forms, contemplating them. The doctor murmured inarticulately, +gave a long gasp or two and was still. The half-breed muttered: + +"THAT score is settled--damn you." + +Then he robbed the body. After which he put the fatal knife in +Potter's open right hand, and sat down on the dismantled coffin. Three +--four--five minutes passed, and then Potter began to stir and moan. His +hand closed upon the knife; he raised it, glanced at it, and let it +fall, with a shudder. Then he sat up, pushing the body from him, and +gazed at it, and then around him, confusedly. His eyes met Joe's. + +"Lord, how is this, Joe?" he said. + +"It's a dirty business," said Joe, without moving. + +"What did you do it for?" + +"I! I never done it!" + +"Look here! That kind of talk won't wash." + +Potter trembled and grew white. + +"I thought I'd got sober. I'd no business to drink to-night. But it's +in my head yet--worse'n when we started here. I'm all in a muddle; +can't recollect anything of it, hardly. Tell me, Joe--HONEST, now, old +feller--did I do it? Joe, I never meant to--'pon my soul and honor, I +never meant to, Joe. Tell me how it was, Joe. Oh, it's awful--and him +so young and promising." + +"Why, you two was scuffling, and he fetched you one with the headboard +and you fell flat; and then up you come, all reeling and staggering +like, and snatched the knife and jammed it into him, just as he fetched +you another awful clip--and here you've laid, as dead as a wedge til +now." + +"Oh, I didn't know what I was a-doing. I wish I may die this minute if +I did. It was all on account of the whiskey and the excitement, I +reckon. I never used a weepon in my life before, Joe. I've fought, but +never with weepons. They'll all say that. Joe, don't tell! Say you +won't tell, Joe--that's a good feller. I always liked you, Joe, and +stood up for you, too. Don't you remember? You WON'T tell, WILL you, +Joe?" And the poor creature dropped on his knees before the stolid +murderer, and clasped his appealing hands. + +"No, you've always been fair and square with me, Muff Potter, and I +won't go back on you. There, now, that's as fair as a man can say." + +"Oh, Joe, you're an angel. I'll bless you for this the longest day I +live." And Potter began to cry. + +"Come, now, that's enough of that. This ain't any time for blubbering. +You be off yonder way and I'll go this. Move, now, and don't leave any +tracks behind you." + +Potter started on a trot that quickly increased to a run. The +half-breed stood looking after him. He muttered: + +"If he's as much stunned with the lick and fuddled with the rum as he +had the look of being, he won't think of the knife till he's gone so +far he'll be afraid to come back after it to such a place by himself +--chicken-heart!" + +Two or three minutes later the murdered man, the blanketed corpse, the +lidless coffin, and the open grave were under no inspection but the +moon's. The stillness was complete again, too. + + + +CHAPTER X + +THE two boys flew on and on, toward the village, speechless with +horror. They glanced backward over their shoulders from time to time, +apprehensively, as if they feared they might be followed. Every stump +that started up in their path seemed a man and an enemy, and made them +catch their breath; and as they sped by some outlying cottages that lay +near the village, the barking of the aroused watch-dogs seemed to give +wings to their feet. + +"If we can only get to the old tannery before we break down!" +whispered Tom, in short catches between breaths. "I can't stand it much +longer." + +Huckleberry's hard pantings were his only reply, and the boys fixed +their eyes on the goal of their hopes and bent to their work to win it. +They gained steadily on it, and at last, breast to breast, they burst +through the open door and fell grateful and exhausted in the sheltering +shadows beyond. By and by their pulses slowed down, and Tom whispered: + +"Huckleberry, what do you reckon'll come of this?" + +"If Doctor Robinson dies, I reckon hanging'll come of it." + +"Do you though?" + +"Why, I KNOW it, Tom." + +Tom thought a while, then he said: + +"Who'll tell? We?" + +"What are you talking about? S'pose something happened and Injun Joe +DIDN'T hang? Why, he'd kill us some time or other, just as dead sure as +we're a laying here." + +"That's just what I was thinking to myself, Huck." + +"If anybody tells, let Muff Potter do it, if he's fool enough. He's +generally drunk enough." + +Tom said nothing--went on thinking. Presently he whispered: + +"Huck, Muff Potter don't know it. How can he tell?" + +"What's the reason he don't know it?" + +"Because he'd just got that whack when Injun Joe done it. D'you reckon +he could see anything? D'you reckon he knowed anything?" + +"By hokey, that's so, Tom!" + +"And besides, look-a-here--maybe that whack done for HIM!" + +"No, 'taint likely, Tom. He had liquor in him; I could see that; and +besides, he always has. Well, when pap's full, you might take and belt +him over the head with a church and you couldn't phase him. He says so, +his own self. So it's the same with Muff Potter, of course. But if a +man was dead sober, I reckon maybe that whack might fetch him; I dono." + +After another reflective silence, Tom said: + +"Hucky, you sure you can keep mum?" + +"Tom, we GOT to keep mum. You know that. That Injun devil wouldn't +make any more of drownding us than a couple of cats, if we was to +squeak 'bout this and they didn't hang him. Now, look-a-here, Tom, less +take and swear to one another--that's what we got to do--swear to keep +mum." + +"I'm agreed. It's the best thing. Would you just hold hands and swear +that we--" + +"Oh no, that wouldn't do for this. That's good enough for little +rubbishy common things--specially with gals, cuz THEY go back on you +anyway, and blab if they get in a huff--but there orter be writing +'bout a big thing like this. And blood." + +Tom's whole being applauded this idea. It was deep, and dark, and +awful; the hour, the circumstances, the surroundings, were in keeping +with it. He picked up a clean pine shingle that lay in the moonlight, +took a little fragment of "red keel" out of his pocket, got the moon on +his work, and painfully scrawled these lines, emphasizing each slow +down-stroke by clamping his tongue between his teeth, and letting up +the pressure on the up-strokes. [See next page.] + + "Huck Finn and + Tom Sawyer swears + they will keep mum + about This and They + wish They may Drop + down dead in Their + Tracks if They ever + Tell and Rot." + +Huckleberry was filled with admiration of Tom's facility in writing, +and the sublimity of his language. He at once took a pin from his lapel +and was going to prick his flesh, but Tom said: + +"Hold on! Don't do that. A pin's brass. It might have verdigrease on +it." + +"What's verdigrease?" + +"It's p'ison. That's what it is. You just swaller some of it once +--you'll see." + +So Tom unwound the thread from one of his needles, and each boy +pricked the ball of his thumb and squeezed out a drop of blood. In +time, after many squeezes, Tom managed to sign his initials, using the +ball of his little finger for a pen. Then he showed Huckleberry how to +make an H and an F, and the oath was complete. They buried the shingle +close to the wall, with some dismal ceremonies and incantations, and +the fetters that bound their tongues were considered to be locked and +the key thrown away. + +A figure crept stealthily through a break in the other end of the +ruined building, now, but they did not notice it. + +"Tom," whispered Huckleberry, "does this keep us from EVER telling +--ALWAYS?" + +"Of course it does. It don't make any difference WHAT happens, we got +to keep mum. We'd drop down dead--don't YOU know that?" + +"Yes, I reckon that's so." + +They continued to whisper for some little time. Presently a dog set up +a long, lugubrious howl just outside--within ten feet of them. The boys +clasped each other suddenly, in an agony of fright. + +"Which of us does he mean?" gasped Huckleberry. + +"I dono--peep through the crack. Quick!" + +"No, YOU, Tom!" + +"I can't--I can't DO it, Huck!" + +"Please, Tom. There 'tis again!" + +"Oh, lordy, I'm thankful!" whispered Tom. "I know his voice. It's Bull +Harbison." * + +[* If Mr. Harbison owned a slave named Bull, Tom would have spoken of +him as "Harbison's Bull," but a son or a dog of that name was "Bull +Harbison."] + +"Oh, that's good--I tell you, Tom, I was most scared to death; I'd a +bet anything it was a STRAY dog." + +The dog howled again. The boys' hearts sank once more. + +"Oh, my! that ain't no Bull Harbison!" whispered Huckleberry. "DO, Tom!" + +Tom, quaking with fear, yielded, and put his eye to the crack. His +whisper was hardly audible when he said: + +"Oh, Huck, IT S A STRAY DOG!" + +"Quick, Tom, quick! Who does he mean?" + +"Huck, he must mean us both--we're right together." + +"Oh, Tom, I reckon we're goners. I reckon there ain't no mistake 'bout +where I'LL go to. I been so wicked." + +"Dad fetch it! This comes of playing hookey and doing everything a +feller's told NOT to do. I might a been good, like Sid, if I'd a tried +--but no, I wouldn't, of course. But if ever I get off this time, I lay +I'll just WALLER in Sunday-schools!" And Tom began to snuffle a little. + +"YOU bad!" and Huckleberry began to snuffle too. "Consound it, Tom +Sawyer, you're just old pie, 'longside o' what I am. Oh, LORDY, lordy, +lordy, I wisht I only had half your chance." + +Tom choked off and whispered: + +"Look, Hucky, look! He's got his BACK to us!" + +Hucky looked, with joy in his heart. + +"Well, he has, by jingoes! Did he before?" + +"Yes, he did. But I, like a fool, never thought. Oh, this is bully, +you know. NOW who can he mean?" + +The howling stopped. Tom pricked up his ears. + +"Sh! What's that?" he whispered. + +"Sounds like--like hogs grunting. No--it's somebody snoring, Tom." + +"That IS it! Where 'bouts is it, Huck?" + +"I bleeve it's down at 'tother end. Sounds so, anyway. Pap used to +sleep there, sometimes, 'long with the hogs, but laws bless you, he +just lifts things when HE snores. Besides, I reckon he ain't ever +coming back to this town any more." + +The spirit of adventure rose in the boys' souls once more. + +"Hucky, do you das't to go if I lead?" + +"I don't like to, much. Tom, s'pose it's Injun Joe!" + +Tom quailed. But presently the temptation rose up strong again and the +boys agreed to try, with the understanding that they would take to +their heels if the snoring stopped. So they went tiptoeing stealthily +down, the one behind the other. When they had got to within five steps +of the snorer, Tom stepped on a stick, and it broke with a sharp snap. +The man moaned, writhed a little, and his face came into the moonlight. +It was Muff Potter. The boys' hearts had stood still, and their hopes +too, when the man moved, but their fears passed away now. They tiptoed +out, through the broken weather-boarding, and stopped at a little +distance to exchange a parting word. That long, lugubrious howl rose on +the night air again! They turned and saw the strange dog standing +within a few feet of where Potter was lying, and FACING Potter, with +his nose pointing heavenward. + +"Oh, geeminy, it's HIM!" exclaimed both boys, in a breath. + +"Say, Tom--they say a stray dog come howling around Johnny Miller's +house, 'bout midnight, as much as two weeks ago; and a whippoorwill +come in and lit on the banisters and sung, the very same evening; and +there ain't anybody dead there yet." + +"Well, I know that. And suppose there ain't. Didn't Gracie Miller fall +in the kitchen fire and burn herself terrible the very next Saturday?" + +"Yes, but she ain't DEAD. And what's more, she's getting better, too." + +"All right, you wait and see. She's a goner, just as dead sure as Muff +Potter's a goner. That's what the niggers say, and they know all about +these kind of things, Huck." + +Then they separated, cogitating. When Tom crept in at his bedroom +window the night was almost spent. He undressed with excessive caution, +and fell asleep congratulating himself that nobody knew of his +escapade. He was not aware that the gently-snoring Sid was awake, and +had been so for an hour. + +When Tom awoke, Sid was dressed and gone. There was a late look in the +light, a late sense in the atmosphere. He was startled. Why had he not +been called--persecuted till he was up, as usual? The thought filled +him with bodings. Within five minutes he was dressed and down-stairs, +feeling sore and drowsy. The family were still at table, but they had +finished breakfast. There was no voice of rebuke; but there were +averted eyes; there was a silence and an air of solemnity that struck a +chill to the culprit's heart. He sat down and tried to seem gay, but it +was up-hill work; it roused no smile, no response, and he lapsed into +silence and let his heart sink down to the depths. + +After breakfast his aunt took him aside, and Tom almost brightened in +the hope that he was going to be flogged; but it was not so. His aunt +wept over him and asked him how he could go and break her old heart so; +and finally told him to go on, and ruin himself and bring her gray +hairs with sorrow to the grave, for it was no use for her to try any +more. This was worse than a thousand whippings, and Tom's heart was +sorer now than his body. He cried, he pleaded for forgiveness, promised +to reform over and over again, and then received his dismissal, feeling +that he had won but an imperfect forgiveness and established but a +feeble confidence. + +He left the presence too miserable to even feel revengeful toward Sid; +and so the latter's prompt retreat through the back gate was +unnecessary. He moped to school gloomy and sad, and took his flogging, +along with Joe Harper, for playing hookey the day before, with the air +of one whose heart was busy with heavier woes and wholly dead to +trifles. Then he betook himself to his seat, rested his elbows on his +desk and his jaws in his hands, and stared at the wall with the stony +stare of suffering that has reached the limit and can no further go. +His elbow was pressing against some hard substance. After a long time +he slowly and sadly changed his position, and took up this object with +a sigh. It was in a paper. He unrolled it. A long, lingering, colossal +sigh followed, and his heart broke. It was his brass andiron knob! + +This final feather broke the camel's back. + + + +CHAPTER XI + +CLOSE upon the hour of noon the whole village was suddenly electrified +with the ghastly news. No need of the as yet undreamed-of telegraph; +the tale flew from man to man, from group to group, from house to +house, with little less than telegraphic speed. Of course the +schoolmaster gave holiday for that afternoon; the town would have +thought strangely of him if he had not. + +A gory knife had been found close to the murdered man, and it had been +recognized by somebody as belonging to Muff Potter--so the story ran. +And it was said that a belated citizen had come upon Potter washing +himself in the "branch" about one or two o'clock in the morning, and +that Potter had at once sneaked off--suspicious circumstances, +especially the washing which was not a habit with Potter. It was also +said that the town had been ransacked for this "murderer" (the public +are not slow in the matter of sifting evidence and arriving at a +verdict), but that he could not be found. Horsemen had departed down +all the roads in every direction, and the Sheriff "was confident" that +he would be captured before night. + +All the town was drifting toward the graveyard. Tom's heartbreak +vanished and he joined the procession, not because he would not a +thousand times rather go anywhere else, but because an awful, +unaccountable fascination drew him on. Arrived at the dreadful place, +he wormed his small body through the crowd and saw the dismal +spectacle. It seemed to him an age since he was there before. Somebody +pinched his arm. He turned, and his eyes met Huckleberry's. Then both +looked elsewhere at once, and wondered if anybody had noticed anything +in their mutual glance. But everybody was talking, and intent upon the +grisly spectacle before them. + +"Poor fellow!" "Poor young fellow!" "This ought to be a lesson to +grave robbers!" "Muff Potter'll hang for this if they catch him!" This +was the drift of remark; and the minister said, "It was a judgment; His +hand is here." + +Now Tom shivered from head to heel; for his eye fell upon the stolid +face of Injun Joe. At this moment the crowd began to sway and struggle, +and voices shouted, "It's him! it's him! he's coming himself!" + +"Who? Who?" from twenty voices. + +"Muff Potter!" + +"Hallo, he's stopped!--Look out, he's turning! Don't let him get away!" + +People in the branches of the trees over Tom's head said he wasn't +trying to get away--he only looked doubtful and perplexed. + +"Infernal impudence!" said a bystander; "wanted to come and take a +quiet look at his work, I reckon--didn't expect any company." + +The crowd fell apart, now, and the Sheriff came through, +ostentatiously leading Potter by the arm. The poor fellow's face was +haggard, and his eyes showed the fear that was upon him. When he stood +before the murdered man, he shook as with a palsy, and he put his face +in his hands and burst into tears. + +"I didn't do it, friends," he sobbed; "'pon my word and honor I never +done it." + +"Who's accused you?" shouted a voice. + +This shot seemed to carry home. Potter lifted his face and looked +around him with a pathetic hopelessness in his eyes. He saw Injun Joe, +and exclaimed: + +"Oh, Injun Joe, you promised me you'd never--" + +"Is that your knife?" and it was thrust before him by the Sheriff. + +Potter would have fallen if they had not caught him and eased him to +the ground. Then he said: + +"Something told me 't if I didn't come back and get--" He shuddered; +then waved his nerveless hand with a vanquished gesture and said, "Tell +'em, Joe, tell 'em--it ain't any use any more." + +Then Huckleberry and Tom stood dumb and staring, and heard the +stony-hearted liar reel off his serene statement, they expecting every +moment that the clear sky would deliver God's lightnings upon his head, +and wondering to see how long the stroke was delayed. And when he had +finished and still stood alive and whole, their wavering impulse to +break their oath and save the poor betrayed prisoner's life faded and +vanished away, for plainly this miscreant had sold himself to Satan and +it would be fatal to meddle with the property of such a power as that. + +"Why didn't you leave? What did you want to come here for?" somebody +said. + +"I couldn't help it--I couldn't help it," Potter moaned. "I wanted to +run away, but I couldn't seem to come anywhere but here." And he fell +to sobbing again. + +Injun Joe repeated his statement, just as calmly, a few minutes +afterward on the inquest, under oath; and the boys, seeing that the +lightnings were still withheld, were confirmed in their belief that Joe +had sold himself to the devil. He was now become, to them, the most +balefully interesting object they had ever looked upon, and they could +not take their fascinated eyes from his face. + +They inwardly resolved to watch him nights, when opportunity should +offer, in the hope of getting a glimpse of his dread master. + +Injun Joe helped to raise the body of the murdered man and put it in a +wagon for removal; and it was whispered through the shuddering crowd +that the wound bled a little! The boys thought that this happy +circumstance would turn suspicion in the right direction; but they were +disappointed, for more than one villager remarked: + +"It was within three feet of Muff Potter when it done it." + +Tom's fearful secret and gnawing conscience disturbed his sleep for as +much as a week after this; and at breakfast one morning Sid said: + +"Tom, you pitch around and talk in your sleep so much that you keep me +awake half the time." + +Tom blanched and dropped his eyes. + +"It's a bad sign," said Aunt Polly, gravely. "What you got on your +mind, Tom?" + +"Nothing. Nothing 't I know of." But the boy's hand shook so that he +spilled his coffee. + +"And you do talk such stuff," Sid said. "Last night you said, 'It's +blood, it's blood, that's what it is!' You said that over and over. And +you said, 'Don't torment me so--I'll tell!' Tell WHAT? What is it +you'll tell?" + +Everything was swimming before Tom. There is no telling what might +have happened, now, but luckily the concern passed out of Aunt Polly's +face and she came to Tom's relief without knowing it. She said: + +"Sho! It's that dreadful murder. I dream about it most every night +myself. Sometimes I dream it's me that done it." + +Mary said she had been affected much the same way. Sid seemed +satisfied. Tom got out of the presence as quick as he plausibly could, +and after that he complained of toothache for a week, and tied up his +jaws every night. He never knew that Sid lay nightly watching, and +frequently slipped the bandage free and then leaned on his elbow +listening a good while at a time, and afterward slipped the bandage +back to its place again. Tom's distress of mind wore off gradually and +the toothache grew irksome and was discarded. If Sid really managed to +make anything out of Tom's disjointed mutterings, he kept it to himself. + +It seemed to Tom that his schoolmates never would get done holding +inquests on dead cats, and thus keeping his trouble present to his +mind. Sid noticed that Tom never was coroner at one of these inquiries, +though it had been his habit to take the lead in all new enterprises; +he noticed, too, that Tom never acted as a witness--and that was +strange; and Sid did not overlook the fact that Tom even showed a +marked aversion to these inquests, and always avoided them when he +could. Sid marvelled, but said nothing. However, even inquests went out +of vogue at last, and ceased to torture Tom's conscience. + +Every day or two, during this time of sorrow, Tom watched his +opportunity and went to the little grated jail-window and smuggled such +small comforts through to the "murderer" as he could get hold of. The +jail was a trifling little brick den that stood in a marsh at the edge +of the village, and no guards were afforded for it; indeed, it was +seldom occupied. These offerings greatly helped to ease Tom's +conscience. + +The villagers had a strong desire to tar-and-feather Injun Joe and +ride him on a rail, for body-snatching, but so formidable was his +character that nobody could be found who was willing to take the lead +in the matter, so it was dropped. He had been careful to begin both of +his inquest-statements with the fight, without confessing the +grave-robbery that preceded it; therefore it was deemed wisest not +to try the case in the courts at present. + + + +CHAPTER XII + +ONE of the reasons why Tom's mind had drifted away from its secret +troubles was, that it had found a new and weighty matter to interest +itself about. Becky Thatcher had stopped coming to school. Tom had +struggled with his pride a few days, and tried to "whistle her down the +wind," but failed. He began to find himself hanging around her father's +house, nights, and feeling very miserable. She was ill. What if she +should die! There was distraction in the thought. He no longer took an +interest in war, nor even in piracy. The charm of life was gone; there +was nothing but dreariness left. He put his hoop away, and his bat; +there was no joy in them any more. His aunt was concerned. She began to +try all manner of remedies on him. She was one of those people who are +infatuated with patent medicines and all new-fangled methods of +producing health or mending it. She was an inveterate experimenter in +these things. When something fresh in this line came out she was in a +fever, right away, to try it; not on herself, for she was never ailing, +but on anybody else that came handy. She was a subscriber for all the +"Health" periodicals and phrenological frauds; and the solemn ignorance +they were inflated with was breath to her nostrils. All the "rot" they +contained about ventilation, and how to go to bed, and how to get up, +and what to eat, and what to drink, and how much exercise to take, and +what frame of mind to keep one's self in, and what sort of clothing to +wear, was all gospel to her, and she never observed that her +health-journals of the current month customarily upset everything they +had recommended the month before. She was as simple-hearted and honest +as the day was long, and so she was an easy victim. She gathered +together her quack periodicals and her quack medicines, and thus armed +with death, went about on her pale horse, metaphorically speaking, with +"hell following after." But she never suspected that she was not an +angel of healing and the balm of Gilead in disguise, to the suffering +neighbors. + +The water treatment was new, now, and Tom's low condition was a +windfall to her. She had him out at daylight every morning, stood him +up in the woodshed and drowned him with a deluge of cold water; then +she scrubbed him down with a towel like a file, and so brought him to; +then she rolled him up in a wet sheet and put him away under blankets +till she sweated his soul clean and "the yellow stains of it came +through his pores"--as Tom said. + +Yet notwithstanding all this, the boy grew more and more melancholy +and pale and dejected. She added hot baths, sitz baths, shower baths, +and plunges. The boy remained as dismal as a hearse. She began to +assist the water with a slim oatmeal diet and blister-plasters. She +calculated his capacity as she would a jug's, and filled him up every +day with quack cure-alls. + +Tom had become indifferent to persecution by this time. This phase +filled the old lady's heart with consternation. This indifference must +be broken up at any cost. Now she heard of Pain-killer for the first +time. She ordered a lot at once. She tasted it and was filled with +gratitude. It was simply fire in a liquid form. She dropped the water +treatment and everything else, and pinned her faith to Pain-killer. She +gave Tom a teaspoonful and watched with the deepest anxiety for the +result. Her troubles were instantly at rest, her soul at peace again; +for the "indifference" was broken up. The boy could not have shown a +wilder, heartier interest, if she had built a fire under him. + +Tom felt that it was time to wake up; this sort of life might be +romantic enough, in his blighted condition, but it was getting to have +too little sentiment and too much distracting variety about it. So he +thought over various plans for relief, and finally hit pon that of +professing to be fond of Pain-killer. He asked for it so often that he +became a nuisance, and his aunt ended by telling him to help himself +and quit bothering her. If it had been Sid, she would have had no +misgivings to alloy her delight; but since it was Tom, she watched the +bottle clandestinely. She found that the medicine did really diminish, +but it did not occur to her that the boy was mending the health of a +crack in the sitting-room floor with it. + +One day Tom was in the act of dosing the crack when his aunt's yellow +cat came along, purring, eying the teaspoon avariciously, and begging +for a taste. Tom said: + +"Don't ask for it unless you want it, Peter." + +But Peter signified that he did want it. + +"You better make sure." + +Peter was sure. + +"Now you've asked for it, and I'll give it to you, because there ain't +anything mean about me; but if you find you don't like it, you mustn't +blame anybody but your own self." + +Peter was agreeable. So Tom pried his mouth open and poured down the +Pain-killer. Peter sprang a couple of yards in the air, and then +delivered a war-whoop and set off round and round the room, banging +against furniture, upsetting flower-pots, and making general havoc. +Next he rose on his hind feet and pranced around, in a frenzy of +enjoyment, with his head over his shoulder and his voice proclaiming +his unappeasable happiness. Then he went tearing around the house again +spreading chaos and destruction in his path. Aunt Polly entered in time +to see him throw a few double summersets, deliver a final mighty +hurrah, and sail through the open window, carrying the rest of the +flower-pots with him. The old lady stood petrified with astonishment, +peering over her glasses; Tom lay on the floor expiring with laughter. + +"Tom, what on earth ails that cat?" + +"I don't know, aunt," gasped the boy. + +"Why, I never see anything like it. What did make him act so?" + +"Deed I don't know, Aunt Polly; cats always act so when they're having +a good time." + +"They do, do they?" There was something in the tone that made Tom +apprehensive. + +"Yes'm. That is, I believe they do." + +"You DO?" + +"Yes'm." + +The old lady was bending down, Tom watching, with interest emphasized +by anxiety. Too late he divined her "drift." The handle of the telltale +teaspoon was visible under the bed-valance. Aunt Polly took it, held it +up. Tom winced, and dropped his eyes. Aunt Polly raised him by the +usual handle--his ear--and cracked his head soundly with her thimble. + +"Now, sir, what did you want to treat that poor dumb beast so, for?" + +"I done it out of pity for him--because he hadn't any aunt." + +"Hadn't any aunt!--you numskull. What has that got to do with it?" + +"Heaps. Because if he'd had one she'd a burnt him out herself! She'd a +roasted his bowels out of him 'thout any more feeling than if he was a +human!" + +Aunt Polly felt a sudden pang of remorse. This was putting the thing +in a new light; what was cruelty to a cat MIGHT be cruelty to a boy, +too. She began to soften; she felt sorry. Her eyes watered a little, +and she put her hand on Tom's head and said gently: + +"I was meaning for the best, Tom. And, Tom, it DID do you good." + +Tom looked up in her face with just a perceptible twinkle peeping +through his gravity. + +"I know you was meaning for the best, aunty, and so was I with Peter. +It done HIM good, too. I never see him get around so since--" + +"Oh, go 'long with you, Tom, before you aggravate me again. And you +try and see if you can't be a good boy, for once, and you needn't take +any more medicine." + +Tom reached school ahead of time. It was noticed that this strange +thing had been occurring every day latterly. And now, as usual of late, +he hung about the gate of the schoolyard instead of playing with his +comrades. He was sick, he said, and he looked it. He tried to seem to +be looking everywhere but whither he really was looking--down the road. +Presently Jeff Thatcher hove in sight, and Tom's face lighted; he gazed +a moment, and then turned sorrowfully away. When Jeff arrived, Tom +accosted him; and "led up" warily to opportunities for remark about +Becky, but the giddy lad never could see the bait. Tom watched and +watched, hoping whenever a frisking frock came in sight, and hating the +owner of it as soon as he saw she was not the right one. At last frocks +ceased to appear, and he dropped hopelessly into the dumps; he entered +the empty schoolhouse and sat down to suffer. Then one more frock +passed in at the gate, and Tom's heart gave a great bound. The next +instant he was out, and "going on" like an Indian; yelling, laughing, +chasing boys, jumping over the fence at risk of life and limb, throwing +handsprings, standing on his head--doing all the heroic things he could +conceive of, and keeping a furtive eye out, all the while, to see if +Becky Thatcher was noticing. But she seemed to be unconscious of it +all; she never looked. Could it be possible that she was not aware that +he was there? He carried his exploits to her immediate vicinity; came +war-whooping around, snatched a boy's cap, hurled it to the roof of the +schoolhouse, broke through a group of boys, tumbling them in every +direction, and fell sprawling, himself, under Becky's nose, almost +upsetting her--and she turned, with her nose in the air, and he heard +her say: "Mf! some people think they're mighty smart--always showing +off!" + +Tom's cheeks burned. He gathered himself up and sneaked off, crushed +and crestfallen. + + + +CHAPTER XIII + +TOM'S mind was made up now. He was gloomy and desperate. He was a +forsaken, friendless boy, he said; nobody loved him; when they found +out what they had driven him to, perhaps they would be sorry; he had +tried to do right and get along, but they would not let him; since +nothing would do them but to be rid of him, let it be so; and let them +blame HIM for the consequences--why shouldn't they? What right had the +friendless to complain? Yes, they had forced him to it at last: he +would lead a life of crime. There was no choice. + +By this time he was far down Meadow Lane, and the bell for school to +"take up" tinkled faintly upon his ear. He sobbed, now, to think he +should never, never hear that old familiar sound any more--it was very +hard, but it was forced on him; since he was driven out into the cold +world, he must submit--but he forgave them. Then the sobs came thick +and fast. + +Just at this point he met his soul's sworn comrade, Joe Harper +--hard-eyed, and with evidently a great and dismal purpose in his heart. +Plainly here were "two souls with but a single thought." Tom, wiping +his eyes with his sleeve, began to blubber out something about a +resolution to escape from hard usage and lack of sympathy at home by +roaming abroad into the great world never to return; and ended by +hoping that Joe would not forget him. + +But it transpired that this was a request which Joe had just been +going to make of Tom, and had come to hunt him up for that purpose. His +mother had whipped him for drinking some cream which he had never +tasted and knew nothing about; it was plain that she was tired of him +and wished him to go; if she felt that way, there was nothing for him +to do but succumb; he hoped she would be happy, and never regret having +driven her poor boy out into the unfeeling world to suffer and die. + +As the two boys walked sorrowing along, they made a new compact to +stand by each other and be brothers and never separate till death +relieved them of their troubles. Then they began to lay their plans. +Joe was for being a hermit, and living on crusts in a remote cave, and +dying, some time, of cold and want and grief; but after listening to +Tom, he conceded that there were some conspicuous advantages about a +life of crime, and so he consented to be a pirate. + +Three miles below St. Petersburg, at a point where the Mississippi +River was a trifle over a mile wide, there was a long, narrow, wooded +island, with a shallow bar at the head of it, and this offered well as +a rendezvous. It was not inhabited; it lay far over toward the further +shore, abreast a dense and almost wholly unpeopled forest. So Jackson's +Island was chosen. Who were to be the subjects of their piracies was a +matter that did not occur to them. Then they hunted up Huckleberry +Finn, and he joined them promptly, for all careers were one to him; he +was indifferent. They presently separated to meet at a lonely spot on +the river-bank two miles above the village at the favorite hour--which +was midnight. There was a small log raft there which they meant to +capture. Each would bring hooks and lines, and such provision as he +could steal in the most dark and mysterious way--as became outlaws. And +before the afternoon was done, they had all managed to enjoy the sweet +glory of spreading the fact that pretty soon the town would "hear +something." All who got this vague hint were cautioned to "be mum and +wait." + +About midnight Tom arrived with a boiled ham and a few trifles, +and stopped in a dense undergrowth on a small bluff overlooking the +meeting-place. It was starlight, and very still. The mighty river lay +like an ocean at rest. Tom listened a moment, but no sound disturbed the +quiet. Then he gave a low, distinct whistle. It was answered from under +the bluff. Tom whistled twice more; these signals were answered in the +same way. Then a guarded voice said: + +"Who goes there?" + +"Tom Sawyer, the Black Avenger of the Spanish Main. Name your names." + +"Huck Finn the Red-Handed, and Joe Harper the Terror of the Seas." Tom +had furnished these titles, from his favorite literature. + +"'Tis well. Give the countersign." + +Two hoarse whispers delivered the same awful word simultaneously to +the brooding night: + +"BLOOD!" + +Then Tom tumbled his ham over the bluff and let himself down after it, +tearing both skin and clothes to some extent in the effort. There was +an easy, comfortable path along the shore under the bluff, but it +lacked the advantages of difficulty and danger so valued by a pirate. + +The Terror of the Seas had brought a side of bacon, and had about worn +himself out with getting it there. Finn the Red-Handed had stolen a +skillet and a quantity of half-cured leaf tobacco, and had also brought +a few corn-cobs to make pipes with. But none of the pirates smoked or +"chewed" but himself. The Black Avenger of the Spanish Main said it +would never do to start without some fire. That was a wise thought; +matches were hardly known there in that day. They saw a fire +smouldering upon a great raft a hundred yards above, and they went +stealthily thither and helped themselves to a chunk. They made an +imposing adventure of it, saying, "Hist!" every now and then, and +suddenly halting with finger on lip; moving with hands on imaginary +dagger-hilts; and giving orders in dismal whispers that if "the foe" +stirred, to "let him have it to the hilt," because "dead men tell no +tales." They knew well enough that the raftsmen were all down at the +village laying in stores or having a spree, but still that was no +excuse for their conducting this thing in an unpiratical way. + +They shoved off, presently, Tom in command, Huck at the after oar and +Joe at the forward. Tom stood amidships, gloomy-browed, and with folded +arms, and gave his orders in a low, stern whisper: + +"Luff, and bring her to the wind!" + +"Aye-aye, sir!" + +"Steady, steady-y-y-y!" + +"Steady it is, sir!" + +"Let her go off a point!" + +"Point it is, sir!" + +As the boys steadily and monotonously drove the raft toward mid-stream +it was no doubt understood that these orders were given only for +"style," and were not intended to mean anything in particular. + +"What sail's she carrying?" + +"Courses, tops'ls, and flying-jib, sir." + +"Send the r'yals up! Lay out aloft, there, half a dozen of ye +--foretopmaststuns'l! Lively, now!" + +"Aye-aye, sir!" + +"Shake out that maintogalans'l! Sheets and braces! NOW my hearties!" + +"Aye-aye, sir!" + +"Hellum-a-lee--hard a port! Stand by to meet her when she comes! Port, +port! NOW, men! With a will! Stead-y-y-y!" + +"Steady it is, sir!" + +The raft drew beyond the middle of the river; the boys pointed her +head right, and then lay on their oars. The river was not high, so +there was not more than a two or three mile current. Hardly a word was +said during the next three-quarters of an hour. Now the raft was +passing before the distant town. Two or three glimmering lights showed +where it lay, peacefully sleeping, beyond the vague vast sweep of +star-gemmed water, unconscious of the tremendous event that was happening. +The Black Avenger stood still with folded arms, "looking his last" upon +the scene of his former joys and his later sufferings, and wishing +"she" could see him now, abroad on the wild sea, facing peril and death +with dauntless heart, going to his doom with a grim smile on his lips. +It was but a small strain on his imagination to remove Jackson's Island +beyond eyeshot of the village, and so he "looked his last" with a +broken and satisfied heart. The other pirates were looking their last, +too; and they all looked so long that they came near letting the +current drift them out of the range of the island. But they discovered +the danger in time, and made shift to avert it. About two o'clock in +the morning the raft grounded on the bar two hundred yards above the +head of the island, and they waded back and forth until they had landed +their freight. Part of the little raft's belongings consisted of an old +sail, and this they spread over a nook in the bushes for a tent to +shelter their provisions; but they themselves would sleep in the open +air in good weather, as became outlaws. + +They built a fire against the side of a great log twenty or thirty +steps within the sombre depths of the forest, and then cooked some +bacon in the frying-pan for supper, and used up half of the corn "pone" +stock they had brought. It seemed glorious sport to be feasting in that +wild, free way in the virgin forest of an unexplored and uninhabited +island, far from the haunts of men, and they said they never would +return to civilization. The climbing fire lit up their faces and threw +its ruddy glare upon the pillared tree-trunks of their forest temple, +and upon the varnished foliage and festooning vines. + +When the last crisp slice of bacon was gone, and the last allowance of +corn pone devoured, the boys stretched themselves out on the grass, +filled with contentment. They could have found a cooler place, but they +would not deny themselves such a romantic feature as the roasting +camp-fire. + +"AIN'T it gay?" said Joe. + +"It's NUTS!" said Tom. "What would the boys say if they could see us?" + +"Say? Well, they'd just die to be here--hey, Hucky!" + +"I reckon so," said Huckleberry; "anyways, I'm suited. I don't want +nothing better'n this. I don't ever get enough to eat, gen'ally--and +here they can't come and pick at a feller and bullyrag him so." + +"It's just the life for me," said Tom. "You don't have to get up, +mornings, and you don't have to go to school, and wash, and all that +blame foolishness. You see a pirate don't have to do ANYTHING, Joe, +when he's ashore, but a hermit HE has to be praying considerable, and +then he don't have any fun, anyway, all by himself that way." + +"Oh yes, that's so," said Joe, "but I hadn't thought much about it, +you know. I'd a good deal rather be a pirate, now that I've tried it." + +"You see," said Tom, "people don't go much on hermits, nowadays, like +they used to in old times, but a pirate's always respected. And a +hermit's got to sleep on the hardest place he can find, and put +sackcloth and ashes on his head, and stand out in the rain, and--" + +"What does he put sackcloth and ashes on his head for?" inquired Huck. + +"I dono. But they've GOT to do it. Hermits always do. You'd have to do +that if you was a hermit." + +"Dern'd if I would," said Huck. + +"Well, what would you do?" + +"I dono. But I wouldn't do that." + +"Why, Huck, you'd HAVE to. How'd you get around it?" + +"Why, I just wouldn't stand it. I'd run away." + +"Run away! Well, you WOULD be a nice old slouch of a hermit. You'd be +a disgrace." + +The Red-Handed made no response, being better employed. He had +finished gouging out a cob, and now he fitted a weed stem to it, loaded +it with tobacco, and was pressing a coal to the charge and blowing a +cloud of fragrant smoke--he was in the full bloom of luxurious +contentment. The other pirates envied him this majestic vice, and +secretly resolved to acquire it shortly. Presently Huck said: + +"What does pirates have to do?" + +Tom said: + +"Oh, they have just a bully time--take ships and burn them, and get +the money and bury it in awful places in their island where there's +ghosts and things to watch it, and kill everybody in the ships--make +'em walk a plank." + +"And they carry the women to the island," said Joe; "they don't kill +the women." + +"No," assented Tom, "they don't kill the women--they're too noble. And +the women's always beautiful, too. + +"And don't they wear the bulliest clothes! Oh no! All gold and silver +and di'monds," said Joe, with enthusiasm. + +"Who?" said Huck. + +"Why, the pirates." + +Huck scanned his own clothing forlornly. + +"I reckon I ain't dressed fitten for a pirate," said he, with a +regretful pathos in his voice; "but I ain't got none but these." + +But the other boys told him the fine clothes would come fast enough, +after they should have begun their adventures. They made him understand +that his poor rags would do to begin with, though it was customary for +wealthy pirates to start with a proper wardrobe. + +Gradually their talk died out and drowsiness began to steal upon the +eyelids of the little waifs. The pipe dropped from the fingers of the +Red-Handed, and he slept the sleep of the conscience-free and the +weary. The Terror of the Seas and the Black Avenger of the Spanish Main +had more difficulty in getting to sleep. They said their prayers +inwardly, and lying down, since there was nobody there with authority +to make them kneel and recite aloud; in truth, they had a mind not to +say them at all, but they were afraid to proceed to such lengths as +that, lest they might call down a sudden and special thunderbolt from +heaven. Then at once they reached and hovered upon the imminent verge +of sleep--but an intruder came, now, that would not "down." It was +conscience. They began to feel a vague fear that they had been doing +wrong to run away; and next they thought of the stolen meat, and then +the real torture came. They tried to argue it away by reminding +conscience that they had purloined sweetmeats and apples scores of +times; but conscience was not to be appeased by such thin +plausibilities; it seemed to them, in the end, that there was no +getting around the stubborn fact that taking sweetmeats was only +"hooking," while taking bacon and hams and such valuables was plain +simple stealing--and there was a command against that in the Bible. So +they inwardly resolved that so long as they remained in the business, +their piracies should not again be sullied with the crime of stealing. +Then conscience granted a truce, and these curiously inconsistent +pirates fell peacefully to sleep. + + + +CHAPTER XIV + +WHEN Tom awoke in the morning, he wondered where he was. He sat up and +rubbed his eyes and looked around. Then he comprehended. It was the +cool gray dawn, and there was a delicious sense of repose and peace in +the deep pervading calm and silence of the woods. Not a leaf stirred; +not a sound obtruded upon great Nature's meditation. Beaded dewdrops +stood upon the leaves and grasses. A white layer of ashes covered the +fire, and a thin blue breath of smoke rose straight into the air. Joe +and Huck still slept. + +Now, far away in the woods a bird called; another answered; presently +the hammering of a woodpecker was heard. Gradually the cool dim gray of +the morning whitened, and as gradually sounds multiplied and life +manifested itself. The marvel of Nature shaking off sleep and going to +work unfolded itself to the musing boy. A little green worm came +crawling over a dewy leaf, lifting two-thirds of his body into the air +from time to time and "sniffing around," then proceeding again--for he +was measuring, Tom said; and when the worm approached him, of its own +accord, he sat as still as a stone, with his hopes rising and falling, +by turns, as the creature still came toward him or seemed inclined to +go elsewhere; and when at last it considered a painful moment with its +curved body in the air and then came decisively down upon Tom's leg and +began a journey over him, his whole heart was glad--for that meant that +he was going to have a new suit of clothes--without the shadow of a +doubt a gaudy piratical uniform. Now a procession of ants appeared, +from nowhere in particular, and went about their labors; one struggled +manfully by with a dead spider five times as big as itself in its arms, +and lugged it straight up a tree-trunk. A brown spotted lady-bug +climbed the dizzy height of a grass blade, and Tom bent down close to +it and said, "Lady-bug, lady-bug, fly away home, your house is on fire, +your children's alone," and she took wing and went off to see about it +--which did not surprise the boy, for he knew of old that this insect was +credulous about conflagrations, and he had practised upon its +simplicity more than once. A tumblebug came next, heaving sturdily at +its ball, and Tom touched the creature, to see it shut its legs against +its body and pretend to be dead. The birds were fairly rioting by this +time. A catbird, the Northern mocker, lit in a tree over Tom's head, +and trilled out her imitations of her neighbors in a rapture of +enjoyment; then a shrill jay swept down, a flash of blue flame, and +stopped on a twig almost within the boy's reach, cocked his head to one +side and eyed the strangers with a consuming curiosity; a gray squirrel +and a big fellow of the "fox" kind came skurrying along, sitting up at +intervals to inspect and chatter at the boys, for the wild things had +probably never seen a human being before and scarcely knew whether to +be afraid or not. All Nature was wide awake and stirring, now; long +lances of sunlight pierced down through the dense foliage far and near, +and a few butterflies came fluttering upon the scene. + +Tom stirred up the other pirates and they all clattered away with a +shout, and in a minute or two were stripped and chasing after and +tumbling over each other in the shallow limpid water of the white +sandbar. They felt no longing for the little village sleeping in the +distance beyond the majestic waste of water. A vagrant current or a +slight rise in the river had carried off their raft, but this only +gratified them, since its going was something like burning the bridge +between them and civilization. + +They came back to camp wonderfully refreshed, glad-hearted, and +ravenous; and they soon had the camp-fire blazing up again. Huck found +a spring of clear cold water close by, and the boys made cups of broad +oak or hickory leaves, and felt that water, sweetened with such a +wildwood charm as that, would be a good enough substitute for coffee. +While Joe was slicing bacon for breakfast, Tom and Huck asked him to +hold on a minute; they stepped to a promising nook in the river-bank +and threw in their lines; almost immediately they had reward. Joe had +not had time to get impatient before they were back again with some +handsome bass, a couple of sun-perch and a small catfish--provisions +enough for quite a family. They fried the fish with the bacon, and were +astonished; for no fish had ever seemed so delicious before. They did +not know that the quicker a fresh-water fish is on the fire after he is +caught the better he is; and they reflected little upon what a sauce +open-air sleeping, open-air exercise, bathing, and a large ingredient +of hunger make, too. + +They lay around in the shade, after breakfast, while Huck had a smoke, +and then went off through the woods on an exploring expedition. They +tramped gayly along, over decaying logs, through tangled underbrush, +among solemn monarchs of the forest, hung from their crowns to the +ground with a drooping regalia of grape-vines. Now and then they came +upon snug nooks carpeted with grass and jeweled with flowers. + +They found plenty of things to be delighted with, but nothing to be +astonished at. They discovered that the island was about three miles +long and a quarter of a mile wide, and that the shore it lay closest to +was only separated from it by a narrow channel hardly two hundred yards +wide. They took a swim about every hour, so it was close upon the +middle of the afternoon when they got back to camp. They were too +hungry to stop to fish, but they fared sumptuously upon cold ham, and +then threw themselves down in the shade to talk. But the talk soon +began to drag, and then died. The stillness, the solemnity that brooded +in the woods, and the sense of loneliness, began to tell upon the +spirits of the boys. They fell to thinking. A sort of undefined longing +crept upon them. This took dim shape, presently--it was budding +homesickness. Even Finn the Red-Handed was dreaming of his doorsteps +and empty hogsheads. But they were all ashamed of their weakness, and +none was brave enough to speak his thought. + +For some time, now, the boys had been dully conscious of a peculiar +sound in the distance, just as one sometimes is of the ticking of a +clock which he takes no distinct note of. But now this mysterious sound +became more pronounced, and forced a recognition. The boys started, +glanced at each other, and then each assumed a listening attitude. +There was a long silence, profound and unbroken; then a deep, sullen +boom came floating down out of the distance. + +"What is it!" exclaimed Joe, under his breath. + +"I wonder," said Tom in a whisper. + +"'Tain't thunder," said Huckleberry, in an awed tone, "becuz thunder--" + +"Hark!" said Tom. "Listen--don't talk." + +They waited a time that seemed an age, and then the same muffled boom +troubled the solemn hush. + +"Let's go and see." + +They sprang to their feet and hurried to the shore toward the town. +They parted the bushes on the bank and peered out over the water. The +little steam ferryboat was about a mile below the village, drifting +with the current. Her broad deck seemed crowded with people. There were +a great many skiffs rowing about or floating with the stream in the +neighborhood of the ferryboat, but the boys could not determine what +the men in them were doing. Presently a great jet of white smoke burst +from the ferryboat's side, and as it expanded and rose in a lazy cloud, +that same dull throb of sound was borne to the listeners again. + +"I know now!" exclaimed Tom; "somebody's drownded!" + +"That's it!" said Huck; "they done that last summer, when Bill Turner +got drownded; they shoot a cannon over the water, and that makes him +come up to the top. Yes, and they take loaves of bread and put +quicksilver in 'em and set 'em afloat, and wherever there's anybody +that's drownded, they'll float right there and stop." + +"Yes, I've heard about that," said Joe. "I wonder what makes the bread +do that." + +"Oh, it ain't the bread, so much," said Tom; "I reckon it's mostly +what they SAY over it before they start it out." + +"But they don't say anything over it," said Huck. "I've seen 'em and +they don't." + +"Well, that's funny," said Tom. "But maybe they say it to themselves. +Of COURSE they do. Anybody might know that." + +The other boys agreed that there was reason in what Tom said, because +an ignorant lump of bread, uninstructed by an incantation, could not be +expected to act very intelligently when set upon an errand of such +gravity. + +"By jings, I wish I was over there, now," said Joe. + +"I do too" said Huck "I'd give heaps to know who it is." + +The boys still listened and watched. Presently a revealing thought +flashed through Tom's mind, and he exclaimed: + +"Boys, I know who's drownded--it's us!" + +They felt like heroes in an instant. Here was a gorgeous triumph; they +were missed; they were mourned; hearts were breaking on their account; +tears were being shed; accusing memories of unkindness to these poor +lost lads were rising up, and unavailing regrets and remorse were being +indulged; and best of all, the departed were the talk of the whole +town, and the envy of all the boys, as far as this dazzling notoriety +was concerned. This was fine. It was worth while to be a pirate, after +all. + +As twilight drew on, the ferryboat went back to her accustomed +business and the skiffs disappeared. The pirates returned to camp. They +were jubilant with vanity over their new grandeur and the illustrious +trouble they were making. They caught fish, cooked supper and ate it, +and then fell to guessing at what the village was thinking and saying +about them; and the pictures they drew of the public distress on their +account were gratifying to look upon--from their point of view. But +when the shadows of night closed them in, they gradually ceased to +talk, and sat gazing into the fire, with their minds evidently +wandering elsewhere. The excitement was gone, now, and Tom and Joe +could not keep back thoughts of certain persons at home who were not +enjoying this fine frolic as much as they were. Misgivings came; they +grew troubled and unhappy; a sigh or two escaped, unawares. By and by +Joe timidly ventured upon a roundabout "feeler" as to how the others +might look upon a return to civilization--not right now, but-- + +Tom withered him with derision! Huck, being uncommitted as yet, joined +in with Tom, and the waverer quickly "explained," and was glad to get +out of the scrape with as little taint of chicken-hearted homesickness +clinging to his garments as he could. Mutiny was effectually laid to +rest for the moment. + +As the night deepened, Huck began to nod, and presently to snore. Joe +followed next. Tom lay upon his elbow motionless, for some time, +watching the two intently. At last he got up cautiously, on his knees, +and went searching among the grass and the flickering reflections flung +by the camp-fire. He picked up and inspected several large +semi-cylinders of the thin white bark of a sycamore, and finally chose +two which seemed to suit him. Then he knelt by the fire and painfully +wrote something upon each of these with his "red keel"; one he rolled up +and put in his jacket pocket, and the other he put in Joe's hat and +removed it to a little distance from the owner. And he also put into the +hat certain schoolboy treasures of almost inestimable value--among them +a lump of chalk, an India-rubber ball, three fishhooks, and one of that +kind of marbles known as a "sure 'nough crystal." Then he tiptoed his +way cautiously among the trees till he felt that he was out of hearing, +and straightway broke into a keen run in the direction of the sandbar. + + + +CHAPTER XV + +A FEW minutes later Tom was in the shoal water of the bar, wading +toward the Illinois shore. Before the depth reached his middle he was +half-way over; the current would permit no more wading, now, so he +struck out confidently to swim the remaining hundred yards. He swam +quartering upstream, but still was swept downward rather faster than he +had expected. However, he reached the shore finally, and drifted along +till he found a low place and drew himself out. He put his hand on his +jacket pocket, found his piece of bark safe, and then struck through +the woods, following the shore, with streaming garments. Shortly before +ten o'clock he came out into an open place opposite the village, and +saw the ferryboat lying in the shadow of the trees and the high bank. +Everything was quiet under the blinking stars. He crept down the bank, +watching with all his eyes, slipped into the water, swam three or four +strokes and climbed into the skiff that did "yawl" duty at the boat's +stern. He laid himself down under the thwarts and waited, panting. + +Presently the cracked bell tapped and a voice gave the order to "cast +off." A minute or two later the skiff's head was standing high up, +against the boat's swell, and the voyage was begun. Tom felt happy in +his success, for he knew it was the boat's last trip for the night. At +the end of a long twelve or fifteen minutes the wheels stopped, and Tom +slipped overboard and swam ashore in the dusk, landing fifty yards +downstream, out of danger of possible stragglers. + +He flew along unfrequented alleys, and shortly found himself at his +aunt's back fence. He climbed over, approached the "ell," and looked in +at the sitting-room window, for a light was burning there. There sat +Aunt Polly, Sid, Mary, and Joe Harper's mother, grouped together, +talking. They were by the bed, and the bed was between them and the +door. Tom went to the door and began to softly lift the latch; then he +pressed gently and the door yielded a crack; he continued pushing +cautiously, and quaking every time it creaked, till he judged he might +squeeze through on his knees; so he put his head through and began, +warily. + +"What makes the candle blow so?" said Aunt Polly. Tom hurried up. +"Why, that door's open, I believe. Why, of course it is. No end of +strange things now. Go 'long and shut it, Sid." + +Tom disappeared under the bed just in time. He lay and "breathed" +himself for a time, and then crept to where he could almost touch his +aunt's foot. + +"But as I was saying," said Aunt Polly, "he warn't BAD, so to say +--only mischEEvous. Only just giddy, and harum-scarum, you know. He +warn't any more responsible than a colt. HE never meant any harm, and +he was the best-hearted boy that ever was"--and she began to cry. + +"It was just so with my Joe--always full of his devilment, and up to +every kind of mischief, but he was just as unselfish and kind as he +could be--and laws bless me, to think I went and whipped him for taking +that cream, never once recollecting that I throwed it out myself +because it was sour, and I never to see him again in this world, never, +never, never, poor abused boy!" And Mrs. Harper sobbed as if her heart +would break. + +"I hope Tom's better off where he is," said Sid, "but if he'd been +better in some ways--" + +"SID!" Tom felt the glare of the old lady's eye, though he could not +see it. "Not a word against my Tom, now that he's gone! God'll take +care of HIM--never you trouble YOURself, sir! Oh, Mrs. Harper, I don't +know how to give him up! I don't know how to give him up! He was such a +comfort to me, although he tormented my old heart out of me, 'most." + +"The Lord giveth and the Lord hath taken away--Blessed be the name of +the Lord! But it's so hard--Oh, it's so hard! Only last Saturday my +Joe busted a firecracker right under my nose and I knocked him +sprawling. Little did I know then, how soon--Oh, if it was to do over +again I'd hug him and bless him for it." + +"Yes, yes, yes, I know just how you feel, Mrs. Harper, I know just +exactly how you feel. No longer ago than yesterday noon, my Tom took +and filled the cat full of Pain-killer, and I did think the cretur +would tear the house down. And God forgive me, I cracked Tom's head +with my thimble, poor boy, poor dead boy. But he's out of all his +troubles now. And the last words I ever heard him say was to reproach--" + +But this memory was too much for the old lady, and she broke entirely +down. Tom was snuffling, now, himself--and more in pity of himself than +anybody else. He could hear Mary crying, and putting in a kindly word +for him from time to time. He began to have a nobler opinion of himself +than ever before. Still, he was sufficiently touched by his aunt's +grief to long to rush out from under the bed and overwhelm her with +joy--and the theatrical gorgeousness of the thing appealed strongly to +his nature, too, but he resisted and lay still. + +He went on listening, and gathered by odds and ends that it was +conjectured at first that the boys had got drowned while taking a swim; +then the small raft had been missed; next, certain boys said the +missing lads had promised that the village should "hear something" +soon; the wise-heads had "put this and that together" and decided that +the lads had gone off on that raft and would turn up at the next town +below, presently; but toward noon the raft had been found, lodged +against the Missouri shore some five or six miles below the village +--and then hope perished; they must be drowned, else hunger would have +driven them home by nightfall if not sooner. It was believed that the +search for the bodies had been a fruitless effort merely because the +drowning must have occurred in mid-channel, since the boys, being good +swimmers, would otherwise have escaped to shore. This was Wednesday +night. If the bodies continued missing until Sunday, all hope would be +given over, and the funerals would be preached on that morning. Tom +shuddered. + +Mrs. Harper gave a sobbing good-night and turned to go. Then with a +mutual impulse the two bereaved women flung themselves into each +other's arms and had a good, consoling cry, and then parted. Aunt Polly +was tender far beyond her wont, in her good-night to Sid and Mary. Sid +snuffled a bit and Mary went off crying with all her heart. + +Aunt Polly knelt down and prayed for Tom so touchingly, so +appealingly, and with such measureless love in her words and her old +trembling voice, that he was weltering in tears again, long before she +was through. + +He had to keep still long after she went to bed, for she kept making +broken-hearted ejaculations from time to time, tossing unrestfully, and +turning over. But at last she was still, only moaning a little in her +sleep. Now the boy stole out, rose gradually by the bedside, shaded the +candle-light with his hand, and stood regarding her. His heart was full +of pity for her. He took out his sycamore scroll and placed it by the +candle. But something occurred to him, and he lingered considering. His +face lighted with a happy solution of his thought; he put the bark +hastily in his pocket. Then he bent over and kissed the faded lips, and +straightway made his stealthy exit, latching the door behind him. + +He threaded his way back to the ferry landing, found nobody at large +there, and walked boldly on board the boat, for he knew she was +tenantless except that there was a watchman, who always turned in and +slept like a graven image. He untied the skiff at the stern, slipped +into it, and was soon rowing cautiously upstream. When he had pulled a +mile above the village, he started quartering across and bent himself +stoutly to his work. He hit the landing on the other side neatly, for +this was a familiar bit of work to him. He was moved to capture the +skiff, arguing that it might be considered a ship and therefore +legitimate prey for a pirate, but he knew a thorough search would be +made for it and that might end in revelations. So he stepped ashore and +entered the woods. + +He sat down and took a long rest, torturing himself meanwhile to keep +awake, and then started warily down the home-stretch. The night was far +spent. It was broad daylight before he found himself fairly abreast the +island bar. He rested again until the sun was well up and gilding the +great river with its splendor, and then he plunged into the stream. A +little later he paused, dripping, upon the threshold of the camp, and +heard Joe say: + +"No, Tom's true-blue, Huck, and he'll come back. He won't desert. He +knows that would be a disgrace to a pirate, and Tom's too proud for +that sort of thing. He's up to something or other. Now I wonder what?" + +"Well, the things is ours, anyway, ain't they?" + +"Pretty near, but not yet, Huck. The writing says they are if he ain't +back here to breakfast." + +"Which he is!" exclaimed Tom, with fine dramatic effect, stepping +grandly into camp. + +A sumptuous breakfast of bacon and fish was shortly provided, and as +the boys set to work upon it, Tom recounted (and adorned) his +adventures. They were a vain and boastful company of heroes when the +tale was done. Then Tom hid himself away in a shady nook to sleep till +noon, and the other pirates got ready to fish and explore. + + + +CHAPTER XVI + +AFTER dinner all the gang turned out to hunt for turtle eggs on the +bar. They went about poking sticks into the sand, and when they found a +soft place they went down on their knees and dug with their hands. +Sometimes they would take fifty or sixty eggs out of one hole. They +were perfectly round white things a trifle smaller than an English +walnut. They had a famous fried-egg feast that night, and another on +Friday morning. + +After breakfast they went whooping and prancing out on the bar, and +chased each other round and round, shedding clothes as they went, until +they were naked, and then continued the frolic far away up the shoal +water of the bar, against the stiff current, which latter tripped their +legs from under them from time to time and greatly increased the fun. +And now and then they stooped in a group and splashed water in each +other's faces with their palms, gradually approaching each other, with +averted faces to avoid the strangling sprays, and finally gripping and +struggling till the best man ducked his neighbor, and then they all +went under in a tangle of white legs and arms and came up blowing, +sputtering, laughing, and gasping for breath at one and the same time. + +When they were well exhausted, they would run out and sprawl on the +dry, hot sand, and lie there and cover themselves up with it, and by +and by break for the water again and go through the original +performance once more. Finally it occurred to them that their naked +skin represented flesh-colored "tights" very fairly; so they drew a +ring in the sand and had a circus--with three clowns in it, for none +would yield this proudest post to his neighbor. + +Next they got their marbles and played "knucks" and "ring-taw" and +"keeps" till that amusement grew stale. Then Joe and Huck had another +swim, but Tom would not venture, because he found that in kicking off +his trousers he had kicked his string of rattlesnake rattles off his +ankle, and he wondered how he had escaped cramp so long without the +protection of this mysterious charm. He did not venture again until he +had found it, and by that time the other boys were tired and ready to +rest. They gradually wandered apart, dropped into the "dumps," and fell +to gazing longingly across the wide river to where the village lay +drowsing in the sun. Tom found himself writing "BECKY" in the sand with +his big toe; he scratched it out, and was angry with himself for his +weakness. But he wrote it again, nevertheless; he could not help it. He +erased it once more and then took himself out of temptation by driving +the other boys together and joining them. + +But Joe's spirits had gone down almost beyond resurrection. He was so +homesick that he could hardly endure the misery of it. The tears lay +very near the surface. Huck was melancholy, too. Tom was downhearted, +but tried hard not to show it. He had a secret which he was not ready +to tell, yet, but if this mutinous depression was not broken up soon, +he would have to bring it out. He said, with a great show of +cheerfulness: + +"I bet there's been pirates on this island before, boys. We'll explore +it again. They've hid treasures here somewhere. How'd you feel to light +on a rotten chest full of gold and silver--hey?" + +But it roused only faint enthusiasm, which faded out, with no reply. +Tom tried one or two other seductions; but they failed, too. It was +discouraging work. Joe sat poking up the sand with a stick and looking +very gloomy. Finally he said: + +"Oh, boys, let's give it up. I want to go home. It's so lonesome." + +"Oh no, Joe, you'll feel better by and by," said Tom. "Just think of +the fishing that's here." + +"I don't care for fishing. I want to go home." + +"But, Joe, there ain't such another swimming-place anywhere." + +"Swimming's no good. I don't seem to care for it, somehow, when there +ain't anybody to say I sha'n't go in. I mean to go home." + +"Oh, shucks! Baby! You want to see your mother, I reckon." + +"Yes, I DO want to see my mother--and you would, too, if you had one. +I ain't any more baby than you are." And Joe snuffled a little. + +"Well, we'll let the cry-baby go home to his mother, won't we, Huck? +Poor thing--does it want to see its mother? And so it shall. You like +it here, don't you, Huck? We'll stay, won't we?" + +Huck said, "Y-e-s"--without any heart in it. + +"I'll never speak to you again as long as I live," said Joe, rising. +"There now!" And he moved moodily away and began to dress himself. + +"Who cares!" said Tom. "Nobody wants you to. Go 'long home and get +laughed at. Oh, you're a nice pirate. Huck and me ain't cry-babies. +We'll stay, won't we, Huck? Let him go if he wants to. I reckon we can +get along without him, per'aps." + +But Tom was uneasy, nevertheless, and was alarmed to see Joe go +sullenly on with his dressing. And then it was discomforting to see +Huck eying Joe's preparations so wistfully, and keeping up such an +ominous silence. Presently, without a parting word, Joe began to wade +off toward the Illinois shore. Tom's heart began to sink. He glanced at +Huck. Huck could not bear the look, and dropped his eyes. Then he said: + +"I want to go, too, Tom. It was getting so lonesome anyway, and now +it'll be worse. Let's us go, too, Tom." + +"I won't! You can all go, if you want to. I mean to stay." + +"Tom, I better go." + +"Well, go 'long--who's hendering you." + +Huck began to pick up his scattered clothes. He said: + +"Tom, I wisht you'd come, too. Now you think it over. We'll wait for +you when we get to shore." + +"Well, you'll wait a blame long time, that's all." + +Huck started sorrowfully away, and Tom stood looking after him, with a +strong desire tugging at his heart to yield his pride and go along too. +He hoped the boys would stop, but they still waded slowly on. It +suddenly dawned on Tom that it was become very lonely and still. He +made one final struggle with his pride, and then darted after his +comrades, yelling: + +"Wait! Wait! I want to tell you something!" + +They presently stopped and turned around. When he got to where they +were, he began unfolding his secret, and they listened moodily till at +last they saw the "point" he was driving at, and then they set up a +war-whoop of applause and said it was "splendid!" and said if he had +told them at first, they wouldn't have started away. He made a plausible +excuse; but his real reason had been the fear that not even the secret +would keep them with him any very great length of time, and so he had +meant to hold it in reserve as a last seduction. + +The lads came gayly back and went at their sports again with a will, +chattering all the time about Tom's stupendous plan and admiring the +genius of it. After a dainty egg and fish dinner, Tom said he wanted to +learn to smoke, now. Joe caught at the idea and said he would like to +try, too. So Huck made pipes and filled them. These novices had never +smoked anything before but cigars made of grape-vine, and they "bit" +the tongue, and were not considered manly anyway. + +Now they stretched themselves out on their elbows and began to puff, +charily, and with slender confidence. The smoke had an unpleasant +taste, and they gagged a little, but Tom said: + +"Why, it's just as easy! If I'd a knowed this was all, I'd a learnt +long ago." + +"So would I," said Joe. "It's just nothing." + +"Why, many a time I've looked at people smoking, and thought well I +wish I could do that; but I never thought I could," said Tom. + +"That's just the way with me, hain't it, Huck? You've heard me talk +just that way--haven't you, Huck? I'll leave it to Huck if I haven't." + +"Yes--heaps of times," said Huck. + +"Well, I have too," said Tom; "oh, hundreds of times. Once down by the +slaughter-house. Don't you remember, Huck? Bob Tanner was there, and +Johnny Miller, and Jeff Thatcher, when I said it. Don't you remember, +Huck, 'bout me saying that?" + +"Yes, that's so," said Huck. "That was the day after I lost a white +alley. No, 'twas the day before." + +"There--I told you so," said Tom. "Huck recollects it." + +"I bleeve I could smoke this pipe all day," said Joe. "I don't feel +sick." + +"Neither do I," said Tom. "I could smoke it all day. But I bet you +Jeff Thatcher couldn't." + +"Jeff Thatcher! Why, he'd keel over just with two draws. Just let him +try it once. HE'D see!" + +"I bet he would. And Johnny Miller--I wish could see Johnny Miller +tackle it once." + +"Oh, don't I!" said Joe. "Why, I bet you Johnny Miller couldn't any +more do this than nothing. Just one little snifter would fetch HIM." + +"'Deed it would, Joe. Say--I wish the boys could see us now." + +"So do I." + +"Say--boys, don't say anything about it, and some time when they're +around, I'll come up to you and say, 'Joe, got a pipe? I want a smoke.' +And you'll say, kind of careless like, as if it warn't anything, you'll +say, 'Yes, I got my OLD pipe, and another one, but my tobacker ain't +very good.' And I'll say, 'Oh, that's all right, if it's STRONG +enough.' And then you'll out with the pipes, and we'll light up just as +ca'm, and then just see 'em look!" + +"By jings, that'll be gay, Tom! I wish it was NOW!" + +"So do I! And when we tell 'em we learned when we was off pirating, +won't they wish they'd been along?" + +"Oh, I reckon not! I'll just BET they will!" + +So the talk ran on. But presently it began to flag a trifle, and grow +disjointed. The silences widened; the expectoration marvellously +increased. Every pore inside the boys' cheeks became a spouting +fountain; they could scarcely bail out the cellars under their tongues +fast enough to prevent an inundation; little overflowings down their +throats occurred in spite of all they could do, and sudden retchings +followed every time. Both boys were looking very pale and miserable, +now. Joe's pipe dropped from his nerveless fingers. Tom's followed. +Both fountains were going furiously and both pumps bailing with might +and main. Joe said feebly: + +"I've lost my knife. I reckon I better go and find it." + +Tom said, with quivering lips and halting utterance: + +"I'll help you. You go over that way and I'll hunt around by the +spring. No, you needn't come, Huck--we can find it." + +So Huck sat down again, and waited an hour. Then he found it lonesome, +and went to find his comrades. They were wide apart in the woods, both +very pale, both fast asleep. But something informed him that if they +had had any trouble they had got rid of it. + +They were not talkative at supper that night. They had a humble look, +and when Huck prepared his pipe after the meal and was going to prepare +theirs, they said no, they were not feeling very well--something they +ate at dinner had disagreed with them. + +About midnight Joe awoke, and called the boys. There was a brooding +oppressiveness in the air that seemed to bode something. The boys +huddled themselves together and sought the friendly companionship of +the fire, though the dull dead heat of the breathless atmosphere was +stifling. They sat still, intent and waiting. The solemn hush +continued. Beyond the light of the fire everything was swallowed up in +the blackness of darkness. Presently there came a quivering glow that +vaguely revealed the foliage for a moment and then vanished. By and by +another came, a little stronger. Then another. Then a faint moan came +sighing through the branches of the forest and the boys felt a fleeting +breath upon their cheeks, and shuddered with the fancy that the Spirit +of the Night had gone by. There was a pause. Now a weird flash turned +night into day and showed every little grass-blade, separate and +distinct, that grew about their feet. And it showed three white, +startled faces, too. A deep peal of thunder went rolling and tumbling +down the heavens and lost itself in sullen rumblings in the distance. A +sweep of chilly air passed by, rustling all the leaves and snowing the +flaky ashes broadcast about the fire. Another fierce glare lit up the +forest and an instant crash followed that seemed to rend the tree-tops +right over the boys' heads. They clung together in terror, in the thick +gloom that followed. A few big rain-drops fell pattering upon the +leaves. + +"Quick! boys, go for the tent!" exclaimed Tom. + +They sprang away, stumbling over roots and among vines in the dark, no +two plunging in the same direction. A furious blast roared through the +trees, making everything sing as it went. One blinding flash after +another came, and peal on peal of deafening thunder. And now a +drenching rain poured down and the rising hurricane drove it in sheets +along the ground. The boys cried out to each other, but the roaring +wind and the booming thunder-blasts drowned their voices utterly. +However, one by one they straggled in at last and took shelter under +the tent, cold, scared, and streaming with water; but to have company +in misery seemed something to be grateful for. They could not talk, the +old sail flapped so furiously, even if the other noises would have +allowed them. The tempest rose higher and higher, and presently the +sail tore loose from its fastenings and went winging away on the blast. +The boys seized each others' hands and fled, with many tumblings and +bruises, to the shelter of a great oak that stood upon the river-bank. +Now the battle was at its highest. Under the ceaseless conflagration of +lightning that flamed in the skies, everything below stood out in +clean-cut and shadowless distinctness: the bending trees, the billowy +river, white with foam, the driving spray of spume-flakes, the dim +outlines of the high bluffs on the other side, glimpsed through the +drifting cloud-rack and the slanting veil of rain. Every little while +some giant tree yielded the fight and fell crashing through the younger +growth; and the unflagging thunder-peals came now in ear-splitting +explosive bursts, keen and sharp, and unspeakably appalling. The storm +culminated in one matchless effort that seemed likely to tear the island +to pieces, burn it up, drown it to the tree-tops, blow it away, and +deafen every creature in it, all at one and the same moment. It was a +wild night for homeless young heads to be out in. + +But at last the battle was done, and the forces retired with weaker +and weaker threatenings and grumblings, and peace resumed her sway. The +boys went back to camp, a good deal awed; but they found there was +still something to be thankful for, because the great sycamore, the +shelter of their beds, was a ruin, now, blasted by the lightnings, and +they were not under it when the catastrophe happened. + +Everything in camp was drenched, the camp-fire as well; for they were +but heedless lads, like their generation, and had made no provision +against rain. Here was matter for dismay, for they were soaked through +and chilled. They were eloquent in their distress; but they presently +discovered that the fire had eaten so far up under the great log it had +been built against (where it curved upward and separated itself from +the ground), that a handbreadth or so of it had escaped wetting; so +they patiently wrought until, with shreds and bark gathered from the +under sides of sheltered logs, they coaxed the fire to burn again. Then +they piled on great dead boughs till they had a roaring furnace, and +were glad-hearted once more. They dried their boiled ham and had a +feast, and after that they sat by the fire and expanded and glorified +their midnight adventure until morning, for there was not a dry spot to +sleep on, anywhere around. + +As the sun began to steal in upon the boys, drowsiness came over them, +and they went out on the sandbar and lay down to sleep. They got +scorched out by and by, and drearily set about getting breakfast. After +the meal they felt rusty, and stiff-jointed, and a little homesick once +more. Tom saw the signs, and fell to cheering up the pirates as well as +he could. But they cared nothing for marbles, or circus, or swimming, +or anything. He reminded them of the imposing secret, and raised a ray +of cheer. While it lasted, he got them interested in a new device. This +was to knock off being pirates, for a while, and be Indians for a +change. They were attracted by this idea; so it was not long before +they were stripped, and striped from head to heel with black mud, like +so many zebras--all of them chiefs, of course--and then they went +tearing through the woods to attack an English settlement. + +By and by they separated into three hostile tribes, and darted upon +each other from ambush with dreadful war-whoops, and killed and scalped +each other by thousands. It was a gory day. Consequently it was an +extremely satisfactory one. + +They assembled in camp toward supper-time, hungry and happy; but now a +difficulty arose--hostile Indians could not break the bread of +hospitality together without first making peace, and this was a simple +impossibility without smoking a pipe of peace. There was no other +process that ever they had heard of. Two of the savages almost wished +they had remained pirates. However, there was no other way; so with +such show of cheerfulness as they could muster they called for the pipe +and took their whiff as it passed, in due form. + +And behold, they were glad they had gone into savagery, for they had +gained something; they found that they could now smoke a little without +having to go and hunt for a lost knife; they did not get sick enough to +be seriously uncomfortable. They were not likely to fool away this high +promise for lack of effort. No, they practised cautiously, after +supper, with right fair success, and so they spent a jubilant evening. +They were prouder and happier in their new acquirement than they would +have been in the scalping and skinning of the Six Nations. We will +leave them to smoke and chatter and brag, since we have no further use +for them at present. + + + +CHAPTER XVII + +BUT there was no hilarity in the little town that same tranquil +Saturday afternoon. The Harpers, and Aunt Polly's family, were being +put into mourning, with great grief and many tears. An unusual quiet +possessed the village, although it was ordinarily quiet enough, in all +conscience. The villagers conducted their concerns with an absent air, +and talked little; but they sighed often. The Saturday holiday seemed a +burden to the children. They had no heart in their sports, and +gradually gave them up. + +In the afternoon Becky Thatcher found herself moping about the +deserted schoolhouse yard, and feeling very melancholy. But she found +nothing there to comfort her. She soliloquized: + +"Oh, if I only had a brass andiron-knob again! But I haven't got +anything now to remember him by." And she choked back a little sob. + +Presently she stopped, and said to herself: + +"It was right here. Oh, if it was to do over again, I wouldn't say +that--I wouldn't say it for the whole world. But he's gone now; I'll +never, never, never see him any more." + +This thought broke her down, and she wandered away, with tears rolling +down her cheeks. Then quite a group of boys and girls--playmates of +Tom's and Joe's--came by, and stood looking over the paling fence and +talking in reverent tones of how Tom did so-and-so the last time they +saw him, and how Joe said this and that small trifle (pregnant with +awful prophecy, as they could easily see now!)--and each speaker +pointed out the exact spot where the lost lads stood at the time, and +then added something like "and I was a-standing just so--just as I am +now, and as if you was him--I was as close as that--and he smiled, just +this way--and then something seemed to go all over me, like--awful, you +know--and I never thought what it meant, of course, but I can see now!" + +Then there was a dispute about who saw the dead boys last in life, and +many claimed that dismal distinction, and offered evidences, more or +less tampered with by the witness; and when it was ultimately decided +who DID see the departed last, and exchanged the last words with them, +the lucky parties took upon themselves a sort of sacred importance, and +were gaped at and envied by all the rest. One poor chap, who had no +other grandeur to offer, said with tolerably manifest pride in the +remembrance: + +"Well, Tom Sawyer he licked me once." + +But that bid for glory was a failure. Most of the boys could say that, +and so that cheapened the distinction too much. The group loitered +away, still recalling memories of the lost heroes, in awed voices. + +When the Sunday-school hour was finished, the next morning, the bell +began to toll, instead of ringing in the usual way. It was a very still +Sabbath, and the mournful sound seemed in keeping with the musing hush +that lay upon nature. The villagers began to gather, loitering a moment +in the vestibule to converse in whispers about the sad event. But there +was no whispering in the house; only the funereal rustling of dresses +as the women gathered to their seats disturbed the silence there. None +could remember when the little church had been so full before. There +was finally a waiting pause, an expectant dumbness, and then Aunt Polly +entered, followed by Sid and Mary, and they by the Harper family, all +in deep black, and the whole congregation, the old minister as well, +rose reverently and stood until the mourners were seated in the front +pew. There was another communing silence, broken at intervals by +muffled sobs, and then the minister spread his hands abroad and prayed. +A moving hymn was sung, and the text followed: "I am the Resurrection +and the Life." + +As the service proceeded, the clergyman drew such pictures of the +graces, the winning ways, and the rare promise of the lost lads that +every soul there, thinking he recognized these pictures, felt a pang in +remembering that he had persistently blinded himself to them always +before, and had as persistently seen only faults and flaws in the poor +boys. The minister related many a touching incident in the lives of the +departed, too, which illustrated their sweet, generous natures, and the +people could easily see, now, how noble and beautiful those episodes +were, and remembered with grief that at the time they occurred they had +seemed rank rascalities, well deserving of the cowhide. The +congregation became more and more moved, as the pathetic tale went on, +till at last the whole company broke down and joined the weeping +mourners in a chorus of anguished sobs, the preacher himself giving way +to his feelings, and crying in the pulpit. + +There was a rustle in the gallery, which nobody noticed; a moment +later the church door creaked; the minister raised his streaming eyes +above his handkerchief, and stood transfixed! First one and then +another pair of eyes followed the minister's, and then almost with one +impulse the congregation rose and stared while the three dead boys came +marching up the aisle, Tom in the lead, Joe next, and Huck, a ruin of +drooping rags, sneaking sheepishly in the rear! They had been hid in +the unused gallery listening to their own funeral sermon! + +Aunt Polly, Mary, and the Harpers threw themselves upon their restored +ones, smothered them with kisses and poured out thanksgivings, while +poor Huck stood abashed and uncomfortable, not knowing exactly what to +do or where to hide from so many unwelcoming eyes. He wavered, and +started to slink away, but Tom seized him and said: + +"Aunt Polly, it ain't fair. Somebody's got to be glad to see Huck." + +"And so they shall. I'm glad to see him, poor motherless thing!" And +the loving attentions Aunt Polly lavished upon him were the one thing +capable of making him more uncomfortable than he was before. + +Suddenly the minister shouted at the top of his voice: "Praise God +from whom all blessings flow--SING!--and put your hearts in it!" + +And they did. Old Hundred swelled up with a triumphant burst, and +while it shook the rafters Tom Sawyer the Pirate looked around upon the +envying juveniles about him and confessed in his heart that this was +the proudest moment of his life. + +As the "sold" congregation trooped out they said they would almost be +willing to be made ridiculous again to hear Old Hundred sung like that +once more. + +Tom got more cuffs and kisses that day--according to Aunt Polly's +varying moods--than he had earned before in a year; and he hardly knew +which expressed the most gratefulness to God and affection for himself. + + + +CHAPTER XVIII + +THAT was Tom's great secret--the scheme to return home with his +brother pirates and attend their own funerals. They had paddled over to +the Missouri shore on a log, at dusk on Saturday, landing five or six +miles below the village; they had slept in the woods at the edge of the +town till nearly daylight, and had then crept through back lanes and +alleys and finished their sleep in the gallery of the church among a +chaos of invalided benches. + +At breakfast, Monday morning, Aunt Polly and Mary were very loving to +Tom, and very attentive to his wants. There was an unusual amount of +talk. In the course of it Aunt Polly said: + +"Well, I don't say it wasn't a fine joke, Tom, to keep everybody +suffering 'most a week so you boys had a good time, but it is a pity +you could be so hard-hearted as to let me suffer so. If you could come +over on a log to go to your funeral, you could have come over and give +me a hint some way that you warn't dead, but only run off." + +"Yes, you could have done that, Tom," said Mary; "and I believe you +would if you had thought of it." + +"Would you, Tom?" said Aunt Polly, her face lighting wistfully. "Say, +now, would you, if you'd thought of it?" + +"I--well, I don't know. 'Twould 'a' spoiled everything." + +"Tom, I hoped you loved me that much," said Aunt Polly, with a grieved +tone that discomforted the boy. "It would have been something if you'd +cared enough to THINK of it, even if you didn't DO it." + +"Now, auntie, that ain't any harm," pleaded Mary; "it's only Tom's +giddy way--he is always in such a rush that he never thinks of +anything." + +"More's the pity. Sid would have thought. And Sid would have come and +DONE it, too. Tom, you'll look back, some day, when it's too late, and +wish you'd cared a little more for me when it would have cost you so +little." + +"Now, auntie, you know I do care for you," said Tom. + +"I'd know it better if you acted more like it." + +"I wish now I'd thought," said Tom, with a repentant tone; "but I +dreamt about you, anyway. That's something, ain't it?" + +"It ain't much--a cat does that much--but it's better than nothing. +What did you dream?" + +"Why, Wednesday night I dreamt that you was sitting over there by the +bed, and Sid was sitting by the woodbox, and Mary next to him." + +"Well, so we did. So we always do. I'm glad your dreams could take +even that much trouble about us." + +"And I dreamt that Joe Harper's mother was here." + +"Why, she was here! Did you dream any more?" + +"Oh, lots. But it's so dim, now." + +"Well, try to recollect--can't you?" + +"Somehow it seems to me that the wind--the wind blowed the--the--" + +"Try harder, Tom! The wind did blow something. Come!" + +Tom pressed his fingers on his forehead an anxious minute, and then +said: + +"I've got it now! I've got it now! It blowed the candle!" + +"Mercy on us! Go on, Tom--go on!" + +"And it seems to me that you said, 'Why, I believe that that door--'" + +"Go ON, Tom!" + +"Just let me study a moment--just a moment. Oh, yes--you said you +believed the door was open." + +"As I'm sitting here, I did! Didn't I, Mary! Go on!" + +"And then--and then--well I won't be certain, but it seems like as if +you made Sid go and--and--" + +"Well? Well? What did I make him do, Tom? What did I make him do?" + +"You made him--you--Oh, you made him shut it." + +"Well, for the land's sake! I never heard the beat of that in all my +days! Don't tell ME there ain't anything in dreams, any more. Sereny +Harper shall know of this before I'm an hour older. I'd like to see her +get around THIS with her rubbage 'bout superstition. Go on, Tom!" + +"Oh, it's all getting just as bright as day, now. Next you said I +warn't BAD, only mischeevous and harum-scarum, and not any more +responsible than--than--I think it was a colt, or something." + +"And so it was! Well, goodness gracious! Go on, Tom!" + +"And then you began to cry." + +"So I did. So I did. Not the first time, neither. And then--" + +"Then Mrs. Harper she began to cry, and said Joe was just the same, +and she wished she hadn't whipped him for taking cream when she'd +throwed it out her own self--" + +"Tom! The sperrit was upon you! You was a prophesying--that's what you +was doing! Land alive, go on, Tom!" + +"Then Sid he said--he said--" + +"I don't think I said anything," said Sid. + +"Yes you did, Sid," said Mary. + +"Shut your heads and let Tom go on! What did he say, Tom?" + +"He said--I THINK he said he hoped I was better off where I was gone +to, but if I'd been better sometimes--" + +"THERE, d'you hear that! It was his very words!" + +"And you shut him up sharp." + +"I lay I did! There must 'a' been an angel there. There WAS an angel +there, somewheres!" + +"And Mrs. Harper told about Joe scaring her with a firecracker, and +you told about Peter and the Painkiller--" + +"Just as true as I live!" + +"And then there was a whole lot of talk 'bout dragging the river for +us, and 'bout having the funeral Sunday, and then you and old Miss +Harper hugged and cried, and she went." + +"It happened just so! It happened just so, as sure as I'm a-sitting in +these very tracks. Tom, you couldn't told it more like if you'd 'a' +seen it! And then what? Go on, Tom!" + +"Then I thought you prayed for me--and I could see you and hear every +word you said. And you went to bed, and I was so sorry that I took and +wrote on a piece of sycamore bark, 'We ain't dead--we are only off +being pirates,' and put it on the table by the candle; and then you +looked so good, laying there asleep, that I thought I went and leaned +over and kissed you on the lips." + +"Did you, Tom, DID you! I just forgive you everything for that!" And +she seized the boy in a crushing embrace that made him feel like the +guiltiest of villains. + +"It was very kind, even though it was only a--dream," Sid soliloquized +just audibly. + +"Shut up, Sid! A body does just the same in a dream as he'd do if he +was awake. Here's a big Milum apple I've been saving for you, Tom, if +you was ever found again--now go 'long to school. I'm thankful to the +good God and Father of us all I've got you back, that's long-suffering +and merciful to them that believe on Him and keep His word, though +goodness knows I'm unworthy of it, but if only the worthy ones got His +blessings and had His hand to help them over the rough places, there's +few enough would smile here or ever enter into His rest when the long +night comes. Go 'long Sid, Mary, Tom--take yourselves off--you've +hendered me long enough." + +The children left for school, and the old lady to call on Mrs. Harper +and vanquish her realism with Tom's marvellous dream. Sid had better +judgment than to utter the thought that was in his mind as he left the +house. It was this: "Pretty thin--as long a dream as that, without any +mistakes in it!" + +What a hero Tom was become, now! He did not go skipping and prancing, +but moved with a dignified swagger as became a pirate who felt that the +public eye was on him. And indeed it was; he tried not to seem to see +the looks or hear the remarks as he passed along, but they were food +and drink to him. Smaller boys than himself flocked at his heels, as +proud to be seen with him, and tolerated by him, as if he had been the +drummer at the head of a procession or the elephant leading a menagerie +into town. Boys of his own size pretended not to know he had been away +at all; but they were consuming with envy, nevertheless. They would +have given anything to have that swarthy suntanned skin of his, and his +glittering notoriety; and Tom would not have parted with either for a +circus. + +At school the children made so much of him and of Joe, and delivered +such eloquent admiration from their eyes, that the two heroes were not +long in becoming insufferably "stuck-up." They began to tell their +adventures to hungry listeners--but they only began; it was not a thing +likely to have an end, with imaginations like theirs to furnish +material. And finally, when they got out their pipes and went serenely +puffing around, the very summit of glory was reached. + +Tom decided that he could be independent of Becky Thatcher now. Glory +was sufficient. He would live for glory. Now that he was distinguished, +maybe she would be wanting to "make up." Well, let her--she should see +that he could be as indifferent as some other people. Presently she +arrived. Tom pretended not to see her. He moved away and joined a group +of boys and girls and began to talk. Soon he observed that she was +tripping gayly back and forth with flushed face and dancing eyes, +pretending to be busy chasing schoolmates, and screaming with laughter +when she made a capture; but he noticed that she always made her +captures in his vicinity, and that she seemed to cast a conscious eye +in his direction at such times, too. It gratified all the vicious +vanity that was in him; and so, instead of winning him, it only "set +him up" the more and made him the more diligent to avoid betraying that +he knew she was about. Presently she gave over skylarking, and moved +irresolutely about, sighing once or twice and glancing furtively and +wistfully toward Tom. Then she observed that now Tom was talking more +particularly to Amy Lawrence than to any one else. She felt a sharp +pang and grew disturbed and uneasy at once. She tried to go away, but +her feet were treacherous, and carried her to the group instead. She +said to a girl almost at Tom's elbow--with sham vivacity: + +"Why, Mary Austin! you bad girl, why didn't you come to Sunday-school?" + +"I did come--didn't you see me?" + +"Why, no! Did you? Where did you sit?" + +"I was in Miss Peters' class, where I always go. I saw YOU." + +"Did you? Why, it's funny I didn't see you. I wanted to tell you about +the picnic." + +"Oh, that's jolly. Who's going to give it?" + +"My ma's going to let me have one." + +"Oh, goody; I hope she'll let ME come." + +"Well, she will. The picnic's for me. She'll let anybody come that I +want, and I want you." + +"That's ever so nice. When is it going to be?" + +"By and by. Maybe about vacation." + +"Oh, won't it be fun! You going to have all the girls and boys?" + +"Yes, every one that's friends to me--or wants to be"; and she glanced +ever so furtively at Tom, but he talked right along to Amy Lawrence +about the terrible storm on the island, and how the lightning tore the +great sycamore tree "all to flinders" while he was "standing within +three feet of it." + +"Oh, may I come?" said Grace Miller. + +"Yes." + +"And me?" said Sally Rogers. + +"Yes." + +"And me, too?" said Susy Harper. "And Joe?" + +"Yes." + +And so on, with clapping of joyful hands till all the group had begged +for invitations but Tom and Amy. Then Tom turned coolly away, still +talking, and took Amy with him. Becky's lips trembled and the tears +came to her eyes; she hid these signs with a forced gayety and went on +chattering, but the life had gone out of the picnic, now, and out of +everything else; she got away as soon as she could and hid herself and +had what her sex call "a good cry." Then she sat moody, with wounded +pride, till the bell rang. She roused up, now, with a vindictive cast +in her eye, and gave her plaited tails a shake and said she knew what +SHE'D do. + +At recess Tom continued his flirtation with Amy with jubilant +self-satisfaction. And he kept drifting about to find Becky and lacerate +her with the performance. At last he spied her, but there was a sudden +falling of his mercury. She was sitting cosily on a little bench behind +the schoolhouse looking at a picture-book with Alfred Temple--and so +absorbed were they, and their heads so close together over the book, +that they did not seem to be conscious of anything in the world besides. +Jealousy ran red-hot through Tom's veins. He began to hate himself for +throwing away the chance Becky had offered for a reconciliation. He +called himself a fool, and all the hard names he could think of. He +wanted to cry with vexation. Amy chatted happily along, as they walked, +for her heart was singing, but Tom's tongue had lost its function. He +did not hear what Amy was saying, and whenever she paused expectantly he +could only stammer an awkward assent, which was as often misplaced as +otherwise. He kept drifting to the rear of the schoolhouse, again and +again, to sear his eyeballs with the hateful spectacle there. He could +not help it. And it maddened him to see, as he thought he saw, that +Becky Thatcher never once suspected that he was even in the land of the +living. But she did see, nevertheless; and she knew she was winning her +fight, too, and was glad to see him suffer as she had suffered. + +Amy's happy prattle became intolerable. Tom hinted at things he had to +attend to; things that must be done; and time was fleeting. But in +vain--the girl chirped on. Tom thought, "Oh, hang her, ain't I ever +going to get rid of her?" At last he must be attending to those +things--and she said artlessly that she would be "around" when school +let out. And he hastened away, hating her for it. + +"Any other boy!" Tom thought, grating his teeth. "Any boy in the whole +town but that Saint Louis smarty that thinks he dresses so fine and is +aristocracy! Oh, all right, I licked you the first day you ever saw +this town, mister, and I'll lick you again! You just wait till I catch +you out! I'll just take and--" + +And he went through the motions of thrashing an imaginary boy +--pummelling the air, and kicking and gouging. "Oh, you do, do you? You +holler 'nough, do you? Now, then, let that learn you!" And so the +imaginary flogging was finished to his satisfaction. + +Tom fled home at noon. His conscience could not endure any more of +Amy's grateful happiness, and his jealousy could bear no more of the +other distress. Becky resumed her picture inspections with Alfred, but +as the minutes dragged along and no Tom came to suffer, her triumph +began to cloud and she lost interest; gravity and absent-mindedness +followed, and then melancholy; two or three times she pricked up her +ear at a footstep, but it was a false hope; no Tom came. At last she +grew entirely miserable and wished she hadn't carried it so far. When +poor Alfred, seeing that he was losing her, he did not know how, kept +exclaiming: "Oh, here's a jolly one! look at this!" she lost patience +at last, and said, "Oh, don't bother me! I don't care for them!" and +burst into tears, and got up and walked away. + +Alfred dropped alongside and was going to try to comfort her, but she +said: + +"Go away and leave me alone, can't you! I hate you!" + +So the boy halted, wondering what he could have done--for she had said +she would look at pictures all through the nooning--and she walked on, +crying. Then Alfred went musing into the deserted schoolhouse. He was +humiliated and angry. He easily guessed his way to the truth--the girl +had simply made a convenience of him to vent her spite upon Tom Sawyer. +He was far from hating Tom the less when this thought occurred to him. +He wished there was some way to get that boy into trouble without much +risk to himself. Tom's spelling-book fell under his eye. Here was his +opportunity. He gratefully opened to the lesson for the afternoon and +poured ink upon the page. + +Becky, glancing in at a window behind him at the moment, saw the act, +and moved on, without discovering herself. She started homeward, now, +intending to find Tom and tell him; Tom would be thankful and their +troubles would be healed. Before she was half way home, however, she +had changed her mind. The thought of Tom's treatment of her when she +was talking about her picnic came scorching back and filled her with +shame. She resolved to let him get whipped on the damaged +spelling-book's account, and to hate him forever, into the bargain. + + + +CHAPTER XIX + +TOM arrived at home in a dreary mood, and the first thing his aunt +said to him showed him that he had brought his sorrows to an +unpromising market: + +"Tom, I've a notion to skin you alive!" + +"Auntie, what have I done?" + +"Well, you've done enough. Here I go over to Sereny Harper, like an +old softy, expecting I'm going to make her believe all that rubbage +about that dream, when lo and behold you she'd found out from Joe that +you was over here and heard all the talk we had that night. Tom, I +don't know what is to become of a boy that will act like that. It makes +me feel so bad to think you could let me go to Sereny Harper and make +such a fool of myself and never say a word." + +This was a new aspect of the thing. His smartness of the morning had +seemed to Tom a good joke before, and very ingenious. It merely looked +mean and shabby now. He hung his head and could not think of anything +to say for a moment. Then he said: + +"Auntie, I wish I hadn't done it--but I didn't think." + +"Oh, child, you never think. You never think of anything but your own +selfishness. You could think to come all the way over here from +Jackson's Island in the night to laugh at our troubles, and you could +think to fool me with a lie about a dream; but you couldn't ever think +to pity us and save us from sorrow." + +"Auntie, I know now it was mean, but I didn't mean to be mean. I +didn't, honest. And besides, I didn't come over here to laugh at you +that night." + +"What did you come for, then?" + +"It was to tell you not to be uneasy about us, because we hadn't got +drownded." + +"Tom, Tom, I would be the thankfullest soul in this world if I could +believe you ever had as good a thought as that, but you know you never +did--and I know it, Tom." + +"Indeed and 'deed I did, auntie--I wish I may never stir if I didn't." + +"Oh, Tom, don't lie--don't do it. It only makes things a hundred times +worse." + +"It ain't a lie, auntie; it's the truth. I wanted to keep you from +grieving--that was all that made me come." + +"I'd give the whole world to believe that--it would cover up a power +of sins, Tom. I'd 'most be glad you'd run off and acted so bad. But it +ain't reasonable; because, why didn't you tell me, child?" + +"Why, you see, when you got to talking about the funeral, I just got +all full of the idea of our coming and hiding in the church, and I +couldn't somehow bear to spoil it. So I just put the bark back in my +pocket and kept mum." + +"What bark?" + +"The bark I had wrote on to tell you we'd gone pirating. I wish, now, +you'd waked up when I kissed you--I do, honest." + +The hard lines in his aunt's face relaxed and a sudden tenderness +dawned in her eyes. + +"DID you kiss me, Tom?" + +"Why, yes, I did." + +"Are you sure you did, Tom?" + +"Why, yes, I did, auntie--certain sure." + +"What did you kiss me for, Tom?" + +"Because I loved you so, and you laid there moaning and I was so sorry." + +The words sounded like truth. The old lady could not hide a tremor in +her voice when she said: + +"Kiss me again, Tom!--and be off with you to school, now, and don't +bother me any more." + +The moment he was gone, she ran to a closet and got out the ruin of a +jacket which Tom had gone pirating in. Then she stopped, with it in her +hand, and said to herself: + +"No, I don't dare. Poor boy, I reckon he's lied about it--but it's a +blessed, blessed lie, there's such a comfort come from it. I hope the +Lord--I KNOW the Lord will forgive him, because it was such +goodheartedness in him to tell it. But I don't want to find out it's a +lie. I won't look." + +She put the jacket away, and stood by musing a minute. Twice she put +out her hand to take the garment again, and twice she refrained. Once +more she ventured, and this time she fortified herself with the +thought: "It's a good lie--it's a good lie--I won't let it grieve me." +So she sought the jacket pocket. A moment later she was reading Tom's +piece of bark through flowing tears and saying: "I could forgive the +boy, now, if he'd committed a million sins!" + + + +CHAPTER XX + +THERE was something about Aunt Polly's manner, when she kissed Tom, +that swept away his low spirits and made him lighthearted and happy +again. He started to school and had the luck of coming upon Becky +Thatcher at the head of Meadow Lane. His mood always determined his +manner. Without a moment's hesitation he ran to her and said: + +"I acted mighty mean to-day, Becky, and I'm so sorry. I won't ever, +ever do that way again, as long as ever I live--please make up, won't +you?" + +The girl stopped and looked him scornfully in the face: + +"I'll thank you to keep yourself TO yourself, Mr. Thomas Sawyer. I'll +never speak to you again." + +She tossed her head and passed on. Tom was so stunned that he had not +even presence of mind enough to say "Who cares, Miss Smarty?" until the +right time to say it had gone by. So he said nothing. But he was in a +fine rage, nevertheless. He moped into the schoolyard wishing she were +a boy, and imagining how he would trounce her if she were. He presently +encountered her and delivered a stinging remark as he passed. She +hurled one in return, and the angry breach was complete. It seemed to +Becky, in her hot resentment, that she could hardly wait for school to +"take in," she was so impatient to see Tom flogged for the injured +spelling-book. If she had had any lingering notion of exposing Alfred +Temple, Tom's offensive fling had driven it entirely away. + +Poor girl, she did not know how fast she was nearing trouble herself. +The master, Mr. Dobbins, had reached middle age with an unsatisfied +ambition. The darling of his desires was, to be a doctor, but poverty +had decreed that he should be nothing higher than a village +schoolmaster. Every day he took a mysterious book out of his desk and +absorbed himself in it at times when no classes were reciting. He kept +that book under lock and key. There was not an urchin in school but was +perishing to have a glimpse of it, but the chance never came. Every boy +and girl had a theory about the nature of that book; but no two +theories were alike, and there was no way of getting at the facts in +the case. Now, as Becky was passing by the desk, which stood near the +door, she noticed that the key was in the lock! It was a precious +moment. She glanced around; found herself alone, and the next instant +she had the book in her hands. The title-page--Professor Somebody's +ANATOMY--carried no information to her mind; so she began to turn the +leaves. She came at once upon a handsomely engraved and colored +frontispiece--a human figure, stark naked. At that moment a shadow fell +on the page and Tom Sawyer stepped in at the door and caught a glimpse +of the picture. Becky snatched at the book to close it, and had the +hard luck to tear the pictured page half down the middle. She thrust +the volume into the desk, turned the key, and burst out crying with +shame and vexation. + +"Tom Sawyer, you are just as mean as you can be, to sneak up on a +person and look at what they're looking at." + +"How could I know you was looking at anything?" + +"You ought to be ashamed of yourself, Tom Sawyer; you know you're +going to tell on me, and oh, what shall I do, what shall I do! I'll be +whipped, and I never was whipped in school." + +Then she stamped her little foot and said: + +"BE so mean if you want to! I know something that's going to happen. +You just wait and you'll see! Hateful, hateful, hateful!"--and she +flung out of the house with a new explosion of crying. + +Tom stood still, rather flustered by this onslaught. Presently he said +to himself: + +"What a curious kind of a fool a girl is! Never been licked in school! +Shucks! What's a licking! That's just like a girl--they're so +thin-skinned and chicken-hearted. Well, of course I ain't going to tell +old Dobbins on this little fool, because there's other ways of getting +even on her, that ain't so mean; but what of it? Old Dobbins will ask +who it was tore his book. Nobody'll answer. Then he'll do just the way +he always does--ask first one and then t'other, and when he comes to the +right girl he'll know it, without any telling. Girls' faces always tell +on them. They ain't got any backbone. She'll get licked. Well, it's a +kind of a tight place for Becky Thatcher, because there ain't any way +out of it." Tom conned the thing a moment longer, and then added: "All +right, though; she'd like to see me in just such a fix--let her sweat it +out!" + +Tom joined the mob of skylarking scholars outside. In a few moments +the master arrived and school "took in." Tom did not feel a strong +interest in his studies. Every time he stole a glance at the girls' +side of the room Becky's face troubled him. Considering all things, he +did not want to pity her, and yet it was all he could do to help it. He +could get up no exultation that was really worthy the name. Presently +the spelling-book discovery was made, and Tom's mind was entirely full +of his own matters for a while after that. Becky roused up from her +lethargy of distress and showed good interest in the proceedings. She +did not expect that Tom could get out of his trouble by denying that he +spilt the ink on the book himself; and she was right. The denial only +seemed to make the thing worse for Tom. Becky supposed she would be +glad of that, and she tried to believe she was glad of it, but she +found she was not certain. When the worst came to the worst, she had an +impulse to get up and tell on Alfred Temple, but she made an effort and +forced herself to keep still--because, said she to herself, "he'll tell +about me tearing the picture sure. I wouldn't say a word, not to save +his life!" + +Tom took his whipping and went back to his seat not at all +broken-hearted, for he thought it was possible that he had unknowingly +upset the ink on the spelling-book himself, in some skylarking bout--he +had denied it for form's sake and because it was custom, and had stuck +to the denial from principle. + +A whole hour drifted by, the master sat nodding in his throne, the air +was drowsy with the hum of study. By and by, Mr. Dobbins straightened +himself up, yawned, then unlocked his desk, and reached for his book, +but seemed undecided whether to take it out or leave it. Most of the +pupils glanced up languidly, but there were two among them that watched +his movements with intent eyes. Mr. Dobbins fingered his book absently +for a while, then took it out and settled himself in his chair to read! +Tom shot a glance at Becky. He had seen a hunted and helpless rabbit +look as she did, with a gun levelled at its head. Instantly he forgot +his quarrel with her. Quick--something must be done! done in a flash, +too! But the very imminence of the emergency paralyzed his invention. +Good!--he had an inspiration! He would run and snatch the book, spring +through the door and fly. But his resolution shook for one little +instant, and the chance was lost--the master opened the volume. If Tom +only had the wasted opportunity back again! Too late. There was no help +for Becky now, he said. The next moment the master faced the school. +Every eye sank under his gaze. There was that in it which smote even +the innocent with fear. There was silence while one might count ten +--the master was gathering his wrath. Then he spoke: "Who tore this book?" + +There was not a sound. One could have heard a pin drop. The stillness +continued; the master searched face after face for signs of guilt. + +"Benjamin Rogers, did you tear this book?" + +A denial. Another pause. + +"Joseph Harper, did you?" + +Another denial. Tom's uneasiness grew more and more intense under the +slow torture of these proceedings. The master scanned the ranks of +boys--considered a while, then turned to the girls: + +"Amy Lawrence?" + +A shake of the head. + +"Gracie Miller?" + +The same sign. + +"Susan Harper, did you do this?" + +Another negative. The next girl was Becky Thatcher. Tom was trembling +from head to foot with excitement and a sense of the hopelessness of +the situation. + +"Rebecca Thatcher" [Tom glanced at her face--it was white with terror] +--"did you tear--no, look me in the face" [her hands rose in appeal] +--"did you tear this book?" + +A thought shot like lightning through Tom's brain. He sprang to his +feet and shouted--"I done it!" + +The school stared in perplexity at this incredible folly. Tom stood a +moment, to gather his dismembered faculties; and when he stepped +forward to go to his punishment the surprise, the gratitude, the +adoration that shone upon him out of poor Becky's eyes seemed pay +enough for a hundred floggings. Inspired by the splendor of his own +act, he took without an outcry the most merciless flaying that even Mr. +Dobbins had ever administered; and also received with indifference the +added cruelty of a command to remain two hours after school should be +dismissed--for he knew who would wait for him outside till his +captivity was done, and not count the tedious time as loss, either. + +Tom went to bed that night planning vengeance against Alfred Temple; +for with shame and repentance Becky had told him all, not forgetting +her own treachery; but even the longing for vengeance had to give way, +soon, to pleasanter musings, and he fell asleep at last with Becky's +latest words lingering dreamily in his ear-- + +"Tom, how COULD you be so noble!" + + + +CHAPTER XXI + +VACATION was approaching. The schoolmaster, always severe, grew +severer and more exacting than ever, for he wanted the school to make a +good showing on "Examination" day. His rod and his ferule were seldom +idle now--at least among the smaller pupils. Only the biggest boys, and +young ladies of eighteen and twenty, escaped lashing. Mr. Dobbins' +lashings were very vigorous ones, too; for although he carried, under +his wig, a perfectly bald and shiny head, he had only reached middle +age, and there was no sign of feebleness in his muscle. As the great +day approached, all the tyranny that was in him came to the surface; he +seemed to take a vindictive pleasure in punishing the least +shortcomings. The consequence was, that the smaller boys spent their +days in terror and suffering and their nights in plotting revenge. They +threw away no opportunity to do the master a mischief. But he kept +ahead all the time. The retribution that followed every vengeful +success was so sweeping and majestic that the boys always retired from +the field badly worsted. At last they conspired together and hit upon a +plan that promised a dazzling victory. They swore in the sign-painter's +boy, told him the scheme, and asked his help. He had his own reasons +for being delighted, for the master boarded in his father's family and +had given the boy ample cause to hate him. The master's wife would go +on a visit to the country in a few days, and there would be nothing to +interfere with the plan; the master always prepared himself for great +occasions by getting pretty well fuddled, and the sign-painter's boy +said that when the dominie had reached the proper condition on +Examination Evening he would "manage the thing" while he napped in his +chair; then he would have him awakened at the right time and hurried +away to school. + +In the fulness of time the interesting occasion arrived. At eight in +the evening the schoolhouse was brilliantly lighted, and adorned with +wreaths and festoons of foliage and flowers. The master sat throned in +his great chair upon a raised platform, with his blackboard behind him. +He was looking tolerably mellow. Three rows of benches on each side and +six rows in front of him were occupied by the dignitaries of the town +and by the parents of the pupils. To his left, back of the rows of +citizens, was a spacious temporary platform upon which were seated the +scholars who were to take part in the exercises of the evening; rows of +small boys, washed and dressed to an intolerable state of discomfort; +rows of gawky big boys; snowbanks of girls and young ladies clad in +lawn and muslin and conspicuously conscious of their bare arms, their +grandmothers' ancient trinkets, their bits of pink and blue ribbon and +the flowers in their hair. All the rest of the house was filled with +non-participating scholars. + +The exercises began. A very little boy stood up and sheepishly +recited, "You'd scarce expect one of my age to speak in public on the +stage," etc.--accompanying himself with the painfully exact and +spasmodic gestures which a machine might have used--supposing the +machine to be a trifle out of order. But he got through safely, though +cruelly scared, and got a fine round of applause when he made his +manufactured bow and retired. + +A little shamefaced girl lisped, "Mary had a little lamb," etc., +performed a compassion-inspiring curtsy, got her meed of applause, and +sat down flushed and happy. + +Tom Sawyer stepped forward with conceited confidence and soared into +the unquenchable and indestructible "Give me liberty or give me death" +speech, with fine fury and frantic gesticulation, and broke down in the +middle of it. A ghastly stage-fright seized him, his legs quaked under +him and he was like to choke. True, he had the manifest sympathy of the +house but he had the house's silence, too, which was even worse than +its sympathy. The master frowned, and this completed the disaster. Tom +struggled awhile and then retired, utterly defeated. There was a weak +attempt at applause, but it died early. + +"The Boy Stood on the Burning Deck" followed; also "The Assyrian Came +Down," and other declamatory gems. Then there were reading exercises, +and a spelling fight. The meagre Latin class recited with honor. The +prime feature of the evening was in order, now--original "compositions" +by the young ladies. Each in her turn stepped forward to the edge of +the platform, cleared her throat, held up her manuscript (tied with +dainty ribbon), and proceeded to read, with labored attention to +"expression" and punctuation. The themes were the same that had been +illuminated upon similar occasions by their mothers before them, their +grandmothers, and doubtless all their ancestors in the female line +clear back to the Crusades. "Friendship" was one; "Memories of Other +Days"; "Religion in History"; "Dream Land"; "The Advantages of +Culture"; "Forms of Political Government Compared and Contrasted"; +"Melancholy"; "Filial Love"; "Heart Longings," etc., etc. + +A prevalent feature in these compositions was a nursed and petted +melancholy; another was a wasteful and opulent gush of "fine language"; +another was a tendency to lug in by the ears particularly prized words +and phrases until they were worn entirely out; and a peculiarity that +conspicuously marked and marred them was the inveterate and intolerable +sermon that wagged its crippled tail at the end of each and every one +of them. No matter what the subject might be, a brain-racking effort +was made to squirm it into some aspect or other that the moral and +religious mind could contemplate with edification. The glaring +insincerity of these sermons was not sufficient to compass the +banishment of the fashion from the schools, and it is not sufficient +to-day; it never will be sufficient while the world stands, perhaps. +There is no school in all our land where the young ladies do not feel +obliged to close their compositions with a sermon; and you will find +that the sermon of the most frivolous and the least religious girl in +the school is always the longest and the most relentlessly pious. But +enough of this. Homely truth is unpalatable. + +Let us return to the "Examination." The first composition that was +read was one entitled "Is this, then, Life?" Perhaps the reader can +endure an extract from it: + + "In the common walks of life, with what delightful + emotions does the youthful mind look forward to some + anticipated scene of festivity! Imagination is busy + sketching rose-tinted pictures of joy. In fancy, the + voluptuous votary of fashion sees herself amid the + festive throng, 'the observed of all observers.' Her + graceful form, arrayed in snowy robes, is whirling + through the mazes of the joyous dance; her eye is + brightest, her step is lightest in the gay assembly. + + "In such delicious fancies time quickly glides by, + and the welcome hour arrives for her entrance into + the Elysian world, of which she has had such bright + dreams. How fairy-like does everything appear to + her enchanted vision! Each new scene is more charming + than the last. But after a while she finds that + beneath this goodly exterior, all is vanity, the + flattery which once charmed her soul, now grates + harshly upon her ear; the ball-room has lost its + charms; and with wasted health and imbittered heart, + she turns away with the conviction that earthly + pleasures cannot satisfy the longings of the soul!" + +And so forth and so on. There was a buzz of gratification from time to +time during the reading, accompanied by whispered ejaculations of "How +sweet!" "How eloquent!" "So true!" etc., and after the thing had closed +with a peculiarly afflicting sermon the applause was enthusiastic. + +Then arose a slim, melancholy girl, whose face had the "interesting" +paleness that comes of pills and indigestion, and read a "poem." Two +stanzas of it will do: + + "A MISSOURI MAIDEN'S FAREWELL TO ALABAMA + + "Alabama, good-bye! I love thee well! + But yet for a while do I leave thee now! + Sad, yes, sad thoughts of thee my heart doth swell, + And burning recollections throng my brow! + For I have wandered through thy flowery woods; + Have roamed and read near Tallapoosa's stream; + Have listened to Tallassee's warring floods, + And wooed on Coosa's side Aurora's beam. + + "Yet shame I not to bear an o'er-full heart, + Nor blush to turn behind my tearful eyes; + 'Tis from no stranger land I now must part, + 'Tis to no strangers left I yield these sighs. + Welcome and home were mine within this State, + Whose vales I leave--whose spires fade fast from me + And cold must be mine eyes, and heart, and tete, + When, dear Alabama! they turn cold on thee!" + +There were very few there who knew what "tete" meant, but the poem was +very satisfactory, nevertheless. + +Next appeared a dark-complexioned, black-eyed, black-haired young +lady, who paused an impressive moment, assumed a tragic expression, and +began to read in a measured, solemn tone: + + "A VISION + + "Dark and tempestuous was night. Around the + throne on high not a single star quivered; but + the deep intonations of the heavy thunder + constantly vibrated upon the ear; whilst the + terrific lightning revelled in angry mood + through the cloudy chambers of heaven, seeming + to scorn the power exerted over its terror by + the illustrious Franklin! Even the boisterous + winds unanimously came forth from their mystic + homes, and blustered about as if to enhance by + their aid the wildness of the scene. + + "At such a time, so dark, so dreary, for human + sympathy my very spirit sighed; but instead thereof, + + "'My dearest friend, my counsellor, my comforter + and guide--My joy in grief, my second bliss + in joy,' came to my side. She moved like one of + those bright beings pictured in the sunny walks + of fancy's Eden by the romantic and young, a + queen of beauty unadorned save by her own + transcendent loveliness. So soft was her step, it + failed to make even a sound, and but for the + magical thrill imparted by her genial touch, as + other unobtrusive beauties, she would have glided + away un-perceived--unsought. A strange sadness + rested upon her features, like icy tears upon + the robe of December, as she pointed to the + contending elements without, and bade me contemplate + the two beings presented." + +This nightmare occupied some ten pages of manuscript and wound up with +a sermon so destructive of all hope to non-Presbyterians that it took +the first prize. This composition was considered to be the very finest +effort of the evening. The mayor of the village, in delivering the +prize to the author of it, made a warm speech in which he said that it +was by far the most "eloquent" thing he had ever listened to, and that +Daniel Webster himself might well be proud of it. + +It may be remarked, in passing, that the number of compositions in +which the word "beauteous" was over-fondled, and human experience +referred to as "life's page," was up to the usual average. + +Now the master, mellow almost to the verge of geniality, put his chair +aside, turned his back to the audience, and began to draw a map of +America on the blackboard, to exercise the geography class upon. But he +made a sad business of it with his unsteady hand, and a smothered +titter rippled over the house. He knew what the matter was, and set +himself to right it. He sponged out lines and remade them; but he only +distorted them more than ever, and the tittering was more pronounced. +He threw his entire attention upon his work, now, as if determined not +to be put down by the mirth. He felt that all eyes were fastened upon +him; he imagined he was succeeding, and yet the tittering continued; it +even manifestly increased. And well it might. There was a garret above, +pierced with a scuttle over his head; and down through this scuttle +came a cat, suspended around the haunches by a string; she had a rag +tied about her head and jaws to keep her from mewing; as she slowly +descended she curved upward and clawed at the string, she swung +downward and clawed at the intangible air. The tittering rose higher +and higher--the cat was within six inches of the absorbed teacher's +head--down, down, a little lower, and she grabbed his wig with her +desperate claws, clung to it, and was snatched up into the garret in an +instant with her trophy still in her possession! And how the light did +blaze abroad from the master's bald pate--for the sign-painter's boy +had GILDED it! + +That broke up the meeting. The boys were avenged. Vacation had come. + + NOTE:--The pretended "compositions" quoted in + this chapter are taken without alteration from a + volume entitled "Prose and Poetry, by a Western + Lady"--but they are exactly and precisely after + the schoolgirl pattern, and hence are much + happier than any mere imitations could be. + + + +CHAPTER XXII + +TOM joined the new order of Cadets of Temperance, being attracted by +the showy character of their "regalia." He promised to abstain from +smoking, chewing, and profanity as long as he remained a member. Now he +found out a new thing--namely, that to promise not to do a thing is the +surest way in the world to make a body want to go and do that very +thing. Tom soon found himself tormented with a desire to drink and +swear; the desire grew to be so intense that nothing but the hope of a +chance to display himself in his red sash kept him from withdrawing +from the order. Fourth of July was coming; but he soon gave that up +--gave it up before he had worn his shackles over forty-eight hours--and +fixed his hopes upon old Judge Frazer, justice of the peace, who was +apparently on his deathbed and would have a big public funeral, since +he was so high an official. During three days Tom was deeply concerned +about the Judge's condition and hungry for news of it. Sometimes his +hopes ran high--so high that he would venture to get out his regalia +and practise before the looking-glass. But the Judge had a most +discouraging way of fluctuating. At last he was pronounced upon the +mend--and then convalescent. Tom was disgusted; and felt a sense of +injury, too. He handed in his resignation at once--and that night the +Judge suffered a relapse and died. Tom resolved that he would never +trust a man like that again. + +The funeral was a fine thing. The Cadets paraded in a style calculated +to kill the late member with envy. Tom was a free boy again, however +--there was something in that. He could drink and swear, now--but found +to his surprise that he did not want to. The simple fact that he could, +took the desire away, and the charm of it. + +Tom presently wondered to find that his coveted vacation was beginning +to hang a little heavily on his hands. + +He attempted a diary--but nothing happened during three days, and so +he abandoned it. + +The first of all the negro minstrel shows came to town, and made a +sensation. Tom and Joe Harper got up a band of performers and were +happy for two days. + +Even the Glorious Fourth was in some sense a failure, for it rained +hard, there was no procession in consequence, and the greatest man in +the world (as Tom supposed), Mr. Benton, an actual United States +Senator, proved an overwhelming disappointment--for he was not +twenty-five feet high, nor even anywhere in the neighborhood of it. + +A circus came. The boys played circus for three days afterward in +tents made of rag carpeting--admission, three pins for boys, two for +girls--and then circusing was abandoned. + +A phrenologist and a mesmerizer came--and went again and left the +village duller and drearier than ever. + +There were some boys-and-girls' parties, but they were so few and so +delightful that they only made the aching voids between ache the harder. + +Becky Thatcher was gone to her Constantinople home to stay with her +parents during vacation--so there was no bright side to life anywhere. + +The dreadful secret of the murder was a chronic misery. It was a very +cancer for permanency and pain. + +Then came the measles. + +During two long weeks Tom lay a prisoner, dead to the world and its +happenings. He was very ill, he was interested in nothing. When he got +upon his feet at last and moved feebly down-town, a melancholy change +had come over everything and every creature. There had been a +"revival," and everybody had "got religion," not only the adults, but +even the boys and girls. Tom went about, hoping against hope for the +sight of one blessed sinful face, but disappointment crossed him +everywhere. He found Joe Harper studying a Testament, and turned sadly +away from the depressing spectacle. He sought Ben Rogers, and found him +visiting the poor with a basket of tracts. He hunted up Jim Hollis, who +called his attention to the precious blessing of his late measles as a +warning. Every boy he encountered added another ton to his depression; +and when, in desperation, he flew for refuge at last to the bosom of +Huckleberry Finn and was received with a Scriptural quotation, his +heart broke and he crept home and to bed realizing that he alone of all +the town was lost, forever and forever. + +And that night there came on a terrific storm, with driving rain, +awful claps of thunder and blinding sheets of lightning. He covered his +head with the bedclothes and waited in a horror of suspense for his +doom; for he had not the shadow of a doubt that all this hubbub was +about him. He believed he had taxed the forbearance of the powers above +to the extremity of endurance and that this was the result. It might +have seemed to him a waste of pomp and ammunition to kill a bug with a +battery of artillery, but there seemed nothing incongruous about the +getting up such an expensive thunderstorm as this to knock the turf +from under an insect like himself. + +By and by the tempest spent itself and died without accomplishing its +object. The boy's first impulse was to be grateful, and reform. His +second was to wait--for there might not be any more storms. + +The next day the doctors were back; Tom had relapsed. The three weeks +he spent on his back this time seemed an entire age. When he got abroad +at last he was hardly grateful that he had been spared, remembering how +lonely was his estate, how companionless and forlorn he was. He drifted +listlessly down the street and found Jim Hollis acting as judge in a +juvenile court that was trying a cat for murder, in the presence of her +victim, a bird. He found Joe Harper and Huck Finn up an alley eating a +stolen melon. Poor lads! they--like Tom--had suffered a relapse. + + + +CHAPTER XXIII + +AT last the sleepy atmosphere was stirred--and vigorously: the murder +trial came on in the court. It became the absorbing topic of village +talk immediately. Tom could not get away from it. Every reference to +the murder sent a shudder to his heart, for his troubled conscience and +fears almost persuaded him that these remarks were put forth in his +hearing as "feelers"; he did not see how he could be suspected of +knowing anything about the murder, but still he could not be +comfortable in the midst of this gossip. It kept him in a cold shiver +all the time. He took Huck to a lonely place to have a talk with him. +It would be some relief to unseal his tongue for a little while; to +divide his burden of distress with another sufferer. Moreover, he +wanted to assure himself that Huck had remained discreet. + +"Huck, have you ever told anybody about--that?" + +"'Bout what?" + +"You know what." + +"Oh--'course I haven't." + +"Never a word?" + +"Never a solitary word, so help me. What makes you ask?" + +"Well, I was afeard." + +"Why, Tom Sawyer, we wouldn't be alive two days if that got found out. +YOU know that." + +Tom felt more comfortable. After a pause: + +"Huck, they couldn't anybody get you to tell, could they?" + +"Get me to tell? Why, if I wanted that half-breed devil to drownd me +they could get me to tell. They ain't no different way." + +"Well, that's all right, then. I reckon we're safe as long as we keep +mum. But let's swear again, anyway. It's more surer." + +"I'm agreed." + +So they swore again with dread solemnities. + +"What is the talk around, Huck? I've heard a power of it." + +"Talk? Well, it's just Muff Potter, Muff Potter, Muff Potter all the +time. It keeps me in a sweat, constant, so's I want to hide som'ers." + +"That's just the same way they go on round me. I reckon he's a goner. +Don't you feel sorry for him, sometimes?" + +"Most always--most always. He ain't no account; but then he hain't +ever done anything to hurt anybody. Just fishes a little, to get money +to get drunk on--and loafs around considerable; but lord, we all do +that--leastways most of us--preachers and such like. But he's kind of +good--he give me half a fish, once, when there warn't enough for two; +and lots of times he's kind of stood by me when I was out of luck." + +"Well, he's mended kites for me, Huck, and knitted hooks on to my +line. I wish we could get him out of there." + +"My! we couldn't get him out, Tom. And besides, 'twouldn't do any +good; they'd ketch him again." + +"Yes--so they would. But I hate to hear 'em abuse him so like the +dickens when he never done--that." + +"I do too, Tom. Lord, I hear 'em say he's the bloodiest looking +villain in this country, and they wonder he wasn't ever hung before." + +"Yes, they talk like that, all the time. I've heard 'em say that if he +was to get free they'd lynch him." + +"And they'd do it, too." + +The boys had a long talk, but it brought them little comfort. As the +twilight drew on, they found themselves hanging about the neighborhood +of the little isolated jail, perhaps with an undefined hope that +something would happen that might clear away their difficulties. But +nothing happened; there seemed to be no angels or fairies interested in +this luckless captive. + +The boys did as they had often done before--went to the cell grating +and gave Potter some tobacco and matches. He was on the ground floor +and there were no guards. + +His gratitude for their gifts had always smote their consciences +before--it cut deeper than ever, this time. They felt cowardly and +treacherous to the last degree when Potter said: + +"You've been mighty good to me, boys--better'n anybody else in this +town. And I don't forget it, I don't. Often I says to myself, says I, +'I used to mend all the boys' kites and things, and show 'em where the +good fishin' places was, and befriend 'em what I could, and now they've +all forgot old Muff when he's in trouble; but Tom don't, and Huck +don't--THEY don't forget him, says I, 'and I don't forget them.' Well, +boys, I done an awful thing--drunk and crazy at the time--that's the +only way I account for it--and now I got to swing for it, and it's +right. Right, and BEST, too, I reckon--hope so, anyway. Well, we won't +talk about that. I don't want to make YOU feel bad; you've befriended +me. But what I want to say, is, don't YOU ever get drunk--then you won't +ever get here. Stand a litter furder west--so--that's it; it's a prime +comfort to see faces that's friendly when a body's in such a muck of +trouble, and there don't none come here but yourn. Good friendly +faces--good friendly faces. Git up on one another's backs and let me +touch 'em. That's it. Shake hands--yourn'll come through the bars, but +mine's too big. Little hands, and weak--but they've helped Muff Potter +a power, and they'd help him more if they could." + +Tom went home miserable, and his dreams that night were full of +horrors. The next day and the day after, he hung about the court-room, +drawn by an almost irresistible impulse to go in, but forcing himself +to stay out. Huck was having the same experience. They studiously +avoided each other. Each wandered away, from time to time, but the same +dismal fascination always brought them back presently. Tom kept his +ears open when idlers sauntered out of the court-room, but invariably +heard distressing news--the toils were closing more and more +relentlessly around poor Potter. At the end of the second day the +village talk was to the effect that Injun Joe's evidence stood firm and +unshaken, and that there was not the slightest question as to what the +jury's verdict would be. + +Tom was out late, that night, and came to bed through the window. He +was in a tremendous state of excitement. It was hours before he got to +sleep. All the village flocked to the court-house the next morning, for +this was to be the great day. Both sexes were about equally represented +in the packed audience. After a long wait the jury filed in and took +their places; shortly afterward, Potter, pale and haggard, timid and +hopeless, was brought in, with chains upon him, and seated where all +the curious eyes could stare at him; no less conspicuous was Injun Joe, +stolid as ever. There was another pause, and then the judge arrived and +the sheriff proclaimed the opening of the court. The usual whisperings +among the lawyers and gathering together of papers followed. These +details and accompanying delays worked up an atmosphere of preparation +that was as impressive as it was fascinating. + +Now a witness was called who testified that he found Muff Potter +washing in the brook, at an early hour of the morning that the murder +was discovered, and that he immediately sneaked away. After some +further questioning, counsel for the prosecution said: + +"Take the witness." + +The prisoner raised his eyes for a moment, but dropped them again when +his own counsel said: + +"I have no questions to ask him." + +The next witness proved the finding of the knife near the corpse. +Counsel for the prosecution said: + +"Take the witness." + +"I have no questions to ask him," Potter's lawyer replied. + +A third witness swore he had often seen the knife in Potter's +possession. + +"Take the witness." + +Counsel for Potter declined to question him. The faces of the audience +began to betray annoyance. Did this attorney mean to throw away his +client's life without an effort? + +Several witnesses deposed concerning Potter's guilty behavior when +brought to the scene of the murder. They were allowed to leave the +stand without being cross-questioned. + +Every detail of the damaging circumstances that occurred in the +graveyard upon that morning which all present remembered so well was +brought out by credible witnesses, but none of them were cross-examined +by Potter's lawyer. The perplexity and dissatisfaction of the house +expressed itself in murmurs and provoked a reproof from the bench. +Counsel for the prosecution now said: + +"By the oaths of citizens whose simple word is above suspicion, we +have fastened this awful crime, beyond all possibility of question, +upon the unhappy prisoner at the bar. We rest our case here." + +A groan escaped from poor Potter, and he put his face in his hands and +rocked his body softly to and fro, while a painful silence reigned in +the court-room. Many men were moved, and many women's compassion +testified itself in tears. Counsel for the defence rose and said: + +"Your honor, in our remarks at the opening of this trial, we +foreshadowed our purpose to prove that our client did this fearful deed +while under the influence of a blind and irresponsible delirium +produced by drink. We have changed our mind. We shall not offer that +plea." [Then to the clerk:] "Call Thomas Sawyer!" + +A puzzled amazement awoke in every face in the house, not even +excepting Potter's. Every eye fastened itself with wondering interest +upon Tom as he rose and took his place upon the stand. The boy looked +wild enough, for he was badly scared. The oath was administered. + +"Thomas Sawyer, where were you on the seventeenth of June, about the +hour of midnight?" + +Tom glanced at Injun Joe's iron face and his tongue failed him. The +audience listened breathless, but the words refused to come. After a +few moments, however, the boy got a little of his strength back, and +managed to put enough of it into his voice to make part of the house +hear: + +"In the graveyard!" + +"A little bit louder, please. Don't be afraid. You were--" + +"In the graveyard." + +A contemptuous smile flitted across Injun Joe's face. + +"Were you anywhere near Horse Williams' grave?" + +"Yes, sir." + +"Speak up--just a trifle louder. How near were you?" + +"Near as I am to you." + +"Were you hidden, or not?" + +"I was hid." + +"Where?" + +"Behind the elms that's on the edge of the grave." + +Injun Joe gave a barely perceptible start. + +"Any one with you?" + +"Yes, sir. I went there with--" + +"Wait--wait a moment. Never mind mentioning your companion's name. We +will produce him at the proper time. Did you carry anything there with +you." + +Tom hesitated and looked confused. + +"Speak out, my boy--don't be diffident. The truth is always +respectable. What did you take there?" + +"Only a--a--dead cat." + +There was a ripple of mirth, which the court checked. + +"We will produce the skeleton of that cat. Now, my boy, tell us +everything that occurred--tell it in your own way--don't skip anything, +and don't be afraid." + +Tom began--hesitatingly at first, but as he warmed to his subject his +words flowed more and more easily; in a little while every sound ceased +but his own voice; every eye fixed itself upon him; with parted lips +and bated breath the audience hung upon his words, taking no note of +time, rapt in the ghastly fascinations of the tale. The strain upon +pent emotion reached its climax when the boy said: + +"--and as the doctor fetched the board around and Muff Potter fell, +Injun Joe jumped with the knife and--" + +Crash! Quick as lightning the half-breed sprang for a window, tore his +way through all opposers, and was gone! + + + +CHAPTER XXIV + +TOM was a glittering hero once more--the pet of the old, the envy of +the young. His name even went into immortal print, for the village +paper magnified him. There were some that believed he would be +President, yet, if he escaped hanging. + +As usual, the fickle, unreasoning world took Muff Potter to its bosom +and fondled him as lavishly as it had abused him before. But that sort +of conduct is to the world's credit; therefore it is not well to find +fault with it. + +Tom's days were days of splendor and exultation to him, but his nights +were seasons of horror. Injun Joe infested all his dreams, and always +with doom in his eye. Hardly any temptation could persuade the boy to +stir abroad after nightfall. Poor Huck was in the same state of +wretchedness and terror, for Tom had told the whole story to the lawyer +the night before the great day of the trial, and Huck was sore afraid +that his share in the business might leak out, yet, notwithstanding +Injun Joe's flight had saved him the suffering of testifying in court. +The poor fellow had got the attorney to promise secrecy, but what of +that? Since Tom's harassed conscience had managed to drive him to the +lawyer's house by night and wring a dread tale from lips that had been +sealed with the dismalest and most formidable of oaths, Huck's +confidence in the human race was well-nigh obliterated. + +Daily Muff Potter's gratitude made Tom glad he had spoken; but nightly +he wished he had sealed up his tongue. + +Half the time Tom was afraid Injun Joe would never be captured; the +other half he was afraid he would be. He felt sure he never could draw +a safe breath again until that man was dead and he had seen the corpse. + +Rewards had been offered, the country had been scoured, but no Injun +Joe was found. One of those omniscient and awe-inspiring marvels, a +detective, came up from St. Louis, moused around, shook his head, +looked wise, and made that sort of astounding success which members of +that craft usually achieve. That is to say, he "found a clew." But you +can't hang a "clew" for murder, and so after that detective had got +through and gone home, Tom felt just as insecure as he was before. + +The slow days drifted on, and each left behind it a slightly lightened +weight of apprehension. + + + +CHAPTER XXV + +THERE comes a time in every rightly-constructed boy's life when he has +a raging desire to go somewhere and dig for hidden treasure. This +desire suddenly came upon Tom one day. He sallied out to find Joe +Harper, but failed of success. Next he sought Ben Rogers; he had gone +fishing. Presently he stumbled upon Huck Finn the Red-Handed. Huck +would answer. Tom took him to a private place and opened the matter to +him confidentially. Huck was willing. Huck was always willing to take a +hand in any enterprise that offered entertainment and required no +capital, for he had a troublesome superabundance of that sort of time +which is not money. "Where'll we dig?" said Huck. + +"Oh, most anywhere." + +"Why, is it hid all around?" + +"No, indeed it ain't. It's hid in mighty particular places, Huck +--sometimes on islands, sometimes in rotten chests under the end of a +limb of an old dead tree, just where the shadow falls at midnight; but +mostly under the floor in ha'nted houses." + +"Who hides it?" + +"Why, robbers, of course--who'd you reckon? Sunday-school +sup'rintendents?" + +"I don't know. If 'twas mine I wouldn't hide it; I'd spend it and have +a good time." + +"So would I. But robbers don't do that way. They always hide it and +leave it there." + +"Don't they come after it any more?" + +"No, they think they will, but they generally forget the marks, or +else they die. Anyway, it lays there a long time and gets rusty; and by +and by somebody finds an old yellow paper that tells how to find the +marks--a paper that's got to be ciphered over about a week because it's +mostly signs and hy'roglyphics." + +"Hyro--which?" + +"Hy'roglyphics--pictures and things, you know, that don't seem to mean +anything." + +"Have you got one of them papers, Tom?" + +"No." + +"Well then, how you going to find the marks?" + +"I don't want any marks. They always bury it under a ha'nted house or +on an island, or under a dead tree that's got one limb sticking out. +Well, we've tried Jackson's Island a little, and we can try it again +some time; and there's the old ha'nted house up the Still-House branch, +and there's lots of dead-limb trees--dead loads of 'em." + +"Is it under all of them?" + +"How you talk! No!" + +"Then how you going to know which one to go for?" + +"Go for all of 'em!" + +"Why, Tom, it'll take all summer." + +"Well, what of that? Suppose you find a brass pot with a hundred +dollars in it, all rusty and gray, or rotten chest full of di'monds. +How's that?" + +Huck's eyes glowed. + +"That's bully. Plenty bully enough for me. Just you gimme the hundred +dollars and I don't want no di'monds." + +"All right. But I bet you I ain't going to throw off on di'monds. Some +of 'em's worth twenty dollars apiece--there ain't any, hardly, but's +worth six bits or a dollar." + +"No! Is that so?" + +"Cert'nly--anybody'll tell you so. Hain't you ever seen one, Huck?" + +"Not as I remember." + +"Oh, kings have slathers of them." + +"Well, I don' know no kings, Tom." + +"I reckon you don't. But if you was to go to Europe you'd see a raft +of 'em hopping around." + +"Do they hop?" + +"Hop?--your granny! No!" + +"Well, what did you say they did, for?" + +"Shucks, I only meant you'd SEE 'em--not hopping, of course--what do +they want to hop for?--but I mean you'd just see 'em--scattered around, +you know, in a kind of a general way. Like that old humpbacked Richard." + +"Richard? What's his other name?" + +"He didn't have any other name. Kings don't have any but a given name." + +"No?" + +"But they don't." + +"Well, if they like it, Tom, all right; but I don't want to be a king +and have only just a given name, like a nigger. But say--where you +going to dig first?" + +"Well, I don't know. S'pose we tackle that old dead-limb tree on the +hill t'other side of Still-House branch?" + +"I'm agreed." + +So they got a crippled pick and a shovel, and set out on their +three-mile tramp. They arrived hot and panting, and threw themselves +down in the shade of a neighboring elm to rest and have a smoke. + +"I like this," said Tom. + +"So do I." + +"Say, Huck, if we find a treasure here, what you going to do with your +share?" + +"Well, I'll have pie and a glass of soda every day, and I'll go to +every circus that comes along. I bet I'll have a gay time." + +"Well, ain't you going to save any of it?" + +"Save it? What for?" + +"Why, so as to have something to live on, by and by." + +"Oh, that ain't any use. Pap would come back to thish-yer town some +day and get his claws on it if I didn't hurry up, and I tell you he'd +clean it out pretty quick. What you going to do with yourn, Tom?" + +"I'm going to buy a new drum, and a sure-'nough sword, and a red +necktie and a bull pup, and get married." + +"Married!" + +"That's it." + +"Tom, you--why, you ain't in your right mind." + +"Wait--you'll see." + +"Well, that's the foolishest thing you could do. Look at pap and my +mother. Fight! Why, they used to fight all the time. I remember, mighty +well." + +"That ain't anything. The girl I'm going to marry won't fight." + +"Tom, I reckon they're all alike. They'll all comb a body. Now you +better think 'bout this awhile. I tell you you better. What's the name +of the gal?" + +"It ain't a gal at all--it's a girl." + +"It's all the same, I reckon; some says gal, some says girl--both's +right, like enough. Anyway, what's her name, Tom?" + +"I'll tell you some time--not now." + +"All right--that'll do. Only if you get married I'll be more lonesomer +than ever." + +"No you won't. You'll come and live with me. Now stir out of this and +we'll go to digging." + +They worked and sweated for half an hour. No result. They toiled +another half-hour. Still no result. Huck said: + +"Do they always bury it as deep as this?" + +"Sometimes--not always. Not generally. I reckon we haven't got the +right place." + +So they chose a new spot and began again. The labor dragged a little, +but still they made progress. They pegged away in silence for some +time. Finally Huck leaned on his shovel, swabbed the beaded drops from +his brow with his sleeve, and said: + +"Where you going to dig next, after we get this one?" + +"I reckon maybe we'll tackle the old tree that's over yonder on +Cardiff Hill back of the widow's." + +"I reckon that'll be a good one. But won't the widow take it away from +us, Tom? It's on her land." + +"SHE take it away! Maybe she'd like to try it once. Whoever finds one +of these hid treasures, it belongs to him. It don't make any difference +whose land it's on." + +That was satisfactory. The work went on. By and by Huck said: + +"Blame it, we must be in the wrong place again. What do you think?" + +"It is mighty curious, Huck. I don't understand it. Sometimes witches +interfere. I reckon maybe that's what's the trouble now." + +"Shucks! Witches ain't got no power in the daytime." + +"Well, that's so. I didn't think of that. Oh, I know what the matter +is! What a blamed lot of fools we are! You got to find out where the +shadow of the limb falls at midnight, and that's where you dig!" + +"Then consound it, we've fooled away all this work for nothing. Now +hang it all, we got to come back in the night. It's an awful long way. +Can you get out?" + +"I bet I will. We've got to do it to-night, too, because if somebody +sees these holes they'll know in a minute what's here and they'll go +for it." + +"Well, I'll come around and maow to-night." + +"All right. Let's hide the tools in the bushes." + +The boys were there that night, about the appointed time. They sat in +the shadow waiting. It was a lonely place, and an hour made solemn by +old traditions. Spirits whispered in the rustling leaves, ghosts lurked +in the murky nooks, the deep baying of a hound floated up out of the +distance, an owl answered with his sepulchral note. The boys were +subdued by these solemnities, and talked little. By and by they judged +that twelve had come; they marked where the shadow fell, and began to +dig. Their hopes commenced to rise. Their interest grew stronger, and +their industry kept pace with it. The hole deepened and still deepened, +but every time their hearts jumped to hear the pick strike upon +something, they only suffered a new disappointment. It was only a stone +or a chunk. At last Tom said: + +"It ain't any use, Huck, we're wrong again." + +"Well, but we CAN'T be wrong. We spotted the shadder to a dot." + +"I know it, but then there's another thing." + +"What's that?". + +"Why, we only guessed at the time. Like enough it was too late or too +early." + +Huck dropped his shovel. + +"That's it," said he. "That's the very trouble. We got to give this +one up. We can't ever tell the right time, and besides this kind of +thing's too awful, here this time of night with witches and ghosts +a-fluttering around so. I feel as if something's behind me all the time; +and I'm afeard to turn around, becuz maybe there's others in front +a-waiting for a chance. I been creeping all over, ever since I got here." + +"Well, I've been pretty much so, too, Huck. They most always put in a +dead man when they bury a treasure under a tree, to look out for it." + +"Lordy!" + +"Yes, they do. I've always heard that." + +"Tom, I don't like to fool around much where there's dead people. A +body's bound to get into trouble with 'em, sure." + +"I don't like to stir 'em up, either. S'pose this one here was to +stick his skull out and say something!" + +"Don't Tom! It's awful." + +"Well, it just is. Huck, I don't feel comfortable a bit." + +"Say, Tom, let's give this place up, and try somewheres else." + +"All right, I reckon we better." + +"What'll it be?" + +Tom considered awhile; and then said: + +"The ha'nted house. That's it!" + +"Blame it, I don't like ha'nted houses, Tom. Why, they're a dern sight +worse'n dead people. Dead people might talk, maybe, but they don't come +sliding around in a shroud, when you ain't noticing, and peep over your +shoulder all of a sudden and grit their teeth, the way a ghost does. I +couldn't stand such a thing as that, Tom--nobody could." + +"Yes, but, Huck, ghosts don't travel around only at night. They won't +hender us from digging there in the daytime." + +"Well, that's so. But you know mighty well people don't go about that +ha'nted house in the day nor the night." + +"Well, that's mostly because they don't like to go where a man's been +murdered, anyway--but nothing's ever been seen around that house except +in the night--just some blue lights slipping by the windows--no regular +ghosts." + +"Well, where you see one of them blue lights flickering around, Tom, +you can bet there's a ghost mighty close behind it. It stands to +reason. Becuz you know that they don't anybody but ghosts use 'em." + +"Yes, that's so. But anyway they don't come around in the daytime, so +what's the use of our being afeard?" + +"Well, all right. We'll tackle the ha'nted house if you say so--but I +reckon it's taking chances." + +They had started down the hill by this time. There in the middle of +the moonlit valley below them stood the "ha'nted" house, utterly +isolated, its fences gone long ago, rank weeds smothering the very +doorsteps, the chimney crumbled to ruin, the window-sashes vacant, a +corner of the roof caved in. The boys gazed awhile, half expecting to +see a blue light flit past a window; then talking in a low tone, as +befitted the time and the circumstances, they struck far off to the +right, to give the haunted house a wide berth, and took their way +homeward through the woods that adorned the rearward side of Cardiff +Hill. + + + +CHAPTER XXVI + +ABOUT noon the next day the boys arrived at the dead tree; they had +come for their tools. Tom was impatient to go to the haunted house; +Huck was measurably so, also--but suddenly said: + +"Lookyhere, Tom, do you know what day it is?" + +Tom mentally ran over the days of the week, and then quickly lifted +his eyes with a startled look in them-- + +"My! I never once thought of it, Huck!" + +"Well, I didn't neither, but all at once it popped onto me that it was +Friday." + +"Blame it, a body can't be too careful, Huck. We might 'a' got into an +awful scrape, tackling such a thing on a Friday." + +"MIGHT! Better say we WOULD! There's some lucky days, maybe, but +Friday ain't." + +"Any fool knows that. I don't reckon YOU was the first that found it +out, Huck." + +"Well, I never said I was, did I? And Friday ain't all, neither. I had +a rotten bad dream last night--dreampt about rats." + +"No! Sure sign of trouble. Did they fight?" + +"No." + +"Well, that's good, Huck. When they don't fight it's only a sign that +there's trouble around, you know. All we got to do is to look mighty +sharp and keep out of it. We'll drop this thing for to-day, and play. +Do you know Robin Hood, Huck?" + +"No. Who's Robin Hood?" + +"Why, he was one of the greatest men that was ever in England--and the +best. He was a robber." + +"Cracky, I wisht I was. Who did he rob?" + +"Only sheriffs and bishops and rich people and kings, and such like. +But he never bothered the poor. He loved 'em. He always divided up with +'em perfectly square." + +"Well, he must 'a' been a brick." + +"I bet you he was, Huck. Oh, he was the noblest man that ever was. +They ain't any such men now, I can tell you. He could lick any man in +England, with one hand tied behind him; and he could take his yew bow +and plug a ten-cent piece every time, a mile and a half." + +"What's a YEW bow?" + +"I don't know. It's some kind of a bow, of course. And if he hit that +dime only on the edge he would set down and cry--and curse. But we'll +play Robin Hood--it's nobby fun. I'll learn you." + +"I'm agreed." + +So they played Robin Hood all the afternoon, now and then casting a +yearning eye down upon the haunted house and passing a remark about the +morrow's prospects and possibilities there. As the sun began to sink +into the west they took their way homeward athwart the long shadows of +the trees and soon were buried from sight in the forests of Cardiff +Hill. + +On Saturday, shortly after noon, the boys were at the dead tree again. +They had a smoke and a chat in the shade, and then dug a little in +their last hole, not with great hope, but merely because Tom said there +were so many cases where people had given up a treasure after getting +down within six inches of it, and then somebody else had come along and +turned it up with a single thrust of a shovel. The thing failed this +time, however, so the boys shouldered their tools and went away feeling +that they had not trifled with fortune, but had fulfilled all the +requirements that belong to the business of treasure-hunting. + +When they reached the haunted house there was something so weird and +grisly about the dead silence that reigned there under the baking sun, +and something so depressing about the loneliness and desolation of the +place, that they were afraid, for a moment, to venture in. Then they +crept to the door and took a trembling peep. They saw a weed-grown, +floorless room, unplastered, an ancient fireplace, vacant windows, a +ruinous staircase; and here, there, and everywhere hung ragged and +abandoned cobwebs. They presently entered, softly, with quickened +pulses, talking in whispers, ears alert to catch the slightest sound, +and muscles tense and ready for instant retreat. + +In a little while familiarity modified their fears and they gave the +place a critical and interested examination, rather admiring their own +boldness, and wondering at it, too. Next they wanted to look up-stairs. +This was something like cutting off retreat, but they got to daring +each other, and of course there could be but one result--they threw +their tools into a corner and made the ascent. Up there were the same +signs of decay. In one corner they found a closet that promised +mystery, but the promise was a fraud--there was nothing in it. Their +courage was up now and well in hand. They were about to go down and +begin work when-- + +"Sh!" said Tom. + +"What is it?" whispered Huck, blanching with fright. + +"Sh!... There!... Hear it?" + +"Yes!... Oh, my! Let's run!" + +"Keep still! Don't you budge! They're coming right toward the door." + +The boys stretched themselves upon the floor with their eyes to +knot-holes in the planking, and lay waiting, in a misery of fear. + +"They've stopped.... No--coming.... Here they are. Don't whisper +another word, Huck. My goodness, I wish I was out of this!" + +Two men entered. Each boy said to himself: "There's the old deaf and +dumb Spaniard that's been about town once or twice lately--never saw +t'other man before." + +"T'other" was a ragged, unkempt creature, with nothing very pleasant +in his face. The Spaniard was wrapped in a serape; he had bushy white +whiskers; long white hair flowed from under his sombrero, and he wore +green goggles. When they came in, "t'other" was talking in a low voice; +they sat down on the ground, facing the door, with their backs to the +wall, and the speaker continued his remarks. His manner became less +guarded and his words more distinct as he proceeded: + +"No," said he, "I've thought it all over, and I don't like it. It's +dangerous." + +"Dangerous!" grunted the "deaf and dumb" Spaniard--to the vast +surprise of the boys. "Milksop!" + +This voice made the boys gasp and quake. It was Injun Joe's! There was +silence for some time. Then Joe said: + +"What's any more dangerous than that job up yonder--but nothing's come +of it." + +"That's different. Away up the river so, and not another house about. +'Twon't ever be known that we tried, anyway, long as we didn't succeed." + +"Well, what's more dangerous than coming here in the daytime!--anybody +would suspicion us that saw us." + +"I know that. But there warn't any other place as handy after that +fool of a job. I want to quit this shanty. I wanted to yesterday, only +it warn't any use trying to stir out of here, with those infernal boys +playing over there on the hill right in full view." + +"Those infernal boys" quaked again under the inspiration of this +remark, and thought how lucky it was that they had remembered it was +Friday and concluded to wait a day. They wished in their hearts they +had waited a year. + +The two men got out some food and made a luncheon. After a long and +thoughtful silence, Injun Joe said: + +"Look here, lad--you go back up the river where you belong. Wait there +till you hear from me. I'll take the chances on dropping into this town +just once more, for a look. We'll do that 'dangerous' job after I've +spied around a little and think things look well for it. Then for +Texas! We'll leg it together!" + +This was satisfactory. Both men presently fell to yawning, and Injun +Joe said: + +"I'm dead for sleep! It's your turn to watch." + +He curled down in the weeds and soon began to snore. His comrade +stirred him once or twice and he became quiet. Presently the watcher +began to nod; his head drooped lower and lower, both men began to snore +now. + +The boys drew a long, grateful breath. Tom whispered: + +"Now's our chance--come!" + +Huck said: + +"I can't--I'd die if they was to wake." + +Tom urged--Huck held back. At last Tom rose slowly and softly, and +started alone. But the first step he made wrung such a hideous creak +from the crazy floor that he sank down almost dead with fright. He +never made a second attempt. The boys lay there counting the dragging +moments till it seemed to them that time must be done and eternity +growing gray; and then they were grateful to note that at last the sun +was setting. + +Now one snore ceased. Injun Joe sat up, stared around--smiled grimly +upon his comrade, whose head was drooping upon his knees--stirred him +up with his foot and said: + +"Here! YOU'RE a watchman, ain't you! All right, though--nothing's +happened." + +"My! have I been asleep?" + +"Oh, partly, partly. Nearly time for us to be moving, pard. What'll we +do with what little swag we've got left?" + +"I don't know--leave it here as we've always done, I reckon. No use to +take it away till we start south. Six hundred and fifty in silver's +something to carry." + +"Well--all right--it won't matter to come here once more." + +"No--but I'd say come in the night as we used to do--it's better." + +"Yes: but look here; it may be a good while before I get the right +chance at that job; accidents might happen; 'tain't in such a very good +place; we'll just regularly bury it--and bury it deep." + +"Good idea," said the comrade, who walked across the room, knelt down, +raised one of the rearward hearth-stones and took out a bag that +jingled pleasantly. He subtracted from it twenty or thirty dollars for +himself and as much for Injun Joe, and passed the bag to the latter, +who was on his knees in the corner, now, digging with his bowie-knife. + +The boys forgot all their fears, all their miseries in an instant. +With gloating eyes they watched every movement. Luck!--the splendor of +it was beyond all imagination! Six hundred dollars was money enough to +make half a dozen boys rich! Here was treasure-hunting under the +happiest auspices--there would not be any bothersome uncertainty as to +where to dig. They nudged each other every moment--eloquent nudges and +easily understood, for they simply meant--"Oh, but ain't you glad NOW +we're here!" + +Joe's knife struck upon something. + +"Hello!" said he. + +"What is it?" said his comrade. + +"Half-rotten plank--no, it's a box, I believe. Here--bear a hand and +we'll see what it's here for. Never mind, I've broke a hole." + +He reached his hand in and drew it out-- + +"Man, it's money!" + +The two men examined the handful of coins. They were gold. The boys +above were as excited as themselves, and as delighted. + +Joe's comrade said: + +"We'll make quick work of this. There's an old rusty pick over amongst +the weeds in the corner the other side of the fireplace--I saw it a +minute ago." + +He ran and brought the boys' pick and shovel. Injun Joe took the pick, +looked it over critically, shook his head, muttered something to +himself, and then began to use it. The box was soon unearthed. It was +not very large; it was iron bound and had been very strong before the +slow years had injured it. The men contemplated the treasure awhile in +blissful silence. + +"Pard, there's thousands of dollars here," said Injun Joe. + +"'Twas always said that Murrel's gang used to be around here one +summer," the stranger observed. + +"I know it," said Injun Joe; "and this looks like it, I should say." + +"Now you won't need to do that job." + +The half-breed frowned. Said he: + +"You don't know me. Least you don't know all about that thing. 'Tain't +robbery altogether--it's REVENGE!" and a wicked light flamed in his +eyes. "I'll need your help in it. When it's finished--then Texas. Go +home to your Nance and your kids, and stand by till you hear from me." + +"Well--if you say so; what'll we do with this--bury it again?" + +"Yes. [Ravishing delight overhead.] NO! by the great Sachem, no! +[Profound distress overhead.] I'd nearly forgot. That pick had fresh +earth on it! [The boys were sick with terror in a moment.] What +business has a pick and a shovel here? What business with fresh earth +on them? Who brought them here--and where are they gone? Have you heard +anybody?--seen anybody? What! bury it again and leave them to come and +see the ground disturbed? Not exactly--not exactly. We'll take it to my +den." + +"Why, of course! Might have thought of that before. You mean Number +One?" + +"No--Number Two--under the cross. The other place is bad--too common." + +"All right. It's nearly dark enough to start." + +Injun Joe got up and went about from window to window cautiously +peeping out. Presently he said: + +"Who could have brought those tools here? Do you reckon they can be +up-stairs?" + +The boys' breath forsook them. Injun Joe put his hand on his knife, +halted a moment, undecided, and then turned toward the stairway. The +boys thought of the closet, but their strength was gone. The steps came +creaking up the stairs--the intolerable distress of the situation woke +the stricken resolution of the lads--they were about to spring for the +closet, when there was a crash of rotten timbers and Injun Joe landed +on the ground amid the debris of the ruined stairway. He gathered +himself up cursing, and his comrade said: + +"Now what's the use of all that? If it's anybody, and they're up +there, let them STAY there--who cares? If they want to jump down, now, +and get into trouble, who objects? It will be dark in fifteen minutes +--and then let them follow us if they want to. I'm willing. In my +opinion, whoever hove those things in here caught a sight of us and +took us for ghosts or devils or something. I'll bet they're running +yet." + +Joe grumbled awhile; then he agreed with his friend that what daylight +was left ought to be economized in getting things ready for leaving. +Shortly afterward they slipped out of the house in the deepening +twilight, and moved toward the river with their precious box. + +Tom and Huck rose up, weak but vastly relieved, and stared after them +through the chinks between the logs of the house. Follow? Not they. +They were content to reach ground again without broken necks, and take +the townward track over the hill. They did not talk much. They were too +much absorbed in hating themselves--hating the ill luck that made them +take the spade and the pick there. But for that, Injun Joe never would +have suspected. He would have hidden the silver with the gold to wait +there till his "revenge" was satisfied, and then he would have had the +misfortune to find that money turn up missing. Bitter, bitter luck that +the tools were ever brought there! + +They resolved to keep a lookout for that Spaniard when he should come +to town spying out for chances to do his revengeful job, and follow him +to "Number Two," wherever that might be. Then a ghastly thought +occurred to Tom. + +"Revenge? What if he means US, Huck!" + +"Oh, don't!" said Huck, nearly fainting. + +They talked it all over, and as they entered town they agreed to +believe that he might possibly mean somebody else--at least that he +might at least mean nobody but Tom, since only Tom had testified. + +Very, very small comfort it was to Tom to be alone in danger! Company +would be a palpable improvement, he thought. + + + +CHAPTER XXVII + +THE adventure of the day mightily tormented Tom's dreams that night. +Four times he had his hands on that rich treasure and four times it +wasted to nothingness in his fingers as sleep forsook him and +wakefulness brought back the hard reality of his misfortune. As he lay +in the early morning recalling the incidents of his great adventure, he +noticed that they seemed curiously subdued and far away--somewhat as if +they had happened in another world, or in a time long gone by. Then it +occurred to him that the great adventure itself must be a dream! There +was one very strong argument in favor of this idea--namely, that the +quantity of coin he had seen was too vast to be real. He had never seen +as much as fifty dollars in one mass before, and he was like all boys +of his age and station in life, in that he imagined that all references +to "hundreds" and "thousands" were mere fanciful forms of speech, and +that no such sums really existed in the world. He never had supposed +for a moment that so large a sum as a hundred dollars was to be found +in actual money in any one's possession. If his notions of hidden +treasure had been analyzed, they would have been found to consist of a +handful of real dimes and a bushel of vague, splendid, ungraspable +dollars. + +But the incidents of his adventure grew sensibly sharper and clearer +under the attrition of thinking them over, and so he presently found +himself leaning to the impression that the thing might not have been a +dream, after all. This uncertainty must be swept away. He would snatch +a hurried breakfast and go and find Huck. Huck was sitting on the +gunwale of a flatboat, listlessly dangling his feet in the water and +looking very melancholy. Tom concluded to let Huck lead up to the +subject. If he did not do it, then the adventure would be proved to +have been only a dream. + +"Hello, Huck!" + +"Hello, yourself." + +Silence, for a minute. + +"Tom, if we'd 'a' left the blame tools at the dead tree, we'd 'a' got +the money. Oh, ain't it awful!" + +"'Tain't a dream, then, 'tain't a dream! Somehow I most wish it was. +Dog'd if I don't, Huck." + +"What ain't a dream?" + +"Oh, that thing yesterday. I been half thinking it was." + +"Dream! If them stairs hadn't broke down you'd 'a' seen how much dream +it was! I've had dreams enough all night--with that patch-eyed Spanish +devil going for me all through 'em--rot him!" + +"No, not rot him. FIND him! Track the money!" + +"Tom, we'll never find him. A feller don't have only one chance for +such a pile--and that one's lost. I'd feel mighty shaky if I was to see +him, anyway." + +"Well, so'd I; but I'd like to see him, anyway--and track him out--to +his Number Two." + +"Number Two--yes, that's it. I been thinking 'bout that. But I can't +make nothing out of it. What do you reckon it is?" + +"I dono. It's too deep. Say, Huck--maybe it's the number of a house!" + +"Goody!... No, Tom, that ain't it. If it is, it ain't in this +one-horse town. They ain't no numbers here." + +"Well, that's so. Lemme think a minute. Here--it's the number of a +room--in a tavern, you know!" + +"Oh, that's the trick! They ain't only two taverns. We can find out +quick." + +"You stay here, Huck, till I come." + +Tom was off at once. He did not care to have Huck's company in public +places. He was gone half an hour. He found that in the best tavern, No. +2 had long been occupied by a young lawyer, and was still so occupied. +In the less ostentatious house, No. 2 was a mystery. The +tavern-keeper's young son said it was kept locked all the time, and he +never saw anybody go into it or come out of it except at night; he did +not know any particular reason for this state of things; had had some +little curiosity, but it was rather feeble; had made the most of the +mystery by entertaining himself with the idea that that room was +"ha'nted"; had noticed that there was a light in there the night before. + +"That's what I've found out, Huck. I reckon that's the very No. 2 +we're after." + +"I reckon it is, Tom. Now what you going to do?" + +"Lemme think." + +Tom thought a long time. Then he said: + +"I'll tell you. The back door of that No. 2 is the door that comes out +into that little close alley between the tavern and the old rattle trap +of a brick store. Now you get hold of all the door-keys you can find, +and I'll nip all of auntie's, and the first dark night we'll go there +and try 'em. And mind you, keep a lookout for Injun Joe, because he +said he was going to drop into town and spy around once more for a +chance to get his revenge. If you see him, you just follow him; and if +he don't go to that No. 2, that ain't the place." + +"Lordy, I don't want to foller him by myself!" + +"Why, it'll be night, sure. He mightn't ever see you--and if he did, +maybe he'd never think anything." + +"Well, if it's pretty dark I reckon I'll track him. I dono--I dono. +I'll try." + +"You bet I'll follow him, if it's dark, Huck. Why, he might 'a' found +out he couldn't get his revenge, and be going right after that money." + +"It's so, Tom, it's so. I'll foller him; I will, by jingoes!" + +"Now you're TALKING! Don't you ever weaken, Huck, and I won't." + + + +CHAPTER XXVIII + +THAT night Tom and Huck were ready for their adventure. They hung +about the neighborhood of the tavern until after nine, one watching the +alley at a distance and the other the tavern door. Nobody entered the +alley or left it; nobody resembling the Spaniard entered or left the +tavern door. The night promised to be a fair one; so Tom went home with +the understanding that if a considerable degree of darkness came on, +Huck was to come and "maow," whereupon he would slip out and try the +keys. But the night remained clear, and Huck closed his watch and +retired to bed in an empty sugar hogshead about twelve. + +Tuesday the boys had the same ill luck. Also Wednesday. But Thursday +night promised better. Tom slipped out in good season with his aunt's +old tin lantern, and a large towel to blindfold it with. He hid the +lantern in Huck's sugar hogshead and the watch began. An hour before +midnight the tavern closed up and its lights (the only ones +thereabouts) were put out. No Spaniard had been seen. Nobody had +entered or left the alley. Everything was auspicious. The blackness of +darkness reigned, the perfect stillness was interrupted only by +occasional mutterings of distant thunder. + +Tom got his lantern, lit it in the hogshead, wrapped it closely in the +towel, and the two adventurers crept in the gloom toward the tavern. +Huck stood sentry and Tom felt his way into the alley. Then there was a +season of waiting anxiety that weighed upon Huck's spirits like a +mountain. He began to wish he could see a flash from the lantern--it +would frighten him, but it would at least tell him that Tom was alive +yet. It seemed hours since Tom had disappeared. Surely he must have +fainted; maybe he was dead; maybe his heart had burst under terror and +excitement. In his uneasiness Huck found himself drawing closer and +closer to the alley; fearing all sorts of dreadful things, and +momentarily expecting some catastrophe to happen that would take away +his breath. There was not much to take away, for he seemed only able to +inhale it by thimblefuls, and his heart would soon wear itself out, the +way it was beating. Suddenly there was a flash of light and Tom came +tearing by him: "Run!" said he; "run, for your life!" + +He needn't have repeated it; once was enough; Huck was making thirty +or forty miles an hour before the repetition was uttered. The boys +never stopped till they reached the shed of a deserted slaughter-house +at the lower end of the village. Just as they got within its shelter +the storm burst and the rain poured down. As soon as Tom got his breath +he said: + +"Huck, it was awful! I tried two of the keys, just as soft as I could; +but they seemed to make such a power of racket that I couldn't hardly +get my breath I was so scared. They wouldn't turn in the lock, either. +Well, without noticing what I was doing, I took hold of the knob, and +open comes the door! It warn't locked! I hopped in, and shook off the +towel, and, GREAT CAESAR'S GHOST!" + +"What!--what'd you see, Tom?" + +"Huck, I most stepped onto Injun Joe's hand!" + +"No!" + +"Yes! He was lying there, sound asleep on the floor, with his old +patch on his eye and his arms spread out." + +"Lordy, what did you do? Did he wake up?" + +"No, never budged. Drunk, I reckon. I just grabbed that towel and +started!" + +"I'd never 'a' thought of the towel, I bet!" + +"Well, I would. My aunt would make me mighty sick if I lost it." + +"Say, Tom, did you see that box?" + +"Huck, I didn't wait to look around. I didn't see the box, I didn't +see the cross. I didn't see anything but a bottle and a tin cup on the +floor by Injun Joe; yes, I saw two barrels and lots more bottles in the +room. Don't you see, now, what's the matter with that ha'nted room?" + +"How?" + +"Why, it's ha'nted with whiskey! Maybe ALL the Temperance Taverns have +got a ha'nted room, hey, Huck?" + +"Well, I reckon maybe that's so. Who'd 'a' thought such a thing? But +say, Tom, now's a mighty good time to get that box, if Injun Joe's +drunk." + +"It is, that! You try it!" + +Huck shuddered. + +"Well, no--I reckon not." + +"And I reckon not, Huck. Only one bottle alongside of Injun Joe ain't +enough. If there'd been three, he'd be drunk enough and I'd do it." + +There was a long pause for reflection, and then Tom said: + +"Lookyhere, Huck, less not try that thing any more till we know Injun +Joe's not in there. It's too scary. Now, if we watch every night, we'll +be dead sure to see him go out, some time or other, and then we'll +snatch that box quicker'n lightning." + +"Well, I'm agreed. I'll watch the whole night long, and I'll do it +every night, too, if you'll do the other part of the job." + +"All right, I will. All you got to do is to trot up Hooper Street a +block and maow--and if I'm asleep, you throw some gravel at the window +and that'll fetch me." + +"Agreed, and good as wheat!" + +"Now, Huck, the storm's over, and I'll go home. It'll begin to be +daylight in a couple of hours. You go back and watch that long, will +you?" + +"I said I would, Tom, and I will. I'll ha'nt that tavern every night +for a year! I'll sleep all day and I'll stand watch all night." + +"That's all right. Now, where you going to sleep?" + +"In Ben Rogers' hayloft. He lets me, and so does his pap's nigger man, +Uncle Jake. I tote water for Uncle Jake whenever he wants me to, and +any time I ask him he gives me a little something to eat if he can +spare it. That's a mighty good nigger, Tom. He likes me, becuz I don't +ever act as if I was above him. Sometime I've set right down and eat +WITH him. But you needn't tell that. A body's got to do things when +he's awful hungry he wouldn't want to do as a steady thing." + +"Well, if I don't want you in the daytime, I'll let you sleep. I won't +come bothering around. Any time you see something's up, in the night, +just skip right around and maow." + + + +CHAPTER XXIX + +THE first thing Tom heard on Friday morning was a glad piece of news +--Judge Thatcher's family had come back to town the night before. Both +Injun Joe and the treasure sunk into secondary importance for a moment, +and Becky took the chief place in the boy's interest. He saw her and +they had an exhausting good time playing "hi-spy" and "gully-keeper" +with a crowd of their school-mates. The day was completed and crowned +in a peculiarly satisfactory way: Becky teased her mother to appoint +the next day for the long-promised and long-delayed picnic, and she +consented. The child's delight was boundless; and Tom's not more +moderate. The invitations were sent out before sunset, and straightway +the young folks of the village were thrown into a fever of preparation +and pleasurable anticipation. Tom's excitement enabled him to keep +awake until a pretty late hour, and he had good hopes of hearing Huck's +"maow," and of having his treasure to astonish Becky and the picnickers +with, next day; but he was disappointed. No signal came that night. + +Morning came, eventually, and by ten or eleven o'clock a giddy and +rollicking company were gathered at Judge Thatcher's, and everything +was ready for a start. It was not the custom for elderly people to mar +the picnics with their presence. The children were considered safe +enough under the wings of a few young ladies of eighteen and a few +young gentlemen of twenty-three or thereabouts. The old steam ferryboat +was chartered for the occasion; presently the gay throng filed up the +main street laden with provision-baskets. Sid was sick and had to miss +the fun; Mary remained at home to entertain him. The last thing Mrs. +Thatcher said to Becky, was: + +"You'll not get back till late. Perhaps you'd better stay all night +with some of the girls that live near the ferry-landing, child." + +"Then I'll stay with Susy Harper, mamma." + +"Very well. And mind and behave yourself and don't be any trouble." + +Presently, as they tripped along, Tom said to Becky: + +"Say--I'll tell you what we'll do. 'Stead of going to Joe Harper's +we'll climb right up the hill and stop at the Widow Douglas'. She'll +have ice-cream! She has it most every day--dead loads of it. And she'll +be awful glad to have us." + +"Oh, that will be fun!" + +Then Becky reflected a moment and said: + +"But what will mamma say?" + +"How'll she ever know?" + +The girl turned the idea over in her mind, and said reluctantly: + +"I reckon it's wrong--but--" + +"But shucks! Your mother won't know, and so what's the harm? All she +wants is that you'll be safe; and I bet you she'd 'a' said go there if +she'd 'a' thought of it. I know she would!" + +The Widow Douglas' splendid hospitality was a tempting bait. It and +Tom's persuasions presently carried the day. So it was decided to say +nothing anybody about the night's programme. Presently it occurred to +Tom that maybe Huck might come this very night and give the signal. The +thought took a deal of the spirit out of his anticipations. Still he +could not bear to give up the fun at Widow Douglas'. And why should he +give it up, he reasoned--the signal did not come the night before, so +why should it be any more likely to come to-night? The sure fun of the +evening outweighed the uncertain treasure; and, boy-like, he determined +to yield to the stronger inclination and not allow himself to think of +the box of money another time that day. + +Three miles below town the ferryboat stopped at the mouth of a woody +hollow and tied up. The crowd swarmed ashore and soon the forest +distances and craggy heights echoed far and near with shoutings and +laughter. All the different ways of getting hot and tired were gone +through with, and by-and-by the rovers straggled back to camp fortified +with responsible appetites, and then the destruction of the good things +began. After the feast there was a refreshing season of rest and chat +in the shade of spreading oaks. By-and-by somebody shouted: + +"Who's ready for the cave?" + +Everybody was. Bundles of candles were procured, and straightway there +was a general scamper up the hill. The mouth of the cave was up the +hillside--an opening shaped like a letter A. Its massive oaken door +stood unbarred. Within was a small chamber, chilly as an ice-house, and +walled by Nature with solid limestone that was dewy with a cold sweat. +It was romantic and mysterious to stand here in the deep gloom and look +out upon the green valley shining in the sun. But the impressiveness of +the situation quickly wore off, and the romping began again. The moment +a candle was lighted there was a general rush upon the owner of it; a +struggle and a gallant defence followed, but the candle was soon +knocked down or blown out, and then there was a glad clamor of laughter +and a new chase. But all things have an end. By-and-by the procession +went filing down the steep descent of the main avenue, the flickering +rank of lights dimly revealing the lofty walls of rock almost to their +point of junction sixty feet overhead. This main avenue was not more +than eight or ten feet wide. Every few steps other lofty and still +narrower crevices branched from it on either hand--for McDougal's cave +was but a vast labyrinth of crooked aisles that ran into each other and +out again and led nowhere. It was said that one might wander days and +nights together through its intricate tangle of rifts and chasms, and +never find the end of the cave; and that he might go down, and down, +and still down, into the earth, and it was just the same--labyrinth +under labyrinth, and no end to any of them. No man "knew" the cave. +That was an impossible thing. Most of the young men knew a portion of +it, and it was not customary to venture much beyond this known portion. +Tom Sawyer knew as much of the cave as any one. + +The procession moved along the main avenue some three-quarters of a +mile, and then groups and couples began to slip aside into branch +avenues, fly along the dismal corridors, and take each other by +surprise at points where the corridors joined again. Parties were able +to elude each other for the space of half an hour without going beyond +the "known" ground. + +By-and-by, one group after another came straggling back to the mouth +of the cave, panting, hilarious, smeared from head to foot with tallow +drippings, daubed with clay, and entirely delighted with the success of +the day. Then they were astonished to find that they had been taking no +note of time and that night was about at hand. The clanging bell had +been calling for half an hour. However, this sort of close to the day's +adventures was romantic and therefore satisfactory. When the ferryboat +with her wild freight pushed into the stream, nobody cared sixpence for +the wasted time but the captain of the craft. + +Huck was already upon his watch when the ferryboat's lights went +glinting past the wharf. He heard no noise on board, for the young +people were as subdued and still as people usually are who are nearly +tired to death. He wondered what boat it was, and why she did not stop +at the wharf--and then he dropped her out of his mind and put his +attention upon his business. The night was growing cloudy and dark. Ten +o'clock came, and the noise of vehicles ceased, scattered lights began +to wink out, all straggling foot-passengers disappeared, the village +betook itself to its slumbers and left the small watcher alone with the +silence and the ghosts. Eleven o'clock came, and the tavern lights were +put out; darkness everywhere, now. Huck waited what seemed a weary long +time, but nothing happened. His faith was weakening. Was there any use? +Was there really any use? Why not give it up and turn in? + +A noise fell upon his ear. He was all attention in an instant. The +alley door closed softly. He sprang to the corner of the brick store. +The next moment two men brushed by him, and one seemed to have +something under his arm. It must be that box! So they were going to +remove the treasure. Why call Tom now? It would be absurd--the men +would get away with the box and never be found again. No, he would +stick to their wake and follow them; he would trust to the darkness for +security from discovery. So communing with himself, Huck stepped out +and glided along behind the men, cat-like, with bare feet, allowing +them to keep just far enough ahead not to be invisible. + +They moved up the river street three blocks, then turned to the left +up a cross-street. They went straight ahead, then, until they came to +the path that led up Cardiff Hill; this they took. They passed by the +old Welshman's house, half-way up the hill, without hesitating, and +still climbed upward. Good, thought Huck, they will bury it in the old +quarry. But they never stopped at the quarry. They passed on, up the +summit. They plunged into the narrow path between the tall sumach +bushes, and were at once hidden in the gloom. Huck closed up and +shortened his distance, now, for they would never be able to see him. +He trotted along awhile; then slackened his pace, fearing he was +gaining too fast; moved on a piece, then stopped altogether; listened; +no sound; none, save that he seemed to hear the beating of his own +heart. The hooting of an owl came over the hill--ominous sound! But no +footsteps. Heavens, was everything lost! He was about to spring with +winged feet, when a man cleared his throat not four feet from him! +Huck's heart shot into his throat, but he swallowed it again; and then +he stood there shaking as if a dozen agues had taken charge of him at +once, and so weak that he thought he must surely fall to the ground. He +knew where he was. He knew he was within five steps of the stile +leading into Widow Douglas' grounds. Very well, he thought, let them +bury it there; it won't be hard to find. + +Now there was a voice--a very low voice--Injun Joe's: + +"Damn her, maybe she's got company--there's lights, late as it is." + +"I can't see any." + +This was that stranger's voice--the stranger of the haunted house. A +deadly chill went to Huck's heart--this, then, was the "revenge" job! +His thought was, to fly. Then he remembered that the Widow Douglas had +been kind to him more than once, and maybe these men were going to +murder her. He wished he dared venture to warn her; but he knew he +didn't dare--they might come and catch him. He thought all this and +more in the moment that elapsed between the stranger's remark and Injun +Joe's next--which was-- + +"Because the bush is in your way. Now--this way--now you see, don't +you?" + +"Yes. Well, there IS company there, I reckon. Better give it up." + +"Give it up, and I just leaving this country forever! Give it up and +maybe never have another chance. I tell you again, as I've told you +before, I don't care for her swag--you may have it. But her husband was +rough on me--many times he was rough on me--and mainly he was the +justice of the peace that jugged me for a vagrant. And that ain't all. +It ain't a millionth part of it! He had me HORSEWHIPPED!--horsewhipped +in front of the jail, like a nigger!--with all the town looking on! +HORSEWHIPPED!--do you understand? He took advantage of me and died. But +I'll take it out of HER." + +"Oh, don't kill her! Don't do that!" + +"Kill? Who said anything about killing? I would kill HIM if he was +here; but not her. When you want to get revenge on a woman you don't +kill her--bosh! you go for her looks. You slit her nostrils--you notch +her ears like a sow!" + +"By God, that's--" + +"Keep your opinion to yourself! It will be safest for you. I'll tie +her to the bed. If she bleeds to death, is that my fault? I'll not cry, +if she does. My friend, you'll help me in this thing--for MY sake +--that's why you're here--I mightn't be able alone. If you flinch, I'll +kill you. Do you understand that? And if I have to kill you, I'll kill +her--and then I reckon nobody'll ever know much about who done this +business." + +"Well, if it's got to be done, let's get at it. The quicker the +better--I'm all in a shiver." + +"Do it NOW? And company there? Look here--I'll get suspicious of you, +first thing you know. No--we'll wait till the lights are out--there's +no hurry." + +Huck felt that a silence was going to ensue--a thing still more awful +than any amount of murderous talk; so he held his breath and stepped +gingerly back; planted his foot carefully and firmly, after balancing, +one-legged, in a precarious way and almost toppling over, first on one +side and then on the other. He took another step back, with the same +elaboration and the same risks; then another and another, and--a twig +snapped under his foot! His breath stopped and he listened. There was +no sound--the stillness was perfect. His gratitude was measureless. Now +he turned in his tracks, between the walls of sumach bushes--turned +himself as carefully as if he were a ship--and then stepped quickly but +cautiously along. When he emerged at the quarry he felt secure, and so +he picked up his nimble heels and flew. Down, down he sped, till he +reached the Welshman's. He banged at the door, and presently the heads +of the old man and his two stalwart sons were thrust from windows. + +"What's the row there? Who's banging? What do you want?" + +"Let me in--quick! I'll tell everything." + +"Why, who are you?" + +"Huckleberry Finn--quick, let me in!" + +"Huckleberry Finn, indeed! It ain't a name to open many doors, I +judge! But let him in, lads, and let's see what's the trouble." + +"Please don't ever tell I told you," were Huck's first words when he +got in. "Please don't--I'd be killed, sure--but the widow's been good +friends to me sometimes, and I want to tell--I WILL tell if you'll +promise you won't ever say it was me." + +"By George, he HAS got something to tell, or he wouldn't act so!" +exclaimed the old man; "out with it and nobody here'll ever tell, lad." + +Three minutes later the old man and his sons, well armed, were up the +hill, and just entering the sumach path on tiptoe, their weapons in +their hands. Huck accompanied them no further. He hid behind a great +bowlder and fell to listening. There was a lagging, anxious silence, +and then all of a sudden there was an explosion of firearms and a cry. + +Huck waited for no particulars. He sprang away and sped down the hill +as fast as his legs could carry him. + + + +CHAPTER XXX + +AS the earliest suspicion of dawn appeared on Sunday morning, Huck +came groping up the hill and rapped gently at the old Welshman's door. +The inmates were asleep, but it was a sleep that was set on a +hair-trigger, on account of the exciting episode of the night. A call +came from a window: + +"Who's there!" + +Huck's scared voice answered in a low tone: + +"Please let me in! It's only Huck Finn!" + +"It's a name that can open this door night or day, lad!--and welcome!" + +These were strange words to the vagabond boy's ears, and the +pleasantest he had ever heard. He could not recollect that the closing +word had ever been applied in his case before. The door was quickly +unlocked, and he entered. Huck was given a seat and the old man and his +brace of tall sons speedily dressed themselves. + +"Now, my boy, I hope you're good and hungry, because breakfast will be +ready as soon as the sun's up, and we'll have a piping hot one, too +--make yourself easy about that! I and the boys hoped you'd turn up and +stop here last night." + +"I was awful scared," said Huck, "and I run. I took out when the +pistols went off, and I didn't stop for three mile. I've come now becuz +I wanted to know about it, you know; and I come before daylight becuz I +didn't want to run across them devils, even if they was dead." + +"Well, poor chap, you do look as if you'd had a hard night of it--but +there's a bed here for you when you've had your breakfast. No, they +ain't dead, lad--we are sorry enough for that. You see we knew right +where to put our hands on them, by your description; so we crept along +on tiptoe till we got within fifteen feet of them--dark as a cellar +that sumach path was--and just then I found I was going to sneeze. It +was the meanest kind of luck! I tried to keep it back, but no use +--'twas bound to come, and it did come! I was in the lead with my pistol +raised, and when the sneeze started those scoundrels a-rustling to get +out of the path, I sung out, 'Fire boys!' and blazed away at the place +where the rustling was. So did the boys. But they were off in a jiffy, +those villains, and we after them, down through the woods. I judge we +never touched them. They fired a shot apiece as they started, but their +bullets whizzed by and didn't do us any harm. As soon as we lost the +sound of their feet we quit chasing, and went down and stirred up the +constables. They got a posse together, and went off to guard the river +bank, and as soon as it is light the sheriff and a gang are going to +beat up the woods. My boys will be with them presently. I wish we had +some sort of description of those rascals--'twould help a good deal. +But you couldn't see what they were like, in the dark, lad, I suppose?" + +"Oh yes; I saw them down-town and follered them." + +"Splendid! Describe them--describe them, my boy!" + +"One's the old deaf and dumb Spaniard that's ben around here once or +twice, and t'other's a mean-looking, ragged--" + +"That's enough, lad, we know the men! Happened on them in the woods +back of the widow's one day, and they slunk away. Off with you, boys, +and tell the sheriff--get your breakfast to-morrow morning!" + +The Welshman's sons departed at once. As they were leaving the room +Huck sprang up and exclaimed: + +"Oh, please don't tell ANYbody it was me that blowed on them! Oh, +please!" + +"All right if you say it, Huck, but you ought to have the credit of +what you did." + +"Oh no, no! Please don't tell!" + +When the young men were gone, the old Welshman said: + +"They won't tell--and I won't. But why don't you want it known?" + +Huck would not explain, further than to say that he already knew too +much about one of those men and would not have the man know that he +knew anything against him for the whole world--he would be killed for +knowing it, sure. + +The old man promised secrecy once more, and said: + +"How did you come to follow these fellows, lad? Were they looking +suspicious?" + +Huck was silent while he framed a duly cautious reply. Then he said: + +"Well, you see, I'm a kind of a hard lot,--least everybody says so, +and I don't see nothing agin it--and sometimes I can't sleep much, on +account of thinking about it and sort of trying to strike out a new way +of doing. That was the way of it last night. I couldn't sleep, and so I +come along up-street 'bout midnight, a-turning it all over, and when I +got to that old shackly brick store by the Temperance Tavern, I backed +up agin the wall to have another think. Well, just then along comes +these two chaps slipping along close by me, with something under their +arm, and I reckoned they'd stole it. One was a-smoking, and t'other one +wanted a light; so they stopped right before me and the cigars lit up +their faces and I see that the big one was the deaf and dumb Spaniard, +by his white whiskers and the patch on his eye, and t'other one was a +rusty, ragged-looking devil." + +"Could you see the rags by the light of the cigars?" + +This staggered Huck for a moment. Then he said: + +"Well, I don't know--but somehow it seems as if I did." + +"Then they went on, and you--" + +"Follered 'em--yes. That was it. I wanted to see what was up--they +sneaked along so. I dogged 'em to the widder's stile, and stood in the +dark and heard the ragged one beg for the widder, and the Spaniard +swear he'd spile her looks just as I told you and your two--" + +"What! The DEAF AND DUMB man said all that!" + +Huck had made another terrible mistake! He was trying his best to keep +the old man from getting the faintest hint of who the Spaniard might +be, and yet his tongue seemed determined to get him into trouble in +spite of all he could do. He made several efforts to creep out of his +scrape, but the old man's eye was upon him and he made blunder after +blunder. Presently the Welshman said: + +"My boy, don't be afraid of me. I wouldn't hurt a hair of your head +for all the world. No--I'd protect you--I'd protect you. This Spaniard +is not deaf and dumb; you've let that slip without intending it; you +can't cover that up now. You know something about that Spaniard that +you want to keep dark. Now trust me--tell me what it is, and trust me +--I won't betray you." + +Huck looked into the old man's honest eyes a moment, then bent over +and whispered in his ear: + +"'Tain't a Spaniard--it's Injun Joe!" + +The Welshman almost jumped out of his chair. In a moment he said: + +"It's all plain enough, now. When you talked about notching ears and +slitting noses I judged that that was your own embellishment, because +white men don't take that sort of revenge. But an Injun! That's a +different matter altogether." + +During breakfast the talk went on, and in the course of it the old man +said that the last thing which he and his sons had done, before going +to bed, was to get a lantern and examine the stile and its vicinity for +marks of blood. They found none, but captured a bulky bundle of-- + +"Of WHAT?" + +If the words had been lightning they could not have leaped with a more +stunning suddenness from Huck's blanched lips. His eyes were staring +wide, now, and his breath suspended--waiting for the answer. The +Welshman started--stared in return--three seconds--five seconds--ten +--then replied: + +"Of burglar's tools. Why, what's the MATTER with you?" + +Huck sank back, panting gently, but deeply, unutterably grateful. The +Welshman eyed him gravely, curiously--and presently said: + +"Yes, burglar's tools. That appears to relieve you a good deal. But +what did give you that turn? What were YOU expecting we'd found?" + +Huck was in a close place--the inquiring eye was upon him--he would +have given anything for material for a plausible answer--nothing +suggested itself--the inquiring eye was boring deeper and deeper--a +senseless reply offered--there was no time to weigh it, so at a venture +he uttered it--feebly: + +"Sunday-school books, maybe." + +Poor Huck was too distressed to smile, but the old man laughed loud +and joyously, shook up the details of his anatomy from head to foot, +and ended by saying that such a laugh was money in a-man's pocket, +because it cut down the doctor's bill like everything. Then he added: + +"Poor old chap, you're white and jaded--you ain't well a bit--no +wonder you're a little flighty and off your balance. But you'll come +out of it. Rest and sleep will fetch you out all right, I hope." + +Huck was irritated to think he had been such a goose and betrayed such +a suspicious excitement, for he had dropped the idea that the parcel +brought from the tavern was the treasure, as soon as he had heard the +talk at the widow's stile. He had only thought it was not the treasure, +however--he had not known that it wasn't--and so the suggestion of a +captured bundle was too much for his self-possession. But on the whole +he felt glad the little episode had happened, for now he knew beyond +all question that that bundle was not THE bundle, and so his mind was +at rest and exceedingly comfortable. In fact, everything seemed to be +drifting just in the right direction, now; the treasure must be still +in No. 2, the men would be captured and jailed that day, and he and Tom +could seize the gold that night without any trouble or any fear of +interruption. + +Just as breakfast was completed there was a knock at the door. Huck +jumped for a hiding-place, for he had no mind to be connected even +remotely with the late event. The Welshman admitted several ladies and +gentlemen, among them the Widow Douglas, and noticed that groups of +citizens were climbing up the hill--to stare at the stile. So the news +had spread. The Welshman had to tell the story of the night to the +visitors. The widow's gratitude for her preservation was outspoken. + +"Don't say a word about it, madam. There's another that you're more +beholden to than you are to me and my boys, maybe, but he don't allow +me to tell his name. We wouldn't have been there but for him." + +Of course this excited a curiosity so vast that it almost belittled +the main matter--but the Welshman allowed it to eat into the vitals of +his visitors, and through them be transmitted to the whole town, for he +refused to part with his secret. When all else had been learned, the +widow said: + +"I went to sleep reading in bed and slept straight through all that +noise. Why didn't you come and wake me?" + +"We judged it warn't worth while. Those fellows warn't likely to come +again--they hadn't any tools left to work with, and what was the use of +waking you up and scaring you to death? My three negro men stood guard +at your house all the rest of the night. They've just come back." + +More visitors came, and the story had to be told and retold for a +couple of hours more. + +There was no Sabbath-school during day-school vacation, but everybody +was early at church. The stirring event was well canvassed. News came +that not a sign of the two villains had been yet discovered. When the +sermon was finished, Judge Thatcher's wife dropped alongside of Mrs. +Harper as she moved down the aisle with the crowd and said: + +"Is my Becky going to sleep all day? I just expected she would be +tired to death." + +"Your Becky?" + +"Yes," with a startled look--"didn't she stay with you last night?" + +"Why, no." + +Mrs. Thatcher turned pale, and sank into a pew, just as Aunt Polly, +talking briskly with a friend, passed by. Aunt Polly said: + +"Good-morning, Mrs. Thatcher. Good-morning, Mrs. Harper. I've got a +boy that's turned up missing. I reckon my Tom stayed at your house last +night--one of you. And now he's afraid to come to church. I've got to +settle with him." + +Mrs. Thatcher shook her head feebly and turned paler than ever. + +"He didn't stay with us," said Mrs. Harper, beginning to look uneasy. +A marked anxiety came into Aunt Polly's face. + +"Joe Harper, have you seen my Tom this morning?" + +"No'm." + +"When did you see him last?" + +Joe tried to remember, but was not sure he could say. The people had +stopped moving out of church. Whispers passed along, and a boding +uneasiness took possession of every countenance. Children were +anxiously questioned, and young teachers. They all said they had not +noticed whether Tom and Becky were on board the ferryboat on the +homeward trip; it was dark; no one thought of inquiring if any one was +missing. One young man finally blurted out his fear that they were +still in the cave! Mrs. Thatcher swooned away. Aunt Polly fell to +crying and wringing her hands. + +The alarm swept from lip to lip, from group to group, from street to +street, and within five minutes the bells were wildly clanging and the +whole town was up! The Cardiff Hill episode sank into instant +insignificance, the burglars were forgotten, horses were saddled, +skiffs were manned, the ferryboat ordered out, and before the horror +was half an hour old, two hundred men were pouring down highroad and +river toward the cave. + +All the long afternoon the village seemed empty and dead. Many women +visited Aunt Polly and Mrs. Thatcher and tried to comfort them. They +cried with them, too, and that was still better than words. All the +tedious night the town waited for news; but when the morning dawned at +last, all the word that came was, "Send more candles--and send food." +Mrs. Thatcher was almost crazed; and Aunt Polly, also. Judge Thatcher +sent messages of hope and encouragement from the cave, but they +conveyed no real cheer. + +The old Welshman came home toward daylight, spattered with +candle-grease, smeared with clay, and almost worn out. He found Huck +still in the bed that had been provided for him, and delirious with +fever. The physicians were all at the cave, so the Widow Douglas came +and took charge of the patient. She said she would do her best by him, +because, whether he was good, bad, or indifferent, he was the Lord's, +and nothing that was the Lord's was a thing to be neglected. The +Welshman said Huck had good spots in him, and the widow said: + +"You can depend on it. That's the Lord's mark. He don't leave it off. +He never does. Puts it somewhere on every creature that comes from his +hands." + +Early in the forenoon parties of jaded men began to straggle into the +village, but the strongest of the citizens continued searching. All the +news that could be gained was that remotenesses of the cavern were +being ransacked that had never been visited before; that every corner +and crevice was going to be thoroughly searched; that wherever one +wandered through the maze of passages, lights were to be seen flitting +hither and thither in the distance, and shoutings and pistol-shots sent +their hollow reverberations to the ear down the sombre aisles. In one +place, far from the section usually traversed by tourists, the names +"BECKY & TOM" had been found traced upon the rocky wall with +candle-smoke, and near at hand a grease-soiled bit of ribbon. Mrs. +Thatcher recognized the ribbon and cried over it. She said it was the +last relic she should ever have of her child; and that no other memorial +of her could ever be so precious, because this one parted latest from +the living body before the awful death came. Some said that now and +then, in the cave, a far-away speck of light would glimmer, and then a +glorious shout would burst forth and a score of men go trooping down the +echoing aisle--and then a sickening disappointment always followed; the +children were not there; it was only a searcher's light. + +Three dreadful days and nights dragged their tedious hours along, and +the village sank into a hopeless stupor. No one had heart for anything. +The accidental discovery, just made, that the proprietor of the +Temperance Tavern kept liquor on his premises, scarcely fluttered the +public pulse, tremendous as the fact was. In a lucid interval, Huck +feebly led up to the subject of taverns, and finally asked--dimly +dreading the worst--if anything had been discovered at the Temperance +Tavern since he had been ill. + +"Yes," said the widow. + +Huck started up in bed, wild-eyed: + +"What? What was it?" + +"Liquor!--and the place has been shut up. Lie down, child--what a turn +you did give me!" + +"Only tell me just one thing--only just one--please! Was it Tom Sawyer +that found it?" + +The widow burst into tears. "Hush, hush, child, hush! I've told you +before, you must NOT talk. You are very, very sick!" + +Then nothing but liquor had been found; there would have been a great +powwow if it had been the gold. So the treasure was gone forever--gone +forever! But what could she be crying about? Curious that she should +cry. + +These thoughts worked their dim way through Huck's mind, and under the +weariness they gave him he fell asleep. The widow said to herself: + +"There--he's asleep, poor wreck. Tom Sawyer find it! Pity but somebody +could find Tom Sawyer! Ah, there ain't many left, now, that's got hope +enough, or strength enough, either, to go on searching." + + + +CHAPTER XXXI + +NOW to return to Tom and Becky's share in the picnic. They tripped +along the murky aisles with the rest of the company, visiting the +familiar wonders of the cave--wonders dubbed with rather +over-descriptive names, such as "The Drawing-Room," "The Cathedral," +"Aladdin's Palace," and so on. Presently the hide-and-seek frolicking +began, and Tom and Becky engaged in it with zeal until the exertion +began to grow a trifle wearisome; then they wandered down a sinuous +avenue holding their candles aloft and reading the tangled web-work of +names, dates, post-office addresses, and mottoes with which the rocky +walls had been frescoed (in candle-smoke). Still drifting along and +talking, they scarcely noticed that they were now in a part of the cave +whose walls were not frescoed. They smoked their own names under an +overhanging shelf and moved on. Presently they came to a place where a +little stream of water, trickling over a ledge and carrying a limestone +sediment with it, had, in the slow-dragging ages, formed a laced and +ruffled Niagara in gleaming and imperishable stone. Tom squeezed his +small body behind it in order to illuminate it for Becky's +gratification. He found that it curtained a sort of steep natural +stairway which was enclosed between narrow walls, and at once the +ambition to be a discoverer seized him. Becky responded to his call, +and they made a smoke-mark for future guidance, and started upon their +quest. They wound this way and that, far down into the secret depths of +the cave, made another mark, and branched off in search of novelties to +tell the upper world about. In one place they found a spacious cavern, +from whose ceiling depended a multitude of shining stalactites of the +length and circumference of a man's leg; they walked all about it, +wondering and admiring, and presently left it by one of the numerous +passages that opened into it. This shortly brought them to a bewitching +spring, whose basin was incrusted with a frostwork of glittering +crystals; it was in the midst of a cavern whose walls were supported by +many fantastic pillars which had been formed by the joining of great +stalactites and stalagmites together, the result of the ceaseless +water-drip of centuries. Under the roof vast knots of bats had packed +themselves together, thousands in a bunch; the lights disturbed the +creatures and they came flocking down by hundreds, squeaking and +darting furiously at the candles. Tom knew their ways and the danger of +this sort of conduct. He seized Becky's hand and hurried her into the +first corridor that offered; and none too soon, for a bat struck +Becky's light out with its wing while she was passing out of the +cavern. The bats chased the children a good distance; but the fugitives +plunged into every new passage that offered, and at last got rid of the +perilous things. Tom found a subterranean lake, shortly, which +stretched its dim length away until its shape was lost in the shadows. +He wanted to explore its borders, but concluded that it would be best +to sit down and rest awhile, first. Now, for the first time, the deep +stillness of the place laid a clammy hand upon the spirits of the +children. Becky said: + +"Why, I didn't notice, but it seems ever so long since I heard any of +the others." + +"Come to think, Becky, we are away down below them--and I don't know +how far away north, or south, or east, or whichever it is. We couldn't +hear them here." + +Becky grew apprehensive. + +"I wonder how long we've been down here, Tom? We better start back." + +"Yes, I reckon we better. P'raps we better." + +"Can you find the way, Tom? It's all a mixed-up crookedness to me." + +"I reckon I could find it--but then the bats. If they put our candles +out it will be an awful fix. Let's try some other way, so as not to go +through there." + +"Well. But I hope we won't get lost. It would be so awful!" and the +girl shuddered at the thought of the dreadful possibilities. + +They started through a corridor, and traversed it in silence a long +way, glancing at each new opening, to see if there was anything +familiar about the look of it; but they were all strange. Every time +Tom made an examination, Becky would watch his face for an encouraging +sign, and he would say cheerily: + +"Oh, it's all right. This ain't the one, but we'll come to it right +away!" + +But he felt less and less hopeful with each failure, and presently +began to turn off into diverging avenues at sheer random, in desperate +hope of finding the one that was wanted. He still said it was "all +right," but there was such a leaden dread at his heart that the words +had lost their ring and sounded just as if he had said, "All is lost!" +Becky clung to his side in an anguish of fear, and tried hard to keep +back the tears, but they would come. At last she said: + +"Oh, Tom, never mind the bats, let's go back that way! We seem to get +worse and worse off all the time." + +"Listen!" said he. + +Profound silence; silence so deep that even their breathings were +conspicuous in the hush. Tom shouted. The call went echoing down the +empty aisles and died out in the distance in a faint sound that +resembled a ripple of mocking laughter. + +"Oh, don't do it again, Tom, it is too horrid," said Becky. + +"It is horrid, but I better, Becky; they might hear us, you know," and +he shouted again. + +The "might" was even a chillier horror than the ghostly laughter, it +so confessed a perishing hope. The children stood still and listened; +but there was no result. Tom turned upon the back track at once, and +hurried his steps. It was but a little while before a certain +indecision in his manner revealed another fearful fact to Becky--he +could not find his way back! + +"Oh, Tom, you didn't make any marks!" + +"Becky, I was such a fool! Such a fool! I never thought we might want +to come back! No--I can't find the way. It's all mixed up." + +"Tom, Tom, we're lost! we're lost! We never can get out of this awful +place! Oh, why DID we ever leave the others!" + +She sank to the ground and burst into such a frenzy of crying that Tom +was appalled with the idea that she might die, or lose her reason. He +sat down by her and put his arms around her; she buried her face in his +bosom, she clung to him, she poured out her terrors, her unavailing +regrets, and the far echoes turned them all to jeering laughter. Tom +begged her to pluck up hope again, and she said she could not. He fell +to blaming and abusing himself for getting her into this miserable +situation; this had a better effect. She said she would try to hope +again, she would get up and follow wherever he might lead if only he +would not talk like that any more. For he was no more to blame than +she, she said. + +So they moved on again--aimlessly--simply at random--all they could do +was to move, keep moving. For a little while, hope made a show of +reviving--not with any reason to back it, but only because it is its +nature to revive when the spring has not been taken out of it by age +and familiarity with failure. + +By-and-by Tom took Becky's candle and blew it out. This economy meant +so much! Words were not needed. Becky understood, and her hope died +again. She knew that Tom had a whole candle and three or four pieces in +his pockets--yet he must economize. + +By-and-by, fatigue began to assert its claims; the children tried to +pay attention, for it was dreadful to think of sitting down when time +was grown to be so precious, moving, in some direction, in any +direction, was at least progress and might bear fruit; but to sit down +was to invite death and shorten its pursuit. + +At last Becky's frail limbs refused to carry her farther. She sat +down. Tom rested with her, and they talked of home, and the friends +there, and the comfortable beds and, above all, the light! Becky cried, +and Tom tried to think of some way of comforting her, but all his +encouragements were grown threadbare with use, and sounded like +sarcasms. Fatigue bore so heavily upon Becky that she drowsed off to +sleep. Tom was grateful. He sat looking into her drawn face and saw it +grow smooth and natural under the influence of pleasant dreams; and +by-and-by a smile dawned and rested there. The peaceful face reflected +somewhat of peace and healing into his own spirit, and his thoughts +wandered away to bygone times and dreamy memories. While he was deep in +his musings, Becky woke up with a breezy little laugh--but it was +stricken dead upon her lips, and a groan followed it. + +"Oh, how COULD I sleep! I wish I never, never had waked! No! No, I +don't, Tom! Don't look so! I won't say it again." + +"I'm glad you've slept, Becky; you'll feel rested, now, and we'll find +the way out." + +"We can try, Tom; but I've seen such a beautiful country in my dream. +I reckon we are going there." + +"Maybe not, maybe not. Cheer up, Becky, and let's go on trying." + +They rose up and wandered along, hand in hand and hopeless. They tried +to estimate how long they had been in the cave, but all they knew was +that it seemed days and weeks, and yet it was plain that this could not +be, for their candles were not gone yet. A long time after this--they +could not tell how long--Tom said they must go softly and listen for +dripping water--they must find a spring. They found one presently, and +Tom said it was time to rest again. Both were cruelly tired, yet Becky +said she thought she could go a little farther. She was surprised to +hear Tom dissent. She could not understand it. They sat down, and Tom +fastened his candle to the wall in front of them with some clay. +Thought was soon busy; nothing was said for some time. Then Becky broke +the silence: + +"Tom, I am so hungry!" + +Tom took something out of his pocket. + +"Do you remember this?" said he. + +Becky almost smiled. + +"It's our wedding-cake, Tom." + +"Yes--I wish it was as big as a barrel, for it's all we've got." + +"I saved it from the picnic for us to dream on, Tom, the way grown-up +people do with wedding-cake--but it'll be our--" + +She dropped the sentence where it was. Tom divided the cake and Becky +ate with good appetite, while Tom nibbled at his moiety. There was +abundance of cold water to finish the feast with. By-and-by Becky +suggested that they move on again. Tom was silent a moment. Then he +said: + +"Becky, can you bear it if I tell you something?" + +Becky's face paled, but she thought she could. + +"Well, then, Becky, we must stay here, where there's water to drink. +That little piece is our last candle!" + +Becky gave loose to tears and wailings. Tom did what he could to +comfort her, but with little effect. At length Becky said: + +"Tom!" + +"Well, Becky?" + +"They'll miss us and hunt for us!" + +"Yes, they will! Certainly they will!" + +"Maybe they're hunting for us now, Tom." + +"Why, I reckon maybe they are. I hope they are." + +"When would they miss us, Tom?" + +"When they get back to the boat, I reckon." + +"Tom, it might be dark then--would they notice we hadn't come?" + +"I don't know. But anyway, your mother would miss you as soon as they +got home." + +A frightened look in Becky's face brought Tom to his senses and he saw +that he had made a blunder. Becky was not to have gone home that night! +The children became silent and thoughtful. In a moment a new burst of +grief from Becky showed Tom that the thing in his mind had struck hers +also--that the Sabbath morning might be half spent before Mrs. Thatcher +discovered that Becky was not at Mrs. Harper's. + +The children fastened their eyes upon their bit of candle and watched +it melt slowly and pitilessly away; saw the half inch of wick stand +alone at last; saw the feeble flame rise and fall, climb the thin +column of smoke, linger at its top a moment, and then--the horror of +utter darkness reigned! + +How long afterward it was that Becky came to a slow consciousness that +she was crying in Tom's arms, neither could tell. All that they knew +was, that after what seemed a mighty stretch of time, both awoke out of +a dead stupor of sleep and resumed their miseries once more. Tom said +it might be Sunday, now--maybe Monday. He tried to get Becky to talk, +but her sorrows were too oppressive, all her hopes were gone. Tom said +that they must have been missed long ago, and no doubt the search was +going on. He would shout and maybe some one would come. He tried it; +but in the darkness the distant echoes sounded so hideously that he +tried it no more. + +The hours wasted away, and hunger came to torment the captives again. +A portion of Tom's half of the cake was left; they divided and ate it. +But they seemed hungrier than before. The poor morsel of food only +whetted desire. + +By-and-by Tom said: + +"SH! Did you hear that?" + +Both held their breath and listened. There was a sound like the +faintest, far-off shout. Instantly Tom answered it, and leading Becky +by the hand, started groping down the corridor in its direction. +Presently he listened again; again the sound was heard, and apparently +a little nearer. + +"It's them!" said Tom; "they're coming! Come along, Becky--we're all +right now!" + +The joy of the prisoners was almost overwhelming. Their speed was +slow, however, because pitfalls were somewhat common, and had to be +guarded against. They shortly came to one and had to stop. It might be +three feet deep, it might be a hundred--there was no passing it at any +rate. Tom got down on his breast and reached as far down as he could. +No bottom. They must stay there and wait until the searchers came. They +listened; evidently the distant shoutings were growing more distant! a +moment or two more and they had gone altogether. The heart-sinking +misery of it! Tom whooped until he was hoarse, but it was of no use. He +talked hopefully to Becky; but an age of anxious waiting passed and no +sounds came again. + +The children groped their way back to the spring. The weary time +dragged on; they slept again, and awoke famished and woe-stricken. Tom +believed it must be Tuesday by this time. + +Now an idea struck him. There were some side passages near at hand. It +would be better to explore some of these than bear the weight of the +heavy time in idleness. He took a kite-line from his pocket, tied it to +a projection, and he and Becky started, Tom in the lead, unwinding the +line as he groped along. At the end of twenty steps the corridor ended +in a "jumping-off place." Tom got down on his knees and felt below, and +then as far around the corner as he could reach with his hands +conveniently; he made an effort to stretch yet a little farther to the +right, and at that moment, not twenty yards away, a human hand, holding +a candle, appeared from behind a rock! Tom lifted up a glorious shout, +and instantly that hand was followed by the body it belonged to--Injun +Joe's! Tom was paralyzed; he could not move. He was vastly gratified +the next moment, to see the "Spaniard" take to his heels and get +himself out of sight. Tom wondered that Joe had not recognized his +voice and come over and killed him for testifying in court. But the +echoes must have disguised the voice. Without doubt, that was it, he +reasoned. Tom's fright weakened every muscle in his body. He said to +himself that if he had strength enough to get back to the spring he +would stay there, and nothing should tempt him to run the risk of +meeting Injun Joe again. He was careful to keep from Becky what it was +he had seen. He told her he had only shouted "for luck." + +But hunger and wretchedness rise superior to fears in the long run. +Another tedious wait at the spring and another long sleep brought +changes. The children awoke tortured with a raging hunger. Tom believed +that it must be Wednesday or Thursday or even Friday or Saturday, now, +and that the search had been given over. He proposed to explore another +passage. He felt willing to risk Injun Joe and all other terrors. But +Becky was very weak. She had sunk into a dreary apathy and would not be +roused. She said she would wait, now, where she was, and die--it would +not be long. She told Tom to go with the kite-line and explore if he +chose; but she implored him to come back every little while and speak +to her; and she made him promise that when the awful time came, he +would stay by her and hold her hand until all was over. + +Tom kissed her, with a choking sensation in his throat, and made a +show of being confident of finding the searchers or an escape from the +cave; then he took the kite-line in his hand and went groping down one +of the passages on his hands and knees, distressed with hunger and sick +with bodings of coming doom. + + + +CHAPTER XXXII + +TUESDAY afternoon came, and waned to the twilight. The village of St. +Petersburg still mourned. The lost children had not been found. Public +prayers had been offered up for them, and many and many a private +prayer that had the petitioner's whole heart in it; but still no good +news came from the cave. The majority of the searchers had given up the +quest and gone back to their daily avocations, saying that it was plain +the children could never be found. Mrs. Thatcher was very ill, and a +great part of the time delirious. People said it was heartbreaking to +hear her call her child, and raise her head and listen a whole minute +at a time, then lay it wearily down again with a moan. Aunt Polly had +drooped into a settled melancholy, and her gray hair had grown almost +white. The village went to its rest on Tuesday night, sad and forlorn. + +Away in the middle of the night a wild peal burst from the village +bells, and in a moment the streets were swarming with frantic half-clad +people, who shouted, "Turn out! turn out! they're found! they're +found!" Tin pans and horns were added to the din, the population massed +itself and moved toward the river, met the children coming in an open +carriage drawn by shouting citizens, thronged around it, joined its +homeward march, and swept magnificently up the main street roaring +huzzah after huzzah! + +The village was illuminated; nobody went to bed again; it was the +greatest night the little town had ever seen. During the first half-hour +a procession of villagers filed through Judge Thatcher's house, seized +the saved ones and kissed them, squeezed Mrs. Thatcher's hand, tried to +speak but couldn't--and drifted out raining tears all over the place. + +Aunt Polly's happiness was complete, and Mrs. Thatcher's nearly so. It +would be complete, however, as soon as the messenger dispatched with +the great news to the cave should get the word to her husband. Tom lay +upon a sofa with an eager auditory about him and told the history of +the wonderful adventure, putting in many striking additions to adorn it +withal; and closed with a description of how he left Becky and went on +an exploring expedition; how he followed two avenues as far as his +kite-line would reach; how he followed a third to the fullest stretch of +the kite-line, and was about to turn back when he glimpsed a far-off +speck that looked like daylight; dropped the line and groped toward it, +pushed his head and shoulders through a small hole, and saw the broad +Mississippi rolling by! And if it had only happened to be night he would +not have seen that speck of daylight and would not have explored that +passage any more! He told how he went back for Becky and broke the good +news and she told him not to fret her with such stuff, for she was +tired, and knew she was going to die, and wanted to. He described how he +labored with her and convinced her; and how she almost died for joy when +she had groped to where she actually saw the blue speck of daylight; how +he pushed his way out at the hole and then helped her out; how they sat +there and cried for gladness; how some men came along in a skiff and Tom +hailed them and told them their situation and their famished condition; +how the men didn't believe the wild tale at first, "because," said they, +"you are five miles down the river below the valley the cave is in" +--then took them aboard, rowed to a house, gave them supper, made them +rest till two or three hours after dark and then brought them home. + +Before day-dawn, Judge Thatcher and the handful of searchers with him +were tracked out, in the cave, by the twine clews they had strung +behind them, and informed of the great news. + +Three days and nights of toil and hunger in the cave were not to be +shaken off at once, as Tom and Becky soon discovered. They were +bedridden all of Wednesday and Thursday, and seemed to grow more and +more tired and worn, all the time. Tom got about, a little, on +Thursday, was down-town Friday, and nearly as whole as ever Saturday; +but Becky did not leave her room until Sunday, and then she looked as +if she had passed through a wasting illness. + +Tom learned of Huck's sickness and went to see him on Friday, but +could not be admitted to the bedroom; neither could he on Saturday or +Sunday. He was admitted daily after that, but was warned to keep still +about his adventure and introduce no exciting topic. The Widow Douglas +stayed by to see that he obeyed. At home Tom learned of the Cardiff +Hill event; also that the "ragged man's" body had eventually been found +in the river near the ferry-landing; he had been drowned while trying +to escape, perhaps. + +About a fortnight after Tom's rescue from the cave, he started off to +visit Huck, who had grown plenty strong enough, now, to hear exciting +talk, and Tom had some that would interest him, he thought. Judge +Thatcher's house was on Tom's way, and he stopped to see Becky. The +Judge and some friends set Tom to talking, and some one asked him +ironically if he wouldn't like to go to the cave again. Tom said he +thought he wouldn't mind it. The Judge said: + +"Well, there are others just like you, Tom, I've not the least doubt. +But we have taken care of that. Nobody will get lost in that cave any +more." + +"Why?" + +"Because I had its big door sheathed with boiler iron two weeks ago, +and triple-locked--and I've got the keys." + +Tom turned as white as a sheet. + +"What's the matter, boy! Here, run, somebody! Fetch a glass of water!" + +The water was brought and thrown into Tom's face. + +"Ah, now you're all right. What was the matter with you, Tom?" + +"Oh, Judge, Injun Joe's in the cave!" + + + +CHAPTER XXXIII + +WITHIN a few minutes the news had spread, and a dozen skiff-loads of +men were on their way to McDougal's cave, and the ferryboat, well +filled with passengers, soon followed. Tom Sawyer was in the skiff that +bore Judge Thatcher. + +When the cave door was unlocked, a sorrowful sight presented itself in +the dim twilight of the place. Injun Joe lay stretched upon the ground, +dead, with his face close to the crack of the door, as if his longing +eyes had been fixed, to the latest moment, upon the light and the cheer +of the free world outside. Tom was touched, for he knew by his own +experience how this wretch had suffered. His pity was moved, but +nevertheless he felt an abounding sense of relief and security, now, +which revealed to him in a degree which he had not fully appreciated +before how vast a weight of dread had been lying upon him since the day +he lifted his voice against this bloody-minded outcast. + +Injun Joe's bowie-knife lay close by, its blade broken in two. The +great foundation-beam of the door had been chipped and hacked through, +with tedious labor; useless labor, too, it was, for the native rock +formed a sill outside it, and upon that stubborn material the knife had +wrought no effect; the only damage done was to the knife itself. But if +there had been no stony obstruction there the labor would have been +useless still, for if the beam had been wholly cut away Injun Joe could +not have squeezed his body under the door, and he knew it. So he had +only hacked that place in order to be doing something--in order to pass +the weary time--in order to employ his tortured faculties. Ordinarily +one could find half a dozen bits of candle stuck around in the crevices +of this vestibule, left there by tourists; but there were none now. The +prisoner had searched them out and eaten them. He had also contrived to +catch a few bats, and these, also, he had eaten, leaving only their +claws. The poor unfortunate had starved to death. In one place, near at +hand, a stalagmite had been slowly growing up from the ground for ages, +builded by the water-drip from a stalactite overhead. The captive had +broken off the stalagmite, and upon the stump had placed a stone, +wherein he had scooped a shallow hollow to catch the precious drop +that fell once in every three minutes with the dreary regularity of a +clock-tick--a dessertspoonful once in four and twenty hours. That drop +was falling when the Pyramids were new; when Troy fell; when the +foundations of Rome were laid; when Christ was crucified; when the +Conqueror created the British empire; when Columbus sailed; when the +massacre at Lexington was "news." It is falling now; it will still be +falling when all these things shall have sunk down the afternoon of +history, and the twilight of tradition, and been swallowed up in the +thick night of oblivion. Has everything a purpose and a mission? Did +this drop fall patiently during five thousand years to be ready for +this flitting human insect's need? and has it another important object +to accomplish ten thousand years to come? No matter. It is many and +many a year since the hapless half-breed scooped out the stone to catch +the priceless drops, but to this day the tourist stares longest at that +pathetic stone and that slow-dropping water when he comes to see the +wonders of McDougal's cave. Injun Joe's cup stands first in the list of +the cavern's marvels; even "Aladdin's Palace" cannot rival it. + +Injun Joe was buried near the mouth of the cave; and people flocked +there in boats and wagons from the towns and from all the farms and +hamlets for seven miles around; they brought their children, and all +sorts of provisions, and confessed that they had had almost as +satisfactory a time at the funeral as they could have had at the +hanging. + +This funeral stopped the further growth of one thing--the petition to +the governor for Injun Joe's pardon. The petition had been largely +signed; many tearful and eloquent meetings had been held, and a +committee of sappy women been appointed to go in deep mourning and wail +around the governor, and implore him to be a merciful ass and trample +his duty under foot. Injun Joe was believed to have killed five +citizens of the village, but what of that? If he had been Satan himself +there would have been plenty of weaklings ready to scribble their names +to a pardon-petition, and drip a tear on it from their permanently +impaired and leaky water-works. + +The morning after the funeral Tom took Huck to a private place to have +an important talk. Huck had learned all about Tom's adventure from the +Welshman and the Widow Douglas, by this time, but Tom said he reckoned +there was one thing they had not told him; that thing was what he +wanted to talk about now. Huck's face saddened. He said: + +"I know what it is. You got into No. 2 and never found anything but +whiskey. Nobody told me it was you; but I just knowed it must 'a' ben +you, soon as I heard 'bout that whiskey business; and I knowed you +hadn't got the money becuz you'd 'a' got at me some way or other and +told me even if you was mum to everybody else. Tom, something's always +told me we'd never get holt of that swag." + +"Why, Huck, I never told on that tavern-keeper. YOU know his tavern +was all right the Saturday I went to the picnic. Don't you remember you +was to watch there that night?" + +"Oh yes! Why, it seems 'bout a year ago. It was that very night that I +follered Injun Joe to the widder's." + +"YOU followed him?" + +"Yes--but you keep mum. I reckon Injun Joe's left friends behind him, +and I don't want 'em souring on me and doing me mean tricks. If it +hadn't ben for me he'd be down in Texas now, all right." + +Then Huck told his entire adventure in confidence to Tom, who had only +heard of the Welshman's part of it before. + +"Well," said Huck, presently, coming back to the main question, +"whoever nipped the whiskey in No. 2, nipped the money, too, I reckon +--anyways it's a goner for us, Tom." + +"Huck, that money wasn't ever in No. 2!" + +"What!" Huck searched his comrade's face keenly. "Tom, have you got on +the track of that money again?" + +"Huck, it's in the cave!" + +Huck's eyes blazed. + +"Say it again, Tom." + +"The money's in the cave!" + +"Tom--honest injun, now--is it fun, or earnest?" + +"Earnest, Huck--just as earnest as ever I was in my life. Will you go +in there with me and help get it out?" + +"I bet I will! I will if it's where we can blaze our way to it and not +get lost." + +"Huck, we can do that without the least little bit of trouble in the +world." + +"Good as wheat! What makes you think the money's--" + +"Huck, you just wait till we get in there. If we don't find it I'll +agree to give you my drum and every thing I've got in the world. I +will, by jings." + +"All right--it's a whiz. When do you say?" + +"Right now, if you say it. Are you strong enough?" + +"Is it far in the cave? I ben on my pins a little, three or four days, +now, but I can't walk more'n a mile, Tom--least I don't think I could." + +"It's about five mile into there the way anybody but me would go, +Huck, but there's a mighty short cut that they don't anybody but me +know about. Huck, I'll take you right to it in a skiff. I'll float the +skiff down there, and I'll pull it back again all by myself. You +needn't ever turn your hand over." + +"Less start right off, Tom." + +"All right. We want some bread and meat, and our pipes, and a little +bag or two, and two or three kite-strings, and some of these +new-fangled things they call lucifer matches. I tell you, many's +the time I wished I had some when I was in there before." + +A trifle after noon the boys borrowed a small skiff from a citizen who +was absent, and got under way at once. When they were several miles +below "Cave Hollow," Tom said: + +"Now you see this bluff here looks all alike all the way down from the +cave hollow--no houses, no wood-yards, bushes all alike. But do you see +that white place up yonder where there's been a landslide? Well, that's +one of my marks. We'll get ashore, now." + +They landed. + +"Now, Huck, where we're a-standing you could touch that hole I got out +of with a fishing-pole. See if you can find it." + +Huck searched all the place about, and found nothing. Tom proudly +marched into a thick clump of sumach bushes and said: + +"Here you are! Look at it, Huck; it's the snuggest hole in this +country. You just keep mum about it. All along I've been wanting to be +a robber, but I knew I'd got to have a thing like this, and where to +run across it was the bother. We've got it now, and we'll keep it +quiet, only we'll let Joe Harper and Ben Rogers in--because of course +there's got to be a Gang, or else there wouldn't be any style about it. +Tom Sawyer's Gang--it sounds splendid, don't it, Huck?" + +"Well, it just does, Tom. And who'll we rob?" + +"Oh, most anybody. Waylay people--that's mostly the way." + +"And kill them?" + +"No, not always. Hive them in the cave till they raise a ransom." + +"What's a ransom?" + +"Money. You make them raise all they can, off'n their friends; and +after you've kept them a year, if it ain't raised then you kill them. +That's the general way. Only you don't kill the women. You shut up the +women, but you don't kill them. They're always beautiful and rich, and +awfully scared. You take their watches and things, but you always take +your hat off and talk polite. They ain't anybody as polite as robbers +--you'll see that in any book. Well, the women get to loving you, and +after they've been in the cave a week or two weeks they stop crying and +after that you couldn't get them to leave. If you drove them out they'd +turn right around and come back. It's so in all the books." + +"Why, it's real bully, Tom. I believe it's better'n to be a pirate." + +"Yes, it's better in some ways, because it's close to home and +circuses and all that." + +By this time everything was ready and the boys entered the hole, Tom +in the lead. They toiled their way to the farther end of the tunnel, +then made their spliced kite-strings fast and moved on. A few steps +brought them to the spring, and Tom felt a shudder quiver all through +him. He showed Huck the fragment of candle-wick perched on a lump of +clay against the wall, and described how he and Becky had watched the +flame struggle and expire. + +The boys began to quiet down to whispers, now, for the stillness and +gloom of the place oppressed their spirits. They went on, and presently +entered and followed Tom's other corridor until they reached the +"jumping-off place." The candles revealed the fact that it was not +really a precipice, but only a steep clay hill twenty or thirty feet +high. Tom whispered: + +"Now I'll show you something, Huck." + +He held his candle aloft and said: + +"Look as far around the corner as you can. Do you see that? There--on +the big rock over yonder--done with candle-smoke." + +"Tom, it's a CROSS!" + +"NOW where's your Number Two? 'UNDER THE CROSS,' hey? Right yonder's +where I saw Injun Joe poke up his candle, Huck!" + +Huck stared at the mystic sign awhile, and then said with a shaky voice: + +"Tom, less git out of here!" + +"What! and leave the treasure?" + +"Yes--leave it. Injun Joe's ghost is round about there, certain." + +"No it ain't, Huck, no it ain't. It would ha'nt the place where he +died--away out at the mouth of the cave--five mile from here." + +"No, Tom, it wouldn't. It would hang round the money. I know the ways +of ghosts, and so do you." + +Tom began to fear that Huck was right. Misgivings gathered in his +mind. But presently an idea occurred to him-- + +"Lookyhere, Huck, what fools we're making of ourselves! Injun Joe's +ghost ain't a going to come around where there's a cross!" + +The point was well taken. It had its effect. + +"Tom, I didn't think of that. But that's so. It's luck for us, that +cross is. I reckon we'll climb down there and have a hunt for that box." + +Tom went first, cutting rude steps in the clay hill as he descended. +Huck followed. Four avenues opened out of the small cavern which the +great rock stood in. The boys examined three of them with no result. +They found a small recess in the one nearest the base of the rock, with +a pallet of blankets spread down in it; also an old suspender, some +bacon rind, and the well-gnawed bones of two or three fowls. But there +was no money-box. The lads searched and researched this place, but in +vain. Tom said: + +"He said UNDER the cross. Well, this comes nearest to being under the +cross. It can't be under the rock itself, because that sets solid on +the ground." + +They searched everywhere once more, and then sat down discouraged. +Huck could suggest nothing. By-and-by Tom said: + +"Lookyhere, Huck, there's footprints and some candle-grease on the +clay about one side of this rock, but not on the other sides. Now, +what's that for? I bet you the money IS under the rock. I'm going to +dig in the clay." + +"That ain't no bad notion, Tom!" said Huck with animation. + +Tom's "real Barlow" was out at once, and he had not dug four inches +before he struck wood. + +"Hey, Huck!--you hear that?" + +Huck began to dig and scratch now. Some boards were soon uncovered and +removed. They had concealed a natural chasm which led under the rock. +Tom got into this and held his candle as far under the rock as he +could, but said he could not see to the end of the rift. He proposed to +explore. He stooped and passed under; the narrow way descended +gradually. He followed its winding course, first to the right, then to +the left, Huck at his heels. Tom turned a short curve, by-and-by, and +exclaimed: + +"My goodness, Huck, lookyhere!" + +It was the treasure-box, sure enough, occupying a snug little cavern, +along with an empty powder-keg, a couple of guns in leather cases, two +or three pairs of old moccasins, a leather belt, and some other rubbish +well soaked with the water-drip. + +"Got it at last!" said Huck, ploughing among the tarnished coins with +his hand. "My, but we're rich, Tom!" + +"Huck, I always reckoned we'd get it. It's just too good to believe, +but we HAVE got it, sure! Say--let's not fool around here. Let's snake +it out. Lemme see if I can lift the box." + +It weighed about fifty pounds. Tom could lift it, after an awkward +fashion, but could not carry it conveniently. + +"I thought so," he said; "THEY carried it like it was heavy, that day +at the ha'nted house. I noticed that. I reckon I was right to think of +fetching the little bags along." + +The money was soon in the bags and the boys took it up to the cross +rock. + +"Now less fetch the guns and things," said Huck. + +"No, Huck--leave them there. They're just the tricks to have when we +go to robbing. We'll keep them there all the time, and we'll hold our +orgies there, too. It's an awful snug place for orgies." + +"What orgies?" + +"I dono. But robbers always have orgies, and of course we've got to +have them, too. Come along, Huck, we've been in here a long time. It's +getting late, I reckon. I'm hungry, too. We'll eat and smoke when we +get to the skiff." + +They presently emerged into the clump of sumach bushes, looked warily +out, found the coast clear, and were soon lunching and smoking in the +skiff. As the sun dipped toward the horizon they pushed out and got +under way. Tom skimmed up the shore through the long twilight, chatting +cheerily with Huck, and landed shortly after dark. + +"Now, Huck," said Tom, "we'll hide the money in the loft of the +widow's woodshed, and I'll come up in the morning and we'll count it +and divide, and then we'll hunt up a place out in the woods for it +where it will be safe. Just you lay quiet here and watch the stuff till +I run and hook Benny Taylor's little wagon; I won't be gone a minute." + +He disappeared, and presently returned with the wagon, put the two +small sacks into it, threw some old rags on top of them, and started +off, dragging his cargo behind him. When the boys reached the +Welshman's house, they stopped to rest. Just as they were about to move +on, the Welshman stepped out and said: + +"Hallo, who's that?" + +"Huck and Tom Sawyer." + +"Good! Come along with me, boys, you are keeping everybody waiting. +Here--hurry up, trot ahead--I'll haul the wagon for you. Why, it's not +as light as it might be. Got bricks in it?--or old metal?" + +"Old metal," said Tom. + +"I judged so; the boys in this town will take more trouble and fool +away more time hunting up six bits' worth of old iron to sell to the +foundry than they would to make twice the money at regular work. But +that's human nature--hurry along, hurry along!" + +The boys wanted to know what the hurry was about. + +"Never mind; you'll see, when we get to the Widow Douglas'." + +Huck said with some apprehension--for he was long used to being +falsely accused: + +"Mr. Jones, we haven't been doing nothing." + +The Welshman laughed. + +"Well, I don't know, Huck, my boy. I don't know about that. Ain't you +and the widow good friends?" + +"Yes. Well, she's ben good friends to me, anyway." + +"All right, then. What do you want to be afraid for?" + +This question was not entirely answered in Huck's slow mind before he +found himself pushed, along with Tom, into Mrs. Douglas' drawing-room. +Mr. Jones left the wagon near the door and followed. + +The place was grandly lighted, and everybody that was of any +consequence in the village was there. The Thatchers were there, the +Harpers, the Rogerses, Aunt Polly, Sid, Mary, the minister, the editor, +and a great many more, and all dressed in their best. The widow +received the boys as heartily as any one could well receive two such +looking beings. They were covered with clay and candle-grease. Aunt +Polly blushed crimson with humiliation, and frowned and shook her head +at Tom. Nobody suffered half as much as the two boys did, however. Mr. +Jones said: + +"Tom wasn't at home, yet, so I gave him up; but I stumbled on him and +Huck right at my door, and so I just brought them along in a hurry." + +"And you did just right," said the widow. "Come with me, boys." + +She took them to a bedchamber and said: + +"Now wash and dress yourselves. Here are two new suits of clothes +--shirts, socks, everything complete. They're Huck's--no, no thanks, +Huck--Mr. Jones bought one and I the other. But they'll fit both of you. +Get into them. We'll wait--come down when you are slicked up enough." + +Then she left. + + + +CHAPTER XXXIV + +HUCK said: "Tom, we can slope, if we can find a rope. The window ain't +high from the ground." + +"Shucks! what do you want to slope for?" + +"Well, I ain't used to that kind of a crowd. I can't stand it. I ain't +going down there, Tom." + +"Oh, bother! It ain't anything. I don't mind it a bit. I'll take care +of you." + +Sid appeared. + +"Tom," said he, "auntie has been waiting for you all the afternoon. +Mary got your Sunday clothes ready, and everybody's been fretting about +you. Say--ain't this grease and clay, on your clothes?" + +"Now, Mr. Siddy, you jist 'tend to your own business. What's all this +blow-out about, anyway?" + +"It's one of the widow's parties that she's always having. This time +it's for the Welshman and his sons, on account of that scrape they +helped her out of the other night. And say--I can tell you something, +if you want to know." + +"Well, what?" + +"Why, old Mr. Jones is going to try to spring something on the people +here to-night, but I overheard him tell auntie to-day about it, as a +secret, but I reckon it's not much of a secret now. Everybody knows +--the widow, too, for all she tries to let on she don't. Mr. Jones was +bound Huck should be here--couldn't get along with his grand secret +without Huck, you know!" + +"Secret about what, Sid?" + +"About Huck tracking the robbers to the widow's. I reckon Mr. Jones +was going to make a grand time over his surprise, but I bet you it will +drop pretty flat." + +Sid chuckled in a very contented and satisfied way. + +"Sid, was it you that told?" + +"Oh, never mind who it was. SOMEBODY told--that's enough." + +"Sid, there's only one person in this town mean enough to do that, and +that's you. If you had been in Huck's place you'd 'a' sneaked down the +hill and never told anybody on the robbers. You can't do any but mean +things, and you can't bear to see anybody praised for doing good ones. +There--no thanks, as the widow says"--and Tom cuffed Sid's ears and +helped him to the door with several kicks. "Now go and tell auntie if +you dare--and to-morrow you'll catch it!" + +Some minutes later the widow's guests were at the supper-table, and a +dozen children were propped up at little side-tables in the same room, +after the fashion of that country and that day. At the proper time Mr. +Jones made his little speech, in which he thanked the widow for the +honor she was doing himself and his sons, but said that there was +another person whose modesty-- + +And so forth and so on. He sprung his secret about Huck's share in the +adventure in the finest dramatic manner he was master of, but the +surprise it occasioned was largely counterfeit and not as clamorous and +effusive as it might have been under happier circumstances. However, +the widow made a pretty fair show of astonishment, and heaped so many +compliments and so much gratitude upon Huck that he almost forgot the +nearly intolerable discomfort of his new clothes in the entirely +intolerable discomfort of being set up as a target for everybody's gaze +and everybody's laudations. + +The widow said she meant to give Huck a home under her roof and have +him educated; and that when she could spare the money she would start +him in business in a modest way. Tom's chance was come. He said: + +"Huck don't need it. Huck's rich." + +Nothing but a heavy strain upon the good manners of the company kept +back the due and proper complimentary laugh at this pleasant joke. But +the silence was a little awkward. Tom broke it: + +"Huck's got money. Maybe you don't believe it, but he's got lots of +it. Oh, you needn't smile--I reckon I can show you. You just wait a +minute." + +Tom ran out of doors. The company looked at each other with a +perplexed interest--and inquiringly at Huck, who was tongue-tied. + +"Sid, what ails Tom?" said Aunt Polly. "He--well, there ain't ever any +making of that boy out. I never--" + +Tom entered, struggling with the weight of his sacks, and Aunt Polly +did not finish her sentence. Tom poured the mass of yellow coin upon +the table and said: + +"There--what did I tell you? Half of it's Huck's and half of it's mine!" + +The spectacle took the general breath away. All gazed, nobody spoke +for a moment. Then there was a unanimous call for an explanation. Tom +said he could furnish it, and he did. The tale was long, but brimful of +interest. There was scarcely an interruption from any one to break the +charm of its flow. When he had finished, Mr. Jones said: + +"I thought I had fixed up a little surprise for this occasion, but it +don't amount to anything now. This one makes it sing mighty small, I'm +willing to allow." + +The money was counted. The sum amounted to a little over twelve +thousand dollars. It was more than any one present had ever seen at one +time before, though several persons were there who were worth +considerably more than that in property. + + + +CHAPTER XXXV + +THE reader may rest satisfied that Tom's and Huck's windfall made a +mighty stir in the poor little village of St. Petersburg. So vast a +sum, all in actual cash, seemed next to incredible. It was talked +about, gloated over, glorified, until the reason of many of the +citizens tottered under the strain of the unhealthy excitement. Every +"haunted" house in St. Petersburg and the neighboring villages was +dissected, plank by plank, and its foundations dug up and ransacked for +hidden treasure--and not by boys, but men--pretty grave, unromantic +men, too, some of them. Wherever Tom and Huck appeared they were +courted, admired, stared at. The boys were not able to remember that +their remarks had possessed weight before; but now their sayings were +treasured and repeated; everything they did seemed somehow to be +regarded as remarkable; they had evidently lost the power of doing and +saying commonplace things; moreover, their past history was raked up +and discovered to bear marks of conspicuous originality. The village +paper published biographical sketches of the boys. + +The Widow Douglas put Huck's money out at six per cent., and Judge +Thatcher did the same with Tom's at Aunt Polly's request. Each lad had +an income, now, that was simply prodigious--a dollar for every week-day +in the year and half of the Sundays. It was just what the minister got +--no, it was what he was promised--he generally couldn't collect it. A +dollar and a quarter a week would board, lodge, and school a boy in +those old simple days--and clothe him and wash him, too, for that +matter. + +Judge Thatcher had conceived a great opinion of Tom. He said that no +commonplace boy would ever have got his daughter out of the cave. When +Becky told her father, in strict confidence, how Tom had taken her +whipping at school, the Judge was visibly moved; and when she pleaded +grace for the mighty lie which Tom had told in order to shift that +whipping from her shoulders to his own, the Judge said with a fine +outburst that it was a noble, a generous, a magnanimous lie--a lie that +was worthy to hold up its head and march down through history breast to +breast with George Washington's lauded Truth about the hatchet! Becky +thought her father had never looked so tall and so superb as when he +walked the floor and stamped his foot and said that. She went straight +off and told Tom about it. + +Judge Thatcher hoped to see Tom a great lawyer or a great soldier some +day. He said he meant to look to it that Tom should be admitted to the +National Military Academy and afterward trained in the best law school +in the country, in order that he might be ready for either career or +both. + +Huck Finn's wealth and the fact that he was now under the Widow +Douglas' protection introduced him into society--no, dragged him into +it, hurled him into it--and his sufferings were almost more than he +could bear. The widow's servants kept him clean and neat, combed and +brushed, and they bedded him nightly in unsympathetic sheets that had +not one little spot or stain which he could press to his heart and know +for a friend. He had to eat with a knife and fork; he had to use +napkin, cup, and plate; he had to learn his book, he had to go to +church; he had to talk so properly that speech was become insipid in +his mouth; whithersoever he turned, the bars and shackles of +civilization shut him in and bound him hand and foot. + +He bravely bore his miseries three weeks, and then one day turned up +missing. For forty-eight hours the widow hunted for him everywhere in +great distress. The public were profoundly concerned; they searched +high and low, they dragged the river for his body. Early the third +morning Tom Sawyer wisely went poking among some old empty hogsheads +down behind the abandoned slaughter-house, and in one of them he found +the refugee. Huck had slept there; he had just breakfasted upon some +stolen odds and ends of food, and was lying off, now, in comfort, with +his pipe. He was unkempt, uncombed, and clad in the same old ruin of +rags that had made him picturesque in the days when he was free and +happy. Tom routed him out, told him the trouble he had been causing, +and urged him to go home. Huck's face lost its tranquil content, and +took a melancholy cast. He said: + +"Don't talk about it, Tom. I've tried it, and it don't work; it don't +work, Tom. It ain't for me; I ain't used to it. The widder's good to +me, and friendly; but I can't stand them ways. She makes me get up just +at the same time every morning; she makes me wash, they comb me all to +thunder; she won't let me sleep in the woodshed; I got to wear them +blamed clothes that just smothers me, Tom; they don't seem to any air +git through 'em, somehow; and they're so rotten nice that I can't set +down, nor lay down, nor roll around anywher's; I hain't slid on a +cellar-door for--well, it 'pears to be years; I got to go to church and +sweat and sweat--I hate them ornery sermons! I can't ketch a fly in +there, I can't chaw. I got to wear shoes all Sunday. The widder eats by +a bell; she goes to bed by a bell; she gits up by a bell--everything's +so awful reg'lar a body can't stand it." + +"Well, everybody does that way, Huck." + +"Tom, it don't make no difference. I ain't everybody, and I can't +STAND it. It's awful to be tied up so. And grub comes too easy--I don't +take no interest in vittles, that way. I got to ask to go a-fishing; I +got to ask to go in a-swimming--dern'd if I hain't got to ask to do +everything. Well, I'd got to talk so nice it wasn't no comfort--I'd got +to go up in the attic and rip out awhile, every day, to git a taste in +my mouth, or I'd a died, Tom. The widder wouldn't let me smoke; she +wouldn't let me yell, she wouldn't let me gape, nor stretch, nor +scratch, before folks--" [Then with a spasm of special irritation and +injury]--"And dad fetch it, she prayed all the time! I never see such a +woman! I HAD to shove, Tom--I just had to. And besides, that school's +going to open, and I'd a had to go to it--well, I wouldn't stand THAT, +Tom. Looky here, Tom, being rich ain't what it's cracked up to be. It's +just worry and worry, and sweat and sweat, and a-wishing you was dead +all the time. Now these clothes suits me, and this bar'l suits me, and +I ain't ever going to shake 'em any more. Tom, I wouldn't ever got into +all this trouble if it hadn't 'a' ben for that money; now you just take +my sheer of it along with your'n, and gimme a ten-center sometimes--not +many times, becuz I don't give a dern for a thing 'thout it's tollable +hard to git--and you go and beg off for me with the widder." + +"Oh, Huck, you know I can't do that. 'Tain't fair; and besides if +you'll try this thing just a while longer you'll come to like it." + +"Like it! Yes--the way I'd like a hot stove if I was to set on it long +enough. No, Tom, I won't be rich, and I won't live in them cussed +smothery houses. I like the woods, and the river, and hogsheads, and +I'll stick to 'em, too. Blame it all! just as we'd got guns, and a +cave, and all just fixed to rob, here this dern foolishness has got to +come up and spile it all!" + +Tom saw his opportunity-- + +"Lookyhere, Huck, being rich ain't going to keep me back from turning +robber." + +"No! Oh, good-licks; are you in real dead-wood earnest, Tom?" + +"Just as dead earnest as I'm sitting here. But Huck, we can't let you +into the gang if you ain't respectable, you know." + +Huck's joy was quenched. + +"Can't let me in, Tom? Didn't you let me go for a pirate?" + +"Yes, but that's different. A robber is more high-toned than what a +pirate is--as a general thing. In most countries they're awful high up +in the nobility--dukes and such." + +"Now, Tom, hain't you always ben friendly to me? You wouldn't shet me +out, would you, Tom? You wouldn't do that, now, WOULD you, Tom?" + +"Huck, I wouldn't want to, and I DON'T want to--but what would people +say? Why, they'd say, 'Mph! Tom Sawyer's Gang! pretty low characters in +it!' They'd mean you, Huck. You wouldn't like that, and I wouldn't." + +Huck was silent for some time, engaged in a mental struggle. Finally +he said: + +"Well, I'll go back to the widder for a month and tackle it and see if +I can come to stand it, if you'll let me b'long to the gang, Tom." + +"All right, Huck, it's a whiz! Come along, old chap, and I'll ask the +widow to let up on you a little, Huck." + +"Will you, Tom--now will you? That's good. If she'll let up on some of +the roughest things, I'll smoke private and cuss private, and crowd +through or bust. When you going to start the gang and turn robbers?" + +"Oh, right off. We'll get the boys together and have the initiation +to-night, maybe." + +"Have the which?" + +"Have the initiation." + +"What's that?" + +"It's to swear to stand by one another, and never tell the gang's +secrets, even if you're chopped all to flinders, and kill anybody and +all his family that hurts one of the gang." + +"That's gay--that's mighty gay, Tom, I tell you." + +"Well, I bet it is. And all that swearing's got to be done at +midnight, in the lonesomest, awfulest place you can find--a ha'nted +house is the best, but they're all ripped up now." + +"Well, midnight's good, anyway, Tom." + +"Yes, so it is. And you've got to swear on a coffin, and sign it with +blood." + +"Now, that's something LIKE! Why, it's a million times bullier than +pirating. I'll stick to the widder till I rot, Tom; and if I git to be +a reg'lar ripper of a robber, and everybody talking 'bout it, I reckon +she'll be proud she snaked me in out of the wet." + + + +CONCLUSION + +SO endeth this chronicle. It being strictly a history of a BOY, it +must stop here; the story could not go much further without becoming +the history of a MAN. When one writes a novel about grown people, he +knows exactly where to stop--that is, with a marriage; but when he +writes of juveniles, he must stop where he best can. + +Most of the characters that perform in this book still live, and are +prosperous and happy. Some day it may seem worth while to take up the +story of the younger ones again and see what sort of men and women they +turned out to be; therefore it will be wisest not to reveal any of that +part of their lives at present. diff --git a/pkg/filter/testdata/e.txt b/pkg/filter/testdata/e.txt new file mode 100644 index 0000000000000000000000000000000000000000..5ca186f14c1591c83e5348389ae018235af354e8 --- /dev/null +++ b/pkg/filter/testdata/e.txt @@ -0,0 +1 @@ +2.7182818284590452353602874713526624977572470936999595749669676277240766303535475945713821785251664274274663919320030599218174135966290435729003342952605956307381323286279434907632338298807531952510190115738341879307021540891499348841675092447614606680822648001684774118537423454424371075390777449920695517027618386062613313845830007520449338265602976067371132007093287091274437470472306969772093101416928368190255151086574637721112523897844250569536967707854499699679468644549059879316368892300987931277361782154249992295763514822082698951936680331825288693984964651058209392398294887933203625094431173012381970684161403970198376793206832823764648042953118023287825098194558153017567173613320698112509961818815930416903515988885193458072738667385894228792284998920868058257492796104841984443634632449684875602336248270419786232090021609902353043699418491463140934317381436405462531520961836908887070167683964243781405927145635490613031072085103837505101157477041718986106873969655212671546889570350354021234078498193343210681701210056278802351930332247450158539047304199577770935036604169973297250886876966403555707162268447162560798826517871341951246652010305921236677194325278675398558944896970964097545918569563802363701621120477427228364896134225164450781824423529486363721417402388934412479635743702637552944483379980161254922785092577825620926226483262779333865664816277251640191059004916449982893150566047258027786318641551956532442586982946959308019152987211725563475463964479101459040905862984967912874068705048958586717479854667757573205681288459205413340539220001137863009455606881667400169842055804033637953764520304024322566135278369511778838638744396625322498506549958862342818997077332761717839280349465014345588970719425863987727547109629537415211151368350627526023264847287039207643100595841166120545297030236472549296669381151373227536450988890313602057248176585118063036442812314965507047510254465011727211555194866850800368532281831521960037356252794495158284188294787610852639813955990067376482922443752871846245780361929819713991475644882626039033814418232625150974827987779964373089970388867782271383605772978824125611907176639465070633045279546618550966661856647097113444740160704626215680717481877844371436988218559670959102596862002353718588748569652200050311734392073211390803293634479727355955277349071783793421637012050054513263835440001863239914907054797780566978533580489669062951194324730995876552368128590413832411607226029983305353708761389396391779574540161372236187893652605381558415871869255386061647798340254351284396129460352913325942794904337299085731580290958631382683291477116396337092400316894586360606458459251269946557248391865642097526850823075442545993769170419777800853627309417101634349076964237222943523661255725088147792231519747780605696725380171807763603462459278778465850656050780844211529697521890874019660906651803516501792504619501366585436632712549639908549144200014574760819302212066024330096412704894390397177195180699086998606636583232278709376502260149291011517177635944602023249300280401867723910288097866605651183260043688508817157238669842242201024950551881694803221002515426494639812873677658927688163598312477886520141174110913601164995076629077943646005851941998560162647907615321038727557126992518275687989302761761146162549356495903798045838182323368612016243736569846703785853305275833337939907521660692380533698879565137285593883499894707416181550125397064648171946708348197214488898790676503795903669672494992545279033729636162658976039498576741397359441023744329709355477982629614591442936451428617158587339746791897571211956187385783644758448423555581050025611492391518893099463428413936080383091662818811503715284967059741625628236092168075150177725387402564253470879089137291722828611515915683725241630772254406337875931059826760944203261924285317018781772960235413060672136046000389661093647095141417185777014180606443636815464440053316087783143174440811949422975599314011888683314832802706553833004693290115744147563139997221703804617092894579096271662260740718749975359212756084414737823303270330168237193648002173285734935947564334129943024850235732214597843282641421684878721673367010615094243456984401873312810107945127223737886126058165668053714396127888732527373890392890506865324138062796025930387727697783792868409325365880733988457218746021005311483351323850047827169376218004904795597959290591655470505777514308175112698985188408718564026035305583737832422924185625644255022672155980274012617971928047139600689163828665277009752767069777036439260224372841840883251848770472638440379530166905465937461619323840363893131364327137688841026811219891275223056256756254701725086349765367288605966752740868627407912856576996313789753034660616669804218267724560530660773899624218340859882071864682623215080288286359746839654358856685503773131296587975810501214916207656769950659715344763470320853215603674828608378656803073062657633469774295634643716709397193060876963495328846833613038829431040800296873869117066666146800015121143442256023874474325250769387077775193299942137277211258843608715834835626961661980572526612206797540621062080649882918454395301529982092503005498257043390553570168653120526495614857249257386206917403695213533732531666345466588597286659451136441370331393672118569553952108458407244323835586063106806964924851232632699514603596037297253198368423363904632136710116192821711150282801604488058802382031981493096369596735832742024988245684941273860566491352526706046234450549227581151709314921879592718001940968866986837037302200475314338181092708030017205935530520700706072233999463990571311587099635777359027196285061146514837526209565346713290025994397663114545902685898979115837093419370441155121920117164880566945938131183843765620627846310490346293950029458341164824114969758326011800731699437393506966295712410273239138741754923071862454543222039552735295240245903805744502892246886285336542213815722131163288112052146489805180092024719391710555390113943316681515828843687606961102505171007392762385553386272553538830960671644662370922646809671254061869502143176211668140097595281493907222601112681153108387317617323235263605838173151034595736538223534992935822836851007810884634349983518404451704270189381994243410090575376257767571118090088164183319201962623416288166521374717325477727783488774366518828752156685719506371936565390389449366421764003121527870222366463635755503565576948886549500270853923617105502131147413744106134445544192101336172996285694899193369184729478580729156088510396781959429833186480756083679551496636448965592948187851784038773326247051945050419847742014183947731202815886845707290544057510601285258056594703046836344592652552137008068752009593453607316226118728173928074623094685367823106097921599360019946237993434210687813497346959246469752506246958616909178573976595199392993995567542714654910456860702099012606818704984178079173924071945996323060254707901774527513186809982284730860766536866855516467702911336827563107223346726113705490795365834538637196235856312618387156774118738527722922594743373785695538456246801013905727871016512966636764451872465653730402443684140814488732957847348490003019477888020460324660842875351848364959195082888323206522128104190448047247949291342284951970022601310430062410717971502793433263407995960531446053230488528972917659876016667811937932372453857209607582277178483361613582612896226118129455927462767137794487586753657544861407611931125958512655759734573015333642630767985443385761715333462325270572005303988289499034259566232975782488735029259166825894456894655992658454762694528780516501720674785417887982276806536650641910973434528878338621726156269582654478205672987756426325321594294418039943217000090542650763095588465895171709147607437136893319469090981904501290307099566226620303182649365733698419555776963787624918852865686607600566025605445711337286840205574416030837052312242587223438854123179481388550075689381124935386318635287083799845692619981794523364087429591180747453419551420351726184200845509170845682368200897739455842679214273477560879644279202708312150156406341341617166448069815483764491573900121217041547872591998943825364950514771379399147205219529079396137621107238494290616357604596231253506068537651423115349665683715116604220796394466621163255157729070978473156278277598788136491951257483328793771571459091064841642678309949723674420175862269402159407924480541255360431317992696739157542419296607312393763542139230617876753958711436104089409966089471418340698362993675362621545247298464213752891079884381306095552622720837518629837066787224430195793793786072107254277289071732854874374355781966511716618330881129120245204048682200072344035025448202834254187884653602591506445271657700044521097735585897622655484941621714989532383421600114062950718490427789258552743035221396835679018076406042138307308774460170842688272261177180842664333651780002171903449234264266292261456004337383868335555343453004264818473989215627086095650629340405264943244261445665921291225648893569655009154306426134252668472594914314239398845432486327461842846655985332312210466259890141712103446084271616619001257195870793217569698544013397622096749454185407118446433946990162698351607848924514058940946395267807354579700307051163682519487701189764002827648414160587206184185297189154019688253289309149665345753571427318482016384644832499037886069008072709327673127581966563941148961716832980455139729506687604740915420428429993541025829113502241690769431668574242522509026939034814856451303069925199590436384028429267412573422447765584177886171737265462085498294498946787350929581652632072258992368768457017823038096567883112289305809140572610865884845873101658151167533327674887014829167419701512559782572707406431808601428149024146780472327597684269633935773542930186739439716388611764209004068663398856841681003872389214483176070116684503887212364367043314091155733280182977988736590916659612402021778558854876176161989370794380056663364884365089144805571039765214696027662583599051987042300179465536788567430285974600143785483237068701190078499404930918919181649327259774030074879681484882342932023012128032327460392219687528340516906974194257614673978110715464186273369091584973185011183960482533518748438923177292613543024932562896371361977285456622924461644497284597867711574125670307871885109336344480149675240618536569532074170533486782754827815415561966911055101472799040386897220465550833170782394808785990501947563108984124144672821865459971596639015641941751820935932616316888380132758752601460507676098392625726411120135288591317848299475682472564885533357279772205543568126302535748216585414000805314820697137262149755576051890481622376790414926742600071045922695314835188137463887104273544767623577933993970632396604969145303273887874557905934937772320142954803345000695256980935282887783710670585567749481373858630385762823040694005665340584887527005308832459182183494318049834199639981458773435863115940570443683515285383609442955964360676090221741896883548131643997437764158365242234642619597390455450680695232850751868719449064767791886720306418630751053512149851051207313846648717547518382979990189317751550639981016466414592102406838294603208535554058147159273220677567669213664081505900806952540610628536408293276621931939933861623836069111767785448236129326858199965239275488427435414402884536455595124735546139403154952097397051896240157976832639450633230452192645049651735466775699295718989690470902730288544945416699791992948038254980285946029052763145580316514066229171223429375806143993484914362107993576737317948964252488813720435579287511385856973381976083524423240466778020948399639946684833774706725483618848273000648319163826022110555221246733323184463005504481849916996622087746140216157021029603318588727333298779352570182393861244026868339555870607758169954398469568540671174444932479519572159419645863736126915526457574786985964242176592896862383506370433939811671397544736228625506803682664135541448048997721373174119199970017293907303350869020922519124447393278376156321810842898207706974138707053266117683698647741787180202729412982310888796831880854367327806879771659111654224453806625861711729498038248879986504061563975629936962809358189761491017145343556659542757064194408833816841111166200759787244137082333917886114708228657531078536674695018462140736493917366254937783014074302668422150335117736471853872324040421037907750266020114814935482228916663640782450166815341213505278578539332606110249802273093636740213515386431693015267460536064351732154701091440650878823636764236831187390937464232609021646365627553976834019482932795750624399645272578624400375983422050808935129023122475970644105678361870877172333555465482598906861201410107222465904008553798235253885171623518256518482203125214950700378300411216212126052726059944320443056274522916128891766814160639131235975350390320077529587392412476451850809163911459296071156344204347133544720981178461451077872399140606290228276664309264900592249810291068759434533858330391178747575977065953570979640012224092199031158229259667913153991561438070129260780197022589662923368154312499412259460023399472228171056603931877226800493833148980338548909468685130789292064242819174795866199944411196208730498064385006852620258432842085582338566936649849720817046135376163584015342840674118587581546514598270228676671855309311923340191286170613364873183197560812569460089402953094429119590295968563923037689976327462283900735457144596414108229285922239332836210192822937243590283003884445701383771632056518351970100115722010956997890484964453434612129224964732356126321951155701565824427661599326463155806672053127596948538057364208384918887095176052287817339462747644656858900936266123311152910816041524100214195937349786431661556732702792109593543055579732660554677963552005378304619540636971842916168582734122217145885870814274090248185446421774876925093328785670674677381226752831653559245204578070541352576903253522738963847495646255940378924925007624386893776475310102323746733771474581625530698032499033676455430305274561512961214585944432150749051491453950981001388737926379964873728396416897555132275962011838248650746985492038097691932606437608743209385602815642849756549307909733854185583515789409814007691892389063090542534883896831762904120212949167195811935791203162514344096503132835216728021372415947344095498316138322505486708172221475138425166790445416617303200820330902895488808516797258495813407132180533988828139346049850532340472595097214331492586604248511405819579711564191458842833000525684776874305916390494306871343118796189637475503362820939949343690321031976898112055595369465424704173323895394046035325396758354395350516720261647961347790912327995264929045151148307923369382166010702872651938143844844532639517394110131152502750465749343063766541866128915264446926222884366299462732467958736383501937142786471398054038215513463223702071533134887083174146591492406359493020921122052610312390682941345696785958518393491382340884274312419099152870804332809132993078936867127413922890033069995875921815297612482409116951587789964090352577345938248232053055567238095022266790439614231852991989181065554412477204508510210071522352342792531266930108270633942321762570076323139159349709946933241013908779161651226804414809765618979735043151396066913258379033748620836695475083280318786707751177525663963479259219733577949555498655214193398170268639987388347010255262052312317215254062571636771270010760912281528326508984359568975961038372157726831170734552250194121701541318793651818502020877326906133592182000762327269503283827391243828198170871168108951187896746707073377869592565542713340052326706040004348843432902760360498027862160749469654989210474443927871934536701798673920803845633723311983855862638008516345597194441994344624761123844617615736242015935078520825600604101556889899501732554337298073561699861101908472096600708320280569917042590103876928658336557728758684250492690370934262028022399861803400211320742198642917383679176232826444645756330336556777374808644109969141827774253417010988435853189339175934511574023847292909015468559163792696196841000676598399744972047287881831200233383298030567865480871476464512824264478216644266616732096012564794514827125671326697067367144617795643752391742928503987022583734069852309190464967260243411270345611114149835783901793499713790913696706497637127248466613279908254305449295528594932793818341607827091326680865655921102733746700132583428715240835661522165574998431236278287106649401564670141943713823863454729606978693335973109537126499416282656463708490580151538205338326511289504938566468752921135932220265681856418260827538790002407915892646028490894922299966167437731347776134150965262448332709343898412056926145108857812249139616912534202918139898683901335795857624435194008943955180554746554000051766240202825944828833811886381749594284892013520090951007864941868256009273977667585642598378587497776669563350170748579027248701370264203283965756348010818356182372177082236423186591595883669487322411726504487268392328453010991677518376831599821263237123854357312681202445175401852132663740538802901249728180895021553100673598184430429105288459323064725590442355960551978839325930339572934663055160430923785677229293537208416693134575284011873746854691620648991164726909428982971065606801805807843600461866223562874591385185904416250663222249561448724413813849763797102676020845531824111963927941069619465426480006761727618115630063644321116224837379105623611358836334550102286170517890440570419577859833348463317921904494652923021469259756566389965893747728751393377105569802455757436190501772466214587592374418657530064998056688376964229825501195065837843125232135309371235243969149662310110328243570065781487677299160941153954063362752423712935549926713485031578238899567545287915578420483105749330060197958207739558522807307048950936235550769837881926357141779338750216344391014187576711938914416277109602859415809719913429313295145924373636456473035037374538503489286113141638094752301745088784885645741275003353303416138096560043105860548355773946625033230034341587814634602169235079216111013148948281895391028916816328709309713184139815427678818067628650978085718262117003140003377301581536334149093237034703637513354537634521050370995452942055232078817449370937677056009306353645510913481627378204985657055608784211964039972344556458607689515569686899384896439195225232309703301037277227710870564912966121061494072782442033414057441446459968236966118878411656290355117839944070961772567164919790168195234523807446299877664824873753313018142763910519234685081979001796519907050490865237442841652776611425351538665162781316090964802801234493372427866930894827913465443931965254154829494577875758599482099181824522449312077768250830768282335001597040419199560509705364696473142448453825888112602753909548852639708652339052941829691802357120545328231809270356491743371932080628731303589640570873779967845174740515317401384878082881006046388936711640477755985481263907504747295012609419990373721246201677030517790352952793168766305099837441859803498821239340919805055103821539827677291373138006715339240126954586376422065097810852907639079727841301764553247527073788764069366420012194745702358295481365781809867944020220280822637957006755393575808086318932075864444206644691649334467698180811716568665213389686173592450920801465312529777966137198695916451869432324246404401672381978020728394418264502183131483366019384891972317817154372192103946638473715630226701801343515930442853848941825678870721238520597263859224934763623122188113706307506918260109689069251417142514218153491532129077723748506635489170892850760234351768218355008829647410655814882049239533702270536705630750317499788187009989251020178015601042277836283644323729779929935160925884515772055232896978333126427671291093993103773425910592303277652667641874842441076564447767097790392324958416348527735171981064673837142742974468992320406932506062834468937543016787815320616009057693404906146176607094380110915443261929000745209895959201159412324102274845482605404361871836330268992858623582145643879695210235266673372434423091577183277565800211928270391042391966426911155333594569685782817020325495552528875464466074620294766116004435551604735044292127916358748473501590215522120388281168021413865865168464569964810015633741255098479730138656275460161279246359783661480163871602794405482710196290774543628092612567507181773641749763254436773503632580004042919906963117397787875081560227368824967077635559869284901628768699628053790181848148810833946900016380791075960745504688912686792812391148880036720729730801354431325347713094186717178607522981373539126772812593958220524289991371690685650421575056729991274177149279608831502358697816190894908487717722503860872618384947939757440664912760518878124233683125467278331513186758915668300679210215947336858591201395360301678110413444411030903388761520488296909104689167671555373346622545575975202624771242796225983278405833585897671474205724047439720232895903726148688388003174146490203843590358527993123871042845981608996101945691646983837718267264685264869172948414153004604004299585035164101899027529366867431834955447458124140190754681607770977920579383895378192128847409929537040546962226547278807248685508046571043123854873351653070570784584243335550958221912862797205455466267099131902370311779690892786623112661337671178512943059323281605826535623848164192144732543731002062738466812351691016359252588256806438946389880872735284406462208149513862275239938938734905082625472417781702582044129853760499827899020083498387362992498125742354568439023012261733665820546785671147973065077035475620567428300187473019197310881157516777005071432012726354601912460800451608108641835539669946936947322271670748972850464195392966434725254724357659192969949061670189061433616907056148280980363243454128229968275980226694045642181328624517549652147221620839824594576613342710564957193564431561774500828376935700995419541839029151033187933907614207467028867968594985439789457300768939890070073924697461812855764662265412913204052279071212820653775058280040897163467163709024906774736309136904002615646432159560910851092445162454420141442641660181385990017417408244245378610158433361777292580611159192008414091888191208858207627011483671760749046980914443057262211104583300789331698191603917150622792986282709446275915009683226345073725451366858172483498470080840163868209726371345205439802277866337293290829914010645589761697455978409211409167684020269370229231743334499986901841510888993165125090001163719114994852024821586396216294981753094623047604832399379391002142532996476235163569009445086058091202459904612118623318278614464727795523218635916551883057930657703331498510068357135624341881884405780028844018129031378653794869614630467726914552953690154167025838032477842272417994513653582260971652588356712133519546838335349801503269359798167463231847628306340588324731228951257944267639877946713121042763380872695738609314631539148548792514028885025189788076023838995615684850391995855029256054176767663145354058496296796781349420116003325874431438746248313850214980401681940795687219268462617287403480967931949965604299190281810597603263251746405016454606266765529010639868703668263299050577706266397868453584384057673298268163448646707439990917504018892319267557518354054956017732907127219134577524905771512773358423314008356080926962298894163047287780054743798498545562870729968407382937218623831766524716090967192007237658894226186550487552614557855898773008703234726418384831040394818743616224455286163287628541175946460497027724490799275146445792982549802258601001772437840167723166802004162547244179415547810554178036773553354467030326469619447560812831933095679685582771932031205941616693902049665352189672822671972640029493307384717544753761937017882976382487233361813499414541694736549254840633793674361541081593464960431603544354737728802361047743115330785159902977771499610274627769759612488879448609863349422852847651310277926279743981957617505591300993377368240510902583759345170015340522266144077237050890044496613295859536020556034009492820943862994618834790932894161098856594954213114335608810239423706087108026465913203560121875933791639666437282836752328391688865373751335794859860107569374889645657187292540448508624449947816273842517229343960137212406286783636675845331904743954740664015260871940915743955282773904303868772728262065663129387459875317749973799293043294371763801856280061141619563942414312254397099163565102848315765427037906837175764870230052388197498746636856292655058222887713221781440489538099681072143012394693530931524054081215705402274414521876541901428386744260011889041724570537470755550581632831687247110220353727166112304857340460879272501694701067831178927095527253222125224361673343366384756590949728221809418684074238351567868893421148203905824224324264643630201441787982022116248471657468291146315407563770222740135841109076078464780070182766336227978104546331131294044833570134869585165267459515187680033395522410548181767867772152798270250117195816577603549732923724732067853690257536233971216884390878879262188202305529937132397194333083536231248870386416194361506529551267334207198502259771408638122015980894363561808597010080081622557455039101321981979045520049618583777721048046635533806616517023595097133203631578945644487800945620369784973459902004606886572701865867757842758530645706617127194967371083950603267501532435909029491516973738110897934782297684100117657987098185725131372267749706609250481876835516003714638685918913011736805218743265426063700710595364425062760458252336880552521181566417553430681181548267844169315284408461087588214317641649835663127518728182948655658524206852221830755306118393326934164459415342651778653397980580828158806300749952897558204686612590853678738603318442905510689778698417735603118111677563872589911516803236547002987989628986181014596471307916144369564690909518788574398821730583884980809523077569358851616027719521488998358632323127308909861560777386006984035267826785387215920936255817889813416247486456433211043194821421299793188104636399541496539441501383868748384870224681829391860319598667962363489309283087840712400431022706137591368056518861313458307990705003607588327248867879324093380071864152853317943535073401891193638546730000660453783784472469288830546979000131248952100446949032058838294923613919284305249167833012980192255157050378521810552961623637523647962685751660066539364142273063001648652613891842243501797455993616794063303522111829071597538821839777552812981538570168702202620274678647916644030729018445497956399844836807851997088201407769199261674991148329821854382718946282165387064858588646221611410343570342878862979083418871606214430014533275029715104673156021000043869510583773779766003460887624861640938645252177935289947578496255243925598620521409052346250847830487046492688313289470553891357290706967599556298586669559721686506052072801342104355762779184021797626656484580261591407173477009039475168017709900129391137881248534255949312866653465033728846390649968460644741907524313323903404908195233044389559060547854954620263256676813262435925020249516275607080900436460421497025691488555265022810327762115842282433269528629137662675481993546118143913367579700141255870143319434764035725376914388899683088262844616425575034001428982557620386364384137906519612917777354183694676232982904981261717676191554292570438432239918482261744350470199171258214687683172646078959690569981353264435973965173473319484798758064137926885413552523275720457329477215706850016950046959758389373527538622664943456437071610511521617176237598050900553232154896062817794302268640579555845730600598376482703339859420098582351400179507104569019191359062304102336798080907240196312675268916362136351032648077232914950859151265812143823371072949148088472355286394195993455684156344577951727033374238129903260198160571971183950662758220321837136059718025940870615534713104482272716848395524105913605919812444978458110854511231668173534838253724825347636777581712867205865148285317273569069839935110763432091319780314031658897379628301178409806410175016511072932907832177487566289310650383806093372841399226733384778203302020700517188941706465146238366720632742644336612174011766914919235570905644803016342294301837655263108450172510307540942604409687066288066265900569082451407632599158164499361455172452057020443093722305550217222299706209749268609762787409626448772056043078634808885709143464793241536214303199965695610753570417207285334250171325558818113295504095217830139465216436594262960768570585698507157151317262928960072587601564840556088613165411835958628710665496282599535127193244635791046554389165150954187306071015034430609582302257455974944275067630926322529966338219395202927917973247094559691016402983683080426309910481567503623509654924302589575273521412445149542462972258510120707802110188106722347972579330653187713438466713807546383471635428854957610942841898601794658721444495198801550804042506452191484989920400007310672369944655246020908767882300064337725657385010969899058191290957079866699453765080407917852438222041070599278889267745752084287526377986730360561230710723922581504781379172731261234878334034473833573601973235946604273704635201327182592410906040097638585857716958419563109577748529579836844756803121874818202833941887076311731615289811756429711334181497218078040465077657204457082859417475114926179367379999220181789399433337731146911970737861041963986422166045588965683206701337505745038872111332436739840284188639147633491695114032583475841514170325690161784931455706904169858050217798497637014758914810543205854914100662201721719726878930012101267481270235940855162601689425111458499658315589660460091525797881670384625905383256920520425791378948827579603278877535466861441826827797651258953563761485994485049706638406266121957141911063246061774180577212381659872472432252969098533628440799030007594546281549235506086481557928961969617060715201589825299772803520002610888814176506636216905928021516429198484077446143617891415191517976537848282687018750030264867608433204658525470555882410254654806040437372771834769014720664234434374255514129178503032471263418076525187802925534774001104853996960549926508093910691337614841834884596365621526610332239417467064368340504749943339802285610313083038484571294767389856293937641914407036507544622061186499127249643799875806537850203753189972618014404667793050140301580709266213229273649718653952866567538572115133606114457222800851183757899219543063413692302293139751143702404830227357629039911794499248480915071002444078482866598579406525539141041497342780203520135419925977628178182825372022920108186449448349255421793982723279357095828748597126780783134286180750497175747373730296280477376908932558914598141724852658299510882230055223242218586191394795184220131553319634363922684259164168669438122537135960710031743651959027712571604588486044820674410935215327906816032054215967959066411120187618531256710150212239401285668608469435937408158536481912528004920724042172170913983123118054043277015835629513656274610248827706488865037765175678806872498861657094846665770674577000207144332525555736557083150320019082992096545498737419756608619533492312940263904930982014700371161829485939931199955070455381196711289367735249958182011774799788636393286405807810818657337668157893827656450642917396685579555053188715314552353070355994740186225988149854660737787698781542360397080977412361518245964026869979609564523828584235953564615185448165799966460648261396618720304839119560250381111550938420209894591555760083897989949964566262540514195610780090298667014635238532066032574466820259430618801773091109212741138269148784355679352572808875543164693077235363768226036080174040660997151176880434927489197133087822951123746632635635328517394189466510943745768270782209928468034684157443127739811044186762032954475468077511126663685479944460934809992951875666499902261686019672053749149951226823637895865245462813439289338365156536992413109638102559114643923805213907862893561660998836479175633176725856523591069520326895990054884753424160586689820067483163174286329119633399132709086065074595260357157323069712106423424081597068328707624437165532750228797802598690981111226558888151520837482450034463046505984569690276166958278982913613535306291331427881888249342136442417833519319786543940201465328083410341785272489879050919932369270996567133507711905899945951923990615156165480300145359212550696405345263823452155999210578191371030188979206408883974767667144727314254467923500524618849237455307575734902707342496298879996942094595961008702501329453325358045689285707241207965919809225550560061971283541270202072583994171175520920820151096509526685113897577150810849443508285458749912943857563115668324566827992991861539009255871716840495663991959154034218364537212023678608655364745175654879318925644085274489190918193411667583563439758886046349413111875241038425467937999203546910411935443113219136068129657568583611774564654674861061988591414805799318725367531243470335482637527081353105570818049642498584646147973467599315946514787025065271083508782350656532331797738656666181652390017664988485456054961300215776115255813396184027067814900350252876823607822107397102339146870159735868589015297010347780503292154014359595298683404657471756232196640515401477953167461726208727304820634652469109953327375561090578378455945469160223687689641425960164689647106348074109928546482353083540132332924864037318003195202317476206537726163717445360549726690601711176761047774971666890152163838974311714180622222345718567941507299526201086205084783127474791909996889937275229053674785020500038630036526218800670926674104806027341997756660029427941090400064654281074454007616429525362460261476180471744322889953285828397762184600967669267581270302806519535452053173536808954589902180783145775891280203970053633193821100095443241244197949192916205234421346395653840781209416214835001155883618421164283992454027590719621537570187067083731012246141362048926555668109467076386536083015847614512581588569610030337081197058344452874666198891534664244887911940711423940115986970795745946337170243268484864632018986352827092313047089215684758207753034387689978702323438584381125011714013265769320554911860153519551654627941175593967947958810333935413289702528893533748106257875620364294270257512121137330213811951395756419122685155962476203282038726342066227347868223036522019655729325905068134849292299647248229359787842720945578267329975853818536442370617353517653060396801087899490506654491544577952166038552398013798104340564182403396162494910454712104839439200945914647542424785991096900046541371091630096785951563947332190934511838669964622788855817353221326876634958059123761251203010983867841195725887799206041260049865895027247133146763722204388398558347770112599424691208308595666787531942465131444389971195968105937957532155524204659410081418351120174196853432672343271868099625045432475688702055341969199545300952644398446384346598830418262932239295612610045884644244285011551557765935780379565026806130721758672048541797157896401554276881090475899564605488362989140226580026134158039480357971019004151547655018391755772677897148793477372747525743898158705040701968215101218826088040084551332795162841280679678965570163917067779841529149397403158167896865448841319046368332179115059107813898261026271979696826411179918656038993895418928488851750122504754778999508544083983800725431468842988412616042682248823097788556495765424017114510393927980290997604904428832198976751320535115230545666467143795931915272680278210241540629795828828466355623580986725638200565215519951793551069127710538552661926903526081367717666435071213453983711357500975854405939558661737828297120544693182260401670308530911657973113259516101749193468250063285777004686987177255226525708428745733039859744230639751837209975339055095883623642814493247460522424051972825153787541962759327436278819283740253185668545040893929401040561666867664402868211607294830305236465560955351079987185041352121321534713770667681396211443891632403235741573773787908838267618458756361026435182951815392455211729022985278518025598478407179607904114472041476091765804302984501746867981277584971731733287305281134969591668387877072315968334322509070204019030503595891994666652037530271923764252552910347950343816357721698115464329245608951158732012675424975710520894362639501382962152214033621065422821876739580121286442788547491928976959315766891987305176388698461503354594898541849550251690616888419122873385522699976822609645007504500096116866129171093180282355042553653997166054753907348915189650027442328981181709248273610863801576007240601649547082331349361582435128299050405405333992577071321011503713898695076713447940748097845416328110406350804863393555238405735580863718763530261867971725608155328716436111474875107033512913923595452951407437943144900950809932872153235195999616750297532475931909938012968640379783553559071355708369947311923538531051736669154087312467233440702525006918026747725078958903448856673081487299464807786497709361969389290891718228134002845552513917355978456150353144603409441211512001738697261466786933733154341007587514908295822756919350542184106448264951943804240543255345965248373785310657979037977505031436474651422484768831323479762673689855474944277949916560108528257618964374464656819789319422077536824661110427671936481836360534108748971066866318805026555929568123959680449295166615409802610781691689418764353363449482900125929366840591370059526914934421861891742142561071896846626335874414976973921566392767687720145153302241853125308442727245771161505550519076276250016522166274796257424425420546785767478190959486500575711016264847833741198041625940813327229905891486422127968042984725356237202887830051788539737909455265135144073130049869453403245984236934627060242579432563660640597549471239092372458126154582526667304702319359866523378856244229188278436440434628094888288712101968642736370461639297485616780079779959696843367730352483047478240669928277140069031660709951473154191919911453182543906294573298686613524886500574780251977607442660798300291573030523199052185718628543687577860915726925232573171665625274275808460620177046433101212443409281314659760221360416223031167750085960128475289259463348312408766740128170543067985261868949895004918275008304998926472034986965363326210919830621495095877228260815566702155693484634079776879525038204442326697479264829899016938511552124688935873289878336267819361764023681714606495185508780596635354698788205094762016350757090024201498400967867845405354130050482404996646978558002628931826518708714613909521454987992300431779500489569529280112698632533646737179519363094399609176354568799002814515169743717518330632232942199132137614506411391269837128970829395360832883050256072727563548374205497856659895469089938558918441085605111510354367477810778500572718180809661542709143010161515013086522842238721618109043183163796046431523184434669799904865336375319295967726080853457652274714047941973192220960296582500937408249714373040087376988068797038047223488825819819025644086847749767508999164153502160223967816357097637814023962825054332801828798160046910336602415904504637333597488119998663995617171089911809851197616486499233594328274275983382931099806461605360243604040848379619072542165869409486682092396143083817303621520642297839982533698027039931804024928814430649614747600087654305571672697259114631990688823893005380061568007730984416061355843701277573463708822073792921409548717956947854414951731561828176343929570234710460088230637509877521391223419548471196982303169544468045517922669260631327498272520906329003279972932906827204647650366969765227673645419031639887433042226322021325368176044169612053532174352764937901877252263626883107879345194133825996368795020985033021472307603375442346871647223795507794130304865403488955400210765171630884759704098331306109510294140865574071074640401937347718815339902047036749084359309086354777210564861918603858715882024476138160390378532660185842568914109194464566162667753712365992832481865739251429498555141512136758288423285957759412684479036912662015308418041737698963759002546999454131659341985624780714434977201991702665380714107259910648709897259362243300706760476097690456341576573395549588448948093604077155688747288451838106069038026528318275560395905381507241627615047252487759578650784894547389096573312763852962664517004459626327934637721151028545472312880039058405918498833810711366073657536918428084655898982349219315205257478363855266205400703561310260405145079325925798227406012199249391735122145336707913500607486561657301854049217477162051678486507913573336334257685988361252720250944019430674728667983441293018131344299088234006652915385763779110955708000600143579956351811596764725075668367726052352939773016348235753572874236648294604770429166438403558846422370760111774821079625901180265548868995181239470625954254584491340203400196442965370643088660925268811549596291166168612036195319253262662271108142149856132646467211954801142455133946382385908540917878668826947602781853283155445565265933912487885639504644196022475186011405239187543742526581685003052301877096152411653980646785444273124462179491306502631062903402737260479940181929954454297256377507172705659271779285537195547433852182309492703218343678206382655341157162788603990157495208065443409462446634653253581574814022471260618973060860559065082163068709634119751925774318683671722139063093061019303182326666420628155129647685313861018672921889347039342072245556791239578260248978371473556820782675452142687314252252601795889759116238720807580527221031327444754083319215135934526961397220564699247718289310588394769170851420631557192703636345039529604362885088555160008371973526383838996789184600327073682083234847108471706160879195227388252347506380811606090840124222431476103563328940609282430125462013806032608121942876847907192546246309055749298781661271916548229644317263587524548607563020667656942355342774617635549231817456159185668061686428714964129290560130053913469569829490891003991259088290348791943368696942620662946948514931472688923571615032405542263391673583102728579723061998175868700492227418629077079508809336215346303842967525604369606110193842723883107587771653594778681499030978765900869583480043137176832954871752604714113064847270887246697164585218774442100900090916189819413456305028950484575822161887397443918833085509908566008543102796375247476265353031558684515120283396640547496946343986288291957510384781539068343717740714095628337554413567955424664601335663617305811711646062717854078898495334329100315985673932305693426085376230981047171826940937686754301837015557540822371538037838383342702379535934403549452173960327095407712107332936507766465603712364707109272580867897181182493799540477008369348889220963814281561595610931815183701135104790176383595168144627670903450457460997444500166918675661035889313483800512736411157304599205955471122443903196476642761038164285918037488354360663299436899730090925177601162043761411616688128178292382311221745850238080733727204908880095181889576314103157447684338100457385008523652069340710078955916549813037292944462306371284357984809871964143085146878525033128989319500645722582281175483887671061073178169281242483613796475692482076321356427357261609825142445262515952514875273805633150964052552659776922077806644338105562443538136258941809788015677378951310313157361136026047890761945591820289365770116416881703644242694283057457471567494391573593353763114830246668754727566653059819746822346578699972291792416156043557665183382167059157867799311835820189855730344883681934418305987021880502259192818047775223884407167894780414701414651073580452021499197980812095692195622632313741870979731320870864552236740416185590793816745658234353037283309503729022429802768451559528656923189798000383061378732434546500582722712325031420712488100290697226311129067629080951145758060270806092801504406139446350643069742785469477459876821004441453438033759717384777232052065301037861326418823586036569054773343070911759152582503029410738914441818378779490613137536794654893375260322906277631983337976816641721083140551864133302224787118511817036598365960493964571491686005656771360533192423185262166760222073368844844409234470948568027905894191829969467724456269443308241243846160408284006424867072583661011433404214473683453638496544701067827313169538435919120440283949541956874453676459875488726170687163109591315801609722382049772577307454562979127906177531663252857205858766376754282917933549923678212008601904369428956102301731743150352204665675088491593025926618816581008701658499456495586855628208747248318351516339189292646558880593601275151838235485893426165223086697314511412035659916934103076974774451947043836739600076578628245472064617380804602903639144493859012422380173377038154675297645596518492676039300171943042511794045679862114630138402371099347243455794730048929825402680821621522346560274258486595687074510352794291633405915025075992398611224340312056999780516223878772230396359709132856830486160362127579561601328561866388146004722200580017580282279272167842720649966956840905752590774886105493806116954293569077377792821084159737469613143291808510446953973485067590503662391722108732333169909603363771705474725026941732982890400239372879549386540463828596742216318201530139629734398479588628632934746650690284066719018081265539973675916799759010867483920062877888531102781695087545740384607594616919584610655963327283485609570305572502494416337066573150237126843581984154103154401008430380631442183776750349813408169325201240813452285974626715177152223063741359255747513535160669108359443999692315898156732033027129284241219651936303734407981204656795322986357374589031654007016472204989445629050395873788912680565516464274460174738175296313458739390484560414203426465560422112239134631023161290836446988901247285192778589195228773637440432659264672239982186452797664826673070168802722052338600372842903155828454593854349099449420750911108532138744823216151007808922516285123275724355101999038195993350032641446053470357293073912578481757987468353429629749652545426864234949270336399427519354240001973125098882419600095766257217621860474573769577649582201796258392376391717855799468922496750179251915218219624653575570564228220399546682648329822996167217080156801080799777126517156274295763666959661983507435667132218383358509536665806605597148376773866922551603463644386269977295750658468929599809168949981898588529537874489519527097766262684177088590284321676352132630838812766335363319004134332844347630067982023716933653652880580156390360562722752187272454764258840995216482554453662083811789117725225682611478014242896970967121967502094421226279437073328703410646312100557376727450271638975234111426287828736758358819056742163061523416789476056879277154789714326222041069587947186435439940738639948986836168919377836648327137363654676901173760246643082285362494712605173293777247276797635865806019396287718060679122426813922872134061694882029506831654589707623668302556167559477498715183426989208952182644710514911419441192277010977616645850068963849426165593473112961064282379048216056210094265076173838082479030510998790719611852832556787472942907151041468948104916751035295897242381802288151276582257190705537652455285511598636421244284176256230139538669970308943645907600684938040875210854159851278070333207779865635907968462191534944587677170063778573171211036517486371634098385626541555573292664616402279791195975248525300376741774056125700303625811704838385391207273191845064713669122576415213769896260940351804147432053600369234179035440735703058314741623452840188940808983125191307741823338981880316339159565954543405777784331681162551898060409183018907512170192983622897099598983405484962284289398469847938668614293324543983592637036699355184231661615244505980576745765335552338715678211466689996845227042954589710922163652573965950289645637766038988037941517917867910675199009966139206238732318786758420544279396366759104126821843375015743069045967947046685602358283919759975285865384338189120042853787549302768972168199113340697282255535300044743958830079799736518459131437946494086272149669719100359399974735262764126125995350902609540048669398955899487421379590802893196914845826873123710180229775301190684280440780938156598081694611679374425663244656799606363751546304833112722231812338371779800439731087402647536582575657351059978314264831879619843765495877803685261751835391844920488198629786329743136948511780579298636452193232481339393090754566368038513630619718033957979522539508697432546502659123585049283028832934489284591373621624852528877442891851104093746333590660233239711922814450735588373324057814862662207486215513375036775585494138678352928273109003823116855374520901095101174796663003330352534143230024288248051396631446632656081582045216883922312025671065388459503224002320453633895521539919011035217362720909565500846486605368975498478995875596103167696587161281951919668893326641203784750417081752273735270989343717167642329956935697166213782736138899530515711822960896394055380431939398453970864418654291655853168697537052760701061488025700785387150835779480952313152747735711713643356413242974208137266896149109564214803567792270566625834289773407718710649866150447478726164249976671481383053947984958938064202886667951943482750168192023591633247099185942520392818083953020434979919361853380201407072481627304313418985942503858404365993281651941497377286729589582881907490040331593436076189609669494800067194371424058105327517721952474344983414191979918179909864631583246021516575531754156198940698289315745851842783390581029411600498699307751428513021286202539508732388779357409781288187000829944831476678183644656510024467827445695591845768068704978044824105799710771577579093525803824227377612436908709875189149049904225568041463131309240101049368241449253427992201346380538342369643767428862595140146178201810734100565466708236854312816339049676558789901487477972479202502227218169405159042170892104287552188658308608452708423928652597536146290037780167001654671681605343292907573031466562485809639550080023347676187068086526878722783177420214068980703410506200235273632267291964034093571225623659496432076928058165514428643204955256838543079254299909353199329432966018220787933122323225928276556048763399988478426451731890365879756498207607478270258861409976050788036706732268192473513646356758611212953074644777149423343867876705824452296605797007134458987594126654609414211447540007211790607458330686866231309155780005966522736183536340439991445294960728379007338249976020630448806064574892740547730693971337007962746135534442514745423654662752252624869916077111131569725392943756732215758704952417232428206555322808868670153681482911738542735797154157943689491063759749151524510096986573825654899585216747260540468342338610760823605782941948009334370046866568258579827323875158302566720152604684361412652956519894291184887986819088277339147282063794512260294515707367105637720023427811802621502691790400488001808901847311751199425460594416773315777951735444490965752131026306836047140331442314298077895617051256930051804287472368435536402764392777908638966566390166776625678575354239947427919442544664643315554138265543388487778859972063679660692327601733858843763144148113561693030468420017434061395220072403658812798249143261731617813894970955038369479594617979829257740992171922783223006387384996138434398468502234780438733784470928703890536420557474836284616809363650973790900204118525835525201575239280826462555785658190226958376345342663420946214426672453987171047721482128157607275305173330963455909323664528978019175132987747952929099598069790148515839540444283988381797511245355548426126784217797728268989735007954505834273726937288386902125284843370917479603207479554080911491866208687184899550445210616155437083299502854903659617362726552868081324793106686855857401668022408227992433394360936223390321499357262507480617409173636062365464458476384647869520547719533384203403990244761056010612777546471464177412625548519830144627405538601855708359981544891286863480720710061787059669365218674805943569985859699554089329219507269337550235821561424994538234781138316591662683103065194730233419384164076823699357668723462219641322516076261161976034708844046473083172682611277723613381938490606534404043904909864126903479263503943531836741051762565704797064478004684323069430241749029731181951132935746854550484711078742905499870600373983113761544808189067620753424526993443755719446665453524088287267537759197074526286322840219629557247932987132852479994638938924943286917770190128914220188747760484939855471168524810559991574441551507431214406120333762869533792439547155394213121021954430556748370425907553004950664994802614794524739012802842646689229455664958621308118913500279654910344806150170407268010067948926855360944990373928383520627992820181576427054962997401900837493444950600754365525758905546552402103412862124809003162941975876195941956592556732874237856112669741771367104424821916671499611728903944393665340294226514575682907490402153401026923964977275904729573320027982816062130523130658731513076913832317193626664465502290735017347656293033318520949298475227462534564256702254695786484819977513326393221579478212493307051107367474918016345667888810782101151826314878755138027101379868751299375133303843885631415175908928986956197561123025310875057188962535763225834275763348421016668109884514141469311719314272028007223449941999003964948245457520704922091620614222912795322688239046498239081592961111003756999529251250673688233852648213896986384052437049402152187547825163347082430303521036927849762517317825860862215614519165573478940019558704784741658847364803865995119651409542615026615147651220820245816010801218275982577477652393859159165067449846149161165153821266726927461290533753163055654440793427876550267301214578324885948736899073512166118397877342715872870912311383472485146035661382188014840560716074652441118841800734067898587159273982452147328317214621907330492060817440914125388918087968538960627860118193099489240811702350413554126823863744341209267781729790694714759018264824761112414556423937732224538665992861551475342773370683344173073150805440138894084087253197595538897613986400165639906934600670780501058567196636796167140097031535132386972899001749862948883362389858632127176571330142071330179992326381982094042993377790345261665892577931395405145369730429462079488033141099249907113241694504241391265397274078984953073730364134893688060340009640631540701820289244667315059736321311926231179142794944897281477264038321021720718017561601025111179022163703476297572233435788863537030535008357679180120653016668316780269873860755423748298548246360981608957670421903145684942967286646362305101773132268579232832164818921732941553151386988781837232271364011755881332524294135348699384658137175857614330952147617551708342432434174779579226338663454959438736807839569911987059388085500837507984051126658973018149321061950769007587519836861526164087252594820126991923916722273718430385263107266000047367872474915828601694439920041571102706081507270147619679971490141639274282889578424398001497985658130305740620028554097382687819891158955487586486645709231721825870342960508203415938806006561845735081804032347750084214100574577342802985404049555529215986404933246481040773076611691605586804857302606467764258503301836174306413323887707999698641372275526317649662882467901094531117120243890323410259937511584651917675138077575448307953064925086002835629697045016137935696266759775923436166369375035368699454550392874449940328328128905560530091416446608691247256021455381248285307613556149618444364923014290938289373215312818797541139219415606631622784836152140668972661027123715779503062132916001988806369127647416567067485490795342762338253943990022498972883660263920518704790601584084302914787302246651371144395418253441269003331181914268070735159284180415100555199146564934872796969351992963117195821262627236458009708099166752820365818699111948365866102758375863322993225541477479210421324166848264953111826527351008031659958888814809945737293785681411438021523876706455063233067233939551964260397443829874822322662036352861302543796600943104500158604854027036789711934695579989189112302233381602302236277726084846296189550730850698061500281436425336666311433321645213882557346329366870956708432252564333895997812402164189946978348320376011613913855499933990786652305860332060641949298931012423081105800169745975038516887112037747631577311831360002742502722451570906304496369230938382329175076469684003556425503797106891999812319602533733677437970687713814747552190142928586781724044248049323750330957002929126630316970587409214456472022710796484778657310660832173093768033821742156446602190335203981531618935787083561603302255162155107179460621892674335641960083663483835896703409115513087820138723494714321400450513941428998350576038799343355677628023346565854351219361896876831439866735726040869511136649881229957801618882834124004126142251475184552502502640896823664946401177803776799157180146386554733265278569418005501363433953502870836220605121839418516239153709790768084909674194289061134979961034672077354959593868862427986411437928435620575955500144308051267664432183688321434583708549082240014585748228606859593502657405750939203135881722442164955416889785558265198046245527898343289578416968890756237467281044803018524217706136533236073856228166664597654076844715963930782091017090763377917711485205493367936868430832404126789220929930411890501756484917499452393770674524578019171841679541825554377930299249277892416277257788147974770446005423669346157135208417428211847353652367573702352791459837645712257646122605628127852169580892808988394594406165340521932514843306105322700231133680378433377389724881307874325614952744243584753011150345103737688223837573804282007358586938044331529253129961025096113761670187568525921208929131354473196308440066835155160913925692912175784379179004808848023029304392630921342768601226558630456913133560978156776098711809238440656353136182676923761613389237802972720736243967239854144480757286813436768000573823963610796223140429490728058551444771338682314499547929338131259971996894072233847404542592316639781608209399269744676323921370773991899853301483814622364299493902073285072098040905300059160091641710175605409814301906444379905831277826625762288108104414704097708248077905168225857235732665234414956169007985520848841886027352780861218049418060017941147110410688703738674378147161236141950474056521041002268987858525470689031657094677131822113205505046579701869337769278257145248837213394613987859786320048011792814546859096532616616068403160077901584946840224344163938313618742275417712170336151163782359059685168880561304838542087505126933144171705880517278127917564053282929427357971823360842784676292324980318169828654166132873909074116734612367109059236155113860447246378721244612580406931724769152219217409096880209008801535633471775664392125733993165330324425899852598966724744126503608416484160724482125980550754851232313331300621490042708542735985913041306918279258584509440150719217604794274047740253314305451367710311947544521321732225875550489799267468541529538871443696399406391099267018219539890685186755868574434469213792094590683677929528246795437302263472495359466300235998990248299853826140395410812427393530207575128774273992824866921285637240069184859771126480352376025469714309316636539718514623865421671429236191647402172547787238964043145364190541101514371773797752463632741619269990461595895793940622986041489302535678633503526382069821487003578061101552210224486633247184367035502326672749787730470216165019711937442505629639916559369593557640005236360445141148916155147776301876302136068825296274460238077523189646894043033182148655637014692476427395401909403584437251915352134557610698046469739424511797999048754951422010043090235713636892619493763602673645872492900162675597083797995647487354531686531900176427222751039446099641439322672532108666047912598938351926694497553568096931962642014042788365702610390456105151611792018698900673027082384103280213487456720062839744828713298223957579105420819286308176631987048287388639069922461848323992902685392499812367091421613488781501234093387999776097433615750910992585468475923085725368613605356762146929424264323906626708602846163376051573599050869800314239735368928435294958099434465414316189806451480849292695749412903363373410480943579407321266012450796613789442208485840536446021616517885568969302685188950832476793300404851688934411125834396590422211152736276278672366665845757559585409486248261694480201791748223085835007862255216359325125768382924978090431102048708975715033330963651576804501966025215527080352103848176167004443740572131294252820989545456276344353575741673638980108310579931697917916718271145837435222026387771805250290791645414791173616253155840768495583288190293564201219633684854080865928095131505012602919562576032932512847250469881908146475324342363863860247943921015193235101390117789997483527186469346024554247028375300033725403910085997650987642832802908445662021678362267272292737780213652404028817217012490974899454430826861772239385250883760749742195942655217301733355851389407457348144161511380845358039740277795072051893487170722955427683655826706766313911972211811528466502223383490906676554168336907959409404576472940901354356409277969379842065738891481990225399022315913388145851487225126560927576795873759207013915029216513720851137197522734365458411622066281660256333632074449918511469174455062297146086578736313585389023662557285424516018080487167823688885575325066254262367702604215835160174851981885460860036597606743233346410471991027562358645341748631726556391320606407754779439671383653877377610828300019937359760370467245737880967939894493795829602910746901609451288456550071458091887879542641820145369659962842686882363495879277007025298960996798975941955735253914237782443302746708282008722602053415292735847582937522487377937899136764642153727843553986244015856488692101644781661602962113570056638347990334049623875941092886778920270077504951511405782565295015024484968204744379710872943108541684540513016310902267112951959140520827546866418137305837933236150599142045255880213558474751516267815309465541240524091663857551298894834797423322854504140527354235070335984964593699534959698554244978249586929179182415068053002553370412778703476446244329205906832901886692400222391918714603175399666877477960121790688623311002908668305431787009355066944389131913333586368037447530664502418437136030852288582121720231274167009740351431532131803978033680228154223490183737494117973254478594157962104378787072154814091725163615415163381388912588517924237727229603497305533840942889918919161186249580560073570527227874940321250645426206304469470804277945973817146810395192821550688079136701210109944220737024613687196031491162370967939354636396448139025711768057799751751298979667073292674886430097398814873780767363792886767781170520534367705731566895899181530825761606591843760505051704242093231358724816618683821026679970982966436224723644898648976857100173643547336955619347638598187756855912376232580849341570570863450733443976604780386678461711520325115528237161469200634713570383377229877321365028868868859434051205798386937002783312365427450532283462669786446920780944052138528653384627970748017872477988461146015077617116261800781557915472305214759943058006652042710117125674185860274188801377931279938153727692612114066810156521441903567333926116697140453812010040811760123270513163743154487571768761575554916236601762880220601068655524141619314312671535587154866747899398685510873576261006923021359580838145290642217792987748784161516349497309700794368305080955621264592795333690631936594413261117944256602433064619312002953123619348034504503004315096798588111896950537335671086336886944665564112662287921812114121425167348136472449021275252555647623248505638391391630760976364990288930588053406631352470996993362568102360392264043588787550723319888417590521211390376609272658409023873553418516426444865247805763826160023858280693148922231457758783791564902227590699346481624734399733206013058796068136378152964615963260698744961105368384203105364183675373594176373955988088591188920114871545460924735613515979992999722298041707112256996310945945097765566409972722824015293663094891067963296735505830412258608050740410916678539569261234499102819759563955711753011823480304181029089719655278245770283085321733741593938595853203645590564229716679900322284081259569032886928291260139267587858284765599075828016611120063145411315144108875767081854894287737618991537664505164279985451077400771946398046265077776614053524831090497899859510873112620613018757108643735744708366215377470972660188656210681516328000908086198554303597948479869789466434027029290899143432223920333487108261968698934611177160561910681226015874410833093070377506876977485840324132474643763087889666151972556180371472590029550718424245405129246729039791532535999005557334600111693557020225722442772950263840538309433999383388018839553821540371447394465152512354603526742382254148328248990134023054550811390236768038649723899924257800315803725555410178461863478690646045865826036072306952576113184134225274786464852363324759102670562466350802553058142201552282050989197818420425028259521880098846231828512448393059455162005455907776121981297954040150653985341579053629101777939776957892084510979265382905626736402636703151957650493344879513766262192237185642999150828898080904189181015450813145034385734032579549707819385285699926238835221520814478940626889936085239827537174490903769904145555260249190126341431327373827075950390882531223536876389814182564965563294518709637484074360669912550026080424160562533591856230955376566866124027875883101021495284600804805028045254063691285010599912421270508133194975917146762267305044225075915290251742774636494555052325186322411388406191257012917881384181566918237215400893603475101448554254698937834239606460813666829750019379115061709452680984785152862123171377897417492087541064556959508967969794980679770961683057941674310519254486327358885118436597143583348756027405400165571178309126113117314169066606067613797690123141099672013123730329707678988740099317309687380126740538923612230370779727025191340850390101739924877352408881040807749924412635346413181858792480760553268122881584307471326768283097203149049868884456187976015468233715478415429742230166504759393312132256510189175368566338139736836336126010908419590215582111816677413843969205870515074254852744810154541079359513596653630049188769523677579147319184225806802539818418929888943038224766186405856591859943091324575886587044653095332668532261321209825839180538360814144791320319699276037194760191286674308615217243049852806380129834255379486287824758850820609389214668693729881191560115633701248675404205911464930888219050248857645752083363921499441937170268576222251074166230901665867067714568862793343153513505688216165112807318529333124070912343832502302341169501745502360505475824093175657701604884577017762183184615567978427541088499501610912720817913532406784267161792013428902861583277304794830971705537485109380418091491750245433432217445924133037928381694330975012918544596923388733288616144238100112755828623259628572648121538348900698511503485369544461542161283241700533583180520082915722904696365553178152398468725451306350506984981006205514844020769539324155096762680887603572463913955278222246439122592651921288446961107463586148252820017348957533954255019475442643148903233373926763409115527189768429887783617346613535388507656327107814312435018965109238453660236940276060642119384227665755210663671879603217527184404651560427289869560206997012906367847161654793068868305846508082886614111979138822898112498261434559408961813509226857611474609406147937240008842153535862052780125014270055274468359151840373309373580494342483940467505708347927948338133276237937844629209323999417593374917899786484958148818865149169302451512835579818112344900827168644548306546633975256079615935830821400021951611342337058359111545217293721664061708131602078213341260356852013161345136871600980378712556766143923146458085652084039744217352744813741215277475202259244561520365608268890193913957991844109971588312780020898275935898106482117936157951837937026741451400902833064466209280549839169261068975151083963132117128513257434964510681479694782619701483204392206140109523453209269311762298139422044308117317394338867965739135764377642819353621467837436136161591167926578700137748127848510041447845416464568496606699139509524527949914769441031612575776863713634644477006787131066832417871556281779122339077841275184193161188155887229676749605752053192594847679397486414128879475647133049543555044790277128690095643357913405127375570391806822344718167939329121448449553897728696601037841520390662890781218240141299368590465146519209198605347788576842696538459445700169758422531241268031418456268722581132040056433413524302102739213788415250475704533878002467378571470021087314693254557923134757243640544448132093266582986850659125571745568328831440322798049274104403921761438405750750288608423536966715191668510428001748971774811216784160854454400190449242294333666338347684438072624307319019363571067447363413698467328522605570126450123348367412135721830146848071241856625742852208909104583727386227300781566668914250733456373259567253354316171586533339843321723688126003809020585719930855573100508771533737446465211874481748868710652311198691114058503492239156755462142467550498676710264926176510110766876596258810039163948397811986615585196216487695936398904500383258041054420595482859955239065758108017936807080830518996468540836412752905182813744878769639548306385089756146421874889271294890398025623046812175145502330254086076115859321603465240763923593699949180470780496764486889980902123735780457040380820770357387588525976042434608851075199334470112741787878845674656640471901619633546770714090590826954225196409446319547658653032104723804625249971910690110456227579220926904132753699634145768795242244563973018311291451151322757841320376225862458224784696669785947914981610522628786944136373683125108310682898766123782697506343047263278453719024447970975017396831214493357290791648779915089163278018852504558488782722376705263811803792477835540018117452957747339714012352011459901984753358434861297092928529424139865507522507808919352104173963493428604871342370429572757862549365917805401652536330410692033704691093097588782938291296447890613200063096560747882082122140978472301680600835812336957051454650181292694364578357815608503303392466039553797630836137289498678842851139853615593352782103740733076818433040893624460576706096188294529171362940967592507631348636606011346115980434147450705511490716640635688739020690279453438236930531133440901381392849163507484449076828386687476663619303412376248380175840467851210698290605196112357188811150723607303158506622574566366740720668999061320627793994112805759798332878792144188725498543014546662945079670707688135022230580562225942983096887732856788971494623888272184647618153045844390967248232348259587963698908456664795754200195991919240707615823002328977439748112690476546256873684352229063217889227643289360535947903046811114130586348244566489159211382258867880972564351646404364328416076247766114349880319792230537889671148058968061594279189647401954989466232962162567264739015818692956765601444248501821713300527995551312539849919933907083138030214072556753022600033565715934283182650908979350869698950542635843046765145668997627989606295925119763672907762567862769469947280606094290314917493590511523235698715397127866718077578671910380368991445381484562682604003456798248689847811138328054940490519768008320299631757043011485087384048591850157264392187414592464617404735275250506783992273121600117160338604710710015235631159734711153198198710616109850375758965576728904060387168114313084172893710817412764581206119054145955378853200366615264923610030157044627231777788649806700723598889528747481372190175074700005571108178930354895017924552067329003818814068686247959272205591627902292600592107710510448103392878991286820705448979977319695574374529708195463942431669050083984398993036790655541596099324867822475424361758944371791403787168166189093900243862038610001362193667280872414291108080291896093127526202667881902085595708111853836166128848729527875143202956393295910508349687029060692838441522579419764824996318479414814660898281725690484184326061946254276693688953540732363428302189694947766126078346328490315128061501009539164530614554234923393806214007779256337619373052025699319099789404390847443596972052065999017828537676265683558625452697455260991024576619614037537859594506363227095122489241931813728141668427013096050734578659047904243852086508154491350136491698639048125666610843702294730266721499164849610746803261583352580352858275799038584091667618877199539888680431991650866887781701439663176815592262016991396613153738021294160006906947533431677802632207226265881842757216055461439677336258462997385077307751473833315101468395296411397329672457933540390136107395245686243008096720460995545708974893048753897955544443791303790422346037768729236001386569593952300768091377768847789746299699489949016141866131552200856673695770822720338936659590666350594330040363762591189195691561626122704788696510356062748423100605472091437069471661080277379848576543481249822444235828329813543645124092220896643987201997945619030397327254617823136363375927622656301565813545578319730419339269008282952718252138855126583037630477490625995514925943105307478901043009876580816508144862607975129633326675259272351611791836777128931053144471668835182920514343609292493191180249366051791485330421043899773019267686085347768149502299280938065840007311767895491286098112311307002535600347898600653805084532572431553654422067661352337408211307834360326940015926958459588297845649462271300855594293344520727007718206398887404742186697709349647758173683580193168322111365547392288184271373843690526638607662451284299368435082612881367358536293873792369928837047900484722240370919885912556341130849457067599032002751632513926694249485692320904596897775676762684224768120033279577059394613185252356456291805905295974791266162882381429824622654141067246487216174351317397697122228010100668178786776119825961537643641828573481088089988571570279722274734750248439022607880448075724807701621064670166965100202654371260046641935546165838945950143502160890185703558173661823437491622669077311800121188299737319891006060966841193266075165452741829459541189277264192546108246351931647783837078295218389645376236304858042774417907169146356546201215125418664885396161542055152375000426794253417764590821513675258479774465114750438460596325820468809667795709044645884673847481638045635188183210386594798204376334738389017759714236223057776395541011294523488098341476645559342209402059733452337956309441446698222457026367119493286653989491344225517746402732596722993581333110831711807234044326813737231209669052411856734897392234152750707954137453460386506786693396236535556479102508529284294227710593056660625152290924148057080971159783458351173168204129645967070633303569271821496292272073250126955216172649821895790908865085382490848904421755530946832055636316431893917626269931034289485184392539670922412565933079102365485294162132200251193795272480340133135247014182195618419055761030190199521647459734401211601239235679307823190770288415814605647291481745105388060109787505925537152356112290181284710137917215124667428500061818271276125025241876177485994084521492727902567005925854431027704636911098800554312457229683836980470864041706010966962231877065395275783874454229129966623016408054769705821417128636329650130416501278156397799631957412627634011130135082721772287129164002237230234809031485343677016544959380750634285293053131127965945266651960426350406454862543383772209428482543536823186182982713182489884498260285705690699045790998144649193654563259496570044689011049923939218088155626191834404362264965506449848521612498442375928443642612004256628602157801140467879662339228190804577624109076487087406157070486658398144845855803277997327929143195789110373530019873110486895656281917362036703039179710646309906285483702836118486672219457621775034511770110458001291255925462680537427727378863726783016568351092332280649908459179620305691566806180826586923920561895421631986004793961133953226395999749526798801074576466538377400437463695133685671362553184054638475191646737948743270916620098057717103475575333102702706317395612448413745782734376330101853438497450236265733191742446567787499665000938706441886733491099877926005340862442833450486907338279348425305698737469497333364267191968992849534561045719338665222471536681145666596959735075972188416698767321649331898967182978657974612216573922404856900225324160367805329990925438960169901664189038843548375648056012628830409421321300206164540821986138099462721214327234457806819925823202851398237118926541234460723597174777907172041523181575194793527456442984630888846385381068621715274531612303165705848974316209831401326306699896632888532682145204083110738032052784669279984003137878996525635126885368435559620598057278951754498694219326972133205286374577983487319388899574634252048213337552584571056619586932031563299451502519194559691231437579991138301656117185508816658756751184338145761060365142858427872190232598107834593970738225147111878311540875777560020664124562293239116606733386480367086953749244898068000217666674827426925968686433731916548717750106343608307376281613984107392410037196754833838054369880310983922140260514297591221159148505938770679068701351029862207502287721123345624421024715163941251258954337788492834236361124473822814504596821452253550035968325337489186278678359443979041598043992124889848660795045011701169092519383155609441705397900600291315024253848282782826223304151370929502192196508374714697845805550615914539506437316401173317807741497557116733034632008408954066541694665746735785483133770133628948904397670025863002540635264006601631712883920305576358989492412827022489373848906764385339931878608019223108328847459816417701264089078551777830131616162049792779670521847212730327970738223860581986744668610994383049960437407323195784473254857416239738852016202384784256163512597161783106850156299135559874758848151014815490937380933394074455700842090155903853444962128368313687375166780513082594599771257467939781491953642874321122421579851584491669362551569370916855252644720786527971466476760328471332985501945689772758983450586004316822658631176606237201721007922216410188299330808409384014213759697185976897042759041500946595252763487628135867117352364964121058854934496645898651826545634382851159137631569519895230262881794959971545221250667461174394884433312659432286710965281109501693028351496524082850120190831078678067061851145740970787563117610746428835593915985421673115153096948758378955979586132649569817205284291038172721213138681565524428109871168862743968021885581515367531218374119972919471325465199144188500672036481975944167950887487934416759598361960010994838744709079104099785974656112459851972157558134628546189728615020774374529539536929655449012953097288963767713353842429715394179547179095580120134210175150931491664699052366350233024087218654727629639065723341455005903913890253699317155917179823065162679744711857951506573868504088229934804445549850597823297898617029498418376255258757455303112991914341109413088238114443068843062655305601658801408561023324210300218460588586954418502977463085858496130037238190325162225570729975710727306066072916922978033647048840958711228045188511908718588299514331534128549297173849768523136276076868494780364948299904475715771141080958058141208956059471668626290036145602625334863284986816039463372436667112964460292915746181117789169695839947080954788863503281129626899231110099889317815313946681882028368363373822281414974006917942192888817139116283910295684918233358930813360131488748366464224381776081007739183393749346933644748150564933649323157235306109385796839902153381449126925350768211098738352197507736653475499431740580563099143218212547336281359488317681489194306530426029773885492974570569448783077945878865062970895499843760181694031056909587141386804846359853684034105948341788438963179956468815791937174656705047441528027712541569401365862097760735632832966564135817028088013546326104892768731829917950379944446328158595181380144716817284996793061814177131912099236282922612543236071226270324572637946863533391758737446552006008819975294017572421299723542069630427857950608911113416534893431149175314953530067419744979017235181671568754163484949491289001739377451431928382431183263265079530371177806185851153508809998200482761808307209649636476943066172549186143700971387567940218696710148540307471561091358933165600167252126542502898612259306484105898847129649230941215144563947889999327145875969555737090855150648002321476443037232466147111552578583071024936898814562568786834745518893385181791667579054210421036349316257870476543126790661216644142285017446278477132740595579600648343288827864837043456066966456899746910373987712891593313271266247505582258634928427718355831641593667712218537642376222104779338956378722902509543014182257180331300148113377736941508488867501893156994849838936052666818012783912005801431596441910546663236810148207799356523056490420711364192200177189107935243234322761787712568251126481332974354926568682748715986654943041648468220593921673359485057849622807932422649812705271398407720995707236227009245067665680069149966555737866411877079767754867028786431817941521796178310655030287157272282250812017060713380339641841211253856248920130010782462165136989511064611133562443838185366273563783436921279354709230119655914915800561707258518503167289370411936374780625824298250726464801821523430268081486978164824349353456855843696378384153838051184406043696871666416514036129729992912630842812149152469877429332305214999981829046119471676727503742221367186614654042534463141660649871499001000660041544868437352208483059495953182872280520828676300361091734508632133033647289584176588755345227938480297724485711815574893561311524926772006362198369980664159549388683836411891430443767715498026544959061738265591178545999378510861446014967645550103653971251138583505085112442517772923814396233043724036032603181442991365750246012787514117944901305803452199992701148071712847770301254994886841867572975189214295652512486943983729047410363121899124217339550688778643130750024823361832738729697376598820053895902935486054979802320400472236873557411858132734337978931582039412878989728973298812553514507641535360519462112217000676321611195841029252568536561813138784086477147099724553013170761712163186600291464501378587854802096244703771373587720086738054108140042311418525803293267396324596914044834665722042880679280616029884043400536534009706581694636096660911110968789751801325224478246957913251892122653056085866541115373584912790254654369020869419871125588453729063224423222287139122012248769976837147645598526739225904997885514250047585260297929306159913444898341973583316070107516452301310796620382579278533125161760789984630103493496981494261055367836366022561213767081421091373531780682420175737470287189310207606953355721704357535177461573524838432101571399813798596607129664438314791296359275429627129436142685922138993054980645399144588692472767598544271527788443836760149912897358259961869729756588978741082189422337344547375227693199222635973520722998387368484349176841191020246627479579564349615012657433845758638834735832242535328142047826934473129971189346354502994681747128179298167439644524956655532311649920677163664580318205849626132234652606175413532444702007661807418914040158148560001030119994109595492321434406067634769713089513389171050503856336503545166431774489640061738861761193622676890576955693918707703942304940038440622614449572516631017080642923345170422426679607075404028551182398361531383751432493056398381877995594942545196756559181968690885283434886050828529642437578712929439366177362830136595872723080969468398938676366226456791132977469812675226595621009318322081754694778878755356188335083870248295346078597023609865656376722755704495258739871812593441903785275571333409842450127258596692434317689018966145404453679047136294238156127656824247864736176671770647002431119711090007474065945650315375044177982192306323700872039212085499569681061379189029961178936752146022386905665481382858280449537530160921422195940638787074787991194920898374091788534417523064715030278397979864517336625329511775105559014160459873338186887977858817291976604516353353556047648420520888811722831990044504284486852338334530105533929637308039738230604714104525470094899407601215247602819963846343554852932377161410869591950786873276075400085220065031871239272857835807010762542769655355964789450166013816295177908531139811092831583216931563867459747449584385282701658246192092219529134323496779345585613140207765996142546463288677356891785576835169608392864188830094883324700447958316931533832382377876344426323456301679513671047510469669001217777128065522453689371871451567394733440447280450959433090683667110655953338602938000999949010642769859623260401863733572846679531229683156358145420890540651226419162015504500430562136991850941034609601030543816694795964585804425194905110733387679946734471718615647723811737035654917628707589456035519195603962301157866323750234725054461073979402475184415558178087962822231972692984516683306919505079993357259165675557294585962182052650473353712351623662770479333289322136141858785972771685682725303734836891911847197133753088446777943274857148827821608844765700041403499921376794209627560883081509438030705666022764678117533361028187800710219794428777313146387857817205661409023041499923248268982477222109852189758140879763486146763606368674611966620347304608917277240045953051376938375381543486981101990651706961774052218247422657652138152740612699012706880875386408669901461740890540981877671880076124151967064152117653084325544261017536348281196837493395825742541244634247233586360777980960199745187758845459645895956779558869098404768259253477849930457883128541747079059795909431627722327844578918694214929451540174214623240300841907975296782445969183509474202123617940309048634960534054931299919496087957952586977170236680033862505764938088740994009589948109397983231108838769236490221499111120870639202892490698435333152727991330986335454324971441378059132240814960156485679843966464780280409057580889190254236606774500413415794312112501275232250148067232979652230488493751166084976116412777395311302041566848265531411348993243747890268935173904043294851610659785832253168204202834993641595980197343889883020994152152288611175126686173051956249367180053845637855129171848417841594797435580617856680758491080185805695567990185198397660693358224779136504562705766735170961550493338390452612404395517449136885115987454340932040102218982707539212403241042424451570052968378815749468441508011138612561164102477190903050040240662278945607061512108266146098662040425010583978098192019726759010749924884966139441184159734610382401178556739080566483321039073867083298691078093495828888707110651559651222542929154212923108071159723275797510859911398076844732639426419452063138217862260999160086752446265457028969067192282283045169111363652774517975842147102219099906257373383472726498678244401048998507631630668050267115944636293525120269424810854530602810627264236538250773340575475701704367039596467715959261029438313074897245505729085688496091346323165819468660587092144653716755655531962091865952628448253731353698162517351930115341581171353292035873164168839107994000677266031617527582917398395852606454113318985505747847121053505795649095931672167565624818782002769963734155880000867852567422461511406015760115910256449002264980039498403358091309140197877843650167960167465370287466062584346329708303725980494653589318912163976013193079476972058034710553111117215859219066231028099212084069283091906017370764654655683413207556315315006453462321007133584907633048328153458698497332599801187479664273140279381289961720524540674695271948079930396730194274036466594154400092799908634806622334906695224044652158992864203435098858422692019340575496840904812955522654754650713532842543496616084954788090727649930252702815067862810825243222979985391759845188868387004477101866772159439708514664612871148749531862180941719676843144666435175837688436786081446319641912566574047718699160915550910878919431253671945651261878486910876729910565595155159739659034383628124629118117760949411880105946336671039049777312004243578115790429823045072038322781246413671297959415082918378213212876890545963586369344879749784841123274921331663162812456388238288715648447883142417650147980187858215768793063001153788998014623690135803753306246148576074932567807682651045738059018831237617271889933790487113395588485234240255002352200613574914318259142479829367775490496399350755839668967578364316618369307625603528602940662803255416535431518013714821941772672244005268401996533334184004345525296592918502940131600651124395297874364222806977720437363717873457948420238745151249157913139411148608416429347958793681868609689684640858334131017858142710955416293375915178392341303110543328703526599993904966822112768158316511246866451167351378214345336650598328347443536290312393672084593164394941881138607974670134709640378534907149089842317891739783650654751982883367395714360000003439863363212091718954899055748693397700245632475954504411422582410783866837655467400137324322809113692670682805397549111166171102397437749479335174036135005397581475520834285772800986189401984375446435081498218360112577632447389452051636938585136484259964518361856989088721789764694721246807900330925083496645841656554261294195108847197209106605105540933731954888406444080280579549008076040034154662137669606444293774985897353625591959618552448187940317374508256072895120945456562159540405425814886929842786582357673195799285293120866275922366115137445767916063621675267440451221051052090834707443986137829082352772895849625656881972792768694795806100573787084121444815034797422312103295359297822377134077549545477791813823542607184617108389097825964406170543546968567030745411634244134486308676327949177682923093183221341455482591367202823284396549001805653203960795517074496039006696990334199278212696767771835209083959545341866777944872740383733381985235884202840150981579594685874537989503257362809837592216229258598599123843993575573285028613155970362934249814178056461615863415338635077223269996508860870999964899373049307170967888740149746147542880387421250689212155876692242387434701120990859082164073576380817386959755176083877600277517253037133445654852635661720197563001580049790223419586738061442401502436288957503206533690825756785507020555105572381878574650371086308158185862815883054564662297694803970618265491385181326737485227188267917919091354407852685476254126683398240534022469989966652573155637645862251862823092085424412805997628505488913098331761884983352975136073772030571342739638126588567405013841074788943393996603591853934198416322617654857376671943132840050626295140357877264680649549355746326408186979718630218760025813995719923601345374229758918285167511358171472625828596940798518571870075823122317068134867930884899275181661399609753105295773584618525865211893339375771859916335112163441037910451845019023066893064178977808158101360449495409665363660370075881004450265734935127707426742578608784898185628869980851665713320835842613381142623855420315774246613108873106318111989880289722849790551075148403702290580483052731884959994156606537314021296702220821915862905952604040620011815269664910068587592655660567562963361434230232810747488395040380984981860056164646099819257616235478710913832967563761506732550860683433720438748186791668975746563456020002562889601191100980453350423842063824039434163502977688802779835087481178298349417211674919425601608685332435385951152061809031241698182079314615062073826097180458265687043623935757495737332781578904386011378078508110273049446611821957450170106059384336519458628360682108585130499820420578458577175933849015564447305834515291412561679970569657426139901681932056241927977282026714297258700193234337873153939403115411184101414292741703537542003698760608765500109345299007034032401334806388514095769557147190364152027721127070187421548123931953220997506553022646844227700020589045922742423904937051507367764629844971682121994198274794049092601715727439368569721862936007387077810797440975556627807371228030350048829843919546433753355787895064018998685060281902452191177018634505171087023903398550540704454189088472042376499749035038518949505897971286631644699407490959473411581934618336692169573605081585080837952036335619947691937965065016808710250735070825260046821242820434367245824478859256555487861614478717581068572356895150707602217433511627331709472765932413249132702425519391509083601346239612335001086614623850633127072987745618984384288764099836164964775714638573247333226653894523588365972955159905187411779288608760239306160016168434070611663449248395156319152882728822831375458678269830696691220130954815935450754923554167766876455212545681242936427474153815692219503331560151614492247512488957534835926226263545406704767033866410025277276800886383266629488582740369655329362236090572479794734434077704284318507901973469071141230364111729224929307731939309795452877412451183953480382210373644697046967493042810911797232448615413264031578430955396671061468083815548947146733652483679138566431084747848676243012018489329109615281108087617422779131629345494425395422727309645057976122885347393189600810965202090151104579377602529543130188938184010247010134929317443562883578609861545691161669857388024973756940558138630581099823372565164920155443216861690537054630176154809626620800633059320775897175589925862195462096455464624399535391743228225433267174308492508396461328929584567927365409119947616225155964704061297047759818551878441419948614013153859322060745185909608884280218943358691959604936409651570327527570641500776261323783648149005245481413195989296398441371781402764122087644989688629798910870164270169014007825748311598976330612951195680427485317886333041169767175063822135213839779138443325644288490872919067009802496281560626258636942322658490628628035057282983101266919109637258378149363774960594515216932644945188292639525772348420077356021656909077097264985642831778694777804964343991762549216500608626285329471055602670413384500507827390640287529864161287496473708235188892189612641279553536442286955430551308700009878557534223100547153412810957024870812654319123261956462149376527526356402127388765103883255007364899937167183280028398832319373301564123277185395654932422977953016534830128490677845037490891749347389015649588574802194996722621185874361039774946338633057887487405540005440439344888192044102134790034598411927024921557026873700970995205391930979319495883265922171508324621942300185974396706491149559411733728199869021311629886680267446443489233020607003821262841723679627307191405008084085703978151998148822390059948911946474438682533745889962375133378280532928272016815977970066488394482446332210928320504045983008943565954267256879714918703447338237767914829203283196838105907715727191903042365315650957464549643425328069510396558733549803850995143463506175361480050195045201350200180281506933241918267855737764414097080945745624854867704904368368717590918057269794010465019484853146726642978667687697789291431128505043098192949736165944259471754765135205245072597538577958372797702972231435199958499522344049394502115428867244188717409524554771867484911475031801773304689909317974472957035192387686405544278134169807249382219749124257510162187439772902147704638010731470653154201300583810458905006764557332998149945854655105526374914354195867992595981412218735238407957416123372264063860431988936249867649693592569592128495906254446474331759999685163660305216426770428154681777589339252115538590526823311608302751194384823861552852465010329467297198112105314125898165100120742688143577590825227466863206188376830450921784582526239594189673003640808624233657620979111641766331328852352062487922978959456450333733139422384778582717195412347860434376165241568717943562570215636666680088531006728947033079540804583324192188488870712275670333173939262509073556164513677064199539111948881240659821685787131385056850623094155206877987539740658484250135205615103489821873770245063583314243624807432542464195984647411575625441010389671576677263196442524931941806472423789334668561083789808830313571333157729435664956078125304917594015895146954965223118559669048559467607968190167266634650186182955669893965019614544401768162810604465068448139561667220729261210164692339016793399632833013163850830967942792934551268435760356901970523138364640961311774904600772840862214747547653221505518116489887879087780918009050706040061220010051271575991225725282523378026809030528461581739558198122397010092017202251606352922464781615533532275453264543087093320924631855976580561717446840450048285353396546862678852330044967795580761661801833668792312510460809773895565488962815089519622093675058841609752282328250433712970186608193748968699961301486924694482420723632912367052542145464162968910442981633373266871675946715392611950649224725627254543274193495995569590243279097174392258098103601486364409101491734183079646345064833303404765711827040276868271418084574998493392039317445402616663674646668754385093967129918067471909885312710726724428584870694307099756567949198418996425748884764622030325637751112534060087936904565779272035205921345924272965206683338510673615276261016026647772485083344719891986802656197236420847504962661607797092906844757798251795569758235084371746103310387911789239441630112634077535773520558040066982523191225570519133631407211349723226549151062961739050617857127509403623146700931176133132018631158730886798239298009805089491510788371194099750375473674305745187265414016446924576792185753680363289139664155342066705623272936001177781498886100830877849571709880858667023104043242526785955562077310543072298032125941107957349146684680220501816192150766649106862033378713826058987655210423668198670177861672671972374156917880001690656659046965316154923604061891820982414006103779407166342002735828911994182647812782659666207030384795881442790246669264032799404016800137293477301530941805070587421153284642203006550763966756168318897005152026656649929417382840327305940740147117478464839241225676523593418554066440983706083636457657081801664285044258224551650808864421212113914352453935225522162483791737330329812349528984098613273709957407786789349311975204237925022851375880436791854547836416773151821457226504640800104202100410766027807729152555503218182387221708112766208665317651926458452495269685376314437998340336947124447247796973890514941120010934140073794061859447165516612674930799374705772930521750426383798367668159183589049652163726492960837147204067428996276720315410211504333742057182854090136325721437592054640471894328548696883599785122262130812989581571391597464534806099601555877223193450760315411663112963843719400333736013305526352571490454327925190794007111504785378036370897340146753465517470747096935814912797188187854376797751675927822300312945518595042883902735494672667647506072643698761394806879080593531793001711000214417701504495496412454361656210150919997862972495905809191825255486358703529320142005857057855419217730505342687533799076038746689684283402648733290888881745453047194740939258407362058242849349024756883352446212456101562729065130618520732925434179252299417447855189995098959999877410951464170076989305620163502192692653166599093238118295411937545448509428621839424186218067457128099385258842631930670182098008050900019819621758458932516877698594110522845465835679362969619219080897536813210484518784516230623911878024604050824909336069998094776253792973597037759066145994638578378211017122446355845171941670344732162722443265914858595797823752976323442911242311368603724514438765801271594060878788638511089680883165505046309006148832545452819908256238805872042843941834687865142541377686054291079721004271658 diff --git a/pkg/filter/testdata/gettysburg.txt b/pkg/filter/testdata/gettysburg.txt new file mode 100644 index 0000000000000000000000000000000000000000..2c9bcde36057d1579b0463ab543c9f82704cb821 --- /dev/null +++ b/pkg/filter/testdata/gettysburg.txt @@ -0,0 +1,29 @@ + Four score and seven years ago our fathers brought forth on +this continent, a new nation, conceived in Liberty, and dedicated +to the proposition that all men are created equal. + Now we are engaged in a great Civil War, testing whether that +nation, or any nation so conceived and so dedicated, can long +endure. + We are met on a great battle-field of that war. + We have come to dedicate a portion of that field, as a final +resting place for those who here gave their lives that that +nation might live. It is altogether fitting and proper that +we should do this. + But, in a larger sense, we can not dedicate - we can not +consecrate - we can not hallow - this ground. + The brave men, living and dead, who struggled here, have +consecrated it, far above our poor power to add or detract. +The world will little note, nor long remember what we say here, +but it can never forget what they did here. + It is for us the living, rather, to be dedicated here to the +unfinished work which they who fought here have thus far so +nobly advanced. It is rather for us to be here dedicated to +the great task remaining before us - that from these honored +dead we take increased devotion to that cause for which they +gave the last full measure of devotion - + that we here highly resolve that these dead shall not have +died in vain - that this nation, under God, shall have a new +birth of freedom - and that government of the people, by the +people, for the people, shall not perish from this earth. + +Abraham Lincoln, November 19, 1863, Gettysburg, Pennsylvania diff --git a/pkg/filter/testdata/pi.txt b/pkg/filter/testdata/pi.txt new file mode 100644 index 0000000000000000000000000000000000000000..ca99bbc2a2553d83b4a34d062294b9043bbbb46b --- /dev/null +++ b/pkg/filter/testdata/pi.txt @@ -0,0 +1 @@ +3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679821480865132823066470938446095505822317253594081284811174502841027019385211055596446229489549303819644288109756659334461284756482337867831652712019091456485669234603486104543266482133936072602491412737245870066063155881748815209209628292540917153643678925903600113305305488204665213841469519415116094330572703657595919530921861173819326117931051185480744623799627495673518857527248912279381830119491298336733624406566430860213949463952247371907021798609437027705392171762931767523846748184676694051320005681271452635608277857713427577896091736371787214684409012249534301465495853710507922796892589235420199561121290219608640344181598136297747713099605187072113499999983729780499510597317328160963185950244594553469083026425223082533446850352619311881710100031378387528865875332083814206171776691473035982534904287554687311595628638823537875937519577818577805321712268066130019278766111959092164201989380952572010654858632788659361533818279682303019520353018529689957736225994138912497217752834791315155748572424541506959508295331168617278558890750983817546374649393192550604009277016711390098488240128583616035637076601047101819429555961989467678374494482553797747268471040475346462080466842590694912933136770289891521047521620569660240580381501935112533824300355876402474964732639141992726042699227967823547816360093417216412199245863150302861829745557067498385054945885869269956909272107975093029553211653449872027559602364806654991198818347977535663698074265425278625518184175746728909777727938000816470600161452491921732172147723501414419735685481613611573525521334757418494684385233239073941433345477624168625189835694855620992192221842725502542568876717904946016534668049886272327917860857843838279679766814541009538837863609506800642251252051173929848960841284886269456042419652850222106611863067442786220391949450471237137869609563643719172874677646575739624138908658326459958133904780275900994657640789512694683983525957098258226205224894077267194782684826014769909026401363944374553050682034962524517493996514314298091906592509372216964615157098583874105978859597729754989301617539284681382686838689427741559918559252459539594310499725246808459872736446958486538367362226260991246080512438843904512441365497627807977156914359977001296160894416948685558484063534220722258284886481584560285060168427394522674676788952521385225499546667278239864565961163548862305774564980355936345681743241125150760694794510965960940252288797108931456691368672287489405601015033086179286809208747609178249385890097149096759852613655497818931297848216829989487226588048575640142704775551323796414515237462343645428584447952658678210511413547357395231134271661021359695362314429524849371871101457654035902799344037420073105785390621983874478084784896833214457138687519435064302184531910484810053706146806749192781911979399520614196634287544406437451237181921799983910159195618146751426912397489409071864942319615679452080951465502252316038819301420937621378559566389377870830390697920773467221825625996615014215030680384477345492026054146659252014974428507325186660021324340881907104863317346496514539057962685610055081066587969981635747363840525714591028970641401109712062804390397595156771577004203378699360072305587631763594218731251471205329281918261861258673215791984148488291644706095752706957220917567116722910981690915280173506712748583222871835209353965725121083579151369882091444210067510334671103141267111369908658516398315019701651511685171437657618351556508849099898599823873455283316355076479185358932261854896321329330898570642046752590709154814165498594616371802709819943099244889575712828905923233260972997120844335732654893823911932597463667305836041428138830320382490375898524374417029132765618093773444030707469211201913020330380197621101100449293215160842444859637669838952286847831235526582131449576857262433441893039686426243410773226978028073189154411010446823252716201052652272111660396665573092547110557853763466820653109896526918620564769312570586356620185581007293606598764861179104533488503461136576867532494416680396265797877185560845529654126654085306143444318586769751456614068007002378776591344017127494704205622305389945613140711270004078547332699390814546646458807972708266830634328587856983052358089330657574067954571637752542021149557615814002501262285941302164715509792592309907965473761255176567513575178296664547791745011299614890304639947132962107340437518957359614589019389713111790429782856475032031986915140287080859904801094121472213179476477726224142548545403321571853061422881375850430633217518297986622371721591607716692547487389866549494501146540628433663937900397692656721463853067360965712091807638327166416274888800786925602902284721040317211860820419000422966171196377921337575114959501566049631862947265473642523081770367515906735023507283540567040386743513622224771589150495309844489333096340878076932599397805419341447377441842631298608099888687413260472156951623965864573021631598193195167353812974167729478672422924654366800980676928238280689964004824354037014163149658979409243237896907069779422362508221688957383798623001593776471651228935786015881617557829735233446042815126272037343146531977774160319906655418763979293344195215413418994854447345673831624993419131814809277771038638773431772075456545322077709212019051660962804909263601975988281613323166636528619326686336062735676303544776280350450777235547105859548702790814356240145171806246436267945612753181340783303362542327839449753824372058353114771199260638133467768796959703098339130771098704085913374641442822772634659470474587847787201927715280731767907707157213444730605700733492436931138350493163128404251219256517980694113528013147013047816437885185290928545201165839341965621349143415956258658655705526904965209858033850722426482939728584783163057777560688876446248246857926039535277348030480290058760758251047470916439613626760449256274204208320856611906254543372131535958450687724602901618766795240616342522577195429162991930645537799140373404328752628889639958794757291746426357455254079091451357111369410911939325191076020825202618798531887705842972591677813149699009019211697173727847684726860849003377024242916513005005168323364350389517029893922334517220138128069650117844087451960121228599371623130171144484640903890644954440061986907548516026327505298349187407866808818338510228334508504860825039302133219715518430635455007668282949304137765527939751754613953984683393638304746119966538581538420568533862186725233402830871123282789212507712629463229563989898935821167456270102183564622013496715188190973038119800497340723961036854066431939509790190699639552453005450580685501956730229219139339185680344903982059551002263535361920419947455385938102343955449597783779023742161727111723643435439478221818528624085140066604433258885698670543154706965747458550332323342107301545940516553790686627333799585115625784322988273723198987571415957811196358330059408730681216028764962867446047746491599505497374256269010490377819868359381465741268049256487985561453723478673303904688383436346553794986419270563872931748723320837601123029911367938627089438799362016295154133714248928307220126901475466847653576164773794675200490757155527819653621323926406160136358155907422020203187277605277219005561484255518792530343513984425322341576233610642506390497500865627109535919465897514131034822769306247435363256916078154781811528436679570611086153315044521274739245449454236828860613408414863776700961207151249140430272538607648236341433462351897576645216413767969031495019108575984423919862916421939949072362346468441173940326591840443780513338945257423995082965912285085558215725031071257012668302402929525220118726767562204154205161841634847565169998116141010029960783869092916030288400269104140792886215078424516709087000699282120660418371806535567252532567532861291042487761825829765157959847035622262934860034158722980534989650226291748788202734209222245339856264766914905562842503912757710284027998066365825488926488025456610172967026640765590429099456815065265305371829412703369313785178609040708667114965583434347693385781711386455873678123014587687126603489139095620099393610310291616152881384379099042317473363948045759314931405297634757481193567091101377517210080315590248530906692037671922033229094334676851422144773793937517034436619910403375111735471918550464490263655128162288244625759163330391072253837421821408835086573917715096828874782656995995744906617583441375223970968340800535598491754173818839994469748676265516582765848358845314277568790029095170283529716344562129640435231176006651012412006597558512761785838292041974844236080071930457618932349229279650198751872127267507981255470958904556357921221033346697499235630254947802490114195212382815309114079073860251522742995818072471625916685451333123948049470791191532673430282441860414263639548000448002670496248201792896476697583183271314251702969234889627668440323260927524960357996469256504936818360900323809293459588970695365349406034021665443755890045632882250545255640564482465151875471196218443965825337543885690941130315095261793780029741207665147939425902989695946995565761218656196733786236256125216320862869222103274889218654364802296780705765615144632046927906821207388377814233562823608963208068222468012248261177185896381409183903673672220888321513755600372798394004152970028783076670944474560134556417254370906979396122571429894671543578468788614445812314593571984922528471605049221242470141214780573455105008019086996033027634787081081754501193071412233908663938339529425786905076431006383519834389341596131854347546495569781038293097164651438407007073604112373599843452251610507027056235266012764848308407611830130527932054274628654036036745328651057065874882256981579367897669742205750596834408697350201410206723585020072452256326513410559240190274216248439140359989535394590944070469120914093870012645600162374288021092764579310657922955249887275846101264836999892256959688159205600101655256375678566722796619885782794848855834397518744545512965634434803966420557982936804352202770984294232533022576341807039476994159791594530069752148293366555661567873640053666564165473217043903521329543529169414599041608753201868379370234888689479151071637852902345292440773659495630510074210871426134974595615138498713757047101787957310422969066670214498637464595280824369445789772330048764765241339075920434019634039114732023380715095222010682563427471646024335440051521266932493419673977041595683753555166730273900749729736354964533288869844061196496162773449518273695588220757355176651589855190986665393549481068873206859907540792342402300925900701731960362254756478940647548346647760411463233905651343306844953979070903023460461470961696886885014083470405460742958699138296682468185710318879065287036650832431974404771855678934823089431068287027228097362480939962706074726455399253994428081137369433887294063079261595995462624629707062594845569034711972996409089418059534393251236235508134949004364278527138315912568989295196427287573946914272534366941532361004537304881985517065941217352462589548730167600298865925786628561249665523533829428785425340483083307016537228563559152534784459818313411290019992059813522051173365856407826484942764411376393866924803118364453698589175442647399882284621844900877769776312795722672655562596282542765318300134070922334365779160128093179401718598599933849235495640057099558561134980252499066984233017350358044081168552653117099570899427328709258487894436460050410892266917835258707859512983441729535195378855345737426085902908176515578039059464087350612322611200937310804854852635722825768203416050484662775045003126200800799804925485346941469775164932709504934639382432227188515974054702148289711177792376122578873477188196825462981268685817050740272550263329044976277894423621674119186269439650671515779586756482399391760426017633870454990176143641204692182370764887834196896861181558158736062938603810171215855272668300823834046564758804051380801633638874216371406435495561868964112282140753302655100424104896783528588290243670904887118190909494533144218287661810310073547705498159680772009474696134360928614849417850171807793068108546900094458995279424398139213505586422196483491512639012803832001097738680662877923971801461343244572640097374257007359210031541508936793008169980536520276007277496745840028362405346037263416554259027601834840306811381855105979705664007509426087885735796037324514146786703688098806097164258497595138069309449401515422221943291302173912538355915031003330325111749156969174502714943315155885403922164097229101129035521815762823283182342548326111912800928252561902052630163911477247331485739107775874425387611746578671169414776421441111263583553871361011023267987756410246824032264834641766369806637857681349204530224081972785647198396308781543221166912246415911776732253264335686146186545222681268872684459684424161078540167681420808850280054143613146230821025941737562389942075713627516745731891894562835257044133543758575342698699472547031656613991999682628247270641336222178923903176085428943733935618891651250424404008952719837873864805847268954624388234375178852014395600571048119498842390606136957342315590796703461491434478863604103182350736502778590897578272731305048893989009923913503373250855982655867089242612429473670193907727130706869170926462548423240748550366080136046689511840093668609546325002145852930950000907151058236267293264537382104938724996699339424685516483261134146110680267446637334375340764294026682973865220935701626384648528514903629320199199688285171839536691345222444708045923966028171565515656661113598231122506289058549145097157553900243931535190902107119457300243880176615035270862602537881797519478061013715004489917210022201335013106016391541589578037117792775225978742891917915522417189585361680594741234193398420218745649256443462392531953135103311476394911995072858430658361935369329699289837914941939406085724863968836903265564364216644257607914710869984315733749648835292769328220762947282381537409961545598798259891093717126218283025848112389011968221429457667580718653806506487026133892822994972574530332838963818439447707794022843598834100358385423897354243956475556840952248445541392394100016207693636846776413017819659379971557468541946334893748439129742391433659360410035234377706588867781139498616478747140793263858738624732889645643598774667638479466504074111825658378878454858148962961273998413442726086061872455452360643153710112746809778704464094758280348769758948328241239292960582948619196670918958089833201210318430340128495116203534280144127617285830243559830032042024512072872535581195840149180969253395075778400067465526031446167050827682772223534191102634163157147406123850425845988419907611287258059113935689601431668283176323567325417073420817332230462987992804908514094790368878687894930546955703072619009502076433493359106024545086453628935456862958531315337183868265617862273637169757741830239860065914816164049449650117321313895747062088474802365371031150898427992754426853277974311395143574172219759799359685252285745263796289612691572357986620573408375766873884266405990993505000813375432454635967504844235284874701443545419576258473564216198134073468541117668831186544893776979566517279662326714810338643913751865946730024434500544995399742372328712494834706044063471606325830649829795510109541836235030309453097335834462839476304775645015008507578949548931393944899216125525597701436858943585877526379625597081677643800125436502371412783467926101995585224717220177723700417808419423948725406801556035998390548985723546745642390585850216719031395262944554391316631345308939062046784387785054239390524731362012947691874975191011472315289326772533918146607300089027768963114810902209724520759167297007850580717186381054967973100167870850694207092232908070383263453452038027860990556900134137182368370991949516489600755049341267876436746384902063964019766685592335654639138363185745698147196210841080961884605456039038455343729141446513474940784884423772175154334260306698831768331001133108690421939031080143784334151370924353013677631084913516156422698475074303297167469640666531527035325467112667522460551199581831963763707617991919203579582007595605302346267757943936307463056901080114942714100939136913810725813781357894005599500183542511841721360557275221035268037357265279224173736057511278872181908449006178013889710770822931002797665935838758909395688148560263224393726562472776037890814458837855019702843779362407825052704875816470324581290878395232453237896029841669225489649715606981192186584926770403956481278102179913217416305810554598801300484562997651121241536374515005635070127815926714241342103301566165356024733807843028655257222753049998837015348793008062601809623815161366903341111386538510919367393835229345888322550887064507539473952043968079067086806445096986548801682874343786126453815834280753061845485903798217994599681154419742536344399602902510015888272164745006820704193761584547123183460072629339550548239557137256840232268213012476794522644820910235647752723082081063518899152692889108455571126603965034397896278250016110153235160519655904211844949907789992007329476905868577878720982901352956613978884860509786085957017731298155314951681467176959760994210036183559138777817698458758104466283998806006162298486169353373865787735983361613384133853684211978938900185295691967804554482858483701170967212535338758621582310133103877668272115726949518179589754693992642197915523385766231676275475703546994148929041301863861194391962838870543677743224276809132365449485366768000001065262485473055861598999140170769838548318875014293890899506854530765116803337322265175662207526951791442252808165171667766727930354851542040238174608923283917032754257508676551178593950027933895920576682789677644531840404185540104351348389531201326378369283580827193783126549617459970567450718332065034556644034490453627560011250184335607361222765949278393706478426456763388188075656121689605041611390390639601620221536849410926053876887148379895599991120991646464411918568277004574243434021672276445589330127781586869525069499364610175685060167145354315814801054588605645501332037586454858403240298717093480910556211671546848477803944756979804263180991756422809873998766973237695737015808068229045992123661689025962730430679316531149401764737693873514093361833216142802149763399189835484875625298752423873077559555955465196394401821840998412489826236737714672260616336432964063357281070788758164043814850188411431885988276944901193212968271588841338694346828590066640806314077757725705630729400492940302420498416565479736705485580445865720227637840466823379852827105784319753541795011347273625774080213476826045022851579795797647467022840999561601569108903845824502679265942055503958792298185264800706837650418365620945554346135134152570065974881916341359556719649654032187271602648593049039787489589066127250794828276938953521753621850796297785146188432719223223810158744450528665238022532843891375273845892384422535472653098171578447834215822327020690287232330053862163479885094695472004795231120150432932266282727632177908840087861480221475376578105819702226309717495072127248479478169572961423658595782090830733233560348465318730293026659645013718375428897557971449924654038681799213893469244741985097334626793321072686870768062639919361965044099542167627840914669856925715074315740793805323925239477557441591845821562518192155233709607483329234921034514626437449805596103307994145347784574699992128599999399612281615219314888769388022281083001986016549416542616968586788372609587745676182507275992950893180521872924610867639958916145855058397274209809097817293239301067663868240401113040247007350857828724627134946368531815469690466968693925472519413992914652423857762550047485295476814795467007050347999588867695016124972282040303995463278830695976249361510102436555352230690612949388599015734661023712235478911292547696176005047974928060721268039226911027772261025441492215765045081206771735712027180242968106203776578837166909109418074487814049075517820385653909910477594141321543284406250301802757169650820964273484146957263978842560084531214065935809041271135920041975985136254796160632288736181367373244506079244117639975974619383584574915988097667447093006546342423460634237474666080431701260052055928493695941434081468529815053947178900451835755154125223590590687264878635752541911288877371766374860276606349603536794702692322971868327717393236192007774522126247518698334951510198642698878471719396649769070825217423365662725928440620430214113719922785269984698847702323823840055655517889087661360130477098438611687052310553149162517283732728676007248172987637569816335415074608838663640693470437206688651275688266149730788657015685016918647488541679154596507234287730699853713904300266530783987763850323818215535597323530686043010675760838908627049841888595138091030423595782495143988590113185835840667472370297149785084145853085781339156270760356390763947311455495832266945702494139831634332378975955680856836297253867913275055542524491943589128405045226953812179131914513500993846311774017971512283785460116035955402864405902496466930707769055481028850208085800878115773817191741776017330738554758006056014337743299012728677253043182519757916792969965041460706645712588834697979642931622965520168797300035646304579308840327480771811555330909887025505207680463034608658165394876951960044084820659673794731680864156456505300498816164905788311543454850526600698230931577765003780704661264706021457505793270962047825615247145918965223608396645624105195510522357239739512881816405978591427914816542632892004281609136937773722299983327082082969955737727375667615527113922588055201898876201141680054687365580633471603734291703907986396522961312801782679717289822936070288069087768660593252746378405397691848082041021944719713869256084162451123980620113184541244782050110798760717155683154078865439041210873032402010685341947230476666721749869868547076781205124736792479193150856444775379853799732234456122785843296846647513336573692387201464723679427870042503255589926884349592876124007558756946413705625140011797133166207153715436006876477318675587148783989081074295309410605969443158477539700943988394914432353668539209946879645066533985738887866147629443414010498889931600512076781035886116602029611936396821349607501116498327856353161451684576956871090029997698412632665023477167286573785790857466460772283415403114415294188047825438761770790430001566986776795760909966936075594965152736349811896413043311662774712338817406037317439705406703109676765748695358789670031925866259410510533584384656023391796749267844763708474978333655579007384191473198862713525954625181604342253729962863267496824058060296421146386436864224724887283434170441573482481833301640566959668866769563491416328426414974533349999480002669987588815935073578151958899005395120853510357261373640343675347141048360175464883004078464167452167371904831096767113443494819262681110739948250607394950735031690197318521195526356325843390998224986240670310768318446607291248747540316179699411397387765899868554170318847788675929026070043212666179192235209382278788809886335991160819235355570464634911320859189796132791319756490976000139962344455350143464268604644958624769094347048293294140411146540923988344435159133201077394411184074107684981066347241048239358274019449356651610884631256785297769734684303061462418035852933159734583038455410337010916767763742762102137013548544509263071901147318485749233181672072137279355679528443925481560913728128406333039373562420016045664557414588166052166608738748047243391212955877763906969037078828527753894052460758496231574369171131761347838827194168606625721036851321566478001476752310393578606896111259960281839309548709059073861351914591819510297327875571049729011487171897180046961697770017913919613791417162707018958469214343696762927459109940060084983568425201915593703701011049747339493877885989417433031785348707603221982970579751191440510994235883034546353492349826883624043327267415540301619505680654180939409982020609994140216890900708213307230896621197755306659188141191577836272927461561857103721724710095214236964830864102592887457999322374955191221951903424452307535133806856807354464995127203174487195403976107308060269906258076020292731455252078079914184290638844373499681458273372072663917670201183004648190002413083508846584152148991276106513741539435657211390328574918769094413702090517031487773461652879848235338297260136110984514841823808120540996125274580881099486972216128524897425555516076371675054896173016809613803811914361143992106380050832140987604599309324851025168294467260666138151745712559754953580239983146982203613380828499356705575524712902745397762140493182014658008021566536067765508783804304134310591804606800834591136640834887408005741272586704792258319127415739080914383138456424150940849133918096840251163991936853225557338966953749026620923261318855891580832455571948453875628786128859004106006073746501402627824027346962528217174941582331749239683530136178653673760642166778137739951006589528877427662636841830680190804609849809469763667335662282915132352788806157768278159588669180238940333076441912403412022316368577860357276941541778826435238131905028087018575047046312933353757285386605888904583111450773942935201994321971171642235005644042979892081594307167019857469273848653833436145794634175922573898588001698014757420542995801242958105456510831046297282937584161162532562516572498078492099897990620035936509934721582965174135798491047111660791587436986541222348341887722929446335178653856731962559852026072947674072616767145573649812105677716893484917660771705277187601199908144113058645577910525684304811440261938402322470939249802933550731845890355397133088446174107959162511714864874468611247605428673436709046678468670274091881014249711149657817724279347070216688295610877794405048437528443375108828264771978540006509704033021862556147332117771174413350281608840351781452541964320309576018694649088681545285621346988355444560249556668436602922195124830910605377201980218310103270417838665447181260397190688462370857518080035327047185659499476124248110999288679158969049563947624608424065930948621507690314987020673533848349550836366017848771060809804269247132410009464014373603265645184566792456669551001502298330798496079949882497061723674493612262229617908143114146609412341593593095854079139087208322733549572080757165171876599449856937956238755516175754380917805280294642004472153962807463602113294255916002570735628126387331060058910652457080244749375431841494014821199962764531068006631183823761639663180931444671298615527598201451410275600689297502463040173514891945763607893528555053173314164570504996443890936308438744847839616840518452732884032345202470568516465716477139323775517294795126132398229602394548579754586517458787713318138752959809412174227300352296508089177705068259248822322154938048371454781647213976820963320508305647920482085920475499857320388876391601995240918938945576768749730856955958010659526503036266159750662225084067428898265907510637563569968211510949669744580547288693631020367823250182323708459790111548472087618212477813266330412076216587312970811230758159821248639807212407868878114501655825136178903070860870198975889807456643955157415363193191981070575336633738038272152798849350397480015890519420879711308051233933221903466249917169150948541401871060354603794643379005890957721180804465743962806186717861017156740967662080295766577051291209907944304632892947306159510430902221439371849560634056189342513057268291465783293340524635028929175470872564842600349629611654138230077313327298305001602567240141851520418907011542885799208121984493156999059182011819733500126187728036812481995877070207532406361259313438595542547781961142935163561223496661522614735399674051584998603552953329245752388810136202347624669055816438967863097627365504724348643071218494373485300606387644566272186661701238127715621379746149861328744117714552444708997144522885662942440230184791205478498574521634696448973892062401943518310088283480249249085403077863875165911302873958787098100772718271874529013972836614842142871705531796543076504534324600536361472618180969976933486264077435199928686323835088756683595097265574815431940195576850437248001020413749831872259677387154958399718444907279141965845930083942637020875635398216962055324803212267498911402678528599673405242031091797899905718821949391320753431707980023736590985375520238911643467185582906853711897952626234492483392496342449714656846591248918556629589329909035239233333647435203707701010843880032907598342170185542283861617210417603011645918780539367447472059985023582891833692922337323999480437108419659473162654825748099482509991833006976569367159689364493348864744213500840700660883597235039532340179582557036016936990988671132109798897070517280755855191269930673099250704070245568507786790694766126298082251633136399521170984528092630375922426742575599892892783704744452189363203489415521044597261883800300677617931381399162058062701651024458869247649246891924612125310275731390840470007143561362316992371694848132554200914530410371354532966206392105479824392125172540132314902740585892063217589494345489068463993137570910346332714153162232805522972979538018801628590735729554162788676498274186164218789885741071649069191851162815285486794173638906653885764229158342500673612453849160674137340173572779956341043326883569507814931378007362354180070619180267328551191942676091221035987469241172837493126163395001239599240508454375698507957046222664619000103500490183034153545842833764378111988556318777792537201166718539541835984438305203762819440761594106820716970302285152250573126093046898423433152732131361216582808075212631547730604423774753505952287174402666389148817173086436111389069420279088143119448799417154042103412190847094080254023932942945493878640230512927119097513536000921971105412096683111516328705423028470073120658032626417116165957613272351566662536672718998534199895236884830999302757419916463841427077988708874229277053891227172486322028898425125287217826030500994510824783572905691988555467886079462805371227042466543192145281760741482403827835829719301017888345674167811398954750448339314689630763396657226727043393216745421824557062524797219978668542798977992339579057581890622525473582205236424850783407110144980478726691990186438822932305382318559732869780922253529591017341407334884761005564018242392192695062083183814546983923664613639891012102177095976704908305081854704194664371312299692358895384930136356576186106062228705599423371631021278457446463989738188566746260879482018647487672727222062676465338099801966883680994159075776852639865146253336312450536402610569605513183813174261184420189088853196356986962795036738424313011331753305329802016688817481342988681585577810343231753064784983210629718425184385534427620128234570716988530518326179641178579608888150329602290705614476220915094739035946646916235396809201394578175891088931992112260073928149169481615273842736264298098234063200244024495894456129167049508235812487391799648641133480324757775219708932772262349486015046652681439877051615317026696929704928316285504212898146706195331970269507214378230476875280287354126166391708245925170010714180854800636923259462019002278087409859771921805158532147392653251559035410209284665925299914353791825314545290598415817637058927906909896911164381187809435371521332261443625314490127454772695739393481546916311624928873574718824071503995009446731954316193855485207665738825139639163576723151005556037263394867208207808653734942440115799667507360711159351331959197120948964717553024531364770942094635696982226673775209945168450643623824211853534887989395673187806606107885440005508276570305587448541805778891719207881423351138662929667179643468760077047999537883387870348718021842437342112273940255717690819603092018240188427057046092622564178375265263358324240661253311529423457965569502506810018310900411245379015332966156970522379210325706937051090830789479999004999395322153622748476603613677697978567386584670936679588583788795625946464891376652199588286933801836011932368578558558195556042156250883650203322024513762158204618106705195330653060606501054887167245377942831338871631395596905832083416898476065607118347136218123246227258841990286142087284956879639325464285343075301105285713829643709990356948885285190402956047346131138263878897551788560424998748316382804046848618938189590542039889872650697620201995548412650005394428203930127481638158530396439925470201672759328574366661644110962566337305409219519675148328734808957477775278344221091073111351828046036347198185655572957144747682552857863349342858423118749440003229690697758315903858039353521358860079600342097547392296733310649395601812237812854584317605561733861126734780745850676063048229409653041118306671081893031108871728167519579675347188537229309616143204006381322465841111157758358581135018569047815368938137718472814751998350504781297718599084707621974605887423256995828892535041937958260616211842368768511418316068315867994601652057740529423053601780313357263267054790338401257305912339601880137825421927094767337191987287385248057421248921183470876629667207272325650565129333126059505777727542471241648312832982072361750574673870128209575544305968395555686861188397135522084452852640081252027665557677495969626612604565245684086139238265768583384698499778726706555191854468698469478495734622606294219624557085371272776523098955450193037732166649182578154677292005212667143463209637891852323215018976126034373684067194193037746880999296877582441047878123266253181845960453853543839114496775312864260925211537673258866722604042523491087026958099647595805794663973419064010036361904042033113579336542426303561457009011244800890020801478056603710154122328891465722393145076071670643556827437743965789067972687438473076346451677562103098604092717090951280863090297385044527182892749689212106670081648583395537735919136950153162018908887484210798706899114804669270650940762046502772528650728905328548561433160812693005693785417861096969202538865034577183176686885923681488475276498468821949739729707737187188400414323127636504814531122850990020742409255859252926103021067368154347015252348786351643976235860419194129697690405264832347009911154242601273438022089331096686367898694977994001260164227609260823493041180643829138347354679725399262338791582998486459271734059225620749105308531537182911681637219395188700957788181586850464507699343940987433514431626330317247747486897918209239480833143970840673084079589358108966564775859905563769525232653614424780230826811831037735887089240613031336477371011628214614661679404090518615260360092521947218890918107335871964142144478654899528582343947050079830388538860831035719306002771194558021911942899922722353458707566246926177663178855144350218287026685610665003531050216318206017609217984684936863161293727951873078972637353717150256378733579771808184878458866504335824377004147710414934927438457587107159731559439426412570270965125108115548247939403597681188117282472158250109496096625393395380922195591918188552678062149923172763163218339896938075616855911752998450132067129392404144593862398809381240452191484831646210147389182510109096773869066404158973610476436500068077105656718486281496371118832192445663945814491486165500495676982690308911185687986929470513524816091743243015383684707292898982846022237301452655679898627767968091469798378268764311598832109043715611299766521539635464420869197567370005738764978437686287681792497469438427465256316323005551304174227341646455127812784577772457520386543754282825671412885834544435132562054464241011037955464190581168623059644769587054072141985212106734332410756767575818456990693046047522770167005684543969234041711089888993416350585157887353430815520811772071880379104046983069578685473937656433631979786803671873079693924236321448450354776315670255390065423117920153464977929066241508328858395290542637687668968805033317227800185885069736232403894700471897619347344308437443759925034178807972235859134245813144049847701732361694719765715353197754997162785663119046912609182591249890367654176979903623755286526375733763526969344354400473067198868901968147428767790866979688522501636949856730217523132529265375896415171479559538784278499866456302878831962099830494519874396369070682762657485810439112232618794059941554063270131989895703761105323606298674803779153767511583043208498720920280929752649812569163425000522908872646925284666104665392171482080130502298052637836426959733707053922789153510568883938113249757071331029504430346715989448786847116438328050692507766274500122003526203709466023414648998390252588830148678162196775194583167718762757200505439794412459900771152051546199305098386982542846407255540927403132571632640792934183342147090412542533523248021932277075355546795871638358750181593387174236061551171013123525633485820365146141870049205704372018261733194715700867578539336078622739558185797587258744102542077105475361294047460100094095444959662881486915903899071865980563617137692227290764197755177720104276496949611056220592502420217704269622154958726453989227697660310524980855759471631075870133208861463266412591148633881220284440694169488261529577625325019870359870674380469821942056381255833436421949232275937221289056420943082352544084110864545369404969271494003319782861318186188811118408257865928757426384450059944229568586460481033015388911499486935436030221810943466764000022362550573631294626296096198760564259963946138692330837196265954739234624134597795748524647837980795693198650815977675350553918991151335252298736112779182748542008689539658359421963331502869561192012298889887006079992795411188269023078913107603617634779489432032102773359416908650071932804017163840644987871753756781185321328408216571107549528294974936214608215583205687232185574065161096274874375098092230211609982633033915469494644491004515280925089745074896760324090768983652940657920198315265410658136823791984090645712468948470209357761193139980246813405200394781949866202624008902150166163813538381515037735022966074627952910384068685569070157516624192987244482719429331004854824454580718897633003232525821581280327467962002814762431828622171054352898348208273451680186131719593324711074662228508710666117703465352839577625997744672185715816126411143271794347885990892808486694914139097716736900277758502686646540565950394867841110790116104008572744562938425494167594605487117235946429105850909950214958793112196135908315882620682332156153086833730838173279328196983875087083483880463884784418840031847126974543709373298362402875197920802321878744882872843727378017827008058782410749357514889978911739746129320351081432703251409030487462262942344327571260086642508333187688650756429271605525289544921537651751492196367181049435317858383453865255656640657251363575064353236508936790431702597878177190314867963840828810209461490079715137717099061954969640070867667102330048672631475510537231757114322317411411680622864206388906210192355223546711662137499693269321737043105987225039456574924616978260970253359475020913836673772894438696400028110344026084712899000746807764844088711341352503367877316797709372778682166117865344231732264637847697875144332095340001650692130546476890985050203015044880834261845208730530973189492916425322933612431514306578264070283898409841602950309241897120971601649265613413433422298827909921786042679812457285345801338260995877178113102167340256562744007296834066198480676615805021691833723680399027931606420436812079900316264449146190219458229690992122788553948783538305646864881655562294315673128274390826450611628942803501661336697824051770155219626522725455850738640585299830379180350432876703809252167907571204061237596327685674845079151147313440001832570344920909712435809447900462494313455028900680648704293534037436032625820535790118395649089354345101342969617545249573960621490288728932792520696535386396443225388327522499605986974759882329916263545973324445163755334377492928990581175786355555626937426910947117002165411718219750519831787137106051063795558588905568852887989084750915764639074693619881507814685262133252473837651192990156109189777922008705793396463827490680698769168197492365624226087154176100430608904377976678519661891404144925270480881971498801542057787006521594009289777601330756847966992955433656139847738060394368895887646054983871478968482805384701730871117761159663505039979343869339119789887109156541709133082607647406305711411098839388095481437828474528838368079418884342666222070438722887413947801017721392281911992365405516395893474263953824829609036900288359327745855060801317988407162446563997948275783650195514221551339281978226984278638391679715091262410548725700924070045488485692950448110738087996547481568913935380943474556972128919827177020766613602489581468119133614121258783895577357194986317210844398901423948496659251731388171602663261931065366535041473070804414939169363262373767777095850313255990095762731957308648042467701212327020533742667053142448208168130306397378736642483672539837487690980602182785786216512738563513290148903509883270617258932575363993979055729175160097615459044771692265806315111028038436017374742152476085152099016158582312571590733421736576267142390478279587281505095633092802668458937649649770232973641319060982740633531089792464242134583740901169391964250459128813403498810635400887596820054408364386516617880557608956896727531538081942077332597917278437625661184319891025007491829086475149794003160703845549465385946027452447466812314687943441610993338908992638411847425257044572517459325738989565185716575961481266020310797628254165590506042479114016957900338356574869252800743025623419498286467914476322774005529460903940177536335655471931000175430047504719144899841040015867946179241610016454716551337074073950260442769538553834397550548871099785205401175169747581344926079433689543783221172450687344231989878844128542064742809735625807066983106979935260693392135685881391214807354728463227784908087002467776303605551232386656295178853719673034634701222939581606792509153217489030840886516061119011498443412350124646928028805996134283511884715449771278473361766285062169778717743824362565711779450064477718370221999106695021656757644044997940765037999954845002710665987813603802314126836905783190460792765297277694043613023051787080546511542469395265127101052927070306673024447125973939950514628404767431363739978259184541176413327906460636584152927019030276017339474866960348694976541752429306040727005059039503148522921392575594845078867977925253931765156416197168443524369794447355964260633391055126826061595726217036698506473281266724521989060549880280782881429796336696744124805982192146339565745722102298677599746738126069367069134081559412016115960190237753525556300606247983261249881288192937343476862689219239777833910733106588256813777172328315329082525092733047850724977139448333892552081175608452966590553940965568541706001179857293813998258319293679100391844099286575605993598910002969864460974714718470101531283762631146774209145574041815908800064943237855839308530828305476076799524357391631221886057549673832243195650655460852881201902363644712703748634421727257879503428486312944916318475347531435041392096108796057730987201352484075057637199253650470908582513936863463863368042891767107602111159828875539940120076013947033661793715396306139863655492213741597905119083588290097656647300733879314678913181465109316761575821351424860442292445304113160652700974330088499034675405518640677342603583409608605533747362760935658853109760994238347382222087292464497684560579562516765574088410321731345627735856052358236389532038534024842273371639123973215995440828421666636023296545694703577184873442034227706653837387506169212768015766181095420097708363604361110592409117889540338021426523948929686439808926114635414571535194342850721353453018315875628275733898268898523557799295727645229391567477566676051087887648453493636068278050564622813598885879259940946446041705204470046315137975431737187756039815962647501410906658866162180038266989961965580587208639721176995219466789857011798332440601811575658074284182910615193917630059194314434605154047710570054339000182453117733718955857603607182860506356479979004139761808955363669603162193113250223851791672055180659263518036251214575926238369348222665895576994660491938112486609099798128571823494006615552196112207203092277646200999315244273589488710576623894693889446495093960330454340842102462401048723328750081749179875543879387381439894238011762700837196053094383940063756116458560943129517597713935396074322792489221267045808183313764165818269562105872892447740035947009268662659651422050630078592002488291860839743732353849083964326147000532423540647042089499210250404726781059083644007466380020870126664209457181702946752278540074508552377720890581683918446592829417018288233014971554235235911774818628592967605048203864343108779562892925405638946621948268711042828163893975711757786915430165058602965217459581988878680408110328432739867198621306205559855266036405046282152306154594474489908839081999738747452969810776201487134000122535522246695409315213115337915798026979555710508507473874750758068765376445782524432638046143042889235934852961058269382103498000405248407084403561167817170512813378805705643450616119330424440798260377951198548694559152051960093041271007277849301555038895360338261929343797081874320949914159593396368110627557295278004254863060054523839151068998913578820019411786535682149118528207852130125518518493711503422159542244511900207393539627400208110465530207932867254740543652717595893500716336076321614725815407642053020045340183572338292661915308354095120226329165054426123619197051613839357326693760156914429944943744856809775696303129588719161129294681884936338647392747601226964158848900965717086160598147204467428664208765334799858222090619802173211614230419477754990738738567941189824660913091691772274207233367635032678340586301930193242996397204445179288122854478211953530898910125342975524727635730226281382091807439748671453590778633530160821559911314144205091447293535022230817193663509346865858656314855575862447818620108711889760652969899269328178705576435143382060141077329261063431525337182243385263520217735440715281898137698755157574546939727150488469793619500477720970561793913828989845327426227288647108883270173723258818244658436249580592560338105215606206155713299156084892064340303395262263451454283678698288074251422567451806184149564686111635404971897682154227722479474033571527436819409892050113653400123846714296551867344153741615042563256713430247655125219218035780169240326699541746087592409207004669340396510178134857835694440760470232540755557764728450751826890418293966113310160131119077398632462778219023650660374041606724962490137433217246454097412995570529142438208076098364823465973886691349919784013108015581343979194852830436739012482082444814128095443773898320059864909159505322857914576884962578665885999179867520554558099004556461178755249370124553217170194282884617402736649978475508294228020232901221630102309772151569446427909802190826689868834263071609207914085197695235553488657743425277531197247430873043619511396119080030255878387644206085044730631299277888942729189727169890575925244679660189707482960949190648764693702750773866432391919042254290235318923377293166736086996228032557185308919284403805071030064776847863243191000223929785255372375566213644740096760539439838235764606992465260089090624105904215453927904411529580345334500256244101006359530039598864466169595626351878060688513723462707997327233134693971456285542615467650632465676620279245208581347717608521691340946520307673391841147504140168924121319826881568664561485380287539331160232292555618941042995335640095786495340935115266454024418775949316930560448686420862757201172319526405023099774567647838488973464317215980626787671838005247696884084989185086149003432403476742686245952395890358582135006450998178244636087317754378859677672919526111213859194725451400301180503437875277664402762618941017576872680428176623860680477885242887430259145247073950546525135339459598789619778911041890292943818567205070964606263541732944649576612651953495701860015412623962286413897796733329070567376962156498184506842263690367849555970026079867996261019039331263768556968767029295371162528005543100786408728939225714512481135778627664902425161990277471090335933309304948380597856628844787441469841499067123764789582263294904679812089984857163571087831191848630254501620929805829208334813638405421720056121989353669371336733392464416125223196943471206417375491216357008573694397305979709719726666642267431117762176403068681310351899112271339724036887000996862922546465006385288620393800504778276912835603372548255793912985251506829969107754257647488325341412132800626717094009098223529657957997803018282428490221470748111124018607613415150387569830918652780658896682362523937845272634530420418802508442363190383318384550522367992357752929106925043261446950109861088899914658551881873582528164302520939285258077969737620845637482114433988162710031703151334402309526351929588680690821355853680161000213740851154484912685841268695899174149133820578492800698255195740201818105641297250836070356851055331787840829000041552511865779453963317538532092149720526607831260281961164858098684587525129997404092797683176639914655386108937587952214971731728131517932904431121815871023518740757222100123768721944747209349312324107065080618562372526732540733324875754482967573450019321902199119960797989373383673242576103938985349278777473980508080015544764061053522202325409443567718794565430406735896491017610775948364540823486130254718476485189575836674399791508512858020607820554462991723202028222914886959399729974297471155371858924238493855858595407438104882624648788053304271463011941589896328792678327322456103852197011130466587100500083285177311776489735230926661234588873102883515626446023671996644554727608310118788389151149340939344750073025855814756190881398752357812331342279866503522725367171230756861045004548970360079569827626392344107146584895780241408158405229536937499710665594894459246286619963556350652623405339439142111271810691052290024657423604130093691889255865784668461215679554256605416005071276641766056874274200329577160643448606201239821698271723197826816628249938714995449137302051843669076723577400053932662622760323659751718925901801104290384274185507894887438832703063283279963007200698012244365116394086922220745320244624121155804354542064215121585056896157356414313068883443185280853975927734433655384188340303517822946253702015782157373265523185763554098954033236382319219892171177449469403678296185920803403867575834111518824177439145077366384071880489358256868542011645031357633355509440319236720348651010561049872726472131986543435450409131859513145181276437310438972507004981987052176272494065214619959232142314439776546708351714749367986186552791715824080651063799500184295938799158350171580759883784962257398512129810326379376218322456594236685376799113140108043139732335449090824910499143325843298821033984698141715756010829706583065211347076803680695322971990599904451209087275776225351040902392888779424630483280319132710495478599180196967835321464441189260631526618167443193550817081875477050802654025294109218264858213857526688155584113198560022135158887210365696087515063187533002942118682221893775546027227291290504292259787710667873840000616772154638441292371193521828499824350920891801685572798156421858191197490985730570332667646460728757430565372602768982373259745084479649545648030771598153955827779139373601717422996027353102768719449444917939785144631597314435351850491413941557329382048542123508173912549749819308714396615132942045919380106231421774199184060180347949887691051557905554806953878540066453375981862846419905220452803306263695626490910827627115903856995051246529996062855443838330327638599800792922846659503551211245284087516229060262011857775313747949362055496401073001348853150735487353905602908933526400713274732621960311773433943673385759124508149335736911664541281788171454023054750667136518258284898099512139193995633241336556777098003081910272040997148687418134667006094051021462690280449159646545330107754695413088714165312544813061192407821188690056027781824235022696189344352547633573536485619363254417756613981703930632872166905722259745209192917262199844409646158269456380239502837121686446561785235565164127712826918688615572716201474934052276946595712198314943381622114006936307430444173284786101777743837977037231795255434107223445512555589998646183876764903972461167959018100035098928641204195163551108763204267612979826529425882951141275841262732790798807559751851576841264742209479721843309352972665210015662514552994745127631550917636730259462132930190402837954246323258550301096706922720227074863419005438302650681214142135057154175057508639907673946335146209082888934938376439399256900604067311422093312195936202982972351163259386772241477911629572780752395056251581603133359382311500518626890530658368129988108663263271980611271548858798093487912913707498230575929091862939195014721197586067270092547718025750337730799397134539532646195269996596385654917590458333585799102012713204583903200853878881633637685182083727885131175227769609787962142372162545214591281831798216044111311671406914827170981015457781939202311563871950805024679725792497605772625913328559726371211201905720771409148645074094926718035815157571514050397610963846755569298970383547314100223802583468767350129775413279532060971154506484212185936490997917766874774481882870632315515865032898164228288232746866106592732197907162384642153489852476216789050260998045266483929542357287343977680495774091449538391575565485459058976495198513801007958010783759945775299196700547602252552034453988712538780171960718164078124847847257912407824544361682345239570689514272269750431873633263011103053423335821609333191218806608268341428910415173247216053355849993224548730778822905252324234861531520976938461042582849714963475341837562003014915703279685301868631572488401526639835689563634657435321783493199825542117308467745297085839507616458229630324424328237737450517028560698067889521768198156710781633405266759539424926280756968326107495323390536223090807081455919837355377748742029039018142937311529334644468151212945097596534306284215319445727118614900017650558177095302468875263250119705209476159416768727784472000192789137251841622857783792284439084301181121496366424659033634194540657183544771912446621259392656620306888520055599121235363718226922531781458792593750441448933981608657900876165024635197045828895481793756681046474614105142498870252139936870509372305447734112641354892806841059107716677821238332810262185587751312721179344448201440425745083063944738363793906283008973306241380614589414227694747931665717623182472168350678076487573420491557628217583972975134478990696589532548940335615613167403276472469212505759116251529654568544633498114317670257295661844775487469378464233737238981920662048511894378868224807279352022501796545343757274163910791972952950812942922205347717304184477915673991738418311710362524395716152714669005814700002633010452643547865903290733205468338872078735444762647925297690170912007874183736735087713376977683496344252419949951388315074877537433849458259765560996555954318040920178497184685497370696212088524377013853757681416632722412634423982152941645378000492507262765150789085071265997036708726692764308377229685985169122305037462744310852934305273078865283977335246017463527703205938179125396915621063637625882937571373840754406468964783100704580613446731271591194608435935825987782835266531151065041623295329047772174083559349723758552138048305090009646676088301540612824308740645594431853413755220166305812111033453120745086824339432159043594430312431227471385842030390106070940315235556172767994160020393975099897629335325855575624808996691829864222677502360193257974726742578211119734709402357457222271212526852384295874273501563660093188045493338989741571490544182559738080871565281430102670460284316819230392535297795765862414392701549740879273131051636119137577008929564823323648298263024607975875767745377160102490804624301856524161756655600160859121534556267602192689982855377872583145144082654583484409478463178777374794653580169960779405568701192328608041130904629350871827125934668712766694873899824598527786499569165464029458935064964335809824765965165142090986755203808309203230487342703468288751604071546653834619611223013759451579252696743642531927390036038608236450762698827497618723575476762889950752114804852527950845033958570838130476937881321123674281319487950228066320170022460331989671970649163741175854851878484012054844672588851401562725019821719066960812627785485964818369621410721714214986361918774754509650308957099470934337856981674465828267911940611956037845397855839240761276344105766751024307559814552786167815949657062559755074306521085301597908073343736079432866757890533483669555486803913433720156498834220893399971641479746938696905480089193067138057171505857307148815649920714086758259602876056459782423770242469805328056632787041926768467116266879463486950464507420219373945259262668613552940624781361206202636498199999498405143868285258956342264328707663299304891723400725471764188685351372332667877921738347541480022803392997357936152412755829569276837231234798989446274330454566790062032420516396282588443085438307201495672106460533238537203143242112607424485845094580494081820927639140008540422023556260218564348994145439950410980591817948882628052066441086319001688568155169229486203010738897181007709290590480749092427141018933542818429995988169660993836961644381528877214085268088757488293258735809905670755817017949161906114001908553744882726200936685604475596557476485674008177381703307380305476973609786543859382187220583902344443508867499866506040645874346005331827436296177862518081893144363251205107094690813586440519229512932450078833398788429339342435126343365204385812912834345297308652909783300671261798130316794385535726296998740359570458452230856390098913179475948752126397078375944861139451960286751210561638976008880092746115860800207803341591451797073036835196977766076373785333012024120112046988609209339085365773222392412449051532780950955866459477634482269986074813297302630975028812103517723124465095349653693090018637764094094349837313251321862080214809922685502948454661814715557444709669530177690434272031892770604717784527939160472281534379803539679861424370956683221491465438014593829277393396032754048009552231816667380357183932757077142046723838624617803976292377131209580789363841447929802588065522129262093623930637313496640186619510811583471173312025805866727639992763579078063818813069156366274125431259589936119647626101405563503399523140323113819656236327198961837254845333702062563464223952766943568376761368711962921818754576081617053031590728828700712313666308722754918661395773730546065997437810987649802414011242142773668082751390959313404155826266789510846776118665957660165998178089414985754976284387856100263796543178313634025135814161151902096499133548733131115022700681930135929595971640197196053625033558479980963488718039111612813595968565478868325856437896173159762002419621552896297904819822199462269487137462444729093456470028537694958859591606789282491054412515996300781368367490209374915732896270028656829344431342347351239298259166739503425995868970697267332582735903121288746660451461487850346142827765991608090398652575717263081833494441820193533385071292345774375579344062178711330063106003324053991693682603746176638565758877580201229366353270267100681261825172914608202541892885935244491070138206211553827793565296914576502048643282865557934707209634807372692141186895467322767751335690190153723669036865389161291688887876407525493494249733427181178892759931596719354758988097924525262363659036320070854440784544797348291802082044926670634420437555325050527522833778887040804033531923407685630109347772125639088640413101073817853338316038135280828119040832564401842053746792992622037698718018061122624490909242641985820861751177113789051609140381575003366424156095216328197122335023167422600567941281406217219641842705784328959802882335059828208196666249035857789940333152274817776952843681630088531769694783690580671064828083598046698841098135158654906933319522394363287923990534810987830274500172065433699066117784554364687723631844464768069142828004551074686645392805399409108754939166095731619715033166968309929466349142798780842257220697148875580637480308862995118473187124777291910070227588893486939456289515802965372150409603107761289831263589964893410247036036645058687287589051406841238124247386385427908282733827973326885504935874303160274749063129572349742611221517417153133618622410913869500688835898962349276317316478340077460886655598733382113829928776911495492184192087771606068472874673681886167507221017261103830671787856694812948785048943063086169948798703160515884108282351274153538513365895332948629494495061868514779105804696039069372662670386512905201137810858616188886947957607413585534585151768051973334433495230120395770739623771316030242887200537320998253008977618973129817881944671731160647231476248457551928732782825127182446807824215216469567819294098238926284943760248852279003620219386696482215628093605373178040863727268426696421929946819214908701707533361094791381804063287387593848269535583077395761447997270003472880182785281389503217986345216111066608839314053226944905455527867894417579202440021450780192099804461382547805858048442416404775031536054906591430078158372430123137511562284015838644270890718284816757527123846782459534334449622010096071051370608461801187543120725491334994247617115633321408934609156561550600317384218701570226103101916603887064661438897736318780940711527528174689576401581047016965247557740891644568677717158500583269943401677202156767724068128366565264122982439465133197359199709403275938502669557470231813203243716420586141033606524536939160050644953060161267822648942437397166717661231048975031885732165554988342121802846912529086101485527815277625623750456375769497734336846015607727035509629049392487088406281067943622418704747008368842671022558302403599841645951122485272633632645114017395248086194635840783753556885622317115520947223065437092606797351000565549381224575483728545711797393615756167641692895805257297522338558611388322171107362265816218842443178857488798109026653793426664216990914056536432249301334867988154886628665052346997235574738424830590423677143278792316422403877764330192600192284778313837632536121025336935812624086866699738275977365682227907215832478888642369346396164363308730139814211430306008730666164803678984091335926293402304324974926887831643602681011309570716141912830686577323532639653677390317661361315965553584999398600565155921936759977717933019744688148371103206503693192894521402650915465184309936553493337183425298433679915939417466223900389527673813330617747629574943868716978453767219493506590875711917720875477107189937960894774512654757501871194870738736785890200617373321075693302216320628432065671192096950585761173961632326217708945426214609858410237813215817727602222738133495410481003073275107799948991977963883530734443457532975914263768405442264784216063122769646967156473999043715903323906560726644116438605404838847161912109008701019130726071044114143241976796828547885524779476481802959736049439700479596040292746299203572099761950140348315380947714601056333446998820822120587281510729182971211917876424880354672316916541852256729234429187128163232596965413548589577133208339911288775917226115273379010341362085614577992398778325083550730199818459025958355989260553299673770491722454935329683300002230181517226575787524058832249085821280089747909326100762578770428656006996176212176845478996440705066241710213327486796237430229155358200780141165348065647488230615003392068983794766255036549822805329662862117930628430170492402301985719978948836897183043805182174419147660429752437251683435411217038631379411422095295885798060152938752753799030938871683572095760715221900279379292786303637268765822681241993384808166021603722154710143007377537792699069587121289288019052031601285861825494413353820784883465311632650407642428390870121015194231961652268422003711230464300673442064747718021353070124098860353399152667923871101706221865883573781210935179775604425634694999787251125440854522274810914874307259869602040275941178942581281882159952359658979181144077653354321757595255536158128001163846720319346507296807990793963714961774312119402021297573125165253768017359101557338153772001952444543620071848475663415407442328621060997613243487548847434539665981338717466093020535070271952983943271425371155766600025784423031073429551533945060486222764966687624079324353192992639253731076892135352572321080889819339168668278948281170472624501948409700975760920983724090074717973340788141825195842598096241747610138252643955135259311885045636264188300338539652435997416931322894719878308427600401368074703904097238473945834896186539790594118599310356168436869219485382055780395773881360679549900085123259442529724486666766834641402189915944565309423440650667851948417766779470472041958822043295380326310537494883122180391279678446100139726753892195119117836587662528083690053249004597410947068772912328214304635337283519953648274325833119144459017809607782883583730111857543659958982724531925310588115026307542571493943024453931870179923608166611305426253995833897942971602070338767815033010280120095997252222280801423571094760351925544434929986767817891045559063015953809761875920358937341978962358931125983902598310267193304189215109689156225069659119828323455503059081730735195503721665870288053992138576037035377105178021280129566841984140362872725623214428754302210909472721073474134975514190737043318276626177275996888826027225247133683353452816692779591328861381766349857728936900965749562287103024362590772412219094300871755692625758065709912016659622436080242870024547362036394841255954881727272473653467783647201918303998717627037515724649922289467932322693619177641614618795613956699567783068290316589699430767333508234990790624100202506134057344300695745474682175690441651540636584680463692621274211075399042188716127617787014258864825775223889184599523376292377915585744549477361295525952226578636462118377598473700347971408206994145580719080213590732269233100831759510659019121294795408603640757358750205890208704579670007055262505811420663907459215273309406823649441590891009220296680523325266198911311842016291631076894084723564366808182168657219688268358402785500782804043453710183651096951782335743030504852653738073531074185917705610397395062640355442275156101107261779370634723804990666922161971194259120445084641746383589938239946517395509000859479990136026674261494290066467115067175422177038774507673563742154782905911012619157555870238957001405117822646989944917908301795475876760168094100135837613578591356924455647764464178667115391951357696104864922490083446715486383054477914330097680486878348184672733758436892724310447406807685278625585165092088263813233623148733336714764520450876627614950389949504809560460989604329123358348859990294526400284994280878624039811814884767301216754161106629995553668193123287425702063738352020086863691311733469731741219153633246745325630871347302792174956227014687325867891734558379964351358800959350877556356248810493852999007675135513527792412429277488565888566513247302514710210575352516511814850902750476845518252096331899068527614435138213662152368890578786699432288816028377482035506016029894009119713850179871683633744139275973644017007014763706655703504338121113576415018451821413619823495159601064752712575935185304332875537783057509567425442684712219618709178560783936144511383335649103256405733898667178123972237519316430617013859539474367843392670986712452211189690840236327411496601243483098929941738030588417166613073040067588380432111555379440605497721705942821514886165672771240903387727745629097110134885184374118695655449745736845218066982911045058004299887953899027804383596282409421860556287788428802127553884803728640019441614257499904272009595204654170598104989967504511936471172772220436102614079750809686975176600237187748348016120310234680567112644766123747627852190241202569943534716226660893675219833111813511146503854895025120655772636145473604426859498074396932331297127377157347099713952291182653485155587137336629120242714302503763269501350911612952993785864681307226486008270881333538193703682598867893321238327053297625857382790097826460545598555131836688844628265133798491667839409761353766251798258249663458771950124384040359140849209733754642474488176184070023569580177410177696925077814893386672557898564589851056891960924398841569280696983352240225634570497312245269354193837004843183357196516626721575524193401933099018319309196582920969656247667683659647019595754739345514337413708761517323677204227385674279170698204549953095918872434939524094441678998846319845504852393662972079777452814399418256789457795712552426826089940863317371538896262889629402112108884427376568624527612130371017300785135715404533041507959447776143597437803742436646973247138410492124314138903579092416036406314038149831481905251720937103964026808994832572297954564042701757722904173234796073618787889913318305843069394825961318713816423467218730845133877219086975104942843769325024981656673816260615941768252509993741672883951744066932549653403101452225316189009235376486378482881344209870048096227171226407489571939002918573307460104360729190945767994614929290427981687729426487729952858434647775386906950148984133924540394144680263625402118614317031251117577642829914644533408920976961699098372652361768745605894704968170136974909523072082682887890730190018253425805343421705928713931737993142410852647390948284596418093614138475831136130576108462366837237695913492615824516221552134879244145041756848064120636520170386330129532777699023118648020067556905682295016354931992305914246396217025329747573114094220180199368035026495636955866425906762685687372110339156793839895765565193177883000241613539562437777840801748819373095020699900890899328088397430367736595524891300156633294077907139615464534088791510300651321934486673248275907946807879819425019582622320395131252014109960531260696555404248670549986786923021746989009547850725672978794769888831093487464426400718183160331655511534276155622405474473378049246214952133258527698847336269182649174338987824789278468918828054669982303689939783413747587025805716349413568433929396068192061773331791738208562436433635359863494496890781064019674074436583667071586924521182997893804077137501290858646578905771426833582768978554717687184427726120509266486102051535642840632368481807287940717127966820060727559555904040233178749447346454760628189541512139162918444297651066947969354016866010055196077687335396511614930937570968554559381513789569039251014953265628147011998326992200066392875374713135236421589265126204072887716578358405219646054105435443642166562244565042999010256586927279142752931172082793937751326106052881235373451068372939893580871243869385934389175713376300720319760816604464683937725806909237297523486702916910426369262090199605204121024077648190316014085863558427609537086558164273995349346546314504040199528537252004957805254656251154109252437991326262713609099402902262062836752132305065183934057450112099341464918433323646569371725914489324159006242020612885732926133596808726500045628284557574596592120530341310111827501306961509835515632004310784601906565493806542525229161991819959602752327702249855738824899882707465936355768582560518068964285376850772012220347920993936179268206590142165615925306737944568949070853263568196831861772268249911472615732035807646298116244013316737892788689229032593349861797021994981925739617673075834417098559222170171825712777534491508205278430904619460835217402005838672849709411023266953921445461066215006410674740207009189911951376466904481267253691537162290791385403937560077835153374167747942100384002308951850994548779039346122220865060160500351776264831611153325587705073541279249909859373473787081194253055121436979749914951860535920403830235716352727630874693219622190064260886183676103346002255477477813641012691906569686495012688376296907233961276287223041141813610060264044030035996988919945827397624114613744804059697062576764723766065541618574690527229238228275186799156983390747671146103022776606020061246876477728819096791613354019881402757992174167678799231603963569492851513633647219540611171767387372555728522940054361785176502307544693869307873499110352182532929726044553210797887711449898870911511237250604238753734841257086064069052058452122754533848008205302450456517669518576913200042816758054924811780519832646032445792829730129105318385636821206215531288668564956512613892261367064093953334570526986959692350353094224543865278677673027540402702246384483553239914751363441044050092330361271496081355490531539021002299595756583705381261965683144286057956696622154721695620870013727768536960840704833325132793112232507148630206951245395003735723346807094656483089209801534878705633491092366057554050864111521441481434630437273271045027768661953107858323334857840297160925215326092558932655600672124359464255065996771770388445396181632879614460817789272171836908880126778207430106422524634807454300476492885553409062185153654355474125476152769772667769772777058315801412185688011705028365275543214803488004442979998062157904564161957212784508928489806426497427090579129069217807298769477975112447305991406050629946894280931034216416629935614828130998870745292716048433630818404126469637925843094185442216359084576146078558562473814931427078266215185541603870206876980461747400808324343665382354555109449498431093494759944672673665352517662706772194183191977196378015702169933675083760057163454643671776723387588643405644871566964321041282595645349841388412890420682047007615596916843038999348366793542549210328113363184722592305554383058206941675629992013373175489122037230349072681068534454035993561823576312837767640631013125335212141994611869350833176587852047112364331226765129964171325217513553261867681942338790365468908001827135283584888444111761234101179918709236507184857856221021104009776994453121795022479578069506532965940383987369907240797679040826794007618729547835963492793904576973661643405359792219285870574957481696694062334272619733518136626063735982575552496509807260123668283605928341855848026958413772558970883789942910549800331113884603401939166122186696058491571485733568286149500019097591125218800396419762163559375743718011480559442298730418196808085647265713547612831629200449880315402105530597076666362749328308916880932359290081787411985738317192616728834918402429721290434965526942726402559641463525914348400675867690350382320572934132981593533044446496829441367323442158380761694831219333119819061096142952201536170298575105594326461468505452684975764807808009221335811378197749271768545075538328768874474591593731162470601091244609829424841287520224462594477638749491997840446829257360968534549843266536862844489365704111817793806441616531223600214918768769467398407517176307516849856359201486892943105940202457969622924566644881967576294349535326382171613395757790766370764569570259738800438415805894336137106551859987600754924187211714889295221737721146081154344982665479872580056674724051122007383459271575727715218589946948117940644466399432370044291140747218180224825837736017346685300744985564715420036123593397312914458591522887408719508708632218837288262822884631843717261903305777147651564143822306791847386039147683108141358275755853643597721650028277803713422869688787349795096031108899196143386664068450697420787700280509367203387232629637856038653216432348815557557018469089074647879122436375556668678067610544955017260791142930831285761254481944449473244819093795369008206384631678225064809531810406570254327604385703505922818919878065865412184299217273720955103242251079718077833042609086794273428955735559252723805511440438001239041687716445180226491681641927401106451622431101700056691121733189423400547959684669804298017362570406733282129962153684881404102194463424646220745575643960452985313071409084608499653767803793201899140865814662175319337665970114330608625009829566917638846056762972931464911493704624469351984039534449135141193667933301936617663652555149174982307987072280860859626112660504289296966535652516688885572112276802772743708917389639772257564890533401038855931125679991516589025016486961427207005916056166159702451989051832969278935550303934681219761582183980483960562523091462638447386296039848924386187298507775928792722068554807210497817653286210187476766897248841139560349480376727036316921007350834073865261684507482496448597428134936480372426116704266870831925040997615319076855770327421785010006441984124207396400139603601583810565928413684574119102736420274163723488214524101347716529603128408658419787951116511529827814620379139855006399960326591248525308493690313130100799977191362230866011099929142871249388541612038020411340188887219693477904497527454288072803509305828754420755134816660927879353566521255620139988249628478726214432362853676502591450468377635282587652139156480972141929675549384375582600253168536356731379262475878049445944183429172756988376226261846365452743497662411138451305481449836311789784489732076719508784158618879692955819733250699951402601511675529750575437810242238957925786562128432731202200716730574069286869363930186765958251326499145950260917069347519408975357464016830811798846452473618956056479426358070562563281189269663026479535951097127659136233180866921535788607812759910537171402204506186075374866306350591483916467656723205714516886170790984695932236724946737583099607042589220481550799132752088583781117685214269334786921895240622657921043620348852926267984013953216458791151579050460579710838983371864038024417511347226472547010794793996953554669619726763255229914654933499663234185951450360980344092212206712567698723427940708857070474293173329188523896721971353924492426178641188637790962814486917869468177591717150669111480020759432012061969637795103227089029566085562225452602610460736131368869009281721068198618553780982018471154163630326265699283424155023600978046417108525537612728905335045506135684143775854429677977014660294387687225115363801191758154028120818255606485410787933598921064427244898618961629413418001295130683638609294100083136673372153008352696235737175330738653338204842190308186449184093723944033405244909554558016406460761581010301767488475017661908692946098769201691202181688291040870709560951470416921147027413390052253340834812870353031023919699978597413908593605433599697075604460134242453682496098772581311024732798562072126572499003468293886872304895562253204463602639854225258416464324271611419817802482595563544907219226583863662663750835944314877635156145710745528016159677048442714194435183275698407552677926411261765250615965235457187956673170913319358761628255920783080185206890151504713340386100310055914817852110384754542933389188444120517943969970194112695119526564919594189975418393234647424290702718875223534393673633663200307232747037407123982562024662651974090199762452056198557625760008708173083288344381831070054514493545885422678578551915372292379555494333410174420169600090696415612732297770221217951868376359082255128816470021992348864043959153018464004714321186360622527011541122283802778538911098490201342741014121559769965438877197485376431158229838533123071751132961904559007938064276695819014842627991221792947987348901868471676503827328552059082984529806259250352128451925927986593506132961946796252373972565584157853744567558998032405492186962888490332560851455344391660226257775512916200772796852629387937530454181080729285891989715381797343496187232927614747850192611450413274873242970583408471112333746274617274626582415324271059322506255302314738759251724787322881491455915605036334575424233779160374952502493022351481961381162563911415610326844958072508273431765944054098269765269344579863479709743124498271933113863873159636361218623497261409556079920628316999420072054811525353393946076850019909886553861433495781650089961649079678142901148387645682174914075623767618453775144031475411206760160726460556859257799322070337333398916369504346690694828436629980037414527627716547623825546170883189810868806847853705536480469350958818025360529740793538676511195079373282083146268960071075175520614433784114549950136432446328193346389050936545714506900864483440180428363390513578157273973334537284263372174065775771079830517555721036795976901889958494130195999573017901240193908681356585539661941371794487632079868800371607303220547423572266896801882123424391885984168972277652194032493227314793669234004848976059037958094696041754279613782553781223947646147832926976545162290281701100437846038756544151739433960048915318817576650500951697402415644771293656614253949368884230517400129920556854289853897942669956777027089146513736892206104415481662156804219838476730871787590279209175900695273456682026513373111518000181434120962601658629821076663523361774007837783423709152644063054071807843358061072961105550020415131696373046849213356837265400307509829089364612047891114753037049893952833457824082817386441322710002968311940203323456420826473276233830294639378998375836554559919340866235090967961134004867027123176526663710778725111860354037554487418693519733656621772359229396776463251562023487570113795712096237723431370212031004965152111976013176419408203437348512852602913334915125083119802850177855710725373149139215709105130965059885999931560863655477403551898166733535880048214665099741433761182777723351910741217572841592580872591315074606025634903777263373914461377038021318347447301113032670296917335047701632106616227830027269283365584011791419447808748253360714403296252285775009808599609040936312635621328162071453406104224112083010008587264252112262480142647519426184325853386753874054743491072710049754281159466017136122590440158991600229827801796035194080046513534752698777609527839984368086908989197839693532179980139135442552717910225397010810632143048511378291498511381969143043497500189980681644412123273328307192824362406733196554692677851193152775113446468905504248113361434984604849051258345683266441528489713972376040328212660253516693914082049947320486021627759791771234751097502403078935759937715095021751693555827072533911892334070223832077585802137174778378778391015234132098489423459613692340497998279304144463162707214796117456975719681239291913740982925805561955207434243295982898980529233366415419256367380689494201471241340525072204061794355252555225008748790086568314542835167750542294803274783044056438581591952666758282929705226127628711040134801787224801789684052407924360582742467443076721645270313451354167649668901274786801010295133862698649748212118629040337691568576240699296372493097201628707200189835423690364149270236961938547372480329855045112089192879829874467864129159417531675602533435310626745254507114181483239880607297140234725520713490798398982355268723950909365667878992383712578976248755990443228895388377317348941122757071410959790047919301046740750411435381782464630795989555638991884773781341347070246747362112048986226991888517456251732519341352038115863350123913054441910073628447567514161050410973505852762044489190978901984315485280533985777844313933883994310444465669244550885946314081751220331390681596592510546858013133838152176418210433429788826119630443111388796258746090226130900849975430395771243230616906262919403921439740270894777663702488155499322458825979020631257436910946393252806241642476868495455324938017639371615636847859823715902385421265840615367228607131702674740131145261063765383390315921943469817605358380310612887852051546933639241088467632009567089718367490578163085158138161966882222047570437590614338040725853862083565176998426774523195824182683698270160237414938363496629351576854061397342746470899685618170160551104880971554859118617189668025973541705423985135560018720335079060946421271143993196046527424050882225359773481519135438571253258540493946010865793798058620143366078825219717809025817370870916460452727977153509910340736425020386386718220522879694458387652947951048660717390229327455426785669776865939923416834122274663015062155320502655341460995249356050854921756549134830958906536175693817637473644183378974229700703545206663170929607591989627732423090252397443861014263098687733913882518684316501027964911497737582888913450341148865948670215492101084328080783428089417298008983297536940644969903125399863919581601468995220880662285408414864274786281975546629278814621607171381880180840572084715868906836919393381864278454537956719272397972364651667592011057995663962598535512763558768140213409829016296873429850792471846056874828331381259161962476156902875901072733103299140623864608333378638257926302391590003557609032477281338887339178096966601469615031754226751125993315529674213336300222964906480934582008181061802100227664580400278213336758573019011371754672763059044353131319036092489097246427928455549913490005180295707082919052556781889913899625138662319380053611346224294610248954072404857123256628888931722116432947816190554868054943441034090680716088028227959686950133643814268252170472870863010137301155236861416908375675747637239763185757038109443390564564468524183028148107998376918512127201935044041804604721626939445788377090105974693219720558114078775989772072009689382249303236830515862657281114637996983137517937623215111252349734305240622105244234353732905655163406669506165892878218707756794176080712973781335187117931650033155523822487730653444179453415395202424449703410120874072188109388268167512042299404948179449472732894770111574139441228455521828424922240658752689172272780607116754046973008037039618787796694882555614674384392570115829546661358678671897661297311267200072971553613027503556167817765442287442114729881614802705243806817653573275578602505847084013208837932816008769081300492491473682517035382219619039014999523495387105997351143478292339499187936608692301375596368532373806703591144243268561512109404259582639301678017128669239283231057658851714020211196957064799814031505633045141564414623163763809904402816256917576489142569714163598439317433270237812336938043012892626375382667795034169334323607500248175741808750388475094939454896209740485442635637164995949920980884294790363666297526003243856352945844728944547166209297495496616877414120882130477022816116456044007236351581149729739218966737382647204722642221242016560150284971306332795814302516013694825567014780935790889657134926158161346901806965089556310121218491805847922720691871696316330044858020102860657858591269974637661741463934159569539554203314628026518951167938074573315759846086173702687867602943677780500244673391332431669880354073232388281847501051641331189537036488422690270478052742490603492082954755054003457160184072574536938145531175354210726557835615499874447480427323457880061873149341566046352979779455075359304795687209316724536547208381685855606043801977030764246083489876101345709394877002946175792061952549255757109038525171488525265671045349813419803390641529876343695420256080277614421914318921393908834543131769685101840103844472348948869520981943531906506555354617335814045544837884752526253949665869992058417652780125341033896469818642430034146791380619028059607854888010789705516946215228773090104467462497979992627120951684779568482583341402266477210843362437593741610536734041954738964197895425335036301861400951534766961476255651873823292468547356935802896011536791787303553159378363082248615177770541577576561759358512016692943111138863582159667618830326104164651714846979385422621687161400122378213779774131268977266712992025922017408770076956283473932201088159356286281928563571893384958850603853158179760679479840878360975960149733420572704603521790605647603285569276273495182203236144112584182426247712012035776388895974318232827871314608053533574494297621796789034568169889553518504478325616380709476951699086247100019748809205009521943632378719764870339223811540363475488626845956159755193765410115014067001226927474393888589943859730245414801061235908036274585288493563251585384383242493252666087588908318700709100237377106576985056433928854337658342596750653715005333514489908293887737352051459333049626531415141386124437935885070944688045486975358170212908490787347806814366323322819415827345671356443171537967818058195852464840084032909981943781718177302317003989733050495387356116261023999433259780126893432605584710278764901070923443884634011735556865903585244919370181041626208504299258697435817098133894045934471937493877624232409852832762266604942385129709453245586252103600829286649724174919141988966129558076770979594795306013119159011773943104209049079424448868513086844493705909026006120649425744710353547657859242708130410618546219881830090634588187038755856274911587375421064667951346487586771543838018521348281915812462599335160198935595167968932852205824799421034512715877163345222995418839680448835529753361286837225935390079201666941339091168758803988828869216002373257361588207163516271332810518187602104852180675526648673908900907195138058626735124312215691637902277328705410842037841525683288718046987952513073266340278519059417338920358540395677035611329354482585628287610610698229721420961993509331312171187891078766872044548876089410174798647137882462153955933333275562009439580434537919782280590395959927436913793778664940964048777841748336432684026282932406260081908081804390914556351936856063045089142289645219987798849347477729132797266027658401667890136490508741142126861969862044126965282981087045479861559545338021201155646979976785738920186243599326777689454060508218838227909833627167124490026761178498264377033002081844590009717235204331994708242098771514449751017055643029542821819670009202515615844174205933658148134902693111517093872260026458630561325605792560927332265579346280805683443921373688405650434307396574061017779370141424615493070741360805442100295600095663588977899267630517718781943706761498217564186590116160865408635391513039201316805769034172596453692350806417446562351523929050409479953184074862151210561833854566176652606393713658802521666223576132201941701372664966073252010771947931265282763302413805164907174565964853748354669194523580315301969160480994606814904037819829732360930087135760798621425422096419004367905479049930078372421581954535418371129368658430553842717628035279128821129308351575656599944741788438381565148434229858704245592434693295232821803508333726283791830216591836181554217157448465778420134329982594566884558266171979012180849480332448787258183774805522268151011371745368417870280274452442905474518234674919564188551244421337783521423865979925988203287085109338386829906571994614906290257427686038850511032638544540419184958866538545040571323629681069146814847869659166861842756798460041868762298055562963045953227923051616721591968675849523635298935788507746081537321454642984792310511676357749494622952569497660359473962430995343310404994209677883827002714478494069037073249106444151696053256560586778757417472110827435774315194060757983563629143326397812218946287447798119807225646714664054850131009656786314880090303749338875364183165134982546694673316118123364854397649325026179549357204305402182974871251107404011611405899911093062492312813116340549262571356721818628932786138833718028535056503591952741400869510926167541476792668032109237467087213606278332922386413619594121339278036118276324106004740971111048140003623342714514483334641675466354699731494756643423659493496845884551524150756376605086632827424794136062876041290644913828519456402643153225858624043141838669590633245063000392213192647625962691510904457695301444054618037857503036686212462278639752746667870121003392984873375014475600322100622358029343774955032037012738468163061026570300872275462966796880890587127676361066225722352229739206443093524327228100859973095132528630601105497915644791845004618046762408928925680912930592960642357021061524646205023248966593987324933967376952023991760898474571843531936646529125848064480196520162838795189499336759241485626136995945307287254532463291529110128763770605570609531377527751867923292134955245133089867969165129073841302167573238637575820080363575728002754490327953079900799442541108725693188014667935595834676432868876966610097395749967836593397846346959948950610490383647409504695226063858046758073069912290474089879166872117147527644711604401952718169508289733537148530928937046384420893299771125856840846608339934045689026787516008775461267988015465856522061210953490796707365539702576199431376639960606061106406959330828171876426043573425361756943784848495250108266488395159700490598380812105221111091943323951136051446459834210799058082093716464523127704023160072138543723461267260997870385657091998507595634613248460188409850194287687902268734556500519121546544063829253851276317663922050938345204300773017029940362615434001322763910912988327863920412300445551684054889809080779174636092439334912641164240093880746356607262336695842764583698268734815881961058571835767462009650526065929263548291499045768307210893245857073701660717398194485028842603963660746031184786225831056580870870305567595861341700745402965687634774176431051751036732869245558582082372038601781739405175130437994868822320044378043103170921034261674998000073016094814586374488778522273076330495383944345382770608760763542098445008306247630253572781032783461766970544287155315340016497076657195985041748199087201490875686037783591994719343352772947285537925787684832301101859365800717291186967617655053775030293033830706448912811412025506150896411007623824574488655182581058140345320124754723269087547507078577659732542844459353044992070014538748948226556442223696365544194225441338212225477497535494624827680533336983284156138692363443358553868471111430498248398991803165458638289353799130535222833430137953372954016257623228081138499491876144141322933767106563492528814528239506209022357876684650116660097382753660405446941653422239052108314585847035529352219928272760574821266065291385530345549744551470344939486863429459658431024190785923680224560763936784166270518555178702904073557304620639692453307795782245949710420188043000183881429008173039450507342787013124466860092778581811040911511729374873627887874907465285565434748886831064110051023020875107768918781525622735251550379532444857787277617001964853703555167655209119339343762866284619844026295252183678522367475108809781507098978413086245881522660963551401874495836926917799047120726494905737264286005211403581231076006699518536124862746756375896225299116496066876508261734178484789337295056739007878617925351440621045366250640463728815698232317500596261080921955211150859302955654967538862612972339914628358476048627627027309739202001432248707582337354915246085608210328882974183906478869923273691360048837436615223517058437705545210815513361262142911815615301758882573594892507108879262128641392443309383797333867806131795237315266773820858024701433527009243803266951742119507670884326346442749127558907746863582162166042741315170212458586056233631493164646913946562497471741958354218607748711057338458433689939645913740603382159352243594751626239188685307822821763983237306180204246560477527943104796189724299533029792497481684052893791044947004590864991872727345413508101983881864673609392571930511968645601855782450218231065889437986522432050677379966196955472440585922417953006820451795370043472451762893566770508490213107736625751697335527462302943031203596260953423574397249659211010657817826108745318874803187430823573699195156340957162700992444929749105489851519658664740148225106335367949737142510229341882585117371994499115097583746130105505064197721531929354875371191630262030328588658528480193509225875775597425276584011721342323648084027143356367542046375182552524944329657043861387865901965738802868401894087672816714137033661732650120578653915780703088714261519075001492576112927675193096728453971160213606303090542243966320674323582797889332324405779199278484633339777737655901870574806828678347965624146102899508487399692970750432753029972872297327934442988646412725348160603779707298299173029296308695801996312413304939350493325412355071054461182591141116454534710329881047844067780138077131465400099386306481266614330858206811395838319169545558259426895769841428893743467084107946318932539106963955780706021245974898293564613560788983472419979478564362042094613412387613198865352358312996862268948608408456655606876954501274486631405054735351746873009806322780468912246821460806727627708402402266155485024008952891657117617439020337584877842911289623247059191874691042005848326140677333751027195653994697162517248312230633919328707983800748485726516123434933273356664473358556430235280883924348278760886164943289399166399210488307847777048045728491456303353265070029588906265915498509407972767567129795010098229476228961891591441520032283878773485130979081019129267227103778898053964156362364169154985768408398468861684375407065121039062506128107663799047908879674778069738473170475253442156390387201238806323688037017949308954900776331523063548374256816653361606641980030188287123767481898330246836371488309259283375902278942588060087286038859168849730693948020511221766359138251524278670094406942355120201568377778851824670025651708509249623747726813694284350062938814429987905301056217375459182679973217735029368928065210025396268807498092643458011655715886700443503976505323478287327368840863540002740676783821963522226539290939807367391364082898722017776747168118195856133721583119054682936083236976113450281757830202934845982925000895682630271263295866292147653142233351793093387951357095346377183684092444422096319331295620305575517340067973740614162107923633423805646850092037167152642556371853889571416419772387422610596667396997173168169415435095283193556417705668622215217991151355639707143312893657553844648326201206424338016955862698561022460646069330793847858814367407000599769703649019273328826135329363112403650698652160638987250267238087403396744397830258296894256896741864336134979475245526291426522842419243083388103580053787023999542172113686550275341362211693140694669513186928102574795985605145005021715913317751609957865551981886193211282110709442287240442481153406055895958355815232012184605820563592699303478851132068626627588771446035996656108430725696500563064489187599466596772847171539573612108180841547273142661748933134174632662354222072600146012701206934639520564445543291662986660783089068118790090815295063626782075614388815781351134695366303878412092346942868730839320432333872775496805210302821544324723388845215343727250128589747691460808314404125868181540049187772287869801853454537006526655649170915429522756709222217474112062720656622989806032891672068743654948246108697367225547404812889242471854323605753411672850757552057131156697954584887398742228135887985840783135060548290551482785294891121905383195624228719484759407859398047901094194070671764439032730712135887385049993638838205501683402777496070276844880281912220636888636811043569529300652195528261526991271637277388418993287130563464688227398288763198645709836308917786487086676185485680047672552675414742851028145807403152992197814557756843681110185317498167016426647884090262682824448258027532094549915104518517716546311804904567985713257528117913656278158111288816562285876030875974963849435275676612168959261485030785362045274507752950631012480341804584059432926079854435620093708091821523920371790678121992280496069738238743312626730306795943960954957189577217915597300588693646845576676092450906088202212235719254536715191834872587423919410890444115959932760044506556206461164655665487594247369252336955993030355095817626176231849561906494839673002037763874369343999829430209147073618947932692762445186560239559053705128978163455423320114975994896278424327483788032701418676952621180975006405149755889650293004867605208010491537885413909424531691719987628941277221129464568294860281493181560249677887949813777216229359437811004448060797672429276249510784153446429150842764520002042769470698041775832209097020291657347251582904630910359037842977572651720877244740952267166306005469716387943171196873484688738186656751279298575016363411314627530499019135646823804329970695770150789337728658035712790913767420805655493624646 diff --git a/pkg/font/install.go b/pkg/font/install.go new file mode 100644 index 0000000000000000000000000000000000000000..10796c967a3fbcecffef6e2c021387cd1dddcded --- /dev/null +++ b/pkg/font/install.go @@ -0,0 +1,1040 @@ +/* +Copyright 2019 The pdfcpu Authors. + +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 + + 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 font provides support for TrueType fonts. +package font + +import ( + "bytes" + "encoding/binary" + "encoding/gob" + "fmt" + "io" + "os" + "path/filepath" + "reflect" + "sort" + "strings" + "unicode/utf16" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pkg/errors" +) + +const ( + sfntVersionTrueType = "\x00\x01\x00\x00" + sfntVersionTrueTypeApple = "true" + sfntVersionCFF = "OTTO" + ttfHeadMagicNumber = 0x5F0F3CF5 + ttcTag = "ttcf" +) + +type ttf struct { + PostscriptName string // name: NameID 6 + Protected bool // OS/2: fsType + UnitsPerEm int // head: unitsPerEm + Ascent int // OS/2: sTypoAscender + Descent int // OS/2: sTypoDescender + CapHeight int // OS/2: sCapHeight + FirstChar uint16 // OS/2: fsFirstCharIndex + LastChar uint16 // OS/2: fsLastCharIndex + UnicodeRange [4]uint32 // OS/2: Unicode Character Range + LLx, LLy, URx, URy float64 // head: xMin, yMin, xMax, yMax (fontbox) + ItalicAngle float64 // post: italicAngle + FixedPitch bool // post: isFixedPitch + Bold bool // OS/2: usWeightClass == 7 + HorMetricsCount int // hhea: numOfLongHorMetrics + GlyphCount int // maxp: numGlyphs + GlyphWidths []int // hmtx: fd.HorMetricsCount.advanceWidth + Chars map[uint32]uint16 // cmap: Unicode character to glyph index + ToUnicode map[uint16]uint32 // map glyph index to unicode character + Planes map[int]bool // used Unicode planes + FontFile []byte +} + +func (fd ttf) String() string { + return fmt.Sprintf(` + PostscriptName = %s + Protected = %t + UnitsPerEm = %d + Ascent = %d + Descent = %d + CapHeight = %d + FirstChar = %d + LastChar = %d +FontBoundingBox = (%.2f, %.2f, %.2f, %.2f) + ItalicAngle = %.2f + FixedPitch = %t + Bold = %t +HorMetricsCount = %d + GlyphCount = %d`, + fd.PostscriptName, + fd.Protected, + fd.UnitsPerEm, + fd.Ascent, + fd.Descent, + fd.CapHeight, + fd.FirstChar, + fd.LastChar, + fd.LLx, fd.LLy, fd.URx, fd.URy, + fd.ItalicAngle, + fd.FixedPitch, + fd.Bold, + fd.HorMetricsCount, + fd.GlyphCount, + ) +} + +func (fd ttf) toPDFGlyphSpace(i int) int { + return i * 1000 / fd.UnitsPerEm +} + +type myUint32 []uint32 + +func (f myUint32) Len() int { + return len(f) +} + +func (f myUint32) Less(i, j int) bool { + return f[i] < f[j] +} + +func (f myUint32) Swap(i, j int) { + f[i], f[j] = f[j], f[i] +} + +func (fd ttf) PrintChars() string { + var min = uint16(0xFFFF) + var max uint16 + var sb strings.Builder + sb.WriteByte(0x0a) + + keys := make(myUint32, 0, len(fd.Chars)) + for k := range fd.Chars { + keys = append(keys, k) + } + sort.Sort(keys) + + for _, c := range keys { + g := fd.Chars[c] + if g > max { + max = g + } + if g < min { + min = g + } + sb.WriteString(fmt.Sprintf("#%x -> #%x(%d)\n", c, g, g)) + } + fmt.Printf("using glyphs[%08x,%08x] [%d,%d]\n", min, max, min, max) + fmt.Printf("using glyphs #%x - #%x (%d-%d)\n", min, max, min, max) + return sb.String() +} + +type table struct { + chksum uint32 + off uint32 + size uint32 + padded uint32 + data []byte +} + +func (t table) uint16(off int) uint16 { + return binary.BigEndian.Uint16(t.data[off:]) +} + +func (t table) int16(off int) int16 { + return int16(t.uint16(off)) +} + +func (t table) uint32(off int) uint32 { + return binary.BigEndian.Uint32(t.data[off:]) +} + +func (t table) fixed32(off int) float64 { + return float64(t.uint32(off)) / 65536.0 +} + +func (t table) parseFontHeaderTable(fd *ttf) error { + // table "head" + magic := t.uint32(12) + if magic != ttfHeadMagicNumber { + return fmt.Errorf("parseHead: wrong magic number") + } + + unitsPerEm := t.uint16(18) + //fmt.Printf("unitsPerEm: %d\n", unitsPerEm) + fd.UnitsPerEm = int(unitsPerEm) + + llx := t.int16(36) + //fmt.Printf("llx: %d\n", llx) + fd.LLx = float64(fd.toPDFGlyphSpace(int(llx))) + + lly := t.int16(38) + //fmt.Printf("lly: %d\n", lly) + fd.LLy = float64(fd.toPDFGlyphSpace(int(lly))) + + urx := t.int16(40) + //fmt.Printf("urx: %d\n", urx) + fd.URx = float64(fd.toPDFGlyphSpace(int(urx))) + + ury := t.int16(42) + //fmt.Printf("ury: %d\n", ury) + fd.URy = float64(fd.toPDFGlyphSpace(int(ury))) + + return nil +} + +func uint16ToBigEndianBytes(i uint16) []byte { + b := make([]byte, 2) + binary.BigEndian.PutUint16(b, i) + return b +} + +func uint32ToBigEndianBytes(i uint32) []byte { + b := make([]byte, 4) + binary.BigEndian.PutUint32(b, i) + return b +} + +func utf16BEToString(bb []byte) string { + buf := make([]uint16, len(bb)/2) + for i := 0; i < len(buf); i++ { + buf[i] = binary.BigEndian.Uint16(bb[2*i:]) + } + return string(utf16.Decode(buf)) +} + +func (t table) parsePostScriptTable(fd *ttf) error { + // table "post" + italicAngle := t.fixed32(4) + //fmt.Printf("italicAngle: %2.2f\n", italicAngle) + fd.ItalicAngle = italicAngle + + isFixedPitch := t.uint16(16) + //fmt.Printf("isFixedPitch: %t\n", isFixedPitch != 0) + fd.FixedPitch = isFixedPitch != 0 + + return nil +} + +// func printUnicodeRange(off int, r uint32) { +// for i := 0; i < 32; i++ { +// if r&1 > 0 { +// fmt.Printf("bit %d: on\n", off+i) +// } +// r >>= 1 +// } +// } + +func (t table) parseWindowsMetricsTable(fd *ttf) error { + // table "OS/2" + version := t.uint16(0) + fsType := t.uint16(8) + fd.Protected = fsType&2 > 0 + //fmt.Printf("protected: %t\n", fd.Protected) + + uniCodeRange1 := t.uint32(42) + //fmt.Printf("uniCodeRange1: %032b\n", uniCodeRange1) + fd.UnicodeRange[0] = uniCodeRange1 + + uniCodeRange2 := t.uint32(46) + //fmt.Printf("uniCodeRange2: %032b\n", uniCodeRange2) + fd.UnicodeRange[1] = uniCodeRange2 + + uniCodeRange3 := t.uint32(50) + //fmt.Printf("uniCodeRange3: %032b\n", uniCodeRange3) + fd.UnicodeRange[2] = uniCodeRange3 + + uniCodeRange4 := t.uint32(54) + //fmt.Printf("uniCodeRange4: %032b\n", uniCodeRange4) + fd.UnicodeRange[3] = uniCodeRange4 + + // printUnicodeRange(0, uniCodeRange1) + // printUnicodeRange(32, uniCodeRange2) + // printUnicodeRange(64, uniCodeRange3) + // printUnicodeRange(96, uniCodeRange4) + + sTypoAscender := t.int16(68) + fd.Ascent = fd.toPDFGlyphSpace(int(sTypoAscender)) + + sTypoDescender := t.int16(70) + fd.Descent = fd.toPDFGlyphSpace(int(sTypoDescender)) + + // sCapHeight: This field was defined in version 2 of the OS/2 table. + // sCapHeight = int16(0) + if version >= 2 { + sCapHeight := t.int16(88) + fd.CapHeight = fd.toPDFGlyphSpace(int(sCapHeight)) + } else { + // TODO the value may be set equal to the top of the unscaled and unhinted glyph bounding box + // of the glyph encoded at U+0048 (LATIN CAPITAL LETTER H). + fd.CapHeight = fd.Ascent + } + + fsSelection := t.uint16(62) + fd.Bold = fsSelection&0x40 > 0 + + fsFirstCharIndex := t.uint16(64) + fd.FirstChar = fsFirstCharIndex + + fsLastCharIndex := t.uint16(66) + fd.LastChar = fsLastCharIndex + + return nil +} + +func (t table) parseNamingTable(fd *ttf) error { + // table "name" + count := int(t.uint16(2)) + stringOffset := t.uint16(4) + var nameID uint16 + baseOff := 6 + for i := 0; i < count; i++ { + recOff := baseOff + i*12 + pf := t.uint16(recOff) + enc := t.uint16(recOff + 2) + lang := t.uint16(recOff + 4) + nameID = t.uint16(recOff + 6) + l := t.uint16(recOff + 8) + o := t.uint16(recOff + 10) + soff := stringOffset + o + s := t.data[soff : soff+l] + if nameID == 6 { + if pf == 3 && enc == 1 && lang == 0x0409 { + fd.PostscriptName = utf16BEToString(s) + return nil + } + if pf == 1 && enc == 0 && lang == 0 { + fd.PostscriptName = string(s) + return nil + } + } + } + + return errors.New("pdfcpu: unable to identify postscript name") +} + +func (t table) parseHorizontalHeaderTable(fd *ttf) error { + // table "hhea" + ascent := t.int16(4) + //fmt.Printf("ascent: %d\n", ascent) + if fd.Ascent == 0 { + fd.Ascent = fd.toPDFGlyphSpace(int(ascent)) + } + + descent := t.int16(6) + //fmt.Printf("descent: %d\n", descent) + if fd.Descent == 0 { + fd.Descent = fd.toPDFGlyphSpace(int(descent)) + } + + //lineGap := t.int16(8) + //fmt.Printf("lineGap: %d\n", lineGap) + + //advanceWidthMax := t.uint16(10) + //fmt.Printf("advanceWidthMax: %d\n", advanceWidthMax) + + //minLeftSideBearing := t.int16(12) + //fmt.Printf("minLeftSideBearing: %d\n", minLeftSideBearing) + + //minRightSideBearing := t.int16(14) + //fmt.Printf("minRightSideBearing: %d\n", minRightSideBearing) + + //xMaxExtent := t.int16(16) + //fmt.Printf("xMaxExtent: %d\n", xMaxExtent) + + numOfLongHorMetrics := t.uint16(34) + //fmt.Printf("numOfLongHorMetrics: %d\n", numOfLongHorMetrics) + fd.HorMetricsCount = int(numOfLongHorMetrics) + + return nil +} + +func (t table) parseMaximumProfile(fd *ttf) error { + // table "maxp" + numGlyphs := t.uint16(4) + fd.GlyphCount = int(numGlyphs) + return nil +} + +func (t table) parseHorizontalMetricsTable(fd *ttf) error { + // table "hmtx" + fd.GlyphWidths = make([]int, fd.GlyphCount) + + for i := 0; i < int(fd.HorMetricsCount); i++ { + fd.GlyphWidths[i] = fd.toPDFGlyphSpace(int(t.uint16(i * 4))) + } + + for i := fd.HorMetricsCount; i < fd.GlyphCount; i++ { + fd.GlyphWidths[i] = fd.GlyphWidths[fd.HorMetricsCount-1] + } + + return nil +} + +func (t table) parseCMapFormat4(fd *ttf) error { + fd.Planes[0] = true + segCount := int(t.uint16(6) / 2) + endOff := 14 + startOff := endOff + 2*segCount + 2 + deltaOff := startOff + 2*segCount + rangeOff := deltaOff + 2*segCount + + count := 0 + for i := 0; i < segCount; i++ { + sc := t.uint16(startOff + i*2) + startCode := uint32(sc) + if fd.FirstChar == 0 { + fd.FirstChar = sc + } + ec := t.uint16(endOff + i*2) + endCode := uint32(ec) + if fd.LastChar == 0 { + fd.LastChar = ec + } + idDelta := uint32(t.uint16(deltaOff + i*2)) + idRangeOff := int(t.uint16(rangeOff + i*2)) + var v uint16 + for c, j := startCode, 0; c <= endCode && c != 0xFFFF; c++ { + if idRangeOff > 0 { + v = t.uint16(rangeOff + i*2 + idRangeOff + j*2) + } else { + v = uint16(c + idDelta) + } + if gi := v; gi > 0 { + fd.Chars[c] = gi + fd.ToUnicode[gi] = c + count++ + } + j++ + } + } + return nil +} + +func (t table) parseCMapFormat12(fd *ttf) error { + numGroups := int(t.uint32(12)) + off := 16 + count := 0 + var ( + lowestStartCode uint32 + prevCode uint32 + ) + for i := 0; i < numGroups; i++ { + base := off + i*12 + startCode := t.uint32(base) + if lowestStartCode == 0 { + lowestStartCode = startCode + fd.Planes[int(lowestStartCode/0x10000)] = true + } + if startCode/0x10000 != prevCode/0x10000 { + fd.Planes[int(startCode/0x10000)] = true + } + endCode := t.uint32(base + 4) + if startCode != endCode { + if startCode/0x10000 != endCode/0x10000 { + fd.Planes[int(endCode/0x10000)] = true + } + } + prevCode = endCode + startGlyphID := uint16(t.uint32(base + 8)) + for c, gi := startCode, startGlyphID; c <= endCode; c++ { + fd.Chars[c] = gi + fd.ToUnicode[gi] = c + gi++ + count++ + } + } + return nil +} + +func (t table) parseCharToGlyphMappingTable(fd *ttf) error { + // table "cmap" + + fd.Chars = map[uint32]uint16{} + fd.ToUnicode = map[uint16]uint32{} + fd.Planes = map[int]bool{} + tableCount := t.uint16(2) + baseOff := 4 + var pf, enc, f uint16 + m := map[string]table{} + + for i := 0; i < int(tableCount); i++ { + off := baseOff + i*8 + pf = t.uint16(off) + enc = t.uint16(off + 2) + o := t.uint32(off + 4) + f = t.uint16(int(o)) + if f == 14 { + continue + } + l := uint32(t.uint16(int(o) + 2)) + if f >= 8 { + l = t.uint32(int(o) + 4) + } + b := t.data[o : o+l] + t1 := table{off: o, size: uint32(l), data: b} + k := fmt.Sprintf("p%02d.e%02d.f%02d", pf, enc, f) + m[k] = t1 + } + + if t, ok := m["p00.e10.f12"]; ok { + return t.parseCMapFormat12(fd) + } + if t, ok := m["p00.e04.f12"]; ok { + return t.parseCMapFormat12(fd) + } + if t, ok := m["p03.e10.f12"]; ok { + return t.parseCMapFormat12(fd) + } + if t, ok := m["p00.e03.f04"]; ok { + return t.parseCMapFormat4(fd) + } + if t, ok := m["p03.e01.f04"]; ok { + return t.parseCMapFormat4(fd) + } + + return fmt.Errorf("pdfcpu: unsupported cmap table") +} + +func calcTableChecksum(tag string, b []byte) uint32 { + sum := uint32(0) + c := (len(b) + 3) / 4 + for i := 0; i < c; i++ { + if tag == "head" && i == 2 { + continue + } + sum += binary.BigEndian.Uint32(b[i*4:]) + } + return sum +} + +func getNext32BitAlignedLength(i uint32) uint32 { + if i%4 > 0 { + return i + (4 - i%4) + } + return i +} + +func headerAndTables(fn string, r io.ReaderAt, baseOff int64) ([]byte, map[string]*table, error) { + header := make([]byte, 12) + n, err := r.ReadAt(header, baseOff) + if err != nil { + return nil, nil, err + } + if n != 12 { + return nil, nil, fmt.Errorf("pdfcpu: corrupt ttf file: %s", fn) + } + + st := string(header[:4]) + + if st == sfntVersionCFF { + return nil, nil, fmt.Errorf("pdfcpu: %s is based on OpenType CFF and unsupported at the moment :(", fn) + } + + if st != sfntVersionTrueType && st != sfntVersionTrueTypeApple { + return nil, nil, fmt.Errorf("pdfcpu: unrecognized font format: %s", fn) + } + + c := int(binary.BigEndian.Uint16(header[4:])) + + b := make([]byte, c*16) + n, err = r.ReadAt(b, baseOff+12) + if err != nil { + return nil, nil, err + } + if n != c*16 { + return nil, nil, fmt.Errorf("pdfcpu: corrupt ttf file: %s", fn) + } + + byteCount := uint32(12) + tables := map[string]*table{} + + for j := 0; j < c; j++ { + off := j * 16 + b1 := b[off : off+16] + tag := string(b1[:4]) + chk := binary.BigEndian.Uint32(b1[4:]) + o := binary.BigEndian.Uint32(b1[8:]) + l := binary.BigEndian.Uint32(b1[12:]) + ll := getNext32BitAlignedLength(l) + byteCount += ll + t := make([]byte, ll) + n, err = r.ReadAt(t, int64(o)) + if err != nil { + return nil, nil, err + } + if n != int(ll) { + return nil, nil, fmt.Errorf("pdfcpu: corrupt table: %s", tag) + } + sum := calcTableChecksum(tag, t) + if sum != chk { + fmt.Printf("pdfcpu: fixing table<%s> checksum error; want:%d got:%d\n", tag, chk, sum) + chk = sum + } + tables[tag] = &table{chksum: chk, off: o, size: l, padded: ll, data: t} + } + + return header, tables, nil +} + +func parse(tags map[string]*table, tag string, fd *ttf) error { + t, found := tags[tag] + if !found { + // OS/2 is optional for True Type fonts. + if tag == "OS/2" { + return nil + } + return fmt.Errorf("pdfcpu: tag: %s unavailable", tag) + } + if t.data == nil { + return fmt.Errorf("pdfcpu: tag: %s no data", tag) + } + + var err error + + switch tag { + case "head": + err = t.parseFontHeaderTable(fd) + case "OS/2": + err = t.parseWindowsMetricsTable(fd) + case "post": + err = t.parsePostScriptTable(fd) + case "name": + err = t.parseNamingTable(fd) + case "hhea": + err = t.parseHorizontalHeaderTable(fd) + case "maxp": + err = t.parseMaximumProfile(fd) + case "hmtx": + err = t.parseHorizontalMetricsTable(fd) + case "cmap": + err = t.parseCharToGlyphMappingTable(fd) + } + + return err +} + +func writeGob(fileName string, fd ttf) error { + f, err := os.Create(fileName) + if err != nil { + return err + } + defer f.Close() + enc := gob.NewEncoder(f) + return enc.Encode(fd) +} + +func readGob(fileName string, fd *ttf) error { + f, err := os.Open(fileName) + if err != nil { + return err + } + defer f.Close() + dec := gob.NewDecoder(f) + return dec.Decode(fd) +} + +func installTrueTypeRep(fontDir, fontName string, header []byte, tables map[string]*table) error { + fd := ttf{} + //fmt.Println(fontName) + for _, v := range []string{"head", "OS/2", "post", "name", "hhea", "maxp", "hmtx", "cmap"} { + if err := parse(tables, v, &fd); err != nil { + return err + } + } + + bb, err := createTTF(header, tables) + if err != nil { + return err + } + fd.FontFile = bb + + if log.CLIEnabled() { + log.CLI.Println(fd.PostscriptName) + } + + gobName := filepath.Join(fontDir, fd.PostscriptName+".gob") + + // Write the populated ttf struct as gob. + if err := writeGob(gobName, fd); err != nil { + return err + } + + // Read gob and double check integrity. + fdNew := ttf{} + if err := readGob(gobName, &fdNew); err != nil { + return err + } + + if !reflect.DeepEqual(fd, fdNew) { + return errors.Errorf("pdfcpu: %s can't be installed", fontName) + } + + return nil +} + +// InstallTrueTypeCollection saves an internal representation of all fonts +// contained in a TrueType collection to the pdfcpu config dir. +func InstallTrueTypeCollection(fontDir, fn string) error { + f, err := os.Open(fn) + if err != nil { + return err + } + defer f.Close() + + b := make([]byte, 12) + n, err := f.Read(b) + if err != nil { + return err + } + if n != 12 { + return fmt.Errorf("pdfcpu: corrupt ttc file: %s", fn) + } + + if string(b[:4]) != ttcTag { + return fmt.Errorf("pdfcpu: corrupt ttc file: %s", fn) + } + + c := int(binary.BigEndian.Uint32(b[8:])) + + b = make([]byte, c*4) + n, err = f.ReadAt(b, 12) + if err != nil { + return err + } + if n != c*4 { + return fmt.Errorf("pdfcpu: corrupt ttc file: %s", fn) + } + + // Process contained fonts. + for i := 0; i < c; i++ { + off := int64(binary.BigEndian.Uint32(b[i*4:])) + header, tables, err := headerAndTables(fn, f, off) + if err != nil { + return err + } + if err := installTrueTypeRep(fontDir, fn, header, tables); err != nil { + return err + } + } + + return nil +} + +// InstallTrueTypeFont saves an internal representation of TrueType font fontName to the pdfcpu config dir. +func InstallTrueTypeFont(fontDir, fontName string) error { + f, err := os.Open(fontName) + if err != nil { + return err + } + defer f.Close() + + header, tables, err := headerAndTables(fontName, f, 0) + if err != nil { + return err + } + return installTrueTypeRep(fontDir, fontName, header, tables) +} + +// InstallFontFromBytes saves an internal representation of TrueType font fontName to the pdfcpu config dir. +func InstallFontFromBytes(fontDir, fontName string, bb []byte) error { + rd := bytes.NewReader(bb) + header, tables, err := headerAndTables(fontName, rd, 0) + if err != nil { + return err + } + return installTrueTypeRep(fontDir, fontName, header, tables) +} + +func ttfTables(tableCount int, bb []byte) (map[string]*table, error) { + tables := map[string]*table{} + b := bb[12:] + for j := 0; j < tableCount; j++ { + off := j * 16 + b1 := b[off : off+16] + tag := string(b1[:4]) + chksum := binary.BigEndian.Uint32(b1[4:]) + o := binary.BigEndian.Uint32(b1[8:]) + l := binary.BigEndian.Uint32(b1[12:]) + ll := getNext32BitAlignedLength(l) + t := append([]byte(nil), bb[o:o+ll]...) + tables[tag] = &table{chksum: chksum, off: o, size: l, padded: ll, data: t} + } + return tables, nil +} + +func glyfOffset(loca *table, gid, indexToLocFormat int) int { + if indexToLocFormat == 0 { + // short offsets + return 2 * int(loca.uint16(2*gid)) + } + // 1 .. long offsets + return int(loca.uint32(4 * gid)) +} + +func writeGlyfOffset(buf *bytes.Buffer, off, indexToLocFormat int) { + var bb []byte + if indexToLocFormat == 0 { + // 0 .. short offsets + bb = uint16ToBigEndianBytes(uint16(off / 2)) + } else { + // 1 .. long offsets + bb = uint32ToBigEndianBytes(uint32(off)) + } + buf.Write(bb) +} + +func pad(bb []byte) []byte { + i := len(bb) % 4 + if i == 0 { + return bb + } + for j := 0; j < 4-i; j++ { + bb = append(bb, 0x00) + } + return bb +} + +func glyphOffsets(gid int, locaFull, glyfsFull *table, numGlyphs, indexToLocFormat int) (int, int) { + offFrom := glyfOffset(locaFull, gid, indexToLocFormat) + var offThru int + if gid == numGlyphs { + offThru = int(glyfsFull.padded) + } else { + offThru = glyfOffset(locaFull, gid+1, indexToLocFormat) + } + return offFrom, offThru +} + +func resolveCompoundGlyph(fontName string, bb []byte, usedGIDs map[uint16]bool, + locaFull, glyfsFull *table, numGlyphs, indexToLocFormat int) error { + last := false + for off := 10; !last; { + flags := binary.BigEndian.Uint16(bb[off:]) + last = flags&0x20 == 0 + wordArgs := flags&0x01 > 0 + + gid := binary.BigEndian.Uint16(bb[off+2:]) + + // Position behind arguments. + off += 6 + if wordArgs { + off += 2 + } + + // Position behind transform. + if flags&0x08 > 0 { + off += 2 + } else if flags&0x40 > 0 { + off += 4 + } else if flags&0x80 > 0 { + off += 8 + } + + if _, ok := usedGIDs[gid]; ok { + // duplicate + continue + } + + offFrom, offThru := glyphOffsets(int(gid), locaFull, glyfsFull, numGlyphs, indexToLocFormat) + if offThru < offFrom { + return errors.Errorf("pdfcpu: illegal glyfOffset for font: %s", fontName) + } + if offFrom == offThru { + // not available + continue + } + + usedGIDs[gid] = true + + cbb := glyfsFull.data[offFrom:offThru] + if cbb[0]&0x80 == 0 { + // simple + continue + } + + if err := resolveCompoundGlyph(fontName, cbb, usedGIDs, locaFull, glyfsFull, numGlyphs, indexToLocFormat); err != nil { + return err + } + } + return nil +} + +func resolveCompoundGlyphs(fontName string, usedGIDs map[uint16]bool, locaFull, glyfsFull *table, numGlyphs, indexToLocFormat int) error { + gids := make([]uint16, len(usedGIDs)) + for k := range usedGIDs { + gids = append(gids, k) + } + for _, gid := range gids { + offFrom, offThru := glyphOffsets(int(gid), locaFull, glyfsFull, numGlyphs, indexToLocFormat) + if offThru < offFrom { + return errors.Errorf("pdfcpu: illegal glyfOffset for font: %s", fontName) + } + if offFrom == offThru { + continue + } + bb := glyfsFull.data[offFrom:offThru] + if bb[0]&0x80 == 0 { + // simple + continue + } + if err := resolveCompoundGlyph(fontName, bb, usedGIDs, locaFull, glyfsFull, numGlyphs, indexToLocFormat); err != nil { + return err + } + } + return nil +} + +func glyfAndLoca(fontName string, tables map[string]*table, usedGIDs map[uint16]bool) error { + head, ok := tables["head"] + if !ok { + return errors.Errorf("pdfcpu: missing \"head\" table for font: %s", fontName) + } + + maxp, ok := tables["maxp"] + if !ok { + return errors.Errorf("pdfcpu: missing \"maxp\" table for font: %s", fontName) + } + + glyfsFull, ok := tables["glyf"] + if !ok { + return errors.Errorf("pdfcpu: missing \"glyf\" table for font: %s", fontName) + } + + locaFull, ok := tables["loca"] + if !ok { + return errors.Errorf("pdfcpu: missing \"loca\" table for font: %s", fontName) + } + + indexToLocFormat := int(head.uint16(50)) + // 0 .. short offsets + // 1 .. long offsets + numGlyphs := int(maxp.uint16(4)) + + if err := resolveCompoundGlyphs(fontName, usedGIDs, locaFull, glyfsFull, numGlyphs, indexToLocFormat); err != nil { + return err + } + + gids := make([]int, 0, len(usedGIDs)+1) + gids = append(gids, 0) + for gid := range usedGIDs { + gids = append(gids, int(gid)) + } + sort.Ints(gids) + + glyfBytes := []byte{} + var buf bytes.Buffer + off := 0 + firstPendingGID := 0 + + for _, gid := range gids { + offFrom, offThru := glyphOffsets(gid, locaFull, glyfsFull, numGlyphs, indexToLocFormat) + if offThru < offFrom { + return errors.Errorf("pdfcpu: illegal glyfOffset for font: %s", fontName) + } + if offThru != offFrom { + // We have a glyph outline. + for i := 0; i < gid-firstPendingGID; i++ { + writeGlyfOffset(&buf, off, indexToLocFormat) + } + glyfBytes = append(glyfBytes, glyfsFull.data[offFrom:offThru]...) + writeGlyfOffset(&buf, off, indexToLocFormat) + off += offThru - offFrom + firstPendingGID = gid + 1 + } + } + for i := 0; i <= numGlyphs-firstPendingGID; i++ { + writeGlyfOffset(&buf, off, indexToLocFormat) + } + + bb := buf.Bytes() + locaFull.size = uint32(len(bb)) + locaFull.data = pad(bb) + locaFull.padded = uint32(len(locaFull.data)) + + glyfsFull.size = uint32(len(glyfBytes)) + glyfsFull.data = pad(glyfBytes) + glyfsFull.padded = uint32(len(glyfsFull.data)) + + return nil +} + +func createTTF(header []byte, tables map[string]*table) ([]byte, error) { + tags := []string{} + for t := range tables { + tags = append(tags, t) + } + sort.Strings(tags) + + buf := bytes.NewBuffer(header) + off := uint32(len(header) + len(tables)*16) + o := off + for _, tag := range tags { + t := tables[tag] + if _, err := buf.WriteString(tag); err != nil { + return nil, err + } + if tag == "loca" || tag == "glyf" { + t.chksum = calcTableChecksum(tag, t.data) + } + if _, err := buf.Write(uint32ToBigEndianBytes(t.chksum)); err != nil { + return nil, err + } + t.off = o + if _, err := buf.Write(uint32ToBigEndianBytes(t.off)); err != nil { + return nil, err + } + if _, err := buf.Write(uint32ToBigEndianBytes(t.size)); err != nil { + return nil, err + } + o += t.padded + } + + for _, tag := range tags { + t := tables[tag] + n, err := buf.Write(t.data) + if err != nil { + return nil, err + } + if n != len(t.data) || n != int(t.padded) { + return nil, errors.Errorf("pdfcpu: unable to write %s data\n", tag) + } + } + + return buf.Bytes(), nil +} + +// Subset creates a new font file based on usedGIDs. +func Subset(fontName string, usedGIDs map[uint16]bool) ([]byte, error) { + bb, err := Read(fontName) + if err != nil { + return nil, err + } + + header := bb[:12] + tableCount := int(binary.BigEndian.Uint16(header[4:])) + tables, err := ttfTables(tableCount, bb) + if err != nil { + return nil, err + } + + if err := glyfAndLoca(fontName, tables, usedGIDs); err != nil { + return nil, err + } + + return createTTF(header, tables) +} diff --git a/pkg/font/metrics.go b/pkg/font/metrics.go new file mode 100644 index 0000000000000000000000000000000000000000..720b88724885bf36b2b6d9d9e7a85c9d64a3cd41 --- /dev/null +++ b/pkg/font/metrics.go @@ -0,0 +1,426 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 font + +import ( + "encoding/gob" + "fmt" + "math" + "os" + "path" + "path/filepath" + "runtime/debug" + "strconv" + "strings" + "sync" + + "github.com/pdfcpu/pdfcpu/internal/corefont/metrics" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + + "github.com/pkg/errors" +) + +// TTFLight represents a TrueType font w/o font file. +type TTFLight struct { + PostscriptName string // name: NameID 6 + Protected bool // OS/2: fsType + UnitsPerEm int // head: unitsPerEm + Ascent int // OS/2: sTypoAscender + Descent int // OS/2: sTypoDescender + CapHeight int // OS/2: sCapHeight + FirstChar uint16 // OS/2: fsFirstCharIndex + LastChar uint16 // OS/2: fsLastCharIndex + UnicodeRange [4]uint32 // OS/2: Unicode Character Range + LLx, LLy, URx, URy float64 // head: xMin, yMin, xMax, yMax (fontbox) + ItalicAngle float64 // post: italicAngle + FixedPitch bool // post: isFixedPitch + Bold bool // OS/2: usWeightClass == 7 + HorMetricsCount int // hhea: numOfLongHorMetrics + GlyphCount int // maxp: numGlyphs + GlyphWidths []int // hmtx: fd.HorMetricsCount.advanceWidth + Chars map[uint32]uint16 // cmap: Unicode character to glyph index + ToUnicode map[uint16]uint32 // map glyph index to unicode character + Planes map[int]bool // used Unicode planes +} + +func (fd TTFLight) String() string { + return fmt.Sprintf(` + PostscriptName = %s + Protected = %t + UnitsPerEm = %d + Ascent = %d + Descent = %d + CapHeight = %d + FirstChar = %d + LastChar = %d +FontBoundingBox = (%.2f, %.2f, %.2f, %.2f) + ItalicAngle = %.2f + FixedPitch = %t + Bold = %t +HorMetricsCount = %d + GlyphCount = %d +len(GlyphWidths) = %d`, + fd.PostscriptName, + fd.Protected, + fd.UnitsPerEm, + fd.Ascent, + fd.Descent, + fd.CapHeight, + fd.FirstChar, + fd.LastChar, + fd.LLx, fd.LLy, fd.URx, fd.URy, + fd.ItalicAngle, + fd.FixedPitch, + fd.Bold, + fd.HorMetricsCount, + fd.GlyphCount, + len(fd.GlyphWidths), + ) +} + +func (fd TTFLight) supportsUnicodeBlock(bit int) bool { + i := fd.UnicodeRange[bit/32] + i >>= uint32(bit) % 32 + return i&1 > 0 +} + +func (fd TTFLight) supportsUnicodeBlocks(bits []int) bool { + // return true if we have support for the first or one of the following unicodeBlocks. + ok := fd.supportsUnicodeBlock(bits[0]) + if ok || len(bits) == 1 { + return ok + } + for i := range bits[1:] { + if fd.supportsUnicodeBlock(i) { + return true + } + } + return false +} + +func (fd TTFLight) unicodeRangeBits(id string) []int { + // Map iso15924 script codes (=id) to corresponding unicode blocks. + // Returns a slice of relevant unicodeRangeBits. + // + // This mapping is incomplete as we only cover unicode blocks of the most popular scripts. + // Please go to https://github.com/pdfcpu/pdfcpu/issues/new/choose for an extension request. + // + // 0 Basic Latin 0000-007F + // 1 Latin-1 Supplement 0080-00FF + // 2 Latin Extended-A 0100-017F + // 3 Latin Extended-B 0180-024F + // 7 Greek 0370-03FF + // 9 Cyrillic 0400-04FF + // 10 Armenian 0530-058F + // 11 Hebrew 0590-05FF + // 13 Arabic 0600-06FF + // 15 Devanagari 0900-097F + // 16 Bengali 0980-09FF + // 24 Thai 0E00-0E7F + // 28 Hangul Jamo 1100-11FF + // 48 CJK Symbols And Punctuation 3000-303F + // 49 Hiragana 3040-309F + // 50 Katakana 30A0-30FF + // 52 Hangul Compatibility Jamo 3130-318F + // 61 CJK Strokes 31C0-31EF + // 54 Enclosed CJK Letters And Months 3200-32FF + // 55 CJK Compatibility 3300-33FF + // 59 CJK Unified Ideographs 4E00-9FFF + // 56 Hangul Syllables AC00-D7AF + + var a []int + switch id { + case "LATN": // Latin + a = append(a, 0, 1, 2, 3) + case "GREK": // Greek + a = append(a, 7) + case "CYRL": // Cyrillic + a = append(a, 9) + case "ARMN": // Armenian + a = append(a, 10) + case "HEBR": // Hebrew + a = append(a, 11) + case "ARAB": // Arabic + a = append(a, 13) + case "DEVA": // Devanagari + a = append(a, 15) + case "BENG": // Bengali + a = append(a, 16) + case "THAI": // Thai + a = append(a, 24) + case "HIRA": // Hiragana + a = append(a, 49) + case "KANA": // Katakana + a = append(a, 50) + case "JPAN": // Japanese + a = append(a, 59, 49, 50) + case "KORE", "HANG": // Korean, Hangul + a = append(a, 59, 28, 52, 56) + case "HANS", "HANT": // Han Simplified, Han Traditional + a = append(a, 59) + } + + return a +} + +// SupportsScript returns true if ttf supports the unicodeblocks identified by iso15924 id. +func (fd TTFLight) SupportsScript(id string) (bool, error) { + + if len(id) != 4 { + return false, errors.New("\"script\" must be a iso15924 code (length = 4") + } + + bits := fd.unicodeRangeBits(id) + if bits == nil { + return false, errors.New("\"script\" must be one of: ARAB, ARMN, CYRL, GREK, HANG, HANS, HANT, HEBR, HIRA, LATN, JPAN, KANA, KORE, THAI") + } + + return fd.supportsUnicodeBlocks(bits), nil +} + +// UserFontDir is the location for installed TTF or OTF font files. +var UserFontDir string + +// UserFontMetrics represents font metrics for TTF or OTF font files installed into UserFontDir. +var UserFontMetrics = map[string]TTFLight{} +var UserFontMetricsLock = &sync.RWMutex{} + +func load(fileName string, fd *TTFLight) error { + //fmt.Printf("reading gob from: %s\n", fileName) + f, err := os.Open(fileName) + if err != nil { + return err + } + defer f.Close() + dec := gob.NewDecoder(f) + return dec.Decode(fd) +} + +// Read reads in the font file bytes from gob +func Read(fileName string) ([]byte, error) { + fn := filepath.Join(UserFontDir, fileName+".gob") + f, err := os.Open(fn) + if err != nil { + return nil, err + } + defer f.Close() + dec := gob.NewDecoder(f) + ff := &struct{ FontFile []byte }{} + err = dec.Decode(ff) + return ff.FontFile, err +} + +func isSupportedFontFile(filename string) bool { + return strings.HasSuffix(strings.ToLower(filename), ".gob") +} + +// LoadUserFonts loads any installed TTF or OTF font files. +func LoadUserFonts() error { + //fmt.Printf("loading userFonts from %s\n", UserFontDir) + files, err := os.ReadDir(UserFontDir) + if err != nil { + return err + } + for _, f := range files { + if !isSupportedFontFile(f.Name()) { + continue + } + ttf := TTFLight{} + fn := filepath.Join(UserFontDir, f.Name()) + if err := load(fn, &ttf); err != nil { + return err + } + fn = strings.TrimSuffix(f.Name(), path.Ext(f.Name())) + //fmt.Printf("loading %s.ttf...\n", fn) + //fmt.Printf("Loaded %s:\n%s", fn, ttf) + UserFontMetricsLock.Lock() + UserFontMetrics[fn] = ttf + UserFontMetricsLock.Unlock() + } + return nil +} + +// BoundingBox returns the font bounding box for a given font as specified in the corresponding AFM file. +func BoundingBox(fontName string) *types.Rectangle { + if IsCoreFont(fontName) { + return metrics.CoreFontMetrics[fontName].FBox + } + UserFontMetricsLock.RLock() + defer UserFontMetricsLock.RUnlock() + llx := UserFontMetrics[fontName].LLx + lly := UserFontMetrics[fontName].LLy + urx := UserFontMetrics[fontName].URx + ury := UserFontMetrics[fontName].URy + return types.NewRectangle(llx, lly, urx, ury) +} + +// CharWidth returns the character width for a char and font in glyph space units. +func CharWidth(fontName string, r rune) int { + if IsCoreFont(fontName) { + return metrics.CoreFontCharWidth(fontName, int(r)) + } + UserFontMetricsLock.RLock() + defer UserFontMetricsLock.RUnlock() + ttf, ok := UserFontMetrics[fontName] + if !ok { + fmt.Fprintf(os.Stderr, "pdfcpu: user font not loaded: %s\n", fontName) + debug.PrintStack() + os.Exit(1) + } + + pos, ok := ttf.Chars[uint32(r)] + if !ok { + pos = 0 + } + return int(ttf.GlyphWidths[pos]) +} + +// UserSpaceUnits transforms glyphSpaceUnits into userspace units. +func UserSpaceUnits(glyphSpaceUnits float64, fontScalingFactor int) float64 { + return glyphSpaceUnits / 1000 * float64(fontScalingFactor) +} + +// GlyphSpaceUnits transforms userSpaceUnits into glyphspace Units. +func GlyphSpaceUnits(userSpaceUnits float64, fontScalingFactor int) float64 { + return userSpaceUnits * 1000 / float64(fontScalingFactor) +} + +func fontScalingFactor(glyphSpaceUnits, userSpaceUnits float64) int { + return int(math.Round(userSpaceUnits / glyphSpaceUnits * 1000)) +} + +// Descent returns fontname's descent in userspace units corresponding to fontSize. +func Descent(fontName string, fontSize int) float64 { + fbb := BoundingBox(fontName) + return UserSpaceUnits(-fbb.LL.Y, fontSize) +} + +// Ascent returns fontname's ascent in userspace units corresponding to fontSize. +func Ascent(fontName string, fontSize int) float64 { + fbb := BoundingBox(fontName) + return UserSpaceUnits(fbb.Height()+fbb.LL.Y, fontSize) +} + +// LineHeight returns fontname's line height in userspace units corresponding to fontSize. +func LineHeight(fontName string, fontSize int) float64 { + fbb := BoundingBox(fontName) + return UserSpaceUnits(fbb.Height(), fontSize) +} + +func glyphSpaceWidth(text, fontName string) int { + var w int + if IsCoreFont(fontName) { + for i := 0; i < len(text); i++ { + c := text[i] + w += CharWidth(fontName, rune(c)) + } + return w + } + for _, r := range text { + w += CharWidth(fontName, r) + } + return w +} + +// TextWidth represents the width in user space units for a given text string, font name and font size. +func TextWidth(text, fontName string, fontSize int) float64 { + w := glyphSpaceWidth(text, fontName) + return UserSpaceUnits(float64(w), fontSize) +} + +// Size returns the needed font size (aka. font scaling factor) in points +// for rendering a given text string using a given font name with a given user space width. +func Size(text, fontName string, width float64) int { + w := glyphSpaceWidth(text, fontName) + return fontScalingFactor(float64(w), width) +} + +// SizeForLineHeight returns the needed font size in points +// for rendering using a given font name fitting into given line height lh. +func SizeForLineHeight(fontName string, lh float64) int { + fbb := BoundingBox(fontName) + return int(math.Round(lh / (fbb.Height() / 1000))) +} + +// UserSpaceFontBBox returns the font box for given font name and font size in user space coordinates. +func UserSpaceFontBBox(fontName string, fontSize int) *types.Rectangle { + fontBBox := BoundingBox(fontName) + llx := UserSpaceUnits(fontBBox.LL.X, fontSize) + lly := UserSpaceUnits(fontBBox.LL.Y, fontSize) + urx := UserSpaceUnits(fontBBox.UR.X, fontSize) + ury := UserSpaceUnits(fontBBox.UR.Y, fontSize) + return types.NewRectangle(llx, lly, urx, ury) +} + +// IsCoreFont returns true for the 14 PDF standard Type 1 fonts. +func IsCoreFont(fontName string) bool { + _, ok := metrics.CoreFontMetrics[fontName] + return ok +} + +// CoreFontNames returns a list of the 14 PDF standard Type 1 fonts. +func CoreFontNames() []string { + ss := []string{} + for fontName := range metrics.CoreFontMetrics { + ss = append(ss, fontName) + } + return ss +} + +// IsUserFont returns true for installed TrueType fonts. +func IsUserFont(fontName string) bool { + UserFontMetricsLock.RLock() + defer UserFontMetricsLock.RUnlock() + _, ok := UserFontMetrics[fontName] + return ok +} + +// UserFontNames return a list of all installed TrueType fonts. +func UserFontNames() []string { + ss := []string{} + UserFontMetricsLock.RLock() + defer UserFontMetricsLock.RUnlock() + for fontName := range UserFontMetrics { + ss = append(ss, fontName) + } + return ss +} + +// UserFontNamesVerbose return a list of all installed TrueType fonts including glyph count. +func UserFontNamesVerbose() []string { + ss := []string{} + UserFontMetricsLock.RLock() + defer UserFontMetricsLock.RUnlock() + for fName, ttf := range UserFontMetrics { + s := fName + " (" + strconv.Itoa(ttf.GlyphCount) + " glyphs)" + ss = append(ss, s) + } + return ss +} + +// SupportedFont returns true for core fonts or user installed fonts. +func SupportedFont(fontName string) bool { + return IsCoreFont(fontName) || IsUserFont(fontName) +} + +func (fd TTFLight) Gids() []int { + gids := make([]int, 0, len(fd.Chars)) + for _, g := range fd.Chars { + gids = append(gids, int(g)) + } + return gids +} diff --git a/pkg/log/log.go b/pkg/log/log.go new file mode 100644 index 0000000000000000000000000000000000000000..a621680e34096c425fa26997da055ec799c8b20e --- /dev/null +++ b/pkg/log/log.go @@ -0,0 +1,279 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 log provides a logging abstraction. +package log + +import ( + "log" + "os" +) + +// Logger defines an interface for logging messages. +type Logger interface { + + // Printf logs a formatted string. + Printf(format string, args ...interface{}) + + // Println logs a line. + Println(args ...interface{}) + + // Fatalf is equivalent to Printf() followed by a program abort. + Fatalf(format string, args ...interface{}) + + // Fatalln is equivalent to Println() followed by a progam abort. + Fatalln(args ...interface{}) +} + +type logger struct { + log Logger +} + +// pdfcpu's loggers. +var ( + + // Horizontal loggers + Debug = &logger{} + Info = &logger{} + Stats = &logger{} + Trace = &logger{} + + // Vertical loggers + Parse = &logger{} + Read = &logger{} + Validate = &logger{} + Optimize = &logger{} + Write = &logger{} + CLI = &logger{} +) + +// SetDebugLogger sets the debug logger. +func SetDebugLogger(log Logger) { + Debug.log = log +} + +// SetInfoLogger sets the info logger. +func SetInfoLogger(log Logger) { + Info.log = log +} + +// SetStatsLogger sets the stats logger. +func SetStatsLogger(log Logger) { + Stats.log = log +} + +// SetTraceLogger sets the trace logger. +func SetTraceLogger(log Logger) { + Trace.log = log +} + +// SetParseLogger sets the parse logger. +func SetParseLogger(log Logger) { + Parse.log = log +} + +// SetReadLogger sets the read logger. +func SetReadLogger(log Logger) { + Read.log = log +} + +// SetValidateLogger sets the validate logger. +func SetValidateLogger(log Logger) { + Validate.log = log +} + +// SetOptimizeLogger sets the optimize logger. +func SetOptimizeLogger(log Logger) { + Optimize.log = log +} + +// SetWriteLogger sets the write logger. +func SetWriteLogger(log Logger) { + Write.log = log +} + +// SetCLILogger sets the api logger. +func SetCLILogger(log Logger) { + CLI.log = log +} + +// SetDefaultDebugLogger sets the default debug logger. +func SetDefaultDebugLogger() { + SetDebugLogger(log.New(os.Stderr, "DEBUG: ", log.Ldate|log.Ltime)) +} + +// SetDefaultInfoLogger sets the default info logger. +func SetDefaultInfoLogger() { + SetInfoLogger(log.New(os.Stderr, " INFO: ", log.Ldate|log.Ltime)) +} + +// SetDefaultStatsLogger sets the default stats logger. +func SetDefaultStatsLogger() { + SetStatsLogger(log.New(os.Stderr, "STATS: ", log.Ldate|log.Ltime)) +} + +// SetDefaultTraceLogger sets the default trace logger. +func SetDefaultTraceLogger() { + SetTraceLogger(log.New(os.Stderr, "TRACE: ", log.Ldate|log.Ltime)) +} + +// SetDefaultParseLogger sets the default parse logger. +func SetDefaultParseLogger() { + SetParseLogger(log.New(os.Stderr, "PARSE: ", log.Ldate|log.Ltime)) +} + +// SetDefaultReadLogger sets the default read logger. +func SetDefaultReadLogger() { + SetReadLogger(log.New(os.Stderr, " READ: ", log.Ldate|log.Ltime)) +} + +// SetDefaultValidateLogger sets the default validate logger. +func SetDefaultValidateLogger() { + SetValidateLogger(log.New(os.Stderr, "VALID: ", log.Ldate|log.Ltime)) +} + +// SetDefaultOptimizeLogger sets the default optimize logger. +func SetDefaultOptimizeLogger() { + SetOptimizeLogger(log.New(os.Stderr, " OPT: ", log.Ldate|log.Ltime)) +} + +// SetDefaultWriteLogger sets the default write logger. +func SetDefaultWriteLogger() { + SetWriteLogger(log.New(os.Stderr, "WRITE: ", log.Ldate|log.Ltime)) +} + +// SetDefaultCLILogger sets the default cli logger. +func SetDefaultCLILogger() { + SetCLILogger(log.New(os.Stdout, "", 0)) +} + +// SetDefaultLoggers sets all loggers to their default logger. +func SetDefaultLoggers() { + SetDefaultDebugLogger() + SetDefaultInfoLogger() + SetDefaultStatsLogger() + SetDefaultTraceLogger() + SetDefaultParseLogger() + SetDefaultReadLogger() + SetDefaultValidateLogger() + SetDefaultOptimizeLogger() + SetDefaultWriteLogger() + SetDefaultCLILogger() +} + +// DisableLoggers turns off all logging. +func DisableLoggers() { + SetDebugLogger(nil) + SetInfoLogger(nil) + SetStatsLogger(nil) + SetTraceLogger(nil) + SetParseLogger(nil) + SetReadLogger(nil) + SetValidateLogger(nil) + SetOptimizeLogger(nil) + SetWriteLogger(nil) + SetCLILogger(nil) +} + +// CLIEnabled returns true if the CLI Logger is enabled. +func CLIEnabled() bool { + return CLI.log != nil +} + +// DebugEnabled returns true if the Debug Logger is enabled. +func DebugEnabled() bool { + return Debug.log != nil +} + +// InfoEnabled returns true if the Info Logger is enabled. +func InfoEnabled() bool { + return Info.log != nil +} + +// OptimizeEnabled returns true if the Optimize Logger is enabled. +func OptimizeEnabled() bool { + return Optimize.log != nil +} + +// ParseEnabled returns true if the Parse Logger is enabled. +func ParseEnabled() bool { + return Parse.log != nil +} + +// ReadEnabled returns true if the Read Logger is enabled. +func ReadEnabled() bool { + return Read.log != nil +} + +// StatsEnabled returns true if the Read Logger is enabled. +func StatsEnabled() bool { + return Stats.log != nil +} + +// TraceEnabled returns true if the Trace Logger is enabled. +func TraceEnabled() bool { + return Trace.log != nil +} + +// ValidateEnabled returns true if the Validate Logger is enabled. +func ValidateEnabled() bool { + return Validate.log != nil +} + +// WriteEnabled returns true if the Write Logger is enabled. +func WriteEnabled() bool { + return Write.log != nil +} + +// Printf writes a formatted message to the log. +func (l *logger) Printf(format string, args ...interface{}) { + + if l.log == nil { + return + } + + l.log.Printf(format, args...) +} + +// Println writes a line to the log. +func (l *logger) Println(args ...interface{}) { + + if l.log == nil { + return + } + + l.log.Println(args...) +} + +// Fatalf is equivalent to Printf() followed by a program abort. +func (l *logger) Fatalf(format string, args ...interface{}) { + + if l.log == nil { + return + } + + l.log.Fatalf(format, args...) +} + +// Fatalf is equivalent to Println() followed by a program abort. +func (l *logger) Fatalln(args ...interface{}) { + + if l.log == nil { + return + } + + l.log.Fatalln(args...) +} diff --git a/pkg/log/log_test.go b/pkg/log/log_test.go new file mode 100644 index 0000000000000000000000000000000000000000..3a32f0b19e927ea12d14f18875b8087f4a45f54a --- /dev/null +++ b/pkg/log/log_test.go @@ -0,0 +1,32 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 log + +import "testing" + +func TestLog(t *testing.T) { + + Debug.Printf("Test%s\n", "log") + Debug.Println("Testlog") + Debug.Fatalf("Test%s\n", "Fail") + Debug.Fatalln("TestFail") + + SetDefaultLoggers() + Debug.Printf("Testlog\n") + Debug.Println("Testlog") + DisableLoggers() +} diff --git a/pkg/pdfcpu/annotation.go b/pkg/pdfcpu/annotation.go new file mode 100644 index 0000000000000000000000000000000000000000..ae7d73014816162758e04f75af3165e73dc6c709 --- /dev/null +++ b/pkg/pdfcpu/annotation.go @@ -0,0 +1,1030 @@ +/* + Copyright 2021 The pdfcpu Authors. + + 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 + + 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 pdfcpu + +import ( + "fmt" + "sort" + "strconv" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// CachedAnnotationObjNrs returns a list of object numbers representing known annotation dict indirect references. +func CachedAnnotationObjNrs(ctx *model.Context) ([]int, error) { + // Note: Not all cached annotations are based on IndRefs! + // pdfcpu also caches direct annot dict objects (violating the PDF spec) for listing purposes. + // Such annotations may only be removed as part of removing all annotations (for a page). + + objNrs := []int{} + + for _, pageAnnots := range ctx.PageAnnots { + for _, annots := range pageAnnots { + for objNr := range annots.Map { + objNrs = append(objNrs, objNr) + } + } + } + + return objNrs, nil +} + +func sortedPageNrsForAnnotsFromCache(ctx *model.Context) []int { + var pageNrs []int + for k := range ctx.PageAnnots { + pageNrs = append(pageNrs, k) + } + sort.Ints(pageNrs) + return pageNrs +} + +func addAnnotationToCache(ctx *model.Context, ann model.AnnotationRenderer, pageNr, objNr int) error { + pgAnnots, ok := ctx.PageAnnots[pageNr] + if !ok { + pgAnnots = model.PgAnnots{} + ctx.PageAnnots[pageNr] = pgAnnots + } + annots, ok := pgAnnots[ann.Type()] + if !ok { + annots = model.Annot{} + annots.Map = model.AnnotMap{} + pgAnnots[ann.Type()] = annots + } + if _, ok := annots.Map[objNr]; ok { + return errors.Errorf("addAnnotation: obj#%d already cached", objNr) + } + annots.Map[objNr] = ann + return nil +} + +func removeAnnotationFromCache(ctx *model.Context, pageNr, objNr int) error { + pgAnnots, ok := ctx.PageAnnots[pageNr] + if !ok { + return errors.Errorf("removeAnnotation: no page annotations cached for page %d", pageNr) + } + for annType, annots := range pgAnnots { + if _, ok := annots.Map[objNr]; ok { + delete(annots.Map, objNr) + if len(annots.Map) == 0 { + delete(pgAnnots, annType) + if len(pgAnnots) == 0 { + delete(ctx.PageAnnots, pageNr) + } + } + return nil + } + } + return errors.Errorf("removeAnnotation: no page annotation cached for obj#%d", objNr) +} + +func findAnnotByID(ctx *model.Context, id string, annots types.Array) (int, error) { + for i, o := range annots { + d, err := ctx.DereferenceDict(o) + if err != nil { + return -1, err + } + s := d.StringEntry("NM") + if s == nil { + continue + } + if *s == id { + return i, nil + } + } + return -1, nil +} + +func findAnnotByObjNr(objNr int, annots types.Array) (int, error) { + for i, o := range annots { + indRef, _ := o.(types.IndirectRef) + if indRef.ObjectNumber.Value() == objNr { + return i, nil + } + } + return -1, nil +} + +func createAnnot(ctx *model.Context, ar model.AnnotationRenderer, pageIndRef *types.IndirectRef) (*types.IndirectRef, types.Dict, error) { + d, err := ar.RenderDict(ctx.XRefTable, pageIndRef) + if err != nil { + return nil, nil, err + } + indRef, err := ctx.IndRefForNewObject(d) + if err != nil { + return nil, nil, err + } + return indRef, d, nil +} + +// Annotation returns an annotation renderer. +// Validation sets up a cache of annotation renderers. +func Annotation(xRefTable *model.XRefTable, d types.Dict) (model.AnnotationRenderer, error) { + + subtype := d.NameEntry("Subtype") + + o, _ := d.Find("Rect") + arr, err := xRefTable.DereferenceArray(o) + if err != nil { + return nil, err + } + + r, err := xRefTable.RectForArray(arr) + if err != nil { + return nil, err + } + + contents := "" + if c, ok := d["Contents"]; ok { + contents, err = xRefTable.DereferenceStringOrHexLiteral(c, model.V10, nil) + if err != nil { + return nil, err + } + } + + var nm string + s := d.StringEntry("NM") // This is what pdfcpu refers to as the annotation id. + if s != nil { + nm = *s + } + + var f model.AnnotationFlags + i := d.IntEntry("F") + if i != nil { + f = model.AnnotationFlags(*i) + } + + var ann model.AnnotationRenderer + + switch *subtype { + + case "Text": + popupIndRef := d.IndirectRefEntry("Popup") + ann = model.NewTextAnnotation(*r, contents, nm, "", f, nil, "", popupIndRef, nil, "", "", 0, 0, 0, true, "") + + case "Link": + var uri string + o, found := d.Find("A") + if found && o != nil { + d, err := xRefTable.DereferenceDict(o) + if err != nil { + return nil, err + } + + bb, err := xRefTable.DereferenceStringEntryBytes(d, "URI") + if err != nil { + return nil, err + } + if len(bb) > 0 { + uri = string(bb) + } + } + dest := (*model.Destination)(nil) // will not collect link dest during validation. + ann = model.NewLinkAnnotation(*r, contents, nm, "", f, nil, dest, uri, nil, false, 0, model.BSSolid) + + case "Popup": + parentIndRef := d.IndirectRefEntry("Parent") + ann = model.NewPopupAnnotation(*r, contents, nm, "", f, nil, 0, 0, 0, parentIndRef, false) + + // TODO handle remaining annotation types. + + default: + ann = model.NewAnnotationForRawType(*subtype, *r, contents, nm, "", f, nil, 0, 0, 0) + + } + + return ann, nil +} + +func AnnotationsForSelectedPages(ctx *model.Context, selectedPages types.IntSet) map[int]model.PgAnnots { + + var pageNrs []int + for k := range ctx.PageAnnots { + pageNrs = append(pageNrs, k) + } + sort.Ints(pageNrs) + + m := map[int]model.PgAnnots{} + + for _, i := range pageNrs { + + if selectedPages != nil { + if _, found := selectedPages[i]; !found { + continue + } + } + + pageAnnots := ctx.PageAnnots[i] + if len(pageAnnots) == 0 { + continue + } + + m[i] = pageAnnots + } + + return m +} + +func prepareHeader(horSep *[]int, maxLen *AnnotListMaxLengths) string { + s := " Obj# " + if maxLen.ObjNr > 4 { + s += strings.Repeat(" ", maxLen.ObjNr-4) + *horSep = append(*horSep, 10+maxLen.ObjNr-4) + } else { + *horSep = append(*horSep, 10) + } + + s += draw.VBar + " Id " + if maxLen.ID > 2 { + s += strings.Repeat(" ", maxLen.ID-2) + *horSep = append(*horSep, 4+maxLen.ID-2) + } else { + *horSep = append(*horSep, 4) + } + + s += draw.VBar + " Rect " + if maxLen.Rect > 4 { + s += strings.Repeat(" ", maxLen.Rect-4) + *horSep = append(*horSep, 6+maxLen.Rect-4) + } else { + *horSep = append(*horSep, 6) + } + + s += draw.VBar + " Content" + if maxLen.Content > 7 { + s += strings.Repeat(" ", maxLen.Rect-7) + *horSep = append(*horSep, 8+maxLen.Rect-7) + } else { + *horSep = append(*horSep, 8) + } + + return s +} + +type AnnotListMaxLengths struct { + ObjNr, ID, Rect, Content int +} + +// ListAnnotations returns a formatted list of annotations. +func ListAnnotations(annots map[int]model.PgAnnots) (int, []string, error) { + var ( + j int + pageNrs []int + ) + ss := []string{} + + for k := range annots { + pageNrs = append(pageNrs, k) + } + sort.Ints(pageNrs) + + for _, i := range pageNrs { + + pageAnnots := annots[i] + + var annTypes []string + for t := range pageAnnots { + annTypes = append(annTypes, model.AnnotTypeStrings[t]) + } + sort.Strings(annTypes) + + ss = append(ss, "") + ss = append(ss, fmt.Sprintf("Page %d:", i)) + + for _, annType := range annTypes { + annots := pageAnnots[model.AnnotTypes[annType]] + + var maxLen AnnotListMaxLengths + maxLen.ID = 2 + maxLen.Content = len("Content") + + var objNrs []int + for objNr, ann := range annots.Map { + objNrs = append(objNrs, objNr) + s := strconv.Itoa(objNr) + if len(s) > maxLen.ObjNr { + maxLen.ObjNr = len(s) + } + if len(ann.RectString()) > maxLen.Rect { + maxLen.Rect = len(ann.RectString()) + } + if len(ann.ID()) > maxLen.ID { + maxLen.ID = len(ann.ID()) + } + if len(ann.ContentString()) > maxLen.Content { + maxLen.Content = len(ann.ContentString()) + } + } + sort.Ints(objNrs) + ss = append(ss, "") + ss = append(ss, fmt.Sprintf(" %s:", annType)) + + horSep := []int{} + + // Render header. + ss = append(ss, prepareHeader(&horSep, &maxLen)) + + // Render separator. + ss = append(ss, draw.HorSepLine(horSep)) + + // Render content. + for _, objNr := range objNrs { + ann := annots.Map[objNr] + + s := strconv.Itoa(objNr) + fill1 := strings.Repeat(" ", maxLen.ObjNr-len(s)) + if maxLen.ObjNr < 4 { + fill1 += strings.Repeat(" ", 4-maxLen.ObjNr) + } + + s = ann.ID() + fill2 := strings.Repeat(" ", maxLen.ID-len(s)) + if maxLen.ID < 2 { + fill2 += strings.Repeat(" ", 2-maxLen.ID) + } + + s = ann.RectString() + fill3 := strings.Repeat(" ", maxLen.Rect-len(s)) + + ss = append(ss, fmt.Sprintf(" %s%d %s %s%s %s %s%s %s %s", + fill1, objNr, draw.VBar, fill2, ann.ID(), draw.VBar, fill3, ann.RectString(), draw.VBar, ann.ContentString())) + + j++ + } + } + } + + return j, append([]string{fmt.Sprintf("%d annotations available", j)}, ss...), nil +} + +func addAnnotationToDirectObj( + ctx *model.Context, + annots types.Array, + annotIndRef, pageDictIndRef *types.IndirectRef, + pageDict types.Dict, + pageNr int, + ar model.AnnotationRenderer, + incr bool) error { + + i, err := findAnnotByID(ctx, ar.ID(), annots) + if err != nil { + return err + } + if i >= 0 { + return errors.Errorf("page %d: duplicate annotation with id:%s\n", pageNr, ar.ID()) + } + pageDict.Update("Annots", append(annots, *annotIndRef)) + if incr { + // Mark page dict obj for incremental writing. + ctx.Write.IncrementWithObjNr(pageDictIndRef.ObjectNumber.Value()) + } + ctx.EnsureVersionForWriting() + return nil +} + +// AddAnnotation adds ar to pageDict. +func AddAnnotation( + ctx *model.Context, + pageDictIndRef *types.IndirectRef, + pageDict types.Dict, + pageNr int, + ar model.AnnotationRenderer, + incr bool) (*types.IndirectRef, types.Dict, error) { + + // Create xreftable entry for annotation. + annotIndRef, d, err := createAnnot(ctx, ar, pageDictIndRef) + if err != nil { + return nil, nil, err + } + + // Add annotation to xreftable page annotation cache. + err = addAnnotationToCache(ctx, ar, pageNr, annotIndRef.ObjectNumber.Value()) + if err != nil { + return nil, nil, err + } + + if incr { + // Mark new annotaton dict obj for incremental writing. + ctx.Write.IncrementWithObjNr(annotIndRef.ObjectNumber.Value()) + } + + obj, found := pageDict.Find("Annots") + if !found { + pageDict.Insert("Annots", types.Array{*annotIndRef}) + if incr { + // Mark page dict obj for incremental writing. + ctx.Write.IncrementWithObjNr(pageDictIndRef.ObjectNumber.Value()) + } + ctx.EnsureVersionForWriting() + return annotIndRef, d, nil + } + + ir, ok := obj.(types.IndirectRef) + if !ok { + return annotIndRef, d, addAnnotationToDirectObj(ctx, obj.(types.Array), annotIndRef, pageDictIndRef, pageDict, pageNr, ar, incr) + } + + // Annots array is an IndirectReference. + + o, err := ctx.Dereference(ir) + if err != nil || o == nil { + return nil, nil, err + } + + annots, _ := o.(types.Array) + i, err := findAnnotByID(ctx, ar.ID(), annots) + if err != nil { + return nil, nil, err + } + if i >= 0 { + return nil, nil, errors.Errorf("page %d: duplicate annotation with id:%s\n", pageNr, ar.ID()) + } + + entry, ok := ctx.FindTableEntryForIndRef(&ir) + if !ok { + return nil, nil, errors.Errorf("page %d: can't dereference Annots indirect reference(obj#:%d)\n", pageNr, ir.ObjectNumber) + } + entry.Object = append(annots, *annotIndRef) + if incr { + // Mark Annot array obj for incremental writing. + ctx.Write.IncrementWithObjNr(ir.ObjectNumber.Value()) + } + + ctx.EnsureVersionForWriting() + return annotIndRef, d, nil +} + +func AddAnnotationToPage(ctx *model.Context, pageNr int, ar model.AnnotationRenderer, incr bool) (*types.IndirectRef, types.Dict, error) { + pageDictIndRef, err := ctx.PageDictIndRef(pageNr) + if err != nil { + return nil, nil, err + } + + d, err := ctx.DereferenceDict(*pageDictIndRef) + if err != nil { + return nil, nil, err + } + + return AddAnnotation(ctx, pageDictIndRef, d, pageNr, ar, incr) +} + +// AddAnnotations adds ar to selected pages. +func AddAnnotations(ctx *model.Context, selectedPages types.IntSet, ar model.AnnotationRenderer, incr bool) (bool, error) { + var ok bool + if incr { + ctx.Write.Increment = true + ctx.Write.Offset = ctx.Read.FileSize + } + + for k, v := range selectedPages { + if !v { + continue + } + if k > ctx.PageCount { + return false, errors.Errorf("pdfcpu: invalid page number: %d", k) + } + + pageDictIndRef, err := ctx.PageDictIndRef(k) + if err != nil { + return false, err + } + + d, err := ctx.DereferenceDict(*pageDictIndRef) + if err != nil { + return false, err + } + + indRef, _, err := AddAnnotation(ctx, pageDictIndRef, d, k, ar, incr) + if err != nil { + return false, err + } + if indRef != nil { + ok = true + } + } + + return ok, nil +} + +// AddAnnotationsMap adds annotations in m to corresponding pages. +func AddAnnotationsMap(ctx *model.Context, m map[int][]model.AnnotationRenderer, incr bool) (bool, error) { + var ok bool + if incr { + ctx.Write.Increment = true + ctx.Write.Offset = ctx.Read.FileSize + } + for i, annots := range m { + + if i > ctx.PageCount { + return false, errors.Errorf("pdfcpu: invalid page number: %d", i) + } + + pageDictIndRef, err := ctx.PageDictIndRef(i) + if err != nil { + return false, err + } + + d, err := ctx.DereferenceDict(*pageDictIndRef) + if err != nil { + return false, err + } + + for _, annot := range annots { + indRef, _, err := AddAnnotation(ctx, pageDictIndRef, d, i, annot, incr) + if err != nil { + return false, err + } + if indRef != nil { + ok = true + } + } + + } + + return ok, nil +} + +func removeAllAnnotations( + ctx *model.Context, + pageDict types.Dict, + pageDictObjNr, + pageNr int, + incr bool) (bool, error) { + + var err error + obj, found := pageDict.Find("Annots") + if !found { + return false, nil + } + + ir, ok := obj.(types.IndirectRef) + if ok { + obj, err = ctx.Dereference(ir) + if err != nil || obj == nil { + return false, err + } + objNr := ir.ObjectNumber.Value() + if err = ctx.FreeObject(objNr); err != nil { + return false, err + } + if incr { + // Modify Annots array obj for incremental writing. + ctx.Write.IncrementWithObjNr(objNr) + } + } + + annots, _ := obj.(types.Array) + + for _, o := range annots { + if err := ctx.DeleteObject(o); err != nil { + return false, err + } + ir, ok := o.(types.IndirectRef) + if !ok { + continue + } + objNr := ir.ObjectNumber.Value() + if incr { + // Mark annotation dict obj for incremental writing. + ctx.Write.IncrementWithObjNr(objNr) + } + } + + pageDict.Delete("Annots") + if incr { + // Mark page dict obj for incremental writing. + ctx.Write.IncrementWithObjNr(pageDictObjNr) + } + + // Remove xref table page annotation cache. + delete(ctx.PageAnnots, pageNr) + + ctx.EnsureVersionForWriting() + + return true, nil +} + +func removeAnnotationsByType( + ctx *model.Context, + annotTypes []model.AnnotationType, + pageNr int, + annots types.Array, + incr bool) (types.Array, bool, error) { + + pgAnnots, found := ctx.PageAnnots[pageNr] + if !found { + return annots, false, nil + } + + var ok bool + + for _, annotType := range annotTypes { + annot, found := pgAnnots[annotType] + if !found { + continue + } + // We have cached annotType page annotations. + for _, indRef := range *annot.IndRefs { + objNr := indRef.ObjectNumber.Value() + i, err := findAnnotByObjNr(objNr, annots) + if err != nil { + return nil, false, err + } + if i < 0 { + return nil, false, errors.New("pdfcpu: missing annot indRef") + } + if err := ctx.DeleteObject(indRef); err != nil { + return nil, false, err + } + if incr { + // Mark annotation dict obj for incremental writing. + ctx.Write.IncrementWithObjNr(indRef.ObjectNumber.Value()) + } + + if len(annots) == 1 { + annots = nil + break + } + annots = append(annots[:i], annots[i+1:]...) + } + + delete(pgAnnots, annotType) + if len(pgAnnots) == 0 { + delete(ctx.PageAnnots, pageNr) + } + + ok = true + } + + return annots, ok, nil +} + +func removeAnnotationByID( + ctx *model.Context, + id string, + pageNr int, + annots types.Array, + incr bool) (types.Array, bool, error) { + + i, err := findAnnotByID(ctx, id, annots) + if err != nil || i < 0 { + return annots, false, err + } + + indRef, _ := annots[i].(types.IndirectRef) + + // Remove annotation from xreftable page annotation cache. + err = removeAnnotationFromCache(ctx, pageNr, indRef.ObjectNumber.Value()) + if err != nil { + return nil, false, err + } + if err := ctx.DeleteObject(indRef); err != nil { + return nil, false, err + } + if incr { + // Mark annotation dict obj for incremental writing. + ctx.Write.IncrementWithObjNr(indRef.ObjectNumber.Value()) + } + if len(annots) == 1 { + if i != 0 { + return nil, false, err + } + return nil, true, nil + } + annots = append(annots[:i], annots[i+1:]...) + + return annots, true, nil +} + +func removeAnnotationsByID( + ctx *model.Context, + ids []string, + objNrSet types.IntSet, + pageNr int, + annots types.Array, + incr bool) (types.Array, bool, error) { + + var ( + ok, ok1 bool + err error + ) + + for _, id := range ids { + annots, ok1, err = removeAnnotationByID(ctx, id, pageNr, annots, incr) + if err != nil { + return nil, false, err + } + if ok1 { + ok = true + } + } + + for objNr, v := range objNrSet { + if !v { + continue + } + annots, ok1, err = removeAnnotationByID(ctx, strconv.Itoa(objNr), pageNr, annots, incr) + if err != nil { + return nil, false, err + } + if ok1 { + delete(objNrSet, objNr) + ok = true + } + } + + return annots, ok, nil +} + +func removeAnnotationsByObjNr( + ctx *model.Context, + objNrSet types.IntSet, + pageNr int, + annots types.Array, + incr bool) (types.Array, bool, error) { + + var ok bool + for objNr, v := range objNrSet { + if !v || objNr < 0 { + continue + } + i, err := findAnnotByObjNr(objNr, annots) + if err != nil { + return nil, false, err + } + if i >= 0 { + ok = true + indRef, _ := annots[i].(types.IndirectRef) + + // Remove annotation from xreftable page annotation cache. + err = removeAnnotationFromCache(ctx, pageNr, indRef.ObjectNumber.Value()) + if err != nil { + return nil, false, err + } + + if err := ctx.DeleteObject(indRef); err != nil { + return nil, false, err + } + if incr { + // Mark annotation dict obj for incremental writing. + ctx.Write.IncrementWithObjNr(indRef.ObjectNumber.Value()) + } + delete(objNrSet, objNr) + if len(annots) == 1 { + if i != 0 { + return nil, false, err + } + return nil, ok, nil + } + annots = append(annots[:i], annots[i+1:]...) + } + } + return annots, ok, nil +} + +func removeAnnotationsFromAnnots( + ctx *model.Context, + annotTypes []model.AnnotationType, + ids []string, + objNrSet types.IntSet, + pageNr int, + annots types.Array, + incr bool) (types.Array, bool, error) { + + var ( + ok1, ok2, ok3 bool + err error + ) + + // 1. Remove by annotType. + if len(annotTypes) > 0 { + annots, ok1, err = removeAnnotationsByType(ctx, annotTypes, pageNr, annots, incr) + if err != nil || annots == nil { + return nil, ok1, err + } + } + + // 2. Remove by obj#. + if len(objNrSet) > 0 { + annots, ok2, err = removeAnnotationsByObjNr(ctx, objNrSet, pageNr, annots, incr) + if err != nil || annots == nil { + return nil, ok2, err + } + } + + // 3. Remove by id for ids and objNrs considering possibly numeric ids. + if len(ids) > 0 || len(objNrSet) > 0 { + annots, ok3, err = removeAnnotationsByID(ctx, ids, objNrSet, pageNr, annots, incr) + if err != nil || annots == nil { + return nil, ok3, err + } + } + + return annots, ok1 || ok2 || ok3, nil +} + +func removeAnnotationsFromIndAnnots(ctx *model.Context, + annotTypes []model.AnnotationType, + ids []string, + objNrSet types.IntSet, + pageNr int, + annots types.Array, + incr bool, + pageDict types.Dict, + pageDictObjNr int, + indRef types.IndirectRef) (bool, error) { + + ann, ok, err := removeAnnotationsFromAnnots(ctx, annotTypes, ids, objNrSet, pageNr, annots, incr) + if err != nil { + return false, err + } + if !ok { + return false, nil + } + + objNr := indRef.ObjectNumber.Value() + genNr := indRef.GenerationNumber.Value() + entry, _ := ctx.FindTableEntry(objNr, genNr) + + if incr { + // Modify Annots array obj for incremental writing. + ctx.Write.IncrementWithObjNr(objNr) + } + + ctx.EnsureVersionForWriting() + + if annots == nil { + pageDict.Delete("Annots") + if err := ctx.DeleteObject(indRef); err != nil { + return false, err + } + if incr { + // Mark page dict obj for incremental writing. + ctx.Write.IncrementWithObjNr(pageDictObjNr) + } + return ok, nil + } + + entry.Object = ann + return true, nil +} + +// RemoveAnnotationsFromPageDict removes an annotation by annotType, id and obj# from pageDict. +func RemoveAnnotationsFromPageDict( + ctx *model.Context, + annotTypes []model.AnnotationType, + ids []string, + objNrSet types.IntSet, + pageDict types.Dict, + pageDictObjNr, + pageNr int, + incr bool) (bool, error) { + + //fmt.Printf("ids:%v objNrSet:%v\n", ids, objNrSet) + + if len(annotTypes) == 0 && len(ids) == 0 && len(objNrSet) == 0 { + return removeAllAnnotations(ctx, pageDict, pageDictObjNr, pageNr, incr) + } + + obj, found := pageDict.Find("Annots") + if !found { + return false, nil + } + + indRef, ok1 := obj.(types.IndirectRef) + if !ok1 { + annots, _ := obj.(types.Array) + ann, ok, err := removeAnnotationsFromAnnots(ctx, annotTypes, ids, objNrSet, pageNr, annots, incr) + if err != nil { + return false, err + } + if !ok { + return false, nil + } + if incr { + // Mark page dict obj for incremental writing. + ctx.Write.IncrementWithObjNr(pageDictObjNr) + } + ctx.EnsureVersionForWriting() + if annots == nil { + pageDict.Delete("Annots") + return ok, nil + } + pageDict.Update("Annots", ann) + return ok, nil + } + + // Annots array is an IndirectReference. + o, err := ctx.Dereference(indRef) + if err != nil || o == nil { + return false, err + } + + annots, _ := o.(types.Array) + + return removeAnnotationsFromIndAnnots(ctx, annotTypes, ids, objNrSet, pageNr, annots, incr, pageDict, pageDictObjNr, indRef) +} + +func prepForRemoveAnnotations(ctx *model.Context, idsAndTypes []string, objNrs []int, incr bool) ([]model.AnnotationType, []string, types.IntSet, bool) { + var annTypes []model.AnnotationType + var ids []string + + if len(idsAndTypes) > 0 { + for _, s := range idsAndTypes { + if at, ok := model.AnnotTypes[s]; ok { + annTypes = append(annTypes, at) + continue + } + ids = append(ids, s) + } + } + + objNrSet := types.IntSet{} + for _, i := range objNrs { + objNrSet[i] = true + } + + // Remove all annotations for selectedPages + removeAll := len(idsAndTypes) == 0 && len(objNrs) == 0 + if removeAll { + log.CLI.Println("removing all annotations for selected pages!") + } + + if incr { + ctx.Write.Increment = true + ctx.Write.Offset = ctx.Read.FileSize + } + + return annTypes, ids, objNrSet, removeAll +} + +// RemoveAnnotations removes annotations for selected pages by id, type or object number. +// All annotations for selected pages are removed if neither idsAndTypes nor objNrs are provided. +func RemoveAnnotations(ctx *model.Context, selectedPages types.IntSet, idsAndTypes []string, objNrs []int, incr bool) (bool, error) { + + annTypes, ids, objNrSet, removeAll := prepForRemoveAnnotations(ctx, idsAndTypes, objNrs, incr) + + var removed bool + + for _, pageNr := range sortedPageNrsForAnnotsFromCache(ctx) { + + if selectedPages != nil { + if _, found := selectedPages[pageNr]; !found { + continue + } + } + + pageDictIndRef, err := ctx.PageDictIndRef(pageNr) + if err != nil { + return false, err + } + + d, err := ctx.DereferenceDict(*pageDictIndRef) + if err != nil { + return false, err + } + + objNr := pageDictIndRef.ObjectNumber.Value() + + ok, err := RemoveAnnotationsFromPageDict(ctx, annTypes, ids, objNrSet, d, objNr, pageNr, incr) + if err != nil { + return false, err + } + if ok { + removed = true + } + + // if we only remove by obj#, we delete the obj# on annotation removal from objNrSet + // and can terminate once objNrSet is empty. + if !removeAll && len(idsAndTypes) == 0 && len(objNrSet) == 0 { + break + } + } + + if removeAll { + // Hacky, actually we only want to remove struct tree elements using removed annotations + // but this is most probably what we want anyway. + root, _ := ctx.Catalog() + root.Delete("StructTreeRoot") + } + + return removed, nil +} diff --git a/pkg/pdfcpu/booklet.go b/pkg/pdfcpu/booklet.go new file mode 100644 index 0000000000000000000000000000000000000000..aeb3b471fb4efe4b486ca495bede998379daa0a9 --- /dev/null +++ b/pkg/pdfcpu/booklet.go @@ -0,0 +1,619 @@ +/* + Copyright 2021 The pdfcpu Authors. + + 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 + + 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 pdfcpu + +import ( + "bytes" + "fmt" + "math" + "os" + "strconv" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +var errInvalidBookletAdvanced = errors.New("pdfcpu booklet advanced cannot have binding along the top (portrait short-edge, landscape long-edge). use plain booklet instead.") + +var NUpValuesForBooklets = []int{2, 4, 6, 8} + +// DefaultBookletConfig returns the default configuration for a booklet +func DefaultBookletConfig() *model.NUp { + nup := model.DefaultNUpConfig() + nup.Margin = 0 + nup.Border = false + nup.BookletGuides = false + nup.MultiFolio = false + nup.FolioSize = 8 + nup.BookletType = model.Booklet + nup.BookletBinding = model.LongEdge + nup.Enforce = false + return nup +} + +// PDFBookletConfig returns an NUp configuration for booklet-ing PDF files. +func PDFBookletConfig(val int, desc string, conf *model.Configuration) (*model.NUp, error) { + nup := DefaultBookletConfig() + if conf == nil { + conf = model.NewDefaultConfiguration() + } + nup.InpUnit = conf.Unit + if desc != "" { + if err := ParseNUpDetails(desc, nup); err != nil { + return nil, err + } + } + if !types.IntMemberOf(val, NUpValuesForBooklets) { + ss := make([]string, len(NUpValuesForBooklets)) + for i, v := range NUpValuesForBooklets { + ss[i] = strconv.Itoa(v) + } + return nil, errors.Errorf("pdfcpu: n must be one of %s", strings.Join(ss, ", ")) + } + if err := ParseNUpValue(val, nup); err != nil { + return nil, err + } + // 6up and 8up special cases + if nup.IsBooklet() && val > 4 && nup.IsTopFoldBinding() { + // You can't top fold a 6up with 3 rows. + // TODO: support this for 8up + return nup, fmt.Errorf("pdfcpu booklet: n>4 must have binding on side (portrait long-edge or landscape short-edge)") + } + // bookletadvanced + if nup.BookletType == model.BookletAdvanced && val == 4 && nup.IsTopFoldBinding() { + return nup, errInvalidBookletAdvanced + } + return nup, nil +} + +// ImageBookletConfig returns an NUp configuration for booklet-ing image files. +func ImageBookletConfig(val int, desc string, conf *model.Configuration) (*model.NUp, error) { + nup, err := PDFBookletConfig(val, desc, conf) + if err != nil { + return nil, err + } + nup.ImgInputFile = true + return nup, nil +} + +func getPageNumber(pageNumbers []int, n int) int { + if n >= len(pageNumbers) { + // Zero represents blank page at end of booklet. + return 0 + } + return pageNumbers[n] +} + +func nup2OutputPageNr(inputPageNr, inputPageCount int, pageNumbers []int) (int, bool) { + var p int + if inputPageNr%2 == 0 { + p = inputPageCount - 1 - inputPageNr/2 + } else { + p = (inputPageNr - 1) / 2 + } + pageNr := getPageNumber(pageNumbers, p) + + // Rotate odd output pages (the back sides) by 180 degrees. + var rotate bool + if inputPageNr%4 < 2 { + rotate = true + } + return pageNr, rotate +} + +func get4upPos(pos int, isLandscape bool) (out int) { + if isLandscape { + switch pos % 4 { + // landscape short-edge binding page ordering is rotated 90 degrees anti-clockwise from the portrait ordering on the back sides of the pages to make duplexing work + // from portrait to lanscape map {0 => 3, 1 => 2, 2 => 1, 3 => 0} + case 0: + return 3 + case 1: + return 2 + case 2: + return 1 + case 3: + return 0 + } + } + return pos % 4 +} + +func nup4OutputPageNr(inputPageNr int, pageCount int, pageNumbers []int, nup *model.NUp) (int, bool) { + switch nup.BookletType { + case model.Booklet: + // simple booklets are collated by collecting the top of the sheet, then the bottom, then the top of the next sheet, and so on. + // this is conceptually easier for collation without specialized tools. + if nup.IsTopFoldBinding() { + return nup4BasicTopFoldOutputPageNr(inputPageNr, pageCount, pageNumbers, nup) + } else { + return nup4BasicSideFoldOutputPageNr(inputPageNr, pageCount, pageNumbers, nup) + } + case model.BookletAdvanced: + // advanced booklets have a different collation pattern: collect the top of each sheet and then the bottom of each sheet. + // this allows printers to fold the sheets twice and then cut along one of the folds. + return nup4AdvancedSideFoldOutputPageNr(inputPageNr, pageCount, pageNumbers, nup) + } + return 0, false +} + +func nup4BasicSideFoldOutputPageNr(positionNumber int, inputPageCount int, pageNumbers []int, nup *model.NUp) (int, bool) { + var p int + bookletSheetSideNumber := positionNumber / 4 + bookletPageNumber := positionNumber / 8 + if bookletSheetSideNumber%2 == 0 { + // front side + n := bookletPageNumber * 4 + switch positionNumber % 4 { + case 0: + p = inputPageCount - n + case 1: + p = 1 + n + case 2: + p = 3 + n + case 3: + p = inputPageCount - 2 - n + } + } else { + // back side + n := bookletPageNumber * 4 + switch get4upPos(positionNumber, nup.PageDim.Landscape()) { + case 0: + p = 2 + n + case 1: + p = inputPageCount - 1 - n + case 2: + p = inputPageCount - 3 - n + case 3: + p = 4 + n + } + } + pageNr := getPageNumber(pageNumbers, p-1) // p is one-indexed and we want zero-indexed + // Rotate bottom row of each output sheet by 180 degrees. + var rotate bool + if positionNumber%4 >= 2 { + rotate = true + } + return pageNr, rotate +} + +func nup4BasicTopFoldOutputPageNr(positionNumber int, inputPageCount int, pageNumbers []int, nup *model.NUp) (int, bool) { + var p int + bookletSheetSideNumber := positionNumber / 4 + bookletSheetNumber := positionNumber / 8 + if bookletSheetSideNumber%2 == 0 { + // front side + switch positionNumber % 4 { + case 0: + p = inputPageCount - 4*bookletSheetNumber + case 1: + p = 3 + 4*bookletSheetNumber + case 2: + p = 1 + 4*bookletSheetNumber + case 3: + p = inputPageCount - 2 - 4*bookletSheetNumber + } + } else { + // back side + switch get4upPos(positionNumber, nup.PageDim.Landscape()) { + case 0: + p = 4 + 4*bookletSheetNumber + case 1: + p = inputPageCount - 1 - 4*bookletSheetNumber + case 2: + p = inputPageCount - 3 - 4*bookletSheetNumber + case 3: + p = 2 + 4*bookletSheetNumber + } + } + pageNr := getPageNumber(pageNumbers, p-1) // p is one-indexed and we want zero-indexed + // Rotate right side of output page by 180 degrees. + var rotate bool + if positionNumber%2 == 1 { + rotate = true + } + return pageNr, rotate +} + +func nup4AdvancedSideFoldOutputPageNr(inputPageNr int, inputPageCount int, pageNumbers []int, nup *model.NUp) (int, bool) { + // (output page, input page) = [(1,n), (2,1), (3, n/2+1), (4, n/2-0), (5, 2), (6, n-1), (7, n/2-1), (8, n/2+2) ...] + bookletPageNumber := inputPageNr / 4 + var p int + if bookletPageNumber%2 == 0 { + // front side + switch inputPageNr % 4 { + case 0: + p = inputPageCount - 1 - bookletPageNumber + case 1: + p = bookletPageNumber + case 2: + p = inputPageCount/2 + bookletPageNumber + case 3: + p = inputPageCount/2 - 1 - bookletPageNumber + } + } else { + // back side (portrait) + switch get4upPos(inputPageNr, nup.PageDim.Landscape()) { + case 0: + p = bookletPageNumber + case 1: + p = inputPageCount - 1 - bookletPageNumber + case 2: + p = inputPageCount/2 - 1 - bookletPageNumber + case 3: + p = inputPageCount/2 + bookletPageNumber + } + } + pageNr := getPageNumber(pageNumbers, p) + + // Rotate bottom row of each output page by 180 degrees. + var rotate bool + if inputPageNr%4 >= 2 { + rotate = true + } + return pageNr, rotate +} + +func nupLRTBOutputPageNr(positionNumber int, inputPageCount int, pageNumbers []int, nup *model.NUp) (int, bool) { + // move from left to right and then from top to bottom with no rotation + var p int + N := nup.N() + bookletSheetSideNumber := positionNumber / N + bookletSheetNumber := positionNumber / (2 * N) + if bookletSheetSideNumber%2 == 0 { + // front side + if positionNumber%2 == 0 { + // left side - count down from end + p = inputPageCount - N*bookletSheetNumber - positionNumber%N + } else { + // right side - count up from start + p = N*bookletSheetNumber + positionNumber%N + } + } else { + // back side + if positionNumber%2 == 0 { + // left side - count up from start + p = 2 + N*bookletSheetNumber + positionNumber%N + } else { + // right side - count down from end + p = inputPageCount - N*bookletSheetNumber - positionNumber%N + } + } + pageNr := getPageNumber(pageNumbers, p-1) // p is one-indexed and we want zero-indexed + return pageNr, false +} + +func nup8OutputPageNr(portraitPositionNumber int, inputPageCount int, pageNumbers []int, nup *model.NUp) (int, bool) { + // 8up sheet has four rows and two columns + // but the spreads are NOT across the two columns - instead the spreads are rotated 90deg to fit in a portrait orientation on the sheet + // rather than coding up an entire new imposition, we're going to use the left-down-top-bottom imposition as a base + // and the rotate the spreads (ie reorder) to fit on the sheet + + bookletSheetSideNumber := portraitPositionNumber / 8 + var landscapePositionNumber int + switch bookletSheetSideNumber % 2 { + case 0: // front side + // rotate the block of four pages 90deg clockwise to go from portrait to landscape. sequence=[1,3,0,2] + // then because we are rotating the right side by 180deg - so need to change to those positions. sequence=[1,2,0,3] + switch portraitPositionNumber % 4 { + case 0: + landscapePositionNumber = 1 + case 1: + landscapePositionNumber = 2 + case 2: + landscapePositionNumber = 0 + case 3: + landscapePositionNumber = 3 + } + case 1: // back side + // rotate the block of four pages 90deg anti-clockwise to go from portrait to landscape. sequence=[2,0,3,1] + // then because we are rotating the *left* side by 180deg - so need to change to those positions. sequence=[3,0,2,1] + // this is different from the front side because of the non-duplex sheet handling flip along the short edge + + switch portraitPositionNumber % 4 { + case 0: + landscapePositionNumber = 3 + case 1: + landscapePositionNumber = 0 + case 2: + landscapePositionNumber = 2 + case 3: + landscapePositionNumber = 1 + } + + } + positionNumber := landscapePositionNumber + portraitPositionNumber/4*4 + pageNumber, _ := nupLRTBOutputPageNr(positionNumber, inputPageCount, pageNumbers, nup) + // rotate right side so that bottom edge of pages is on the center cut + rotate := portraitPositionNumber%2 == 1 + return pageNumber, rotate +} + +func nupPerfectBound(positionNumber int, inputPageCount int, pageNumbers []int, nup *model.NUp) (int, bool) { + // input: positionNumber + // output: original page number and rotation + var p int + var rotate bool + N := nup.N() + twoN := N * 2 + + bookletSheetSideNumber := positionNumber / N + bookletSheetNumber := positionNumber / twoN + if bookletSheetSideNumber%2 == 0 { + // front side + p = bookletSheetNumber*twoN + 2*(positionNumber%twoN) + 1 + } else { + // back side + p = bookletSheetNumber*twoN + 2*((positionNumber-N)%twoN) + 2 + if N == 4 || N == 6 || N == 8 { + if N == 4 && nup.PageDim.Landscape() { // landscape pages on portrait sheets + // flip top and bottom rows to account for landscape rotation and the page handling flip (short edge flip, no duplex) + if positionNumber%N < 2 { // top side + p += 4 + } else { // bottom side + p -= 4 + } + } else { // portrait pages on portrait sheets + // flip left and right columns to account for the page handling flip (short edge flip, no duplex) + if positionNumber%2 == 0 { // left side + p += 2 + } else { // right side + p -= 2 + } + } + } + // account for page handling flip (short edge flip, no duplex) + rotate = N == 2 || nup.PageDim.Landscape() + } + return getPageNumber(pageNumbers, p-1), rotate // p is one-indexed and we want zero-indexed +} + +func GetBookletOrdering(pages types.IntSet, nup *model.NUp) []model.BookletPage { + pageNumbers := sortSelectedPages(pages) + pageCount := len(pageNumbers) + + // A sheet of paper consists of 2 consecutive output pages. + sheetPageCount := 2 * nup.N() + + // pageCount must be a multiple of the number of pages per sheet. + // If not, we will insert blank pages at the end of the booklet. + if pageCount%sheetPageCount != 0 { + pageCount += sheetPageCount - pageCount%sheetPageCount + } + + if nup.MultiFolio { + bookletPages := make([]model.BookletPage, 0) + // folioSize is the number of sheets - each "folio" has two sides and two pages per side + nPagesPerSignature := nup.FolioSize * 4 + nSignaturesInBooklet := int(math.Ceil(float64(pageCount) / float64(nPagesPerSignature))) + for j := 0; j < nSignaturesInBooklet; j++ { + start := j * nPagesPerSignature + stop := (j + 1) * nPagesPerSignature + if stop > len(pageNumbers) { + // last signature may be short + stop = len(pageNumbers) + nPagesPerSignature = pageCount - start + } + bookletPages = append(bookletPages, getBookletPageOrdering(nup, pageNumbers[start:stop], nPagesPerSignature)...) + } + return bookletPages + } + return getBookletPageOrdering(nup, pageNumbers, pageCount) +} + +func getBookletPageOrdering(nup *model.NUp, pageNumbers []int, pageCount int) []model.BookletPage { + bookletPages := make([]model.BookletPage, pageCount) + + switch nup.BookletType { + case model.Booklet, model.BookletAdvanced: + switch nup.N() { + case 2: + // (output page, input page) = [(1,n), (2,1), (3, n-1), (4, 2), (5, n-2), (6, 3), ...] + for i := 0; i < pageCount; i++ { + pageNr, rotate := nup2OutputPageNr(i, pageCount, pageNumbers) + bookletPages[i].Number = pageNr + bookletPages[i].Rotate = rotate + } + + case 4: + for i := 0; i < pageCount; i++ { + pageNr, rotate := nup4OutputPageNr(i, pageCount, pageNumbers, nup) + bookletPages[i].Number = pageNr + bookletPages[i].Rotate = rotate + } + case 6: + for i := 0; i < pageCount; i++ { + pageNr, rotate := nupLRTBOutputPageNr(i, pageCount, pageNumbers, nup) + bookletPages[i].Number = pageNr + bookletPages[i].Rotate = rotate + } + case 8: + for i := 0; i < pageCount; i++ { + pageNr, rotate := nup8OutputPageNr(i, pageCount, pageNumbers, nup) + bookletPages[i].Number = pageNr + bookletPages[i].Rotate = rotate + } + } + case model.BookletPerfectBound: + for i := 0; i < pageCount; i++ { + pageNr, rotate := nupPerfectBound(i, pageCount, pageNumbers, nup) + bookletPages[i].Number = pageNr + bookletPages[i].Rotate = rotate + } + } + + return bookletPages +} + +func bookletPages( + ctx *model.Context, + selectedPages types.IntSet, + nup *model.NUp, + pagesDict types.Dict, + pagesIndRef *types.IndirectRef) error { + + var buf bytes.Buffer + formsResDict := types.NewDict() + rr := nup.RectsForGrid() + + for i, bp := range GetBookletOrdering(selectedPages, nup) { + + if i > 0 && i%len(rr) == 0 { + // Wrap complete page. + if err := wrapUpPage(ctx, nup, formsResDict, buf, pagesDict, pagesIndRef); err != nil { + return err + } + buf.Reset() + formsResDict = types.NewDict() + } + + rDest := rr[i%len(rr)] + + if bp.Number == 0 { + // This is an empty page at the end. + if nup.BgColor != nil { + draw.FillRectNoBorder(&buf, rDest, *nup.BgColor) + } + continue + } + + if err := ctx.NUpTilePDFBytesForPDF(bp.Number, formsResDict, &buf, rDest, nup, bp.Rotate); err != nil { + return err + } + } + + // Wrap incomplete booklet page. + return wrapUpPage(ctx, nup, formsResDict, buf, pagesDict, pagesIndRef) +} + +// BookletFromImages creates a booklet version of the image sequence represented by fileNames. +func BookletFromImages(ctx *model.Context, fileNames []string, nup *model.NUp, pagesDict types.Dict, pagesIndRef *types.IndirectRef) error { + // The order of images in fileNames corresponds to a desired booklet page sequence. + selectedPages := types.IntSet{} + for i := 1; i <= len(fileNames); i++ { + selectedPages[i] = true + } + + if nup.PageGrid { + nup.PageDim.Width *= nup.Grid.Width + nup.PageDim.Height *= nup.Grid.Height + } + + xRefTable := ctx.XRefTable + formsResDict := types.NewDict() + var buf bytes.Buffer + rr := nup.RectsForGrid() + + for i, bp := range GetBookletOrdering(selectedPages, nup) { + + if i > 0 && i%len(rr) == 0 { + + // Wrap complete page. + if err := wrapUpPage(ctx, nup, formsResDict, buf, pagesDict, pagesIndRef); err != nil { + return err + } + + buf.Reset() + formsResDict = types.NewDict() + } + + rDest := rr[i%len(rr)] + + if bp.Number == 0 { + // This is an empty page at the end of a booklet. + if nup.BgColor != nil { + draw.FillRectNoBorder(&buf, rDest, *nup.BgColor) + } + continue + } + + f, err := os.Open(fileNames[bp.Number-1]) + if err != nil { + return err + } + + imgIndRef, w, h, err := model.CreateImageResource(xRefTable, f, false, false) + if err != nil { + return err + } + + if err := f.Close(); err != nil { + return err + } + + formIndRef, err := createNUpFormForImage(xRefTable, imgIndRef, w, h, i) + if err != nil { + return err + } + + formResID := fmt.Sprintf("Fm%d", i) + formsResDict.Insert(formResID, *formIndRef) + + // Append to content stream of booklet page i. + model.NUpTilePDFBytes(&buf, types.RectForDim(float64(w), float64(h)), rr[i%len(rr)], formResID, nup, bp.Rotate) + } + + // Wrap incomplete booklet page. + return wrapUpPage(ctx, nup, formsResDict, buf, pagesDict, pagesIndRef) +} + +// BookletFromPDF creates a booklet version of the PDF represented by xRefTable. +func BookletFromPDF(ctx *model.Context, selectedPages types.IntSet, nup *model.NUp) error { + n := int(nup.Grid.Width * nup.Grid.Height) + if !(n == 2 || n == 4 || n == 6 || n == 8) { + return fmt.Errorf("pdfcpu: booklet must have n={2,4,6,8} pages per sheet, got %d", n) + } + + var mb *types.Rectangle + + if nup.PageDim == nil { + nup.PageDim = types.PaperSize[nup.PageSize] + } + + mb = types.RectForDim(nup.PageDim.Width, nup.PageDim.Height) + + pagesDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("Pages"), + "Count": types.Integer(0), + "MediaBox": mb.Array(), + }, + ) + + pagesIndRef, err := ctx.IndRefForNewObject(pagesDict) + if err != nil { + return err + } + + nup.PageDim = &types.Dim{Width: mb.Width(), Height: mb.Height()} + + if err = bookletPages(ctx, selectedPages, nup, pagesDict, pagesIndRef); err != nil { + return err + } + + // Replace original pagesDict. + rootDict, err := ctx.Catalog() + if err != nil { + return err + } + + rootDict.Update("Pages", *pagesIndRef) + return nil +} diff --git a/pkg/pdfcpu/booklet_test.go b/pkg/pdfcpu/booklet_test.go new file mode 100644 index 0000000000000000000000000000000000000000..76de86528c009981f3bc9f0a3fd0d176b04577f8 --- /dev/null +++ b/pkg/pdfcpu/booklet_test.go @@ -0,0 +1,348 @@ +/* +Copyright 2024 The pdfcpu Authors. + +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 + + 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 pdfcpu + +import ( + "fmt" + "strings" + "testing" +) + +type pageOrderResults struct { + id string + nup int + pageCount int + expectedPageOrder []int + papersize string + bookletType string + binding string + useSignatures bool + nPagesPerSignature int +} + +var bookletTestCases = []pageOrderResults{ + { + id: "2up", + nup: 2, + pageCount: 16, + expectedPageOrder: []int{ + 16, 1, + 15, 2, + 14, 3, + 13, 4, + 12, 5, + 11, 6, + 10, 7, + 9, 8, + }, + papersize: "A6", + bookletType: "booklet", + binding: "long", + }, + { + id: "2up with trailing blank pages", + nup: 2, + pageCount: 10, + expectedPageOrder: []int{ + 0, 1, + 0, 2, + 10, 3, + 9, 4, + 8, 5, + 7, 6, + }, + papersize: "A6", + bookletType: "booklet", + binding: "long", + }, + // basic booklet sidefold test cases + { + id: "booklet portrait long edge", + nup: 4, + pageCount: 16, + expectedPageOrder: []int{ + 16, 1, 3, 14, + 2, 15, 13, 4, + 12, 5, 7, 10, + 6, 11, 9, 8, + }, + papersize: "A5", // portrait, long-edge binding + bookletType: "booklet", + binding: "long", + }, + { + id: "booklet landscape short edge", + nup: 4, + pageCount: 8, + expectedPageOrder: []int{ + 8, 1, 3, 6, + 4, 5, 7, 2, // this is ordered differently from the portrait layout (because of differences in how duplexing works) + }, + papersize: "A5L", // landscape, short-edge binding + bookletType: "booklet", + binding: "short", + }, + // basic booklet topfold test cases + { + id: "booklet topfold portrait", + nup: 4, + pageCount: 16, + expectedPageOrder: []int{ + 16, 3, 1, 14, + 4, 15, 13, 2, + 12, 7, 5, 10, + 8, 11, 9, 6, + }, + papersize: "A5", // portrait, short-edge binding + bookletType: "booklet", + binding: "short", + }, + { + id: "booklet topfold landscape", + nup: 4, + pageCount: 8, + expectedPageOrder: []int{ + 8, 3, 1, 6, + 2, 5, 7, 4, // this is 180degrees flipped from the portrait layout (because of differences in how duplexing works) + }, + papersize: "A5L", // landscape, long-edge binding + bookletType: "booklet", + binding: "long", + }, + // advanced booklet sidefold test cases + { + id: "advanced portrait long edge", + nup: 4, + pageCount: 8, + expectedPageOrder: []int{ + 8, 1, 5, 4, + 2, 7, 3, 6, + }, + papersize: "A5", // portrait, long-edge binding + bookletType: "bookletadvanced", + binding: "long", + }, + { + id: "advanced landscape short edge", + nup: 4, + pageCount: 8, + expectedPageOrder: []int{ + 8, 1, 5, 4, + 6, 3, 7, 2, // this is ordered differently from the portrait layout (because of differences in how duplexing works) + }, + papersize: "A5L", // landscape, short-edge binding + bookletType: "bookletadvanced", + binding: "short", + }, + // 6up test + { + id: "6up", + nup: 6, + pageCount: 12, + expectedPageOrder: []int{ + 12, 1, 10, 3, 8, 5, + 2, 11, 4, 9, 6, 7, + }, + papersize: "A6", // portrait, long-edge binding + bookletType: "booklet", + binding: "long", + }, + { + id: "6up multisheet", + nup: 6, + pageCount: 24, + expectedPageOrder: []int{ + 24, 1, 22, 3, 20, 5, + 2, 23, 4, 21, 6, 19, + 18, 7, 16, 9, 14, 11, + 8, 17, 10, 15, 12, 13, + }, + papersize: "A6", // portrait, long-edge binding + bookletType: "booklet", + binding: "long", + }, + // 8up test + { + id: "8up", + nup: 8, + pageCount: 32, + expectedPageOrder: []int{ + 1, 30, 32, 3, 5, 26, 28, 7, + 29, 2, 4, 31, 25, 6, 8, 27, + 9, 22, 24, 11, 13, 18, 20, 15, + 21, 10, 12, 23, 17, 14, 16, 19, + }, + papersize: "A6", // portrait, long-edge binding + bookletType: "booklet", + binding: "long", + }, + // perfect bound + { + id: "perfect bound 2up", + nup: 2, + pageCount: 8, + expectedPageOrder: []int{ + 1, 3, + 2, 4, + 5, 7, + 6, 8, + }, + papersize: "A6", // portrait, long-edge binding + bookletType: "perfectbound", + binding: "long", + }, + { + id: "perfect bound 4up", + nup: 4, + pageCount: 16, + expectedPageOrder: []int{ + 1, 3, 5, 7, + 4, 2, 8, 6, + 9, 11, 13, 15, + 12, 10, 16, 14, + }, + papersize: "A6", // portrait, long-edge binding + bookletType: "perfectbound", + binding: "long", + }, + { + id: "perfect bound 4up landscape short-edge", + nup: 4, + pageCount: 16, + expectedPageOrder: []int{ + 1, 3, 5, 7, + 6, 8, 2, 4, + 9, 11, 13, 15, + 14, 16, 10, 12, + }, + papersize: "A6L", // landscape, short-edge binding + bookletType: "perfectbound", + binding: "short", + }, + { + id: "perfect bound 8up", + nup: 8, + pageCount: 16, + expectedPageOrder: []int{ + 1, 3, 5, 7, 9, 11, 13, 15, + 4, 2, 8, 6, 12, 10, 16, 14, + }, + papersize: "A6", // portrait, long-edge binding + bookletType: "perfectbound", + binding: "long", + }, + // signatures + { + id: "signatures 2up", + nup: 2, + pageCount: 16, + expectedPageOrder: []int{ + 12, 1, // signature 1 + 11, 2, + 10, 3, + 9, 4, + 8, 5, + 7, 6, + 16, 13, // signature 2, incomplete + 15, 14, + }, + papersize: "A6", + bookletType: "booklet", + binding: "long", + useSignatures: true, + nPagesPerSignature: 12, + }, + { + id: "signatures 4up", + nup: 4, + pageCount: 24, + expectedPageOrder: []int{ + 16, 1, 3, 14, // signature 1 + 2, 15, 13, 4, + 12, 5, 7, 10, + 6, 11, 9, 8, + 24, 17, 19, 22, // signature 2, incomplete + 18, 23, 21, 20, + }, + papersize: "A5", + bookletType: "booklet", + binding: "long", + useSignatures: true, + nPagesPerSignature: 16, + }, + { + id: "signatures 2up with trailing blank pages", + nup: 2, + pageCount: 18, + expectedPageOrder: []int{ + 12, 1, // signature 1 + 11, 2, + 10, 3, + 9, 4, + 8, 5, + 7, 6, + 0, 13, // signature 2, incomplete, with blanks + 0, 14, + 18, 15, + 17, 16, + }, + papersize: "A6", + bookletType: "booklet", + binding: "long", + useSignatures: true, + nPagesPerSignature: 12, + }, +} + +func TestBookletPageOrder(t *testing.T) { + for _, test := range bookletTestCases { + t.Run(test.id, func(tt *testing.T) { + desc := fmt.Sprintf("papersize:%s, btype:%s, binding: %s", test.papersize, test.bookletType, test.binding) + if test.useSignatures { + desc += fmt.Sprintf(", multifolio:on, foliosize:%d", test.nPagesPerSignature/4) + } + nup, err := PDFBookletConfig(test.nup, desc, nil) + if err != nil { + tt.Fatal(err) + } + pageNumbers := make(map[int]bool) + for i := 0; i < test.pageCount; i++ { + pageNumbers[i+1] = true + } + pageOrder := make([]int, len(test.expectedPageOrder)) + out := GetBookletOrdering(pageNumbers, nup) + if len(test.expectedPageOrder) != len(out) { + tt.Fatalf("page order output has the wrong length, expected %d but got %d", len(test.expectedPageOrder), len(out)) + } + for i, p := range out { + pageOrder[i] = p.Number + } + for i, expected := range test.expectedPageOrder { + if pageOrder[i] != expected { + tt.Fatal("incorrect page order\nexpected:", arrayToString(test.expectedPageOrder), "\n got:", arrayToString(pageOrder)) + } + } + }) + } +} + +func arrayToString(arr []int) string { + out := make([]string, len(arr)) + for i, n := range arr { + out[i] = fmt.Sprintf("%02d", n) + } + return fmt.Sprintf("[%s]", strings.Join(out, " ")) +} diff --git a/pkg/pdfcpu/bookmark.go b/pkg/pdfcpu/bookmark.go new file mode 100644 index 0000000000000000000000000000000000000000..77e94d8d75945b8da5fc93c0a86f11bb366180f9 --- /dev/null +++ b/pkg/pdfcpu/bookmark.go @@ -0,0 +1,633 @@ +/* + Copyright 2020 The pdfcpu Authors. + + 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 + + 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 pdfcpu + +import ( + "bytes" + "encoding/json" + "io" + "path/filepath" + "strings" + "time" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +var ( + errNoBookmarks = errors.New("pdfcpu: no bookmarks available") + errCorruptedBookmarks = errors.New("pdfcpu: corrupt bookmark") + errExistingBookmarks = errors.New("pdfcpu: existing bookmarks") +) + +type Header struct { + Source string `json:"source,omitempty"` + Version string `json:"version"` + Creation string `json:"creation"` + ID []string `json:"id,omitempty"` + Title string `json:"title,omitempty"` + Author string `json:"author,omitempty"` + Creator string `json:"creator,omitempty"` + Producer string `json:"producer,omitempty"` + Subject string `json:"subject,omitempty"` + Keywords string `json:"keywords,omitempty"` +} + +// Bookmark represents an outline item tree. +type Bookmark struct { + Title string `json:"title"` + PageFrom int `json:"page"` + PageThru int `json:"-"` // for extraction only; >= pageFrom and reaches until before pageFrom of the next bookmark. + Bold bool `json:"bold,omitempty"` + Italic bool `json:"italic,omitempty"` + Color *color.SimpleColor `json:"color,omitempty"` + Kids []Bookmark `json:"kids,omitempty"` + Parent *Bookmark `json:"-"` +} + +type BookmarkTree struct { + Header Header `json:"header"` + Bookmarks []Bookmark `json:"bookmarks"` +} + +func header(xRefTable *model.XRefTable, source string) Header { + h := Header{} + h.Source = filepath.Base(source) + h.Version = "pdfcpu " + model.VersionStr + h.Creation = time.Now().Format("2006-01-02 15:04:05 MST") + h.ID = []string{} + h.Title = xRefTable.Title + h.Author = xRefTable.Author + h.Creator = xRefTable.Creator + h.Producer = xRefTable.Producer + h.Subject = xRefTable.Subject + h.Keywords = xRefTable.Keywords + return h +} + +// Style returns an int corresponding to the bookmark style. +func (bm Bookmark) Style() int { + var i int + if bm.Bold { // bit 1 + i += 2 + } + if bm.Italic { // bit 0 + i += 1 + } + return i +} + +func positionToFirstBookmark(ctx *model.Context) (*types.IndirectRef, error) { + d := ctx.Outlines + if d == nil { + return nil, errNoBookmarks + } + return d.IndirectRefEntry("First"), nil +} + +func outlineItemTitle(s string) string { + var sb strings.Builder + for i := 0; i < len(s); i++ { + b := s[i] + if b >= 32 { + sb.WriteByte(b) + } + } + return sb.String() +} + +// PageObjFromDestinationArray returns an IndirectRef of the destinations page. +func PageObjFromDestination(ctx *model.Context, dest types.Object) (*types.IndirectRef, error) { + var ( + err error + ir types.IndirectRef + arr types.Array + ) + switch dest := dest.(type) { + case types.Name: + arr, err = ctx.DereferenceDestArray(dest.Value()) + if err == nil { + ir = arr[0].(types.IndirectRef) + } + case types.StringLiteral: + s, err := types.StringLiteralToString(dest) + if err != nil { + return nil, err + } + arr, err = ctx.DereferenceDestArray(s) + if err == nil { + ir = arr[0].(types.IndirectRef) + } + case types.HexLiteral: + s, err := types.HexLiteralToString(dest) + if err != nil { + return nil, err + } + arr, err = ctx.DereferenceDestArray(s) + if err == nil { + ir = arr[0].(types.IndirectRef) + } + case types.Array: + if dest[0] != nil { + ir = dest[0].(types.IndirectRef) + } + } + return &ir, err +} + +func title(ctx *model.Context, d types.Dict) (string, error) { + obj, err := ctx.Dereference(d["Title"]) + if err != nil { + return "", err + } + + s, err := model.Text(obj) + if err != nil { + return "", err + } + + return outlineItemTitle(s), nil +} + +func bookmark(d types.Dict, title string, pageFrom int, parent *Bookmark) Bookmark { + bm := Bookmark{ + Title: title, + PageFrom: pageFrom, + Parent: parent, + Bold: false, + Italic: false, + } + + if arr := d.ArrayEntry("C"); len(arr) == 3 { + col := color.NewSimpleColorForArray(arr) + bm.Color = &col + } + + if f := d.IntEntry("F"); f != nil { + bm.Bold = *f&0x02 > 0 + bm.Italic = *f&0x01 > 0 + } + + return bm +} + +// BookmarksForOutlineItem returns the bookmarks tree for an outline item. +func BookmarksForOutlineItem(ctx *model.Context, item *types.IndirectRef, parent *Bookmark) ([]Bookmark, error) { + bms := []Bookmark{} + + var ( + d types.Dict + err error + ) + + // Process outline items. + for ir := item; ir != nil; ir = d.IndirectRefEntry("Next") { + + if d, err = ctx.DereferenceDict(*ir); err != nil { + return nil, err + } + + title, err := title(ctx, d) + if err != nil { + return nil, err + } + + // Retrieve page number out of a destination via "Dest" or "Goto Action". + dest, destFound := d["Dest"] + if !destFound { + act, actFound := d["A"] + if !actFound { + continue + } + act, _ = ctx.Dereference(act) + actType := act.(types.Dict)["S"] + if actType.String() != "GoTo" { + continue + } + dest = act.(types.Dict)["D"] + } + + obj, err := ctx.Dereference(dest) + if err != nil { + return nil, err + } + + ir, err := PageObjFromDestination(ctx, obj) + if err != nil { + return nil, err + } + if ir == nil { + continue + } + + pageFrom, err := ctx.PageNumber(ir.ObjectNumber.Value()) + if err != nil { + return nil, err + } + + if len(bms) > 0 { + if pageFrom > bms[len(bms)-1].PageFrom { + bms[len(bms)-1].PageThru = pageFrom - 1 + } else { + bms[len(bms)-1].PageThru = bms[len(bms)-1].PageFrom + } + } + + bm := bookmark(d, title, pageFrom, parent) + + first := d["First"] + if first != nil { + indRef := first.(types.IndirectRef) + kids, _ := BookmarksForOutlineItem(ctx, &indRef, &bm) + bm.Kids = kids + } + + bms = append(bms, bm) + } + + return bms, nil +} + +// Bookmarks returns all ctx bookmark information recursively. +func Bookmarks(ctx *model.Context) ([]Bookmark, error) { + + if err := ctx.LocateNameTree("Dests", false); err != nil { + return nil, err + } + + first, err := positionToFirstBookmark(ctx) + if err != nil { + if err != errNoBookmarks { + return nil, err + } + return nil, nil + } + + return BookmarksForOutlineItem(ctx, first, nil) +} + +func bookmarkList(bms []Bookmark, level int) ([]string, error) { + pre := strings.Repeat(" ", level) + ss := []string{} + for _, bm := range bms { + ss = append(ss, pre+bm.Title) + if len(bm.Kids) > 0 { + ss1, err := bookmarkList(bm.Kids, level+1) + if err != nil { + return nil, err + } + ss = append(ss, ss1...) + } + } + return ss, nil +} + +func BookmarkList(ctx *model.Context) ([]string, error) { + + bms, err := Bookmarks(ctx) + if err != nil { + return nil, err + } + + if bms == nil { + return []string{"no bookmarks available"}, nil + } + + return bookmarkList(bms, 0) +} + +func ExportBookmarks(ctx *model.Context, source string) (*BookmarkTree, error) { + bms, err := Bookmarks(ctx) + if err != nil { + return nil, err + } + if bms == nil { + return nil, nil + } + + bmTree := BookmarkTree{} + bmTree.Header = header(ctx.XRefTable, source) + bmTree.Bookmarks = bms + + return &bmTree, nil +} + +func ExportBookmarksJSON(ctx *model.Context, source string, w io.Writer) (bool, error) { + bookmarkTree, err := ExportBookmarks(ctx, source) + if err != nil || bookmarkTree == nil { + return false, err + } + + bb, err := json.MarshalIndent(bookmarkTree, "", "\t") + if err != nil { + return false, err + } + + _, err = w.Write(bb) + + return true, err +} + +func bmDict(ctx *model.Context, bm Bookmark, parent types.IndirectRef) (types.Dict, error) { + + _, pageIndRef, _, err := ctx.PageDict(bm.PageFrom, false) + if err != nil { + return nil, err + } + + arr := types.Array{*pageIndRef, types.Name("Fit")} + ir, err := ctx.IndRefForNewObject(arr) + if err != nil { + return nil, err + } + + var o types.Object = *ir + + s, err := types.EscapeUTF16String(bm.Title) + if err != nil { + return nil, err + } + + d := types.Dict(map[string]types.Object{ + "Dest": types.NewHexLiteral([]byte(bm.Title)), + "Title": types.StringLiteral(*s), + "Parent": parent}, + ) + + m := model.NameMap{bm.Title: []types.Dict{d}} + if err := ctx.Names["Dests"].Add(ctx.XRefTable, bm.Title, o, m, []string{"D", "Dest"}); err != nil { + return nil, err + } + + if bm.Color != nil { + d["C"] = types.Array{types.Float(bm.Color.R), types.Float(bm.Color.G), types.Float(bm.Color.B)} + } + + if style := bm.Style(); style > 0 { + d["F"] = types.Integer(style) + } + + return d, nil +} + +func createOutlineItemDict(ctx *model.Context, bms []Bookmark, parent *types.IndirectRef, parentPageNr *int) (*types.IndirectRef, *types.IndirectRef, int, int, error) { + var ( + first *types.IndirectRef + irPrev *types.IndirectRef + dPrev types.Dict + total int + visible int + ) + + for i, bm := range bms { + + if i == 0 && parentPageNr != nil && bm.PageFrom < *parentPageNr { + return nil, nil, 0, 0, errCorruptedBookmarks + } + + if i > 0 && bm.PageFrom < bms[i-1].PageFrom { + return nil, nil, 0, 0, errCorruptedBookmarks + } + + total++ + + d, err := bmDict(ctx, bm, *parent) + if err != nil { + return nil, nil, 0, 0, err + } + + ir, err := ctx.IndRefForNewObject(d) + if err != nil { + return nil, nil, 0, 0, err + } + + if first == nil { + first = ir + } + + if len(bm.Kids) > 0 { + + first, last, c, visc, err := createOutlineItemDict(ctx, bm.Kids, ir, &bm.PageFrom) + if err != nil { + return nil, nil, 0, 0, err + } + + d["First"] = *first + d["Last"] = *last + + if visc == 0 { + d["Count"] = types.Integer(c) + total += c + } + + if visc > 0 { + d["Count"] = types.Integer(c + visc) + total += c + visible += visc + } + + } + + if irPrev != nil { + d["Prev"] = *irPrev + dPrev["Next"] = *ir + } + + dPrev = d + irPrev = ir + + } + + return first, irPrev, total, visible, nil +} + +func removeNamedDests(ctx *model.Context, item *types.IndirectRef) error { + var ( + d types.Dict + err error + empty, ok bool + ) + for ir := item; ir != nil; ir = d.IndirectRefEntry("Next") { + + if d, err = ctx.DereferenceDict(*ir); err != nil { + return err + } + + dest, destFound := d["Dest"] + if !destFound { + act, actFound := d["A"] + if !actFound { + continue + } + act, _ = ctx.Dereference(act) + actType := act.(types.Dict)["S"] + if actType.String() != "GoTo" { + continue + } + dest = act.(types.Dict)["D"] + } + + s, err := ctx.DestName(dest) + if err != nil { + return err + } + + if len(s) == 0 { + continue + } + + // Remove destName from dest nametree. + // TODO also try to remove from any existing root.Dests + empty, ok, err = ctx.Names["Dests"].Remove(ctx.XRefTable, s) + if err != nil { + return err + } + if !ok { + if log.DebugEnabled() { + log.Debug.Println("removeNamedDests: unable to remove dest name: " + s) + } + } + + first := d["First"] + if first != nil { + indRef := first.(types.IndirectRef) + if err := removeNamedDests(ctx, &indRef); err != nil { + return err + } + } + } + + if empty { + delete(ctx.Names, "Dests") + if err := ctx.RemoveNameTree("Dests"); err != nil { + return err + } + } + + return nil +} + +// RemoveBookmarks erases all outlines from ctx. +func RemoveBookmarks(ctx *model.Context) (bool, error) { + first, err := positionToFirstBookmark(ctx) + if err != nil { + if err != errNoBookmarks { + return false, err + } + return false, nil + } + if err := removeNamedDests(ctx, first); err != nil { + return false, err + } + + rootDict, err := ctx.Catalog() + if err != nil { + return false, err + } + + rootDict["Outlines"] = nil + + return true, nil +} + +// AddBookmarks adds bms to ctx. +func AddBookmarks(ctx *model.Context, bms []Bookmark, replace bool) error { + + rootDict, err := ctx.Catalog() + if err != nil { + return err + } + + if !replace { + if _, ok := rootDict.Find("Outlines"); ok { + return errExistingBookmarks + } + } + + if _, err = RemoveBookmarks(ctx); err != nil { + return err + } + + if err := ctx.LocateNameTree("Dests", true); err != nil { + return err + } + + outlinesDict := types.Dict(map[string]types.Object{"Type": types.Name("Outlines")}) + outlinesir, err := ctx.IndRefForNewObject(outlinesDict) + if err != nil { + return err + } + + first, last, total, visible, err := createOutlineItemDict(ctx, bms, outlinesir, nil) + if err != nil { + return err + } + + outlinesDict["First"] = *first + outlinesDict["Last"] = *last + outlinesDict["Count"] = types.Integer(total + visible) + + rootDict["Outlines"] = *outlinesir + + return nil +} + +func addBookmarkTree(ctx *model.Context, bmTree *BookmarkTree, replace bool) error { + return AddBookmarks(ctx, bmTree.Bookmarks, replace) +} + +func parseBookmarksFromJSON(bb []byte) (*BookmarkTree, error) { + + if !json.Valid(bb) { + return nil, errors.Errorf("pdfcpu: invalid JSON encoding detected.") + } + + bmTree := &BookmarkTree{} + + if err := json.Unmarshal(bb, bmTree); err != nil { + return nil, err + } + + return bmTree, nil +} + +// ImportBookmarks creates/replaces outlines in ctx as provided by rd. +func ImportBookmarks(ctx *model.Context, rd io.Reader, replace bool) (bool, error) { + + var buf bytes.Buffer + if _, err := io.Copy(&buf, rd); err != nil { + return false, err + } + + bmTree, err := parseBookmarksFromJSON(buf.Bytes()) + if err != nil { + return false, err + } + + err = addBookmarkTree(ctx, bmTree, replace) + if err != nil { + if err == errExistingBookmarks { + return false, nil + } + return true, err + } + + return true, nil +} diff --git a/pkg/pdfcpu/color/color.go b/pkg/pdfcpu/color/color.go new file mode 100644 index 0000000000000000000000000000000000000000..45c29607d1875ff295d315ed7c47fc0e5dae6353 --- /dev/null +++ b/pkg/pdfcpu/color/color.go @@ -0,0 +1,176 @@ +/* +Copyright 2022 The pdfcpu Authors. + +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 + + 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 color + +import ( + "encoding/hex" + "fmt" + "strconv" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// Some popular colors. +var ( + Black = SimpleColor{} + White = SimpleColor{R: 1, G: 1, B: 1} + LightGray = SimpleColor{.9, .9, .9} + Gray = SimpleColor{.5, .5, .5} + DarkGray = SimpleColor{.3, .3, .3} + Red = SimpleColor{1, 0, 0} + Green = SimpleColor{0, 1, 0} + Blue = SimpleColor{0, 0, 1} + Yellow = SimpleColor{.5, .5, 0} +) + +var ErrInvalidColor = errors.New("pdfcpu: invalid color constant") + +// SimpleColor is a simple rgb wrapper. +type SimpleColor struct { + R, G, B float32 // intensities between 0 and 1. +} + +func (sc SimpleColor) String() string { + return fmt.Sprintf("r=%1.1f g=%1.1f b=%1.1f", sc.R, sc.G, sc.B) +} + +func (sc SimpleColor) Array() types.Array { + return types.NewNumberArray(float64(sc.R), float64(sc.G), float64(sc.B)) +} + +// NewSimpleColor returns a SimpleColor for rgb in the form 0x00RRGGBB +func NewSimpleColor(rgb uint32) SimpleColor { + r := float32((rgb>>16)&0xFF) / 255 + g := float32((rgb>>8)&0xFF) / 255 + b := float32(rgb&0xFF) / 255 + return SimpleColor{r, g, b} +} + +// NewSimpleColorForArray returns a SimpleColor for an r,g,b array. +func NewSimpleColorForArray(arr types.Array) SimpleColor { + var r, g, b float32 + + if f, ok := arr[0].(types.Float); ok { + r = float32(f.Value()) + } else { + r = float32(arr[0].(types.Integer)) + } + + if f, ok := arr[1].(types.Float); ok { + g = float32(f.Value()) + } else { + g = float32(arr[1].(types.Integer)) + } + + if f, ok := arr[2].(types.Float); ok { + b = float32(f.Value()) + } else { + b = float32(arr[2].(types.Integer)) + } + + return SimpleColor{r, g, b} +} + +// NewSimpleColorForHexCode returns a SimpleColor for a #FFFFFF type hexadecimal rgb color representation. +func NewSimpleColorForHexCode(hexCol string) (SimpleColor, error) { + var sc SimpleColor + if len(hexCol) != 7 || hexCol[0] != '#' { + return sc, errors.Errorf("pdfcpu: invalid hex color string: #FFFFFF, %s\n", hexCol) + } + b, err := hex.DecodeString(hexCol[1:]) + if err != nil || len(b) != 3 { + return sc, errors.Errorf("pdfcpu: invalid hex color string: #FFFFFF, %s\n", hexCol) + } + return SimpleColor{float32(b[0]) / 255, float32(b[1]) / 255, float32(b[2]) / 255}, nil +} + +func internalSimpleColor(s string) (SimpleColor, error) { + var ( + sc SimpleColor + err error + ) + switch strings.ToLower(s) { + case "black": + sc = Black + case "darkgray": + sc = DarkGray + case "gray": + sc = Gray + case "lightgray": + sc = LightGray + case "white": + sc = White + case "red": + sc = Red + case "green": + sc = Green + case "blue": + sc = Blue + default: + err = ErrInvalidColor + } + return sc, err +} + +// ParseColor turns a color string into a SimpleColor. +func ParseColor(s string) (SimpleColor, error) { + var sc SimpleColor + + cs := strings.Split(s, " ") + if len(cs) != 1 && len(cs) != 3 { + return sc, errors.Errorf("pdfcpu: illegal color string: 3 intensities 0.0 <= i <= 1.0 or #FFFFFF, %s\n", s) + } + + if len(cs) == 1 { + if len(cs[0]) == 7 && cs[0][0] == '#' { + // #FFFFFF to uint32 + return NewSimpleColorForHexCode(cs[0]) + } + return internalSimpleColor(cs[0]) + } + + r, err := strconv.ParseFloat(cs[0], 32) + if err != nil { + return sc, errors.Errorf("red must be a float value: %s\n", cs[0]) + } + if r < 0 || r > 1 { + return sc, errors.New("pdfcpu: red: a color value is an intensity between 0.0 and 1.0") + } + sc.R = float32(r) + + g, err := strconv.ParseFloat(cs[1], 32) + if err != nil { + return sc, errors.Errorf("pdfcpu: green must be a float value: %s\n", cs[1]) + } + if g < 0 || g > 1 { + return sc, errors.New("pdfcpu: green: a color value is an intensity between 0.0 and 1.0") + } + sc.G = float32(g) + + b, err := strconv.ParseFloat(cs[2], 32) + if err != nil { + return sc, errors.Errorf("pdfcpu: blue must be a float value: %s\n", cs[2]) + } + if b < 0 || b > 1 { + return sc, errors.New("pdfcpu: blue: a color value is an intensity between 0.0 and 1.0") + } + sc.B = float32(b) + + return sc, nil +} diff --git a/pkg/pdfcpu/create/create.go b/pkg/pdfcpu/create/create.go new file mode 100644 index 0000000000000000000000000000000000000000..da483b0260158e0c8891563cf2b10810f2f1da79 --- /dev/null +++ b/pkg/pdfcpu/create/create.go @@ -0,0 +1,761 @@ +/* + Copyright 2021 The pdfcpu Authors. + + 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 + + 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 create + +import ( + "bytes" + "encoding/json" + "io" + "sort" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/font" + pdffont "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/font" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +func ensureFontIndRef(xRefTable *model.XRefTable, fontName string, frPage model.FontResource, fonts model.FontMap) (*types.IndirectRef, error) { + + frGlobal, ok := fonts[fontName] + if !ok { + return nil, errors.Errorf("pdfcpu: missing global font: %s", fontName) + } + + // Do we have an already created indRef or an indRef from AP form fonts or fonts we are reusing? + if frGlobal.Res.IndRef != nil { + + if frPage.Res.IndRef != nil && *frPage.Res.IndRef != *frGlobal.Res.IndRef { + return nil, errors.Errorf("pdfcpu: multiple objstreams for font: %s detected: ", fontName) + } + + if font.IsUserFont(fontName) && frGlobal.FontFile != nil { + if err := pdffont.UpdateUserfont(xRefTable, fontName, frGlobal); err != nil { + return nil, err + } + frGlobal.FontFile = nil + } + + } else { + + ir, err := pdffont.EnsureFontDict(xRefTable, fontName, frPage.Lang, "", false, frPage.Res.IndRef) + if err != nil { + return nil, err + } + + frGlobal.Res.IndRef = ir + } + + fonts[fontName] = frGlobal + + return frGlobal.Res.IndRef, nil +} + +func addPageResources(xRefTable *model.XRefTable, d types.Dict, p model.Page, fonts model.FontMap) error { + + fontRes := types.Dict{} + for fontName, frPage := range p.Fm { + ir, err := ensureFontIndRef(xRefTable, fontName, frPage, fonts) + if err != nil { + return err + } + fontRes[frPage.Res.ID] = *ir + } + + imgRes := types.Dict{} + for _, img := range p.Im { + imgRes[img.Res.ID] = *img.Res.IndRef + } + + if len(fontRes) > 0 || len(imgRes) > 0 { + resDict := types.Dict{} + if len(fontRes) > 0 { + resDict["Font"] = fontRes + } + if len(imgRes) > 0 { + resDict["XObject"] = imgRes + } + d["Resources"] = resDict + } + + return nil +} + +func updatePageResources(xRefTable *model.XRefTable, d, resDict types.Dict, p model.Page, fonts model.FontMap) error { + + if len(p.Fm) > 0 { + fontRes, ok := resDict["Font"].(types.Dict) + if !ok { + fontRes = types.Dict{} + } + for fontName, frPage := range p.Fm { + ir, err := ensureFontIndRef(xRefTable, fontName, frPage, fonts) + if err != nil { + return err + } + if ir != nil { + fontRes[frPage.Res.ID] = *ir + } + } + + resDict["Font"] = fontRes + } + + if len(p.Im) > 0 { + imgRes, ok := resDict["XObject"].(types.Dict) + if !ok { + imgRes = types.Dict{} + } + for _, img := range p.Im { + imgRes[img.Res.ID] = *img.Res.IndRef + } + resDict["XObject"] = imgRes + } + + if len(p.Fm) > 0 || len(p.Im) > 0 { + d["Resources"] = resDict + } + + return nil +} + +func setAnnotationParentsAndFields(xRefTable *model.XRefTable, p *model.Page, pIndRef types.IndirectRef) error { + for k, an := range p.AnnotTabs { + if an.IndRef == nil { + an.Dict["P"] = pIndRef + indRef, err := xRefTable.IndRefForNewObject(an.Dict) + if err != nil { + return err + } + an.IndRef = indRef + p.AnnotTabs[k] = an + } + p.Fields = append(p.Fields, *an.IndRef) + } + for k, an := range p.Annots { + if an.IndRef == nil { + an.Dict["P"] = pIndRef + indRef, err := xRefTable.IndRefForNewObject(an.Dict) + if err != nil { + return err + } + an.IndRef = indRef + p.Annots[k] = an + } + p.Fields = append(p.Fields, *an.IndRef) + } + return nil +} + +func addAnnotations(ff []model.FieldAnnotation, m map[int]model.FieldAnnotation) types.Array { + + arr := types.Array{} + + for i, j := 0, 0; j < len(ff); i++ { + an, ok := m[i+1] + if ok { + if an.Kids == nil { + arr = append(arr, *an.IndRef) + } else { + arr = append(arr, an.Kids...) + } + an.Field = true + m[i+1] = an + continue + } + if j < len(ff) { + an = ff[j] + if an.Kids == nil { + arr = append(arr, *an.IndRef) + } else { + arr = append(arr, an.Kids...) + } + j++ + continue + } + break + } + + keys := make([]int, 0, len(m)) + for k, an := range m { + if !an.Field { + keys = append(keys, k) + } + } + sort.Ints(keys) + + for _, k := range keys { + an := m[k] + if an.Kids == nil { + arr = append(arr, *an.IndRef) + } else { + arr = append(arr, an.Kids...) + } + } + + return arr +} + +func mergeAnnotations(oldAnnots types.Array, ff []model.FieldAnnotation, m map[int]model.FieldAnnotation) (types.Array, error) { + + if len(oldAnnots) == 0 { + return addAnnotations(ff, m), nil + } + + arr := types.Array{} + i := 0 + for j := 0; j < len(oldAnnots); i++ { + an, ok := m[i+1] + if ok { + if an.Kids == nil { + arr = append(arr, *an.IndRef) + } else { + arr = append(arr, an.Kids...) + } + an.Field = true + m[i+1] = an + continue + } + if j < len(oldAnnots) { + arr = append(arr, oldAnnots[j]) + j++ + continue + } + break + } + + for j := 0; j < len(ff); i++ { + an, ok := m[i+1] + if ok { + if an.Kids == nil { + arr = append(arr, *an.IndRef) + } else { + arr = append(arr, an.Kids...) + } + an.Field = true + m[i+1] = an + continue + } + if j < len(ff) { + an = ff[j] + if an.Kids == nil { + arr = append(arr, *an.IndRef) + } else { + arr = append(arr, an.Kids...) + } + j++ + continue + } + break + } + + keys := make([]int, 0, len(m)) + for k, an := range m { + if !an.Field { + keys = append(keys, k) + } + } + + sort.Ints(keys) + + for _, k := range keys { + an := m[k] + if an.Kids == nil { + arr = append(arr, *an.IndRef) + } else { + arr = append(arr, an.Kids...) + } + } + + return arr, nil +} + +// CreatePage generates a page dict for p. +func CreatePage( + xRefTable *model.XRefTable, + parentPageIndRef types.IndirectRef, + p *model.Page, + fonts model.FontMap) (*types.IndirectRef, types.Dict, error) { + + pageDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("Page"), + "Parent": parentPageIndRef, + "MediaBox": p.MediaBox.Array(), + "CropBox": p.CropBox.Array(), + }, + ) + + err := addPageResources(xRefTable, pageDict, *p, fonts) + if err != nil { + return nil, nil, err + } + + ir, err := xRefTable.StreamDictIndRef(p.Buf.Bytes()) + if err != nil { + return nil, pageDict, err + } + pageDict.Insert("Contents", *ir) + + pageDictIndRef, err := xRefTable.IndRefForNewObject(pageDict) + if err != nil { + return nil, nil, err + } + + if len(p.AnnotTabs) == 0 && len(p.Annots) == 0 && len(p.LinkAnnots) == 0 { + return pageDictIndRef, pageDict, nil + } + + if err := setAnnotationParentsAndFields(xRefTable, p, *pageDictIndRef); err != nil { + return nil, nil, err + } + + arr, err := mergeAnnotations(nil, p.Annots, p.AnnotTabs) + if err != nil { + return nil, nil, err + } + + for _, la := range p.LinkAnnots { + d, err := la.RenderDict(xRefTable, pageDictIndRef) + if err != nil { + return nil, nil, &json.UnsupportedTypeError{} + } + ir, err := xRefTable.IndRefForNewObject(d) + if err != nil { + return nil, nil, err + } + arr = append(arr, *ir) + } + + pageDict["Annots"] = arr + + return pageDictIndRef, pageDict, err +} + +// UpdatePage updates the existing page dict d with content provided by p. +func UpdatePage(xRefTable *model.XRefTable, dIndRef types.IndirectRef, d, res types.Dict, p *model.Page, fonts model.FontMap) error { + + // TODO Account for existing page rotation. + + err := updatePageResources(xRefTable, d, res, *p, fonts) + if err != nil { + return err + } + + err = xRefTable.AppendContent(d, p.Buf.Bytes()) + if err != nil { + return err + } + + if len(p.AnnotTabs) == 0 && len(p.Annots) == 0 && len(p.LinkAnnots) == 0 { + return nil + } + + if err := setAnnotationParentsAndFields(xRefTable, p, dIndRef); err != nil { + return err + } + + annots, err := xRefTable.DereferenceArray(d["Annots"]) + if err != nil { + return err + } + + arr, err := mergeAnnotations(annots, p.Annots, p.AnnotTabs) + if err != nil { + return err + } + + for _, la := range p.LinkAnnots { + d, err := la.RenderDict(xRefTable, &dIndRef) + if err != nil { + return err + } + ir, err := xRefTable.IndRefForNewObject(d) + if err != nil { + return err + } + arr = append(arr, *ir) + } + + d["Annots"] = arr + + return nil +} + +func cacheFormFieldIDs(ctx *model.Context, pdf *primitives.PDF) error { + + if ctx.Form == nil { + return nil + } + + o, found := ctx.Form.Find("Fields") + if !found { + return nil + } + + arr, err := ctx.DereferenceArray(o) + if err != nil { + return err + } + + for _, ir := range arr { + d, err := ctx.DereferenceDict(ir) + if err != nil { + return err + } + if len(d) == 0 { + continue + } + id := d.StringEntry("T") + if id != nil { + pdf.OldFieldIDs[*id] = true + } + } + + return nil +} + +func cacheResIDs(ctx *model.Context, pdf *primitives.PDF) error { + // Iterate over all pages of ctx and prepare a resIds []string for inherited "Font" and "XObject" resources. + for i := 1; i <= ctx.PageCount; i++ { + _, _, inhPA, err := ctx.PageDict(i, true) + if err != nil { + return err + } + if inhPA.Resources["Font"] != nil { + pdf.FontResIDs[i] = inhPA.Resources["Font"].(types.Dict) + } + if inhPA.Resources["XObject"] != nil { + pdf.XObjectResIDs[i] = inhPA.Resources["XObject"].(types.Dict) + } + } + return nil +} + +func parseFromJSON(ctx *model.Context, bb []byte) (*primitives.PDF, error) { + + if !json.Valid(bb) { + return nil, errors.Errorf("pdfcpu: invalid JSON encoding detected.") + } + + pdf := &primitives.PDF{ + FieldIDs: types.StringSet{}, + Fields: types.Array{}, + FormFonts: map[string]*primitives.FormFont{}, + Pages: map[string]*primitives.PDFPage{}, + FontResIDs: map[int]types.Dict{}, + XObjectResIDs: map[int]types.Dict{}, + Conf: ctx.Configuration, + XRefTable: ctx.XRefTable, + Optimize: ctx.Optimize, + CheckBoxAPs: map[float64]*primitives.AP{}, + RadioBtnAPs: map[float64]*primitives.AP{}, + OldFieldIDs: types.StringSet{}, + } + + if err := json.Unmarshal(bb, pdf); err != nil { + return nil, err + } + + if pdf.Update() { + + _, found := ctx.RootDict.Find("AcroForm") + + pdf.HasForm = found + + if pdf.HasForm { + if err := cacheFormFieldIDs(ctx, pdf); err != nil { + return nil, err + } + } + + if err := cacheResIDs(ctx, pdf); err != nil { + return nil, err + } + + } + + if err := pdf.Validate(); err != nil { + return nil, err + } + + return pdf, nil +} + +func appendPage( + ctx *model.Context, + pagesDictIndRef types.IndirectRef, + pagesDict types.Dict, + p *model.Page, + fonts model.FontMap) error { + + ir, _, err := CreatePage(ctx.XRefTable, pagesDictIndRef, p, fonts) + if err != nil { + return err + } + + if err := ctx.SetValid(*ir); err != nil { + return err + } + + if err := model.AppendPageTree(ir, 1, pagesDict); err != nil { + return err + } + + ctx.PageCount++ + + return nil +} + +func updatePage(ctx *model.Context, pageNr int, p *model.Page, fonts model.FontMap) error { + + pageDict, pageDictIndRef, inhPAttrs, err := ctx.PageDict(pageNr, false) + if err != nil { + return err + } + + // You have to make sure the media/crop boxes align in order to avoid unexpected results! + + if inhPAttrs.Resources == nil { + inhPAttrs.Resources = types.Dict{} + } + + return UpdatePage(ctx.XRefTable, *pageDictIndRef, pageDict, inhPAttrs.Resources, p, fonts) +} + +// UpdatePageTree merges new pages or updates existing pages into ctx. +func UpdatePageTree(ctx *model.Context, pages []*model.Page, fontMap model.FontMap) (types.Array, model.FontMap, error) { + + pageCount := ctx.PageCount + + ir, err := ctx.Pages() + if err != nil { + return nil, nil, err + } + + d, err := ctx.DereferenceDict(*ir) + if err != nil { + return nil, nil, err + } + + fields := types.Array{} + + for i, p := range pages { + + if p == nil { + continue + } + + pageNr := i + 1 + + var err error + + if pageNr > pageCount { + err = appendPage(ctx, *ir, d, p, fontMap) + } else { + err = updatePage(ctx, pageNr, p, fontMap) + } + + if err != nil { + return nil, nil, err + } + + fields = append(fields, p.Fields...) + } + + return fields, fontMap, nil +} + +func prepareFormFontResDict(ctx *model.Context, pdf *primitives.PDF, fonts model.FontMap) (types.Dict, error) { + + d := types.Dict{} + + for id, f := range pdf.FormFonts { + + if font.IsCoreFont(f.Name) { + frGlobal := fonts[f.Name] + if frGlobal.Res.IndRef != nil { + d.Insert(id, *frGlobal.Res.IndRef) + continue + } + ir, err := pdffont.EnsureFontDict(ctx.XRefTable, f.Name, "", "", true, nil) + if err != nil { + return nil, err + } + d.Insert(id, *ir) + continue + } + + var ir *types.IndirectRef + frGlobal, ok := fonts["cjk:"+f.Name] + if ok && frGlobal.Res.IndRef != nil { + ir = frGlobal.Res.IndRef + } + + ir, err := pdffont.EnsureFontDict(ctx.XRefTable, f.Name, f.Lang, f.Script, true, ir) + if err != nil { + return nil, err + } + + d.Insert(id, *ir) + } + + return d, nil +} + +func createForm( + ctx *model.Context, + pdf *primitives.PDF, + fields types.Array, + fonts model.FontMap) error { + + d := types.Dict{"Fields": fields} + + if len(pdf.FormFonts) > 0 { + d1, err := prepareFormFontResDict(ctx, pdf, fonts) + if err != nil { + return err + } + d["DR"] = types.Dict{"Font": d1} + } + + ctx.RootDict.Insert("AcroForm", d) + + return nil +} + +func updateForm( + ctx *model.Context, + pdf *primitives.PDF, + fields types.Array, + fonts model.FontMap) error { + + d := ctx.Form + + o, _ := d.Find("Fields") + arr, err := ctx.DereferenceArray(o) + if err != nil { + return err + } + d["Fields"] = append(arr, fields...) + + if len(pdf.FormFonts) == 0 { + return nil + } + + // Update resources. + + o, found := d.Find("DR") + if !found { + d1, err := prepareFormFontResDict(ctx, pdf, fonts) + if err != nil { + return err + } + d["DR"] = types.Dict{"Font": d1} + return nil + } + + resDict, err := ctx.DereferenceDict(o) + if err != nil { + return err + } + + o, found = resDict.Find("Font") + if !found { + return err + } + + fontResDict, err := ctx.DereferenceDict(o) + if err != nil { + return err + } + + d1, err := prepareFormFontResDict(ctx, pdf, fonts) + if err != nil { + return err + } + + for k, v := range d1 { + if !fontResDict.Insert(k, v) { + return errors.Errorf("pdfcpu: duplicate font resource id detected: %s", k) + } + } + + return nil +} + +func handleForm( + ctx *model.Context, + pdf *primitives.PDF, + fields types.Array, + fonts model.FontMap) error { + + var err error + if pdf.Update() && pdf.HasForm { + err = updateForm(ctx, pdf, fields, fonts) + } else { + err = createForm(ctx, pdf, fields, fonts) + } + if err != nil { + return err + } + + for fName, frGlobal := range fonts { + if !strings.HasPrefix(fName, "cjk:") && font.IsUserFont(fName) { + _, err := pdffont.EnsureFontDict(ctx.XRefTable, fName, frGlobal.Lang, "", false, frGlobal.Res.IndRef) + if err != nil { + return err + } + } + } + + return nil +} + +// FromJSON generates PDF content into ctx as provided by rd. +func FromJSON(ctx *model.Context, rd io.Reader) error { + + var buf bytes.Buffer + if _, err := io.Copy(&buf, rd); err != nil { + return err + } + + pdf, err := parseFromJSON(ctx, buf.Bytes()) + if err != nil { + return err + } + + pages, fontMap, err := pdf.RenderPages() + if err != nil { + return err + } + + fields, fonts, err := UpdatePageTree(ctx, pages, fontMap) + if err != nil { + return err + } + + if len(fields) > 0 { + if err := handleForm(ctx, pdf, fields, fonts); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/pdfcpu/createAnnotations.go b/pkg/pdfcpu/createAnnotations.go new file mode 100644 index 0000000000000000000000000000000000000000..a4641fc222db457ebe07b6358ba7678578decfe8 --- /dev/null +++ b/pkg/pdfcpu/createAnnotations.go @@ -0,0 +1,1220 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 pdfcpu + +import ( + "path/filepath" + "time" + + pdffont "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/font" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +// Functions needed to create a test.pdf that gets used for validation testing (see process_test.go) + +func createTextAnnotation(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + d := types.Dict(map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("Text"), + "Contents": types.StringLiteral("Text Annotation"), + "Rect": annotRect, + "P": pageIndRef, + //"NM": "", + //"Border": NewIntegerArray(0, 0, 5), + //"C": NewNumberArray(1, 0, 0), + //"Name": Name("Note"), + }) + + return xRefTable.IndRefForNewObject(d) +} + +func createLinkAnnotation(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + usageDict := types.Dict( + map[string]types.Object{ + "CreatorInfo": types.Dict( + map[string]types.Object{ + "Creator": types.StringLiteral("pdfcpu"), + "Subtype": types.Name("Technical"), + }, + ), + "Language": types.Dict( + map[string]types.Object{ + "Lang": types.StringLiteral("en-us"), + "Preferred": types.Name("ON"), + }, + ), + "Export": types.Dict( + map[string]types.Object{ + "ExportState": types.Name("ON"), + }, + ), + "Zoom": types.Dict( + map[string]types.Object{ + "min": types.Float(0), + }, + ), + "Print": types.Dict( + map[string]types.Object{ + "Subtype": types.Name("Watermark"), + "PrintState": types.Name("ON"), + }, + ), + "View": types.Dict( + map[string]types.Object{ + "ViewState": types.Name("Ind"), + }, + ), + "User": types.Dict( + map[string]types.Object{ + "Type": types.Name("ON"), + "Name": types.StringLiteral("Horst Rutter"), + }, + ), + "PageElement": types.Dict( + map[string]types.Object{ + "Subtype": types.Name("FG"), + }, + ), + }, + ) + + optionalContentGroupDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("OCG"), + "Name": types.StringLiteral("OCG"), + "Intent": types.Name("Design"), + "Usage": usageDict, + }, + ) + + uriActionDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("Action"), + "S": types.Name("URI"), + "URI": types.StringLiteral("https://golang.org"), + }, + ) + + indRef, err := xRefTable.IndRefForNewObject(uriActionDict) + if err != nil { + return nil, err + } + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("Link"), + "Contents": types.StringLiteral("Link Annotation"), + "Rect": annotRect, + "P": pageIndRef, + "Border": types.NewIntegerArray(0, 0, 5), + "C": types.NewNumberArray(0, 0, 1), + "A": *indRef, + "H": types.Name("I"), + "PA": *indRef, + "OC": optionalContentGroupDict, + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func createFreeTextAnnotation(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("FreeText"), + "Contents": types.StringLiteral("FreeText Annotation"), + "F": types.Integer(128), // Lock + "Rect": annotRect, + "P": pageIndRef, + "Border": types.NewIntegerArray(0, 0, 1), + "C": types.NewNumberArray(0, 1, 0), + "DA": types.StringLiteral("DA"), + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func createLineAnnotation(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("Line"), + "Contents": types.StringLiteral("Line Annotation"), + "F": types.Integer(128), // Lock + "Rect": annotRect, + "P": pageIndRef, + "Border": types.NewIntegerArray(0, 0, 1), + "C": types.NewNumberArray(0, 1, 0), + "L": annotRect, + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func createSquareAnnotation(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("Square"), + "Contents": types.StringLiteral("Square Annotation"), + "F": types.Integer(128), // Lock + "Rect": annotRect, + "P": pageIndRef, + "Border": types.NewIntegerArray(0, 0, 1), + "C": types.NewNumberArray(0, .3, .3), + "IC": types.NewNumberArray(0.8, .8, .8), + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func createCircleAnnotation(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("Circle"), + "Contents": types.StringLiteral("Circle Annotation"), + "F": types.Integer(128), // Lock + "Rect": annotRect, + "P": pageIndRef, + "Border": types.NewIntegerArray(0, 0, 10), + "C": types.NewNumberArray(0.5, 0, 5, 0), + "IC": types.NewNumberArray(0.8, .8, .8), + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func createPolygonAnnotation(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + // Construct a polyline using the annot rects both lower corners and the upper right corner. + v := types.Array{nil, nil, nil, nil} + copy(v, annotRect) + v = append(v, annotRect[2]) + v = append(v, annotRect[1]) + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("Polygon"), + "Contents": types.StringLiteral("Polygon Annotation"), + "Rect": annotRect, + "P": pageIndRef, + "Border": types.NewIntegerArray(0, 0, 1), + "C": types.NewNumberArray(0, 1, 0), + "Vertices": v, + "IC": types.NewNumberArray(0.3, 0.5, 0.0), + "BS": types.Dict( + map[string]types.Object{ + "Type": types.Name("Border"), + "W": types.Float(0.5), + "S": types.Name("D"), + }, + ), + "BE": types.Dict( + map[string]types.Object{ + "S": types.Name("C"), + "I": types.Float(1), + }, + ), + "IT": types.Name("PolygonCloud"), + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func createPolyLineAnnotation(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + // Construct a polyline using the annot rects both lower corners and the upper right corner. + v := types.Array{nil, nil, nil, nil} + copy(v, annotRect) + v = append(v, annotRect[2]) + v = append(v, annotRect[1]) + + optionalContentGroupDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("OCG"), + "Name": types.StringLiteral("OCG"), + "Intent": types.NewNameArray("Design", "View"), + }, + ) + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("PolyLine"), + "Contents": types.StringLiteral("PolyLine Annotation"), + "Rect": annotRect, + "P": pageIndRef, + "Border": types.NewIntegerArray(0, 0, 1), + "C": types.NewNumberArray(0, 1, 0), + "Vertices": v, + "OC": optionalContentGroupDict, + "IC": types.NewNumberArray(0.3, 0.5, 0.0), + "BS": types.Dict( + map[string]types.Object{ + "Type": types.Name("Border"), + "W": types.Float(0.5), + "S": types.Name("D"), + }, + ), + "IT": types.Name("PolygonCloud"), + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func createHighlightAnnotation(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + // Create a quad points array corresponding to the annot rect. + ar := annotRect + + qp := types.Array{} + qp = append(qp, ar[0]) + qp = append(qp, ar[1]) + qp = append(qp, ar[2]) + qp = append(qp, ar[1]) + qp = append(qp, ar[2]) + qp = append(qp, ar[3]) + qp = append(qp, ar[0]) + qp = append(qp, ar[3]) + + optionalContentGroupDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("OCG"), + "Name": types.StringLiteral("OCG"), + }, + ) + + optionalContentMembershipDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("OCMD"), + "OCGs": types.Array{nil, optionalContentGroupDict}, + "P": types.Name("AllOn"), + "VE": types.Array{}, + }, + ) + + _ = optionalContentMembershipDict + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("Highlight"), + "Contents": types.StringLiteral("Highlight Annotation"), + "Rect": annotRect, + "P": pageIndRef, + "Border": types.NewIntegerArray(0, 0, 1), + "C": types.NewNumberArray(.2, 0, 0), + "OC": optionalContentMembershipDict, + "QuadPoints": qp, + "T": types.StringLiteral("MyTitle"), + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func createUnderlineAnnotation(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + // Create a quad points array corresponding to annot rect. + ar := annotRect + + qp := types.Array{} + qp = append(qp, ar[0]) + qp = append(qp, ar[1]) + qp = append(qp, ar[2]) + qp = append(qp, ar[1]) + qp = append(qp, ar[2]) + qp = append(qp, ar[3]) + qp = append(qp, ar[0]) + qp = append(qp, ar[3]) + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("Underline"), + "Contents": types.StringLiteral("Underline Annotation"), + "Rect": annotRect, + "P": pageIndRef, + "Border": types.NewIntegerArray(0, 0, 1), + "C": types.NewNumberArray(.5, 0, 0), + "QuadPoints": qp, + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func createSquigglyAnnotation(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + // Create a quad points array corresponding to annot rect. + ar := annotRect + + qp := types.Array{} + qp = append(qp, ar[0]) + qp = append(qp, ar[1]) + qp = append(qp, ar[2]) + qp = append(qp, ar[1]) + qp = append(qp, ar[2]) + qp = append(qp, ar[3]) + qp = append(qp, ar[0]) + qp = append(qp, ar[3]) + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("Squiggly"), + "Contents": types.StringLiteral("Squiggly Annotation"), + "Rect": annotRect, + "P": pageIndRef, + "Border": types.NewIntegerArray(0, 0, 1), + "C": types.NewNumberArray(.5, 0, 0), + "QuadPoints": qp, + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func createStrikeOutAnnotation(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + // Create a quad points array corresponding to annot rect. + ar := annotRect + + qp := types.Array{} + qp = append(qp, ar[0]) + qp = append(qp, ar[1]) + qp = append(qp, ar[2]) + qp = append(qp, ar[1]) + qp = append(qp, ar[2]) + qp = append(qp, ar[3]) + qp = append(qp, ar[0]) + qp = append(qp, ar[3]) + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("StrikeOut"), + "Contents": types.StringLiteral("StrikeOut Annotation"), + "Rect": annotRect, + "P": pageIndRef, + "Border": types.NewIntegerArray(0, 0, 1), + "C": types.NewNumberArray(.5, 0, 0), + "QuadPoints": qp, + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func createCaretAnnotation(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("Caret"), + "Contents": types.StringLiteral("Caret Annotation"), + "Rect": annotRect, + "P": pageIndRef, + "Border": types.NewIntegerArray(0, 0, 1), + "C": types.NewNumberArray(0.5, 0.5, 0), + "RD": types.NewNumberArray(0, 0, 0, 0), + "Sy": types.Name("None"), + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func createStampAnnotation(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("Stamp"), + "Contents": types.StringLiteral("Stamp Annotation"), + "Rect": annotRect, + "P": pageIndRef, + "Border": types.NewIntegerArray(0, 0, 1), + "C": types.NewNumberArray(0.5, 0.5, 0.9), + "Name": types.Name("Approved"), + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func createInkAnnotation(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + ar := annotRect + + l := types.Array{ + types.Array{ar[0], ar[1], ar[2], ar[1]}, + types.Array{ar[2], ar[1], ar[2], ar[3]}, + types.Array{ar[2], ar[3], ar[0], ar[3]}, + types.Array{ar[0], ar[3], ar[0], ar[1]}, + } + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("Ink"), + "Contents": types.StringLiteral("Ink Annotation"), + "Rect": annotRect, + "P": pageIndRef, + "Border": types.NewIntegerArray(0, 0, 1), + "C": types.NewNumberArray(0.5, 0, 0.3), + "InkList": l, + "ExData": types.Dict( + map[string]types.Object{ + "Type": types.Name("ExData"), + "Subtype": types.Name("Markup3D"), + }, + ), + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func createPopupAnnotation(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("Popup"), + "Contents": types.StringLiteral("Ink Annotation"), + "Rect": annotRect, + "P": pageIndRef, + "Border": types.NewIntegerArray(0, 0, 1), + "C": types.NewNumberArray(0.5, 0, 0.3), + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func createFileAttachmentAnnotation(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + // macOS starts up iTunes for audio file attachments. + + fileName := testAudioFileWAV + + ir, err := xRefTable.NewEmbeddedFileStreamDict(fileName) + if err != nil { + return nil, err + } + + fn := filepath.Base(fileName) + + s, err := types.EscapeUTF16String(fn) + if err != nil { + return nil, err + } + + fileSpecDict, err := xRefTable.NewFileSpecDict(fn, *s, "attached by pdfcpu", *ir) + if err != nil { + return nil, err + } + + ir, err = xRefTable.IndRefForNewObject(fileSpecDict) + if err != nil { + return nil, err + } + + now := types.StringLiteral(types.DateString(time.Now())) + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("FileAttachment"), + "Contents": types.StringLiteral("FileAttachment Annotation"), + "Rect": annotRect, + "P": pageIndRef, + "M": now, + "F": types.Integer(0), + "Border": types.NewIntegerArray(0, 0, 1), + "C": types.NewNumberArray(0.5, 0.0, 0.5), + "CA": types.Float(0.95), + "CreationDate": now, + "Name": types.Name("Paperclip"), + "FS": *ir, + "NM": types.StringLiteral("SoundFileAttachmentAnnot"), + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func createFileSpecDict(xRefTable *model.XRefTable, fileName string) (types.Dict, error) { + ir, err := xRefTable.NewEmbeddedFileStreamDict(fileName) + if err != nil { + return nil, err + } + fn := filepath.Base(fileName) + + s, err := types.EscapeUTF16String(fn) + if err != nil { + return nil, err + } + + return xRefTable.NewFileSpecDict(fn, *s, "attached by pdfcpu", *ir) +} + +func createSoundObject(xRefTable *model.XRefTable) (*types.IndirectRef, error) { + fileName := testAudioFileWAV + fileSpecDict, err := createFileSpecDict(xRefTable, fileName) + if err != nil { + return nil, err + } + return xRefTable.NewSoundStreamDict(fileName, 44100, fileSpecDict) +} + +func createSoundAnnotation(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + indRef, err := createSoundObject(xRefTable) + if err != nil { + return nil, err + } + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("Sound"), + "Contents": types.StringLiteral("Sound Annotation"), + "Rect": annotRect, + "P": pageIndRef, + "Border": types.NewIntegerArray(0, 0, 1), + "C": types.NewNumberArray(0, 0.5, 0.5), + "Sound": *indRef, + "Name": types.Name("Speaker"), + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func createMovieDict(xRefTable *model.XRefTable) (*types.IndirectRef, error) { + + // not supported: mp3,mp4,m4a + + fileSpecDict, err := createFileSpecDict(xRefTable, testAudioFileWAV) + if err != nil { + return nil, err + } + + d := types.Dict( + map[string]types.Object{ + "F": fileSpecDict, + "Aspect": types.NewIntegerArray(200, 200), + "Rotate": types.Integer(0), + "Poster": types.Boolean(true), + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func createMovieAnnotation(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + indRef, err := createMovieDict(xRefTable) + if err != nil { + return nil, err + } + + movieActivationDict := types.Dict( + map[string]types.Object{ + "Start": types.Integer(10), + "Duration": types.Integer(60), + "Rate": types.Float(1.0), + "Volume": types.Float(1.0), + "ShowControls": types.Boolean(true), + "Mode": types.Name("Once"), + "Synchronous": types.Boolean(false), + }, + ) + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("Movie"), + "Contents": types.StringLiteral("Movie Annotation"), + "Rect": annotRect, + "P": pageIndRef, + "Border": types.NewIntegerArray(0, 0, 3), // rounded corners don't work + "C": types.NewNumberArray(0.3, 0.5, 0.5), + "Movie": *indRef, + "T": types.StringLiteral("Sample Movie"), + "A": movieActivationDict, + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func createMediaRenditionAction(xRefTable *model.XRefTable, mediaClipDataDict *types.IndirectRef) types.Dict { + + r := createMediaRendition(xRefTable, mediaClipDataDict) + + return types.Dict( + map[string]types.Object{ + "Type": types.Name("Action"), + "S": types.Name("Rendition"), + "R": *r, // rendition object + "OP": types.Integer(0), // Play + }, + ) +} + +func createSelectorRenditionAction(mediaClipDataDict *types.IndirectRef) types.Dict { + + r := createSelectorRendition(mediaClipDataDict) + + return types.Dict( + map[string]types.Object{ + "Type": types.Name("Action"), + "S": types.Name("Rendition"), + "R": *r, // rendition object + "OP": types.Integer(0), // Play + }, + ) +} + +func createScreenAnnotation(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + ir, err := createMediaClipDataDict(xRefTable) + if err != nil { + return nil, err + } + + mediaRenditionAction := createMediaRenditionAction(xRefTable, ir) + + selectorRenditionAction := createSelectorRenditionAction(ir) + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("Screen"), + "Contents": types.StringLiteral("Screen Annotation"), + "Rect": annotRect, + "P": pageIndRef, + "Border": types.NewIntegerArray(0, 0, 3), + "C": types.NewNumberArray(0.2, 0.8, 0.5), + "A": mediaRenditionAction, + "AA": types.Dict( + map[string]types.Object{ + "D": selectorRenditionAction, + }, + ), + }, + ) + + ir, err = xRefTable.IndRefForNewObject(d) + if err != nil { + return nil, err + } + + // Inject indRef of screen annotation into action dicts. + mediaRenditionAction.Insert("AN", *ir) + selectorRenditionAction.Insert("AN", *ir) + + return ir, nil +} + +func createWidgetAnnotation(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + appearanceCharacteristicsDict := types.Dict( + map[string]types.Object{ + "R": types.Integer(0), + "BC": types.NewNumberArray(0.0, 0.0, 0.0), + "BG": types.NewNumberArray(0.5, 0.0, 0.5), + "RC": types.StringLiteral("Rollover caption"), + "IF": types.Dict( + map[string]types.Object{ + "SW": types.Name("A"), + "S": types.Name("A"), + "FB": types.Boolean(true), + }, + ), + }, + ) + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("Widget"), + "Contents": types.StringLiteral("Widget Annotation"), + "Rect": annotRect, + "P": pageIndRef, + "Border": types.NewIntegerArray(0, 0, 3), + "C": types.NewNumberArray(0.5, 0.5, 0.5), + "MK": appearanceCharacteristicsDict, + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func createXObjectForPrinterMark(xRefTable *model.XRefTable) (*types.IndirectRef, error) { + buf := `0 0 m 0 25 l 25 25 l 25 0 l s` + sd, _ := xRefTable.NewStreamDictForBuf([]byte(buf)) + sd.InsertName("Type", "XObject") + sd.InsertName("Subtype", "Form") + sd.InsertInt("FormType", 1) + sd.Insert("BBox", types.NewNumberArray(0, 0, 25, 25)) + sd.Insert("Matrix", types.NewIntegerArray(1, 0, 0, 1, 0, 0)) + + if err := sd.Encode(); err != nil { + return nil, err + } + + return xRefTable.IndRefForNewObject(*sd) +} + +func createPrinterMarkAnnotation(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + ir, err := createXObjectForPrinterMark(xRefTable) + if err != nil { + return nil, err + } + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("PrinterMark"), + "Contents": types.StringLiteral("PrinterMark Annotation"), + "Rect": annotRect, + "P": pageIndRef, + "Border": types.NewIntegerArray(0, 0, 3), + "C": types.NewNumberArray(0.2, 0.8, 0.5), + "F": types.Integer(0), + "AP": types.Dict( + map[string]types.Object{ + "N": *ir, + }, + ), + "MN": types.Name("ColorBar"), + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func createXObjectForWaterMark(xRefTable *model.XRefTable) (*types.IndirectRef, error) { + fIndRef, err := pdffont.EnsureFontDict(xRefTable, "Helvetica", "", "", false, nil) + if err != nil { + return nil, err + } + + fResDict := types.NewDict() + fResDict.Insert("F1", *fIndRef) + resourceDict := types.NewDict() + resourceDict.Insert("Font", fResDict) + + buf := `0 0 m 0 200 l 200 200 l 200 0 l s BT /F1 48 Tf 0.7 0.7 -0.7 0.7 30 10 Tm 1 Tr 2 w (Watermark) Tj ET` + sd, _ := xRefTable.NewStreamDictForBuf([]byte(buf)) + sd.InsertName("Type", "XObject") + sd.InsertName("Subtype", "Form") + sd.InsertInt("FormType", 1) + sd.Insert("BBox", types.NewNumberArray(0, 0, 200, 200)) + sd.Insert("Matrix", types.NewIntegerArray(1, 0, 0, 1, 0, 0)) + sd.Insert("Resources", resourceDict) + + if err = sd.Encode(); err != nil { + return nil, err + } + + return xRefTable.IndRefForNewObject(*sd) +} + +func createWaterMarkAnnotation(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + ir, err := createXObjectForWaterMark(xRefTable) + if err != nil { + return nil, err + } + + d1 := types.Dict( + map[string]types.Object{ + "Type": types.Name("FixedPrint"), + "Matrix": types.NewIntegerArray(1, 0, 0, 1, 72, -72), + "H": types.Float(0), + "V": types.Float(0), + }, + ) + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("Watermark"), + "Contents": types.StringLiteral("Watermark Annotation"), + "Rect": annotRect, + "P": pageIndRef, + "Border": types.NewIntegerArray(0, 0, 3), + "C": types.NewNumberArray(0.2, 0.8, 0.5), + "F": types.Integer(0), + "AP": types.Dict( + map[string]types.Object{ + "N": *ir, + }, + ), + "FixedPrint": d1, + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func create3DAnnotation(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("3D"), + "Contents": types.StringLiteral("3D Annotation"), + "Rect": annotRect, + "P": pageIndRef, + "Border": types.NewIntegerArray(0, 0, 3), + "C": types.NewNumberArray(0.2, 0.8, 0.5), + "F": types.Integer(0), + "3DD": types.NewDict(), // stream or 3D reference dict + "3DV": types.Name("F"), + "3DA": types.NewDict(), // activation dict + "3DI": types.Boolean(true), + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func createRedactAnnotation(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + // Create a quad points array corresponding to annot rect. + qp := types.Array{} + qp = append(qp, annotRect[0]) + qp = append(qp, annotRect[1]) + qp = append(qp, annotRect[2]) + qp = append(qp, annotRect[1]) + qp = append(qp, annotRect[2]) + qp = append(qp, annotRect[3]) + qp = append(qp, annotRect[0]) + qp = append(qp, annotRect[3]) + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("Redact"), + "Contents": types.StringLiteral("Redact Annotation"), + "Rect": annotRect, + "P": pageIndRef, + "Border": types.NewIntegerArray(0, 0, 3), + "C": types.NewNumberArray(0.2, 0.8, 0.5), + "F": types.Integer(0), + "QuadPoints": qp, + "IC": types.NewNumberArray(0.5, 0.0, 0.9), + "OverlayText": types.StringLiteral("An overlay"), + "Repeat": types.Boolean(true), + "DA": types.StringLiteral("x"), + "Q": types.Integer(1), + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func createRemoteGoToAction(xRefTable *model.XRefTable) (*types.IndirectRef, error) { + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Action"), + "S": types.Name("GoToR"), + "F": types.StringLiteral("./go.pdf"), + "D": types.Array{types.Integer(0), types.Name("Fit")}, + "NewWindow": types.Boolean(true), + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func createLinkAnnotationWithRemoteGoToAction(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + ir, err := createRemoteGoToAction(xRefTable) + if err != nil { + return nil, err + } + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("Link"), + "Contents": types.StringLiteral("Link Annotation"), + "Rect": annotRect, + "P": pageIndRef, + "Border": types.NewIntegerArray(0, 0, 1), + "C": types.NewNumberArray(0, 0, 1), + "A": *ir, + "H": types.Name("I"), + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func createEmbeddedGoToAction(xRefTable *model.XRefTable) (*types.IndirectRef, error) { + + f := filepath.Join(testDir, "go.pdf") + fileSpecDict, err := createFileSpecDict(xRefTable, f) + if err != nil { + return nil, err + } + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Action"), + "S": types.Name("GoToE"), + "F": fileSpecDict, + "D": types.Array{types.Integer(0), types.Name("Fit")}, + "NewWindow": types.Boolean(true), // not honoured by Acrobat Reader. + "T": types.Dict( + map[string]types.Object{ + "R": types.Name("C"), + "N": types.StringLiteral(filepath.Base(f)), + }, + ), + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func createLinkAnnotationWithEmbeddedGoToAction(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + ir, err := createEmbeddedGoToAction(xRefTable) + if err != nil { + return nil, err + } + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("Link"), + "Contents": types.StringLiteral("Link Annotation"), + "Rect": annotRect, + "P": pageIndRef, + "Border": types.NewIntegerArray(0, 0, 1), + "C": types.NewNumberArray(0, 0, 1), + "A": *ir, + "H": types.Name("I"), + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func createLinkAnnotationDictWithLaunchAction(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("Link"), + "Contents": types.StringLiteral("Link Annotation"), + "Rect": annotRect, + "P": pageIndRef, + "Border": types.NewIntegerArray(0, 0, 1), + "C": types.NewNumberArray(0, 0, 1), + "A": types.Dict( + map[string]types.Object{ + "Type": types.Name("Action"), + "S": types.Name("Launch"), + "F": types.StringLiteral("golang.pdf"), + "Win": types.Dict( + map[string]types.Object{ + "F": types.StringLiteral("golang.pdf"), + "O": types.StringLiteral("O"), + }, + ), + "NewWindow": types.Boolean(true), + }, + ), + "H": types.Name("I"), + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func createLinkAnnotationDictWithThreadAction(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("Link"), + "Contents": types.StringLiteral("Link Annotation"), + "Rect": annotRect, + "P": pageIndRef, + "Border": types.NewIntegerArray(0, 0, 1), + "C": types.NewNumberArray(0, 0, 1), + "A": types.Dict( + map[string]types.Object{ + "Type": types.Name("Action"), + "S": types.Name("Thread"), + "D": types.Integer(0), // jump to first article thread + "B": types.Integer(0), // jump to first bead + }, + ), + "H": types.Name("I"), + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func createLinkAnnotationDictWithSoundAction(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + ir, err := createSoundObject(xRefTable) + if err != nil { + return nil, err + } + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("Link"), + "Contents": types.StringLiteral("Link Annotation"), + "Rect": annotRect, + "P": pageIndRef, + "Border": types.NewIntegerArray(0, 0, 1), + "C": types.NewNumberArray(0, 0, 1), + "A": types.Dict( + map[string]types.Object{ + "Type": types.Name("Action"), + "S": types.Name("Sound"), + "Sound": *ir, + "Synchronous": types.Boolean(false), + "Repeat": types.Boolean(false), + "Mix": types.Boolean(false), + }, + ), + "H": types.Name("I"), + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func createLinkAnnotationDictWithMovieAction(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("Link"), + "Contents": types.StringLiteral("Link Annotation"), + "Rect": annotRect, + "P": pageIndRef, + "Border": types.NewIntegerArray(0, 0, 1), + "C": types.NewNumberArray(0, 0, 1), + "A": types.Dict( + map[string]types.Object{ + "Type": types.Name("Action"), + "S": types.Name("Movie"), + "T": types.StringLiteral("Sample Movie"), + "Operation": types.Name("Play"), + }, + ), + "H": types.Name("I"), + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func createLinkAnnotationDictWithHideAction(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + hideActionDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("Action"), + "S": types.Name("Hide"), + "H": types.Boolean(true), + }, + ) + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("Link"), + "Contents": types.StringLiteral("Link Annotation"), + "Rect": annotRect, + "P": pageIndRef, + "Border": types.NewIntegerArray(0, 0, 1), + "C": types.NewNumberArray(0, 0, 1), + "A": hideActionDict, + "H": types.Name("I"), + }, + ) + + ir, err := xRefTable.IndRefForNewObject(d) + if err != nil { + return nil, err + } + + // We hide the link annotation itself. + hideActionDict.Insert("T", *ir) + + return ir, nil +} + +func createTrapNetAnnotation(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, annotRect types.Array) (*types.IndirectRef, error) { + + ir, err := pdffont.EnsureFontDict(xRefTable, "Helvetica", "", "", false, nil) + if err != nil { + return nil, err + } + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("TrapNet"), + "Contents": types.StringLiteral("TrapNet Annotation"), + "Rect": annotRect, + "P": pageIndRef, + "Border": types.NewIntegerArray(0, 0, 3), + "C": types.NewNumberArray(0.2, 0.8, 0.5), + "F": types.Integer(0), + "LastModified": types.StringLiteral(types.DateString(time.Now())), + "FontFauxing": types.Array{*ir}, + }, + ) + + return xRefTable.IndRefForNewObject(d) +} diff --git a/pkg/pdfcpu/createRenditions.go b/pkg/pdfcpu/createRenditions.go new file mode 100644 index 0000000000000000000000000000000000000000..f49a9d2f10c1b8fb7d312ca3223d01315d7b2daa --- /dev/null +++ b/pkg/pdfcpu/createRenditions.go @@ -0,0 +1,340 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 pdfcpu + +import ( + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +// Functions needed to create a test.pdf that gets used for validation testing (see process_test.go) + +func createMHBEDict() *types.Dict { + + softwareIdentDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("SoftwareIdentifier"), + "U": types.StringLiteral("vnd.adobe.swname:ADBE_Acrobat"), + "L": types.NewIntegerArray(0), + "H": types.NewIntegerArray(), + "OS": types.NewStringLiteralArray(), + }, + ) + + mediaCriteriaDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("MediaCriteria"), + "A": types.Boolean(false), + "C": types.Boolean(false), + "O": types.Boolean(false), + "S": types.Boolean(false), + "R": types.Integer(0), + "D": types.Dict( + map[string]types.Object{ + "Type": types.Name("MinBitDepth"), + "V": types.Integer(0), + "M": types.Integer(0), + }, + ), + "V": types.Array{softwareIdentDict}, + "Z": types.Dict( + map[string]types.Object{ + "Type": types.Name("MinScreenSize"), + "V": types.NewIntegerArray(640, 480), + "M": types.Integer(0), + }, + ), + "P": types.NewNameArray("1.3"), + "L": types.NewStringLiteralArray("en-US"), + }, + ) + + mhbe := types.NewDict() + mhbe.Insert("C", mediaCriteriaDict) + + return &mhbe +} + +func createMediaPlayersDict() *types.Dict { + + softwareIdentDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("SoftwareIdentifier"), + "U": types.StringLiteral("vnd.adobe.swname:ADBE_Acrobat"), + "L": types.NewIntegerArray(0), + "H": types.NewIntegerArray(), + "OS": types.NewStringLiteralArray(), + }, + ) + + mediaPlayerInfoDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("MediaPlayerInfo"), + "PID": softwareIdentDict, + }, + ) + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("MediaPlayers"), + "MU": types.Array{mediaPlayerInfoDict}, + }, + ) + + return &d +} + +func createMediaOffsetDict() *types.Dict { + + timeSpanDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("Timespan"), + "S": types.Name("S"), + "V": types.Integer(1), + }, + ) + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("MediaOffset"), + "S": types.Name("T"), + "T": timeSpanDict, + }, + ) + + return &d +} + +func createSectionMHBEDict() *types.Dict { + + d := createMediaOffsetDict() + + d1 := types.Dict( + map[string]types.Object{ + "B": *d, + "E": *d, + }, + ) + + return &d1 +} + +func createMediaClipDataDict(xRefTable *model.XRefTable) (*types.IndirectRef, error) { + + // not supported: mp3,mp4,m4a + + fileSpecDict, err := createFileSpecDict(xRefTable, testAudioFileWAV) + if err != nil { + return nil, err + } + + mediaPermissionsDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("MediaPermissions"), + "TF": types.StringLiteral("TEMPNEVER"), //TEMPALWAYS + }, + ) + + mediaPlayersDict := createMediaPlayersDict() + + mhbe := types.Dict(map[string]types.Object{"BU": nil}) + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("MediaClip"), + "S": types.Name("MCD"), // media clip data + "N": types.StringLiteral("Sample Audio"), + "D": fileSpecDict, + "CT": types.StringLiteral("audio/x-wav"), + //"CT": StringLiteral("audio/mp4"), + //"CT": StringLiteral("video/mp4"), + "P": mediaPermissionsDict, + "Alt": types.NewStringLiteralArray("en-US", "My vacation", "de", "Mein Urlaub", "", "My vacation"), + "PL": *mediaPlayersDict, + "MH": mhbe, + "BE": mhbe, + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func createMediaPlayParamsMHBE() *types.Dict { + + timeSpanDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("Timespan"), + "S": types.Name("S"), + "V": types.Float(10.0), + }, + ) + + mediaDurationDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("MediaDuration"), + "S": types.Name("T"), + "T": timeSpanDict, + }, + ) + + d := types.Dict( + map[string]types.Object{ + "V": types.Integer(100), + "C": types.Boolean(false), + "F": types.Integer(5), + "D": mediaDurationDict, + "A": types.Boolean(true), + "RC": types.Float(1.0), + }, + ) + + return &d +} + +func createMediaPlayParamsDict() *types.Dict { + + d := createMediaPlayersDict() + mhbe := createMediaPlayParamsMHBE() + + d1 := types.Dict( + map[string]types.Object{ + "Type": types.Name("MediaPlayParams"), + "PL": *d, + "MH": *mhbe, + "BE": *mhbe, + }, + ) + + return &d1 +} + +func createFloatingWindowsParamsDict() *types.Dict { + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("FWParams"), + "D": types.NewIntegerArray(200, 200), + "RT": types.Integer(0), + "P": types.Integer(4), + "O": types.Integer(1), + "T": types.Boolean(true), + "UC": types.Boolean(true), + "R": types.Integer(0), + "TT": types.NewStringLiteralArray("en-US", "Special title", "de", "Spezieller Titel", "default title"), + }, + ) + + return &d +} + +func createScreenParamsDict() *types.Dict { + + d := createFloatingWindowsParamsDict() + + mhbe := types.Dict( + map[string]types.Object{ + "Type": types.Name("MediaScreenParams"), + "W": types.Integer(0), + "B": types.NewNumberArray(1.0, 0.0, 0.0), + "O": types.Float(1.0), + "M": types.Integer(0), + "F": *d, + }, + ) + + d1 := types.Dict( + map[string]types.Object{ + "Type": types.Name("MediaScreenParams"), + "MH": mhbe, + "BE": mhbe, + }, + ) + + return &d1 +} + +func createMediaRendition(xRefTable *model.XRefTable, mediaClipDataDict *types.IndirectRef) *types.Dict { + + mhbe := createMHBEDict() + + d1 := createMediaPlayParamsDict() + d2 := createScreenParamsDict() + + d3 := types.Dict( + map[string]types.Object{ + "Type": types.Name("Rendition"), + "S": types.Name("MR"), + "MH": *mhbe, + "BE": *mhbe, + "C": *mediaClipDataDict, + "P": *d1, + "SP": *d2, + }, + ) + + return &d3 +} + +func createSectionMediaRendition(mediaClipDataDict *types.IndirectRef) *types.Dict { + + mhbe := createSectionMHBEDict() + + mediaClipSectionDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("MediaClip"), + "S": types.Name("MCS"), // media clip section + "N": types.StringLiteral("Sample movie"), + "D": *mediaClipDataDict, + "Alt": types.NewStringLiteralArray("en-US", "My vacation", "de", "Mein Urlaub", "", "default vacation"), + "MH": *mhbe, + "BE": *mhbe, + }, + ) + + mhbe = createMHBEDict() + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Rendition"), + "S": types.Name("MR"), + "MH": *mhbe, + "BE": *mhbe, + "C": mediaClipSectionDict, + }, + ) + + return &d +} + +func createSelectorRendition(mediaClipDataDict *types.IndirectRef) *types.Dict { + + mhbe := createMHBEDict() + + r := createSectionMediaRendition(mediaClipDataDict) + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Rendition"), + "S": types.Name("SR"), + "MH": *mhbe, + "BE": *mhbe, + "R": types.Array{*r}, + }, + ) + + return &d +} diff --git a/pkg/pdfcpu/createTestPDF.go b/pkg/pdfcpu/createTestPDF.go new file mode 100644 index 0000000000000000000000000000000000000000..d2a27185f658441ff6cd5b4e5537c55b4ab64cc6 --- /dev/null +++ b/pkg/pdfcpu/createTestPDF.go @@ -0,0 +1,1990 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 pdfcpu + +// Functions needed to create a test.pdf that gets used for validation testing (see process_test.go) + +import ( + "bytes" + "fmt" + "path/filepath" + + pdffont "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/font" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +var ( + testDir = "../../testdata" + testAudioFileWAV = filepath.Join(testDir, "resources", "test.wav") +) + +func CreateXRefTableWithRootDict() (*model.XRefTable, error) { + xRefTable := &model.XRefTable{ + Table: map[int]*model.XRefTableEntry{}, + Names: map[string]*model.Node{}, + PageAnnots: map[int]model.PgAnnots{}, + Stats: model.NewPDFStats(), + URIs: map[int]map[string]string{}, + UsedGIDs: map[string]map[uint16]bool{}, + } + + xRefTable.Table[0] = model.NewFreeHeadXRefTableEntry() + + one := 1 + xRefTable.Size = &one + + v := model.V17 + xRefTable.HeaderVersion = &v + + xRefTable.PageCount = 0 + + // Optional infoDict. + xRefTable.Info = nil + + // Additional streams not implemented. + xRefTable.AdditionalStreams = nil + + rootDict := types.NewDict() + rootDict.InsertName("Type", "Catalog") + + ir, err := xRefTable.IndRefForNewObject(rootDict) + if err != nil { + return nil, err + } + + xRefTable.Root = ir + + return xRefTable, nil +} + +// CreateDemoXRef creates a minimal single page PDF file for demo purposes. +func CreateDemoXRef() (*model.XRefTable, error) { + xRefTable, err := CreateXRefTableWithRootDict() + if err != nil { + return nil, err + } + + return xRefTable, nil +} + +func addPageTreeForResourceDictInheritanceDemo(xRefTable *model.XRefTable, rootDict types.Dict) error { + + // Create root page node. + + fIndRef, err := pdffont.EnsureFontDict(xRefTable, "Courier", "", "", false, nil) + if err != nil { + return err + } + + rootPagesDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("Pages"), + "Count": types.Integer(1), + "MediaBox": types.RectForFormat("A4").Array(), + "Resources": types.Dict( + map[string]types.Object{ + "Font": types.Dict( + map[string]types.Object{ + "F99": *fIndRef, + }, + ), + }, + ), + }, + ) + + rootPageIndRef, err := xRefTable.IndRefForNewObject(rootPagesDict) + if err != nil { + return err + } + + // Create intermediate page node. + + f100IndRef, err := pdffont.EnsureFontDict(xRefTable, "Courier-Bold", "", "", false, nil) + if err != nil { + return err + } + + pagesDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("Pages"), + "Count": types.Integer(1), + "MediaBox": types.RectForFormat("A4").Array(), + "Resources": types.Dict( + map[string]types.Object{ + "Font": types.Dict( + map[string]types.Object{ + "F100": *f100IndRef, + }, + ), + }, + ), + }, + ) + + pagesIndRef, err := xRefTable.IndRefForNewObject(pagesDict) + if err != nil { + return err + } + + // Create leaf page node. + + p := model.Page{MediaBox: types.RectForFormat("A4"), Fm: model.FontMap{}, Buf: new(bytes.Buffer)} + + fontName := "Times-Roman" + k := p.Fm.EnsureKey(fontName) + td := model.TextDescriptor{ + Text: "This font is Times-Roman and it is defined in the resource dict of this page dict.", + FontName: fontName, + FontKey: k, + FontSize: 12, + Scale: 1., + ScaleAbs: true, + X: 300, + Y: 400, + } + + model.WriteMultiLine(xRefTable, p.Buf, p.MediaBox, nil, td) + + fontName = "Courier" + td = model.TextDescriptor{ + Text: "This font is Courier and it is inherited from the page root.", + FontName: fontName, + FontKey: "F99", + FontSize: 12, + Scale: 1., + ScaleAbs: true, + X: 300, + Y: 300, + } + + model.WriteMultiLine(xRefTable, p.Buf, p.MediaBox, nil, td) + + fontName = "Courier-Bold" + td = model.TextDescriptor{ + Text: "This font is Courier-Bold and it is inherited from an intermediate page node.", + FontName: fontName, + FontKey: "F100", + FontSize: 12, + Scale: 1., + ScaleAbs: true, + X: 300, + Y: 350, + } + + model.WriteMultiLine(xRefTable, p.Buf, p.MediaBox, nil, td) + + pageIndRef, err := createDemoPage(xRefTable, *pagesIndRef, p) + if err != nil { + return err + } + + pagesDict.Insert("Kids", types.Array{*pageIndRef}) + pagesDict.Insert("Parent", *rootPageIndRef) + + rootPagesDict.Insert("Kids", types.Array{*pagesIndRef}) + rootDict.Insert("Pages", *rootPageIndRef) + + return nil +} + +// CreateResourceDictInheritanceDemoXRef creates a page tree for testing resource dict inheritance. +func CreateResourceDictInheritanceDemoXRef() (*model.XRefTable, error) { + xRefTable, err := CreateXRefTableWithRootDict() + if err != nil { + return nil, err + } + + rootDict, err := xRefTable.Catalog() + if err != nil { + return nil, err + } + + if err = addPageTreeForResourceDictInheritanceDemo(xRefTable, rootDict); err != nil { + return nil, err + } + + return xRefTable, nil +} + +func createFunctionalShadingDict(xRefTable *model.XRefTable) types.Dict { + f := types.Dict( + map[string]types.Object{ + "FunctionType": types.Integer(2), + "Domain": types.NewNumberArray(1.0, 1.2, 1.4, 1.6, 1.8, 2.0), + "N": types.Float(1), + }, + ) + + d := types.Dict( + map[string]types.Object{ + "ShadingType": types.Integer(1), + "Function": types.Array{f}, + }, + ) + + return d +} + +func createRadialShadingDict(xRefTable *model.XRefTable) types.Dict { + f := types.Dict( + map[string]types.Object{ + "FunctionType": types.Integer(2), + "Domain": types.NewNumberArray(1.0, 1.2, 1.4, 1.6, 1.8, 2.0), + "N": types.Float(1), + }, + ) + + d := types.Dict( + map[string]types.Object{ + "ShadingType": types.Integer(3), + "Coords": types.NewNumberArray(0, 0, 50, 10, 10, 100), + "Function": types.Array{f}, + }, + ) + + return d +} + +func createStreamObjForHalftoneDictType6(xRefTable *model.XRefTable) (*types.IndirectRef, error) { + sd := &types.StreamDict{ + Dict: types.Dict( + map[string]types.Object{ + "Type": types.Name("Halftone"), + "HalftoneType": types.Integer(6), + "Width": types.Integer(100), + "Height": types.Integer(100), + "TransferFunction": types.Name("Identity"), + }, + ), + Content: []byte{}, + } + + if err := sd.Encode(); err != nil { + return nil, err + } + + return xRefTable.IndRefForNewObject(*sd) +} + +func createStreamObjForHalftoneDictType10(xRefTable *model.XRefTable) (*types.IndirectRef, error) { + sd := &types.StreamDict{ + Dict: types.Dict( + map[string]types.Object{ + "Type": types.Name("Halftone"), + "HalftoneType": types.Integer(10), + "Xsquare": types.Integer(100), + "Ysquare": types.Integer(100), + }, + ), + Content: []byte{}, + } + + if err := sd.Encode(); err != nil { + return nil, err + } + + return xRefTable.IndRefForNewObject(*sd) +} + +func createStreamObjForHalftoneDictType16(xRefTable *model.XRefTable) (*types.IndirectRef, error) { + sd := &types.StreamDict{ + Dict: types.Dict( + map[string]types.Object{ + "Type": types.Name("Halftone"), + "HalftoneType": types.Integer(16), + "Width": types.Integer(100), + "Height": types.Integer(100), + }, + ), + Content: []byte{}, + } + + if err := sd.Encode(); err != nil { + return nil, err + } + + return xRefTable.IndRefForNewObject(*sd) +} + +func createPostScriptCalculatorFunctionStreamDict(xRefTable *model.XRefTable) (*types.IndirectRef, error) { + sd := &types.StreamDict{ + Dict: types.Dict( + map[string]types.Object{ + "FunctionType": types.Integer(4), + "Domain": types.NewNumberArray(100.), + "Range": types.NewNumberArray(100.), + }, + ), + Content: []byte{}, + } + + if err := sd.Encode(); err != nil { + return nil, err + } + + return xRefTable.IndRefForNewObject(*sd) +} + +func addResources(xRefTable *model.XRefTable, pageDict types.Dict, fontName string) error { + fIndRef, err := pdffont.EnsureFontDict(xRefTable, fontName, "", "", true, nil) + if err != nil { + return err + } + + functionalBasedShDict := createFunctionalShadingDict(xRefTable) + + radialShDict := createRadialShadingDict(xRefTable) + + f := types.Dict( + map[string]types.Object{ + "FunctionType": types.Integer(2), + "Domain": types.NewNumberArray(0.0, 1.0), + "C0": types.NewNumberArray(0.0), + "C1": types.NewNumberArray(1.0), + "N": types.Float(1), + }, + ) + + fontResources := types.Dict( + map[string]types.Object{ + "F1": *fIndRef, + }, + ) + + shadingResources := types.Dict( + map[string]types.Object{ + "S1": functionalBasedShDict, + "S3": radialShDict, + }, + ) + + colorSpaceResources := types.Dict( + map[string]types.Object{ + "CSCalGray": types.Array{ + types.Name("CalGray"), + types.Dict( + map[string]types.Object{ + "WhitePoint": types.NewNumberArray(0.9505, 1.0000, 1.0890), + }, + ), + }, + "CSCalRGB": types.Array{ + types.Name("CalRGB"), + types.Dict( + map[string]types.Object{ + "WhitePoint": types.NewNumberArray(0.9505, 1.0000, 1.0890), + }, + ), + }, + "CSLab": types.Array{ + types.Name("Lab"), + types.Dict( + map[string]types.Object{ + "WhitePoint": types.NewNumberArray(0.9505, 1.0000, 1.0890), + }, + ), + }, + "CS4DeviceN": types.Array{ + types.Name("DeviceN"), + types.NewNameArray("Orange", "Green", "None"), + types.Name("DeviceCMYK"), + f, + types.Dict( + map[string]types.Object{ + "Subtype": types.Name("DeviceN"), + }, + ), + }, + "CS6DeviceN": types.Array{ + types.Name("DeviceN"), + types.NewNameArray("L", "a", "b", "Spot1"), + types.Name("DeviceCMYK"), + f, + types.Dict( + map[string]types.Object{ + "Subtype": types.Name("NChannel"), + "Process": types.Dict( + map[string]types.Object{ + "ColorSpace": types.Array{ + types.Name("Lab"), + types.Dict( + map[string]types.Object{ + "WhitePoint": types.NewNumberArray(0.9505, 1.0000, 1.0890), + }, + ), + }, + "Components": types.NewNameArray("L", "a", "b"), + }, + ), + "Colorants": types.Dict( + map[string]types.Object{ + "Spot1": types.Array{ + types.Name("Separation"), + types.Name("Spot1"), + types.Name("DeviceCMYK"), + f, + }, + }, + ), + "MixingHints": types.Dict( + map[string]types.Object{ + "Solidities": types.Dict( + map[string]types.Object{ + "Spot1": types.Float(1.0), + }, + ), + "DotGain": types.Dict( + map[string]types.Object{ + "Spot1": f, + "Magenta": f, + "Yellow": f, + }, + ), + "PrintingOrder": types.NewNameArray("Magenta", "Yellow", "Spot1"), + }, + ), + }, + ), + }, + }, + ) + + anyXObject, err := createNormalAppearanceForFormField(xRefTable, 20., 20.) + if err != nil { + return err + } + + indRefHalfToneType6, err := createStreamObjForHalftoneDictType6(xRefTable) + if err != nil { + return err + } + + indRefHalfToneType10, err := createStreamObjForHalftoneDictType10(xRefTable) + if err != nil { + return err + } + + indRefHalfToneType16, err := createStreamObjForHalftoneDictType16(xRefTable) + if err != nil { + return err + } + + indRefFunctionStream, err := createPostScriptCalculatorFunctionStreamDict(xRefTable) + if err != nil { + return err + } + + graphicStateResources := types.Dict( + map[string]types.Object{ + "GS1": types.Dict( + map[string]types.Object{ + "Type": types.Name("ExtGState"), + "HT": types.Dict( + map[string]types.Object{ + "Type": types.Name("Halftone"), + "HalftoneType": types.Integer(1), + "Frequency": types.Integer(120), + "Angle": types.Integer(30), + "SpotFunction": types.Name("CosineDot"), + "TransferFunction": types.Name("Identity"), + }, + ), + "BM": types.NewNameArray("Overlay", "Darken", "Normal"), + "SMask": types.Dict( + map[string]types.Object{ + "Type": types.Name("Mask"), + "S": types.Name("Alpha"), + "G": *anyXObject, + "TR": f, + }, + ), + "TR": f, + "TR2": f, + }, + ), + "GS2": types.Dict( + map[string]types.Object{ + "Type": types.Name("ExtGState"), + "HT": types.Dict( + map[string]types.Object{ + "Type": types.Name("Halftone"), + "HalftoneType": types.Integer(5), + "Default": types.Dict( + map[string]types.Object{ + "Type": types.Name("Halftone"), + "HalftoneType": types.Integer(1), + "Frequency": types.Integer(120), + "Angle": types.Integer(30), + "SpotFunction": types.Name("CosineDot"), + "TransferFunction": types.Name("Identity"), + }, + ), + }, + ), + "BM": types.NewNameArray("Overlay", "Darken", "Normal"), + "SMask": types.Dict( + map[string]types.Object{ + "Type": types.Name("Mask"), + "S": types.Name("Alpha"), + "G": *anyXObject, + "TR": types.Name("Identity"), + }, + ), + "TR": types.Array{f, f, f, f}, + "TR2": types.Array{f, f, f, f}, + "BG2": f, + "UCR2": f, + "D": types.Array{types.Array{}, types.Integer(0)}, + }, + ), + "GS3": types.Dict( + map[string]types.Object{ + "Type": types.Name("ExtGState"), + "HT": *indRefHalfToneType6, + "SMask": types.Dict( + map[string]types.Object{ + "Type": types.Name("Mask"), + "S": types.Name("Alpha"), + "G": *anyXObject, + "TR": *indRefFunctionStream, + }, + ), + "BG2": *indRefFunctionStream, + "UCR2": *indRefFunctionStream, + "TR": *indRefFunctionStream, + "TR2": *indRefFunctionStream, + }, + ), + "GS4": types.Dict( + map[string]types.Object{ + "Type": types.Name("ExtGState"), + "HT": *indRefHalfToneType10, + }, + ), + "GS5": types.Dict( + map[string]types.Object{ + "Type": types.Name("ExtGState"), + "HT": *indRefHalfToneType16, + }, + ), + "GS6": types.Dict( + map[string]types.Object{ + "Type": types.Name("ExtGState"), + "HT": types.Dict( + map[string]types.Object{ + "Type": types.Name("Halftone"), + "HalftoneType": types.Integer(1), + "Frequency": types.Integer(120), + "Angle": types.Integer(30), + "SpotFunction": *indRefFunctionStream, + }, + ), + }, + ), + "GS7": types.Dict( + map[string]types.Object{ + "Type": types.Name("ExtGState"), + "HT": types.Dict( + map[string]types.Object{ + "Type": types.Name("Halftone"), + "HalftoneType": types.Integer(1), + "Frequency": types.Integer(120), + "Angle": types.Integer(30), + "SpotFunction": f, + }, + ), + }, + ), + }, + ) + + resourceDict := types.Dict( + map[string]types.Object{ + "Font": fontResources, + "Shading": shadingResources, + "ColorSpace": colorSpaceResources, + "ExtGState": graphicStateResources, + }, + ) + + pageDict.Insert("Resources", resourceDict) + + return nil +} + +// CreateTestPageContent draws a test grid. +func CreateTestPageContent(p model.Page) { + b := p.Buf + mb := p.MediaBox + + b.WriteString("[3]0 d 0 w ") + + // X + fmt.Fprintf(b, "0 0 m %f %f l s %f 0 m 0 %f l s ", + mb.Width(), mb.Height(), mb.Width(), mb.Height()) + + // Horizontal guides + c := 6 + if mb.Landscape() { + c = 4 + } + j := mb.Height() / float64(c) + for i := 1; i < c; i++ { + k := mb.Height() - float64(i)*j + s := fmt.Sprintf("0 %f m %f %f l s ", k, mb.Width(), k) + b.WriteString(s) + } + + // Vertical guides + c = 4 + if mb.Landscape() { + c = 6 + } + j = mb.Width() / float64(c) + for i := 1; i < c; i++ { + k := float64(i) * j + s := fmt.Sprintf("%f 0 m %f %f l s ", k, k, mb.Height()) + b.WriteString(s) + } +} + +func addContents(xRefTable *model.XRefTable, pageDict types.Dict, p model.Page) error { + CreateTestPageContent(p) + sd, _ := xRefTable.NewStreamDictForBuf(p.Buf.Bytes()) + + if err := sd.Encode(); err != nil { + return err + } + + ir, err := xRefTable.IndRefForNewObject(*sd) + if err != nil { + return err + } + + pageDict.Insert("Contents", *ir) + + return nil +} + +func createBoxColorDict() types.Dict { + cropBoxColorInfoDict := types.Dict( + map[string]types.Object{ + "C": types.NewNumberArray(1.0, 1.0, 0.0), + "W": types.Float(1.0), + "S": types.Name("D"), + "D": types.NewIntegerArray(3, 2), + }, + ) + bleedBoxColorInfoDict := types.Dict( + map[string]types.Object{ + "C": types.NewNumberArray(1.0, 0.0, 0.0), + "W": types.Float(3.0), + "S": types.Name("S"), + }, + ) + trimBoxColorInfoDict := types.Dict( + map[string]types.Object{ + "C": types.NewNumberArray(0.0, 1.0, 0.0), + "W": types.Float(1.0), + "S": types.Name("D"), + "D": types.NewIntegerArray(3, 2), + }, + ) + artBoxColorInfoDict := types.Dict( + map[string]types.Object{ + "C": types.NewNumberArray(0.0, 0.0, 1.0), + "W": types.Float(1.0), + "S": types.Name("S"), + }, + ) + d := types.Dict( + map[string]types.Object{ + "CropBox": cropBoxColorInfoDict, + "BleedBox": bleedBoxColorInfoDict, + "Trim": trimBoxColorInfoDict, + "ArtBox": artBoxColorInfoDict, + }, + ) + return d +} + +func addViewportDict(pageDict types.Dict) { + measureDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("Measure"), + "Subtype": types.Name("RL"), + "R": types.StringLiteral("1in = 0.1m"), + "X": types.Array{ + types.Dict( + map[string]types.Object{ + "Type": types.Name("NumberFormat"), + "U": types.StringLiteral("mi"), + "C": types.Float(0.00139), + "D": types.Integer(100000), + }, + ), + }, + "D": types.Array{ + types.Dict( + map[string]types.Object{ + "Type": types.Name("NumberFormat"), + "U": types.StringLiteral("mi"), + "C": types.Float(1), + }, + ), + types.Dict( + map[string]types.Object{ + "Type": types.Name("NumberFormat"), + "U": types.StringLiteral("feet"), + "C": types.Float(5280), + }, + ), + types.Dict( + map[string]types.Object{ + "Type": types.Name("NumberFormat"), + "U": types.StringLiteral("inch"), + "C": types.Float(12), + "F": types.Name("F"), + "D": types.Integer(8), + }, + ), + }, + "A": types.Array{ + types.Dict( + map[string]types.Object{ + "Type": types.Name("NumberFormat"), + "U": types.StringLiteral("acres"), + "C": types.Float(640), + }, + ), + }, + "O": types.NewIntegerArray(0, 1), + }, + ) + + bbox := types.RectForDim(10, 60) + + vpDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("Viewport"), + "BBox": bbox.Array(), + "Name": types.StringLiteral("viewPort"), + "Measure": measureDict, + }, + ) + + pageDict.Insert("VP", types.Array{vpDict}) +} + +func annotRect(i int, w, h, d, l float64) *types.Rectangle { + // d..distance between annotation rectangles + // l..side length of rectangle + + // max number of rectangles fitting into w + xmax := int((w - d) / (l + d)) + + // max number of rectangles fitting into h + ymax := int((h - d) / (l + d)) + + col := float64(i % xmax) + row := float64(i / xmax % ymax) + + llx := d + col*(l+d) + lly := d + row*(l+d) + + urx := llx + l + ury := lly + l + + return types.NewRectangle(llx, lly, urx, ury) +} + +// createAnnotsArray generates side by side lined up annotations starting in the lower left corner of the page. +func createAnnotsArray(xRefTable *model.XRefTable, pageIndRef types.IndirectRef, mediaBox types.Array) (types.Array, error) { + pageWidth := mediaBox[2].(types.Float) + pageHeight := mediaBox[3].(types.Float) + + a := types.Array{} + + for i, f := range []func(*model.XRefTable, types.IndirectRef, types.Array) (*types.IndirectRef, error){ + createTextAnnotation, + createLinkAnnotation, + createFreeTextAnnotation, + createLineAnnotation, + createSquareAnnotation, + createCircleAnnotation, + createPolygonAnnotation, + createPolyLineAnnotation, + createHighlightAnnotation, + createUnderlineAnnotation, + createSquigglyAnnotation, + createStrikeOutAnnotation, + createCaretAnnotation, + createStampAnnotation, + createInkAnnotation, + createPopupAnnotation, + createFileAttachmentAnnotation, + createSoundAnnotation, + createMovieAnnotation, + createScreenAnnotation, + createWidgetAnnotation, + createPrinterMarkAnnotation, + createWaterMarkAnnotation, + create3DAnnotation, + createRedactAnnotation, + createLinkAnnotationWithRemoteGoToAction, + createLinkAnnotationWithEmbeddedGoToAction, + createLinkAnnotationDictWithLaunchAction, + createLinkAnnotationDictWithThreadAction, + createLinkAnnotationDictWithSoundAction, + createLinkAnnotationDictWithMovieAction, + createLinkAnnotationDictWithHideAction, + createTrapNetAnnotation, // must be the last annotation for this page! + } { + r := annotRect(i, pageWidth.Value(), pageHeight.Value(), 30, 80) + + ir, err := f(xRefTable, pageIndRef, r.Array()) + if err != nil { + return nil, err + } + + a = append(a, *ir) + } + + return a, nil +} + +func createPageWithAnnotations(xRefTable *model.XRefTable, parentPageIndRef types.IndirectRef, mediaBox *types.Rectangle, fontName string) (*types.IndirectRef, error) { + mba := mediaBox.Array() + + pageDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("Page"), + "Parent": parentPageIndRef, + "BleedBox": mba, + "TrimBox": mba, + "ArtBox": mba, + "BoxColorInfo": createBoxColorDict(), + "UserUnit": types.Float(1.5)}, // Note: not honoured by Apple Preview + ) + + err := addResources(xRefTable, pageDict, fontName) + if err != nil { + return nil, err + } + + p := model.Page{MediaBox: mediaBox, Buf: new(bytes.Buffer)} + err = addContents(xRefTable, pageDict, p) + if err != nil { + return nil, err + } + + pageIndRef, err := xRefTable.IndRefForNewObject(pageDict) + if err != nil { + return nil, err + } + + // Fake SeparationInfo related to a single page only. + separationInfoDict := types.Dict( + map[string]types.Object{ + "Pages": types.Array{*pageIndRef}, + "DeviceColorant": types.Name("Cyan"), + "ColorSpace": types.Array{ + types.Name("Separation"), + types.Name("Green"), + types.Name("DeviceCMYK"), + types.Dict( + map[string]types.Object{ + "FunctionType": types.Integer(2), + "Domain": types.NewNumberArray(0.0, 1.0), + "C0": types.NewNumberArray(0.0), + "C1": types.NewNumberArray(1.0), + "N": types.Float(1), + }, + ), + }, + }, + ) + pageDict.Insert("SeparationInfo", separationInfoDict) + + annotsArray, err := createAnnotsArray(xRefTable, *pageIndRef, mba) + if err != nil { + return nil, err + } + pageDict.Insert("Annots", annotsArray) + + addViewportDict(pageDict) + + return pageIndRef, nil +} + +func createPageWithForm(xRefTable *model.XRefTable, parentPageIndRef types.IndirectRef, annotsArray types.Array, mediaBox *types.Rectangle, fontName string) (*types.IndirectRef, error) { + mba := mediaBox.Array() + + pageDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("Page"), + "Parent": parentPageIndRef, + "BleedBox": mba, + "TrimBox": mba, + "ArtBox": mba, + "BoxColorInfo": createBoxColorDict(), + "UserUnit": types.Float(1.0), // Note: not honoured by Apple Preview + }, + ) + + err := addResources(xRefTable, pageDict, fontName) + if err != nil { + return nil, err + } + + p := model.Page{MediaBox: mediaBox, Buf: new(bytes.Buffer)} + err = addContents(xRefTable, pageDict, p) + if err != nil { + return nil, err + } + + pageDict.Insert("Annots", annotsArray) + + return xRefTable.IndRefForNewObject(pageDict) +} + +func addPageTreeWithoutPage(xRefTable *model.XRefTable, rootDict types.Dict, d *types.Dim) error { + // May be modified later on. + mediaBox := types.RectForDim(d.Width, d.Height) + + pagesDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("Pages"), + "Count": types.Integer(0), + "MediaBox": mediaBox.Array(), + }, + ) + + pagesDict.Insert("Kids", types.Array{}) + + pagesRootIndRef, err := xRefTable.IndRefForNewObject(pagesDict) + if err != nil { + return err + } + + rootDict.Insert("Pages", *pagesRootIndRef) + + return nil +} + +func AddPageTreeWithSamplePage(xRefTable *model.XRefTable, rootDict types.Dict, p model.Page) error { + + // mediabox = physical page dimensions + mba := p.MediaBox.Array() + + pagesDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("Pages"), + "Count": types.Integer(1), + "MediaBox": mba, + }, + ) + + parentPageIndRef, err := xRefTable.IndRefForNewObject(pagesDict) + if err != nil { + return err + } + + pageIndRef, err := createDemoPage(xRefTable, *parentPageIndRef, p) + if err != nil { + return err + } + + pagesDict.Insert("Kids", types.Array{*pageIndRef}) + rootDict.Insert("Pages", *parentPageIndRef) + + return nil +} + +func addPageTreeWithAnnotations(xRefTable *model.XRefTable, rootDict types.Dict, fontName string) (*types.IndirectRef, error) { + // mediabox = physical page dimensions + mediaBox := types.RectForFormat("A4") + mba := mediaBox.Array() + + pagesDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("Pages"), + "Count": types.Integer(1), + "MediaBox": mba, + "CropBox": mba, + }, + ) + + parentPageIndRef, err := xRefTable.IndRefForNewObject(pagesDict) + if err != nil { + return nil, err + } + + pageIndRef, err := createPageWithAnnotations(xRefTable, *parentPageIndRef, mediaBox, fontName) + if err != nil { + return nil, err + } + + pagesDict.Insert("Kids", types.Array{*pageIndRef}) + rootDict.Insert("Pages", *parentPageIndRef) + + return pageIndRef, nil +} + +func addPageTreeWithFormFields(xRefTable *model.XRefTable, rootDict types.Dict, annotsArray types.Array, fontName string) (*types.IndirectRef, error) { + // mediabox = physical page dimensions + mediaBox := types.RectForFormat("A4") + mba := mediaBox.Array() + + pagesDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("Pages"), + "Count": types.Integer(1), + "MediaBox": mba, + "CropBox": mba, + }, + ) + + parentPageIndRef, err := xRefTable.IndRefForNewObject(pagesDict) + if err != nil { + return nil, err + } + + pageIndRef, err := createPageWithForm(xRefTable, *parentPageIndRef, annotsArray, mediaBox, fontName) + if err != nil { + return nil, err + } + + pagesDict.Insert("Kids", types.Array{*pageIndRef}) + + rootDict.Insert("Pages", *parentPageIndRef) + + return pageIndRef, nil +} + +// create a thread with 2 beads. +func createThreadDict(xRefTable *model.XRefTable, pageIndRef types.IndirectRef) (*types.IndirectRef, error) { + infoDict := types.NewDict() + infoDict.InsertString("Title", "DummyArticle") + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Thread"), + "I": infoDict, + }, + ) + + dIndRef, err := xRefTable.IndRefForNewObject(d) + if err != nil { + return nil, err + } + + // create first bead + d1 := types.Dict( + map[string]types.Object{ + "Type": types.Name("Bead"), + "T": *dIndRef, + "P": pageIndRef, + "R": types.NewNumberArray(0, 0, 100, 100), + }, + ) + + d1IndRef, err := xRefTable.IndRefForNewObject(d1) + if err != nil { + return nil, err + } + + d.Insert("F", *d1IndRef) + + // create last bead + d2 := types.Dict( + map[string]types.Object{ + "Type": types.Name("Bead"), + "T": *dIndRef, + "N": *d1IndRef, + "V": *d1IndRef, + "P": pageIndRef, + "R": types.NewNumberArray(0, 100, 200, 100), + }, + ) + + d2IndRef, err := xRefTable.IndRefForNewObject(d2) + if err != nil { + return nil, err + } + + d1.Insert("N", *d2IndRef) + d1.Insert("V", *d2IndRef) + + return dIndRef, nil +} + +func addThreads(xRefTable *model.XRefTable, rootDict types.Dict, pageIndRef types.IndirectRef) error { + ir, err := createThreadDict(xRefTable, pageIndRef) + if err != nil { + return err + } + + ir, err = xRefTable.IndRefForNewObject(types.Array{*ir}) + if err != nil { + return err + } + + rootDict.Insert("Threads", *ir) + + return nil +} + +func addOpenAction(xRefTable *model.XRefTable, rootDict types.Dict) error { + nextActionDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("Action"), + "S": types.Name("Movie"), + "T": types.StringLiteral("Sample Movie"), + }, + ) + + script := `app.alert('Hello Gopher!');` + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Action"), + "S": types.Name("JavaScript"), + "JS": types.StringLiteral(script), + "Next": nextActionDict, + }, + ) + + rootDict.Insert("OpenAction", d) + + return nil +} + +func addURI(xRefTable *model.XRefTable, rootDict types.Dict) { + d := types.NewDict() + d.InsertString("Base", "http://www.adobe.com") + + rootDict.Insert("URI", d) +} + +func addSpiderInfo(xRefTable *model.XRefTable, rootDict types.Dict) error { + // webCaptureInfoDict + webCaptureInfoDict := types.NewDict() + webCaptureInfoDict.InsertInt("V", 1.0) + + a := types.Array{} + captureCmdDict := types.NewDict() + captureCmdDict.InsertString("URL", ("")) + + cmdSettingsDict := types.NewDict() + captureCmdDict.Insert("S", cmdSettingsDict) + + ir, err := xRefTable.IndRefForNewObject(captureCmdDict) + if err != nil { + return err + } + + a = append(a, *ir) + + webCaptureInfoDict.Insert("C", a) + + ir, err = xRefTable.IndRefForNewObject(webCaptureInfoDict) + if err != nil { + return err + } + + rootDict.Insert("SpiderInfo", *ir) + + return nil +} + +func addOCProperties(xRefTable *model.XRefTable, rootDict types.Dict) error { + usageAppDict := types.Dict( + map[string]types.Object{ + "Event": types.Name("View"), + "OCGs": types.Array{}, // of indRefs + "Category": types.NewNameArray("Language"), + }, + ) + + optionalContentConfigDict := types.Dict( + map[string]types.Object{ + "Name": types.StringLiteral("OCConf"), + "Creator": types.StringLiteral("Horst Rutter"), + "BaseState": types.Name("ON"), + "OFF": types.Array{}, + "Intent": types.Name("Design"), + "AS": types.Array{usageAppDict}, + "Order": types.Array{}, + "ListMode": types.Name("AllPages"), + "RBGroups": types.Array{}, + "Locked": types.Array{}, + }, + ) + + d := types.Dict( + map[string]types.Object{ + "OCGs": types.Array{}, // of indRefs + "D": optionalContentConfigDict, + "Configs": types.Array{optionalContentConfigDict}, + }, + ) + + rootDict.Insert("OCProperties", d) + + return nil +} + +func addRequirements(xRefTable *model.XRefTable, rootDict types.Dict) { + d := types.NewDict() + d.InsertName("Type", "Requirement") + d.InsertName("S", "EnableJavaScripts") + + rootDict.Insert("Requirements", types.Array{d}) +} + +// CreateAnnotationDemoXRef creates a PDF file with examples of annotations and actions. +func CreateAnnotationDemoXRef() (*model.XRefTable, error) { + fontName := "Helvetica" + + xRefTable, err := CreateXRefTableWithRootDict() + if err != nil { + return nil, err + } + + rootDict, err := xRefTable.Catalog() + if err != nil { + return nil, err + } + + pageIndRef, err := addPageTreeWithAnnotations(xRefTable, rootDict, fontName) + if err != nil { + return nil, err + } + + err = addThreads(xRefTable, rootDict, *pageIndRef) + if err != nil { + return nil, err + } + + err = addOpenAction(xRefTable, rootDict) + if err != nil { + return nil, err + } + + addURI(xRefTable, rootDict) + + err = addSpiderInfo(xRefTable, rootDict) + if err != nil { + return nil, err + } + + err = addOCProperties(xRefTable, rootDict) + if err != nil { + return nil, err + } + + addRequirements(xRefTable, rootDict) + + return xRefTable, nil +} + +func setBit(i uint32, pos uint) uint32 { + // pos 1 == bit 0 + + var mask uint32 = 1 + + mask <<= pos - 1 + + i |= mask + + return i +} + +func createNormalAppearanceForFormField(xRefTable *model.XRefTable, w, h float64) (*types.IndirectRef, error) { + // stroke outline path + var b bytes.Buffer + fmt.Fprintf(&b, "0 0 m 0 %f l %f %f l %f 0 l s", h, w, h, w) + + sd := &types.StreamDict{ + Dict: types.Dict( + map[string]types.Object{ + "Type": types.Name("XObject"), + "Subtype": types.Name("Form"), + "FormType": types.Integer(1), + "BBox": types.NewNumberArray(0, 0, w, h), + "Matrix": types.NewIntegerArray(1, 0, 0, 1, 0, 0), + }, + ), + Content: b.Bytes(), + } + + if err := sd.Encode(); err != nil { + return nil, err + } + + return xRefTable.IndRefForNewObject(*sd) +} + +func createRolloverAppearanceForFormField(xRefTable *model.XRefTable, w, h float64) (*types.IndirectRef, error) { + // stroke outline path + var b bytes.Buffer + fmt.Fprintf(&b, "1 0 0 RG 0 0 m 0 %f l %f %f l %f 0 l s", h, w, h, w) + + sd := &types.StreamDict{ + Dict: types.Dict( + map[string]types.Object{ + "Type": types.Name("XObject"), + "Subtype": types.Name("Form"), + "FormType": types.Integer(1), + "BBox": types.NewNumberArray(0, 0, w, h), + "Matrix": types.NewIntegerArray(1, 0, 0, 1, 0, 0), + }, + ), + Content: b.Bytes(), + } + + if err := sd.Encode(); err != nil { + return nil, err + } + + return xRefTable.IndRefForNewObject(*sd) +} + +func createDownAppearanceForFormField(xRefTable *model.XRefTable, w, h float64) (*types.IndirectRef, error) { + // stroke outline path + var b bytes.Buffer + fmt.Fprintf(&b, "0 0 m 0 %f l %f %f l %f 0 l s", h, w, h, w) + + sd := &types.StreamDict{ + Dict: types.Dict( + map[string]types.Object{ + "Type": types.Name("XObject"), + "Subtype": types.Name("Form"), + "FormType": types.Integer(1), + "BBox": types.NewNumberArray(0, 0, w, h), + "Matrix": types.NewIntegerArray(1, 0, 0, 1, 0, 0), + }, + ), + Content: b.Bytes(), + } + + if err := sd.Encode(); err != nil { + return nil, err + } + + return xRefTable.IndRefForNewObject(*sd) +} + +func createFormTextField(xRefTable *model.XRefTable, pageAnnots *types.Array, fontName string) (*types.IndirectRef, error) { + // lower left corner + x := 100.0 + y := 300.0 + + // width + w := 130.0 + + // height + h := 20.0 + + fN, err := createNormalAppearanceForFormField(xRefTable, w, h) + if err != nil { + return nil, err + } + + fR, err := createRolloverAppearanceForFormField(xRefTable, w, h) + if err != nil { + return nil, err + } + + fD, err := createDownAppearanceForFormField(xRefTable, w, h) + if err != nil { + return nil, err + } + + fontDict, err := pdffont.EnsureFontDict(xRefTable, fontName, "", "", true, nil) + if err != nil { + return nil, err + } + + resourceDict := types.Dict( + map[string]types.Object{ + "Font": types.Dict( + map[string]types.Object{ + fontName: *fontDict, + }, + ), + }, + ) + + d := types.Dict( + map[string]types.Object{ + "AP": types.Dict( + map[string]types.Object{ + "N": *fN, + "R": *fR, + "D": *fD, + }, + ), + "DA": types.StringLiteral("/" + fontName + " 12 Tf 0 g"), + "DR": resourceDict, + "FT": types.Name("Tx"), + "Rect": types.NewNumberArray(x, y, x+w, y+h), + "Border": types.NewIntegerArray(0, 0, 1), + "Type": types.Name("Annot"), + "Subtype": types.Name("Widget"), + "T": types.StringLiteral("inputField"), + "TU": types.StringLiteral("inputField"), + "DV": types.StringLiteral("Default value"), + "V": types.StringLiteral("Default value"), + }, + ) + + ir, err := xRefTable.IndRefForNewObject(d) + if err != nil { + return nil, err + } + + *pageAnnots = append(*pageAnnots, *ir) + + return ir, nil +} + +func createYesAppearance(xRefTable *model.XRefTable, resourceDict types.Dict, w, h float64) (*types.IndirectRef, error) { + var b bytes.Buffer + fmt.Fprintf(&b, "q 0 0 1 rg BT /ZaDb 12 Tf 0 0 Td (8) Tj ET Q") + + sd := &types.StreamDict{ + Dict: types.Dict( + map[string]types.Object{ + "Resources": resourceDict, + "Subtype": types.Name("Form"), + "BBox": types.NewNumberArray(0, 0, w, h), + "OPI": types.Dict( + map[string]types.Object{ + "2.0": types.Dict( + map[string]types.Object{ + "Type": types.Name("OPI"), + "Version": types.Float(2.0), + "F": types.StringLiteral("Proxy"), + "Inks": types.Name("full_color"), + }, + ), + }, + ), + "Ref": types.Dict( + map[string]types.Object{ + "F": types.StringLiteral("Proxy"), + "Page": types.Integer(1), + }, + ), + }, + ), + Content: b.Bytes(), + } + + if err := sd.Encode(); err != nil { + return nil, err + } + + return xRefTable.IndRefForNewObject(*sd) +} + +func createOffAppearance(xRefTable *model.XRefTable, resourceDict types.Dict, w, h float64) (*types.IndirectRef, error) { + var b bytes.Buffer + fmt.Fprintf(&b, "q 0 0 1 rg BT /ZaDb 12 Tf 0 0 Td (4) Tj ET Q") + + sd := &types.StreamDict{ + Dict: types.Dict( + map[string]types.Object{ + "Resources": resourceDict, + "Subtype": types.Name("Form"), + "BBox": types.NewNumberArray(0, 0, w, h), + "OPI": types.Dict( + map[string]types.Object{ + "1.3": types.Dict( + map[string]types.Object{ + "Type": types.Name("OPI"), + "Version": types.Float(1.3), + "F": types.StringLiteral("Proxy"), + "Size": types.NewIntegerArray(400, 400), + "CropRect": types.NewIntegerArray(0, 400, 400, 0), + "Position": types.NewNumberArray(0, 0, 0, 400, 400, 400, 400, 0), + }, + ), + }, + ), + }, + ), + Content: b.Bytes(), + } + + if err := sd.Encode(); err != nil { + return nil, err + } + + return xRefTable.IndRefForNewObject(*sd) +} + +func createCheckBoxButtonField(xRefTable *model.XRefTable, pageAnnots *types.Array) (*types.IndirectRef, error) { + fontDict, err := pdffont.EnsureFontDict(xRefTable, "ZapfDingbats", "", "", false, nil) + if err != nil { + return nil, err + } + + resDict := types.Dict( + map[string]types.Object{ + "Font": types.Dict( + map[string]types.Object{ + "ZaDb": *fontDict, + }, + ), + }, + ) + + yesForm, err := createYesAppearance(xRefTable, resDict, 20.0, 20.0) + if err != nil { + return nil, err + } + + offForm, err := createOffAppearance(xRefTable, resDict, 20.0, 20.0) + if err != nil { + return nil, err + } + + apDict := types.Dict( + map[string]types.Object{ + "N": types.Dict( + map[string]types.Object{ + "Yes": *yesForm, + "Off": *offForm, + }, + ), + }, + ) + + d := types.Dict( + map[string]types.Object{ + "FT": types.Name("Btn"), + "Rect": types.NewNumberArray(250, 300, 270, 320), + "Type": types.Name("Annot"), + "Subtype": types.Name("Widget"), + "T": types.StringLiteral("CheckBox"), + "TU": types.StringLiteral("CheckBox"), + "V": types.Name("Yes"), + "AS": types.Name("Yes"), + "AP": apDict, + }, + ) + + ir, err := xRefTable.IndRefForNewObject(d) + if err != nil { + return nil, err + } + + *pageAnnots = append(*pageAnnots, *ir) + + return ir, nil +} + +func createRadioButtonField(xRefTable *model.XRefTable, pageAnnots *types.Array) (*types.IndirectRef, error) { + var flags uint32 + flags = setBit(flags, 16) + + d := types.Dict( + map[string]types.Object{ + "FT": types.Name("Btn"), + "Ff": types.Integer(flags), + "Rect": types.NewNumberArray(250, 400, 280, 420), + //"Type": Name("Annot"), + //"Subtype": Name("Widget"), + "T": types.StringLiteral("Credit card"), + "V": types.Name("card1"), + }, + ) + + indRef, err := xRefTable.IndRefForNewObject(d) + if err != nil { + return nil, err + } + + fontDict, err := pdffont.EnsureFontDict(xRefTable, "ZapfDingbats", "", "", false, nil) + if err != nil { + return nil, err + } + + resDict := types.Dict( + map[string]types.Object{ + "Font": types.Dict( + map[string]types.Object{ + "ZaDb": *fontDict, + }, + ), + }, + ) + + selectedForm, err := createYesAppearance(xRefTable, resDict, 20.0, 20.0) + if err != nil { + return nil, err + } + + offForm, err := createOffAppearance(xRefTable, resDict, 20.0, 20.0) + if err != nil { + return nil, err + } + + r1 := types.Dict( + map[string]types.Object{ + "Rect": types.NewNumberArray(250, 400, 280, 420), + "Type": types.Name("Annot"), + "Subtype": types.Name("Widget"), + "Parent": *indRef, + "T": types.StringLiteral("Radio1"), + "TU": types.StringLiteral("Radio1"), + "AS": types.Name("card1"), + "AP": types.Dict( + map[string]types.Object{ + "N": types.Dict( + map[string]types.Object{ + "card1": *selectedForm, + "Off": *offForm, + }, + ), + }, + ), + }, + ) + + indRefR1, err := xRefTable.IndRefForNewObject(r1) + if err != nil { + return nil, err + } + + r2 := types.Dict( + map[string]types.Object{ + "Rect": types.NewNumberArray(300, 400, 330, 420), + "Type": types.Name("Annot"), + "Subtype": types.Name("Widget"), + "Parent": *indRef, + "T": types.StringLiteral("Radio2"), + "TU": types.StringLiteral("Radio2"), + "AS": types.Name("Off"), + "AP": types.Dict( + map[string]types.Object{ + "N": types.Dict( + map[string]types.Object{ + "card2": *selectedForm, + "Off": *offForm, + }, + ), + }, + ), + }, + ) + + indRefR2, err := xRefTable.IndRefForNewObject(r2) + if err != nil { + return nil, err + } + + d.Insert("Kids", types.Array{*indRefR1, *indRefR2}) + + *pageAnnots = append(*pageAnnots, *indRefR1) + *pageAnnots = append(*pageAnnots, *indRefR2) + + return indRef, nil +} + +func createResetButton(xRefTable *model.XRefTable, pageAnnots *types.Array) (*types.IndirectRef, error) { + var flags uint32 + flags = setBit(flags, 17) + + fN, err := createNormalAppearanceForFormField(xRefTable, 20, 20) + if err != nil { + return nil, err + } + + resetFormActionDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("Action"), + "S": types.Name("ResetForm"), + "Fields": types.NewStringLiteralArray("inputField"), + "Flags": types.Integer(0), + }, + ) + + d := types.Dict( + map[string]types.Object{ + "FT": types.Name("Btn"), + "Ff": types.Integer(flags), + "Rect": types.NewNumberArray(100, 400, 120, 420), + "Type": types.Name("Annot"), + "Subtype": types.Name("Widget"), + "AP": types.Dict(map[string]types.Object{"N": *fN}), + "T": types.StringLiteral("Reset"), + "TU": types.StringLiteral("Reset"), + "A": resetFormActionDict, + }, + ) + + ir, err := xRefTable.IndRefForNewObject(d) + if err != nil { + return nil, err + } + + *pageAnnots = append(*pageAnnots, *ir) + + return ir, nil +} + +func createSubmitButton(xRefTable *model.XRefTable, pageAnnots *types.Array) (*types.IndirectRef, error) { + var flags uint32 + flags = setBit(flags, 17) + + fN, err := createNormalAppearanceForFormField(xRefTable, 20, 20) + if err != nil { + return nil, err + } + + urlSpec := types.Dict( + map[string]types.Object{ + "FS": types.Name("URL"), + "F": types.StringLiteral("http://www.me.com"), + }, + ) + + submitFormActionDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("Action"), + "S": types.Name("SubmitForm"), + "F": urlSpec, + "Fields": types.NewStringLiteralArray("inputField"), + "Flags": types.Integer(0), + }, + ) + + d := types.Dict( + map[string]types.Object{ + "FT": types.Name("Btn"), + "Ff": types.Integer(flags), + "Rect": types.NewNumberArray(140, 400, 160, 420), + "Type": types.Name("Annot"), + "Subtype": types.Name("Widget"), + "AP": types.Dict(map[string]types.Object{"N": *fN}), + "T": types.StringLiteral("Submit"), + "TU": types.StringLiteral("Submit"), + "A": submitFormActionDict, + }, + ) + + ir, err := xRefTable.IndRefForNewObject(d) + if err != nil { + return nil, err + } + + *pageAnnots = append(*pageAnnots, *ir) + + return ir, nil +} + +func streamObjForXFAElement(xRefTable *model.XRefTable, s string) (*types.IndirectRef, error) { + sd := &types.StreamDict{ + Dict: types.Dict(map[string]types.Object{}), + Content: []byte(s), + } + + if err := sd.Encode(); err != nil { + return nil, err + } + + return xRefTable.IndRefForNewObject(*sd) +} + +func createXFAArray(xRefTable *model.XRefTable) (types.Array, error) { + sd1, err := streamObjForXFAElement(xRefTable, "") + if err != nil { + return nil, err + } + + sd3, err := streamObjForXFAElement(xRefTable, "") + if err != nil { + return nil, err + } + + return types.Array{ + types.StringLiteral("xdp:xdp"), *sd1, + types.StringLiteral("/xdp:xdp"), *sd3, + }, nil +} + +func createFormDict(xRefTable *model.XRefTable, fontName string) (types.Dict, types.Array, error) { + pageAnnots := types.Array{} + + text, err := createFormTextField(xRefTable, &pageAnnots, fontName) + if err != nil { + return nil, nil, err + } + + checkBox, err := createCheckBoxButtonField(xRefTable, &pageAnnots) + if err != nil { + return nil, nil, err + } + + radioButton, err := createRadioButtonField(xRefTable, &pageAnnots) + if err != nil { + return nil, nil, err + } + + resetButton, err := createResetButton(xRefTable, &pageAnnots) + if err != nil { + return nil, nil, err + } + + submitButton, err := createSubmitButton(xRefTable, &pageAnnots) + if err != nil { + return nil, nil, err + } + + xfaArr, err := createXFAArray(xRefTable) + if err != nil { + return nil, nil, err + } + + d := types.Dict( + map[string]types.Object{ + "Fields": types.Array{*text, *checkBox, *radioButton, *resetButton, *submitButton}, // indRefs of fieldDicts + "NeedAppearances": types.Boolean(true), + "CO": types.Array{*text}, + "XFA": xfaArr, + }, + ) + + return d, pageAnnots, nil +} + +// CreateFormDemoXRef creates an xRefTable with an AcroForm example. +func CreateFormDemoXRef() (*model.XRefTable, error) { + fontName := "Helvetica" + + xRefTable, err := CreateXRefTableWithRootDict() + if err != nil { + return nil, err + } + + rootDict, err := xRefTable.Catalog() + if err != nil { + return nil, err + } + + formDict, annotsArray, err := createFormDict(xRefTable, fontName) + if err != nil { + return nil, err + } + + rootDict.Insert("AcroForm", formDict) + + _, err = addPageTreeWithFormFields(xRefTable, rootDict, annotsArray, fontName) + if err != nil { + return nil, err + } + + rootDict.Insert("ViewerPreferences", + types.Dict( + map[string]types.Object{ + "FitWindow": types.Boolean(true), + "CenterWindow": types.Boolean(true), + }, + ), + ) + + return xRefTable, nil +} + +// CreateContext creates a Context for given cross reference table and configuration. +func CreateContext(xRefTable *model.XRefTable, conf *model.Configuration) *model.Context { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + xRefTable.Conf = conf + xRefTable.ValidationMode = conf.ValidationMode + return &model.Context{ + Configuration: conf, + XRefTable: xRefTable, + Write: model.NewWriteContext(conf.Eol), + } +} + +// CreateContextWithXRefTable creates a Context with an xRefTable without pages for given configuration. +func CreateContextWithXRefTable(conf *model.Configuration, pageDim *types.Dim) (*model.Context, error) { + xRefTable, err := CreateXRefTableWithRootDict() + if err != nil { + return nil, err + } + + rootDict, err := xRefTable.Catalog() + if err != nil { + return nil, err + } + + if err = addPageTreeWithoutPage(xRefTable, rootDict, pageDim); err != nil { + return nil, err + } + + return CreateContext(xRefTable, conf), nil +} + +func createDemoContentStreamDict(xRefTable *model.XRefTable, pageDict types.Dict, b []byte) (*types.IndirectRef, error) { + sd, _ := xRefTable.NewStreamDictForBuf(b) + if err := sd.Encode(); err != nil { + return nil, err + } + return xRefTable.IndRefForNewObject(*sd) +} + +func createDemoPage(xRefTable *model.XRefTable, parentPageIndRef types.IndirectRef, p model.Page) (*types.IndirectRef, error) { + + pageDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("Page"), + "Parent": parentPageIndRef, + }, + ) + + fontRes, err := pdffont.FontResources(xRefTable, p.Fm) + if err != nil { + return nil, err + } + + if len(fontRes) > 0 { + resDict := types.Dict( + map[string]types.Object{ + "Font": fontRes, + }, + ) + pageDict.Insert("Resources", resDict) + } + + ir, err := createDemoContentStreamDict(xRefTable, pageDict, p.Buf.Bytes()) + if err != nil { + return nil, err + } + pageDict.Insert("Contents", *ir) + + return xRefTable.IndRefForNewObject(pageDict) +} diff --git a/pkg/pdfcpu/crypto.go b/pkg/pdfcpu/crypto.go new file mode 100644 index 0000000000000000000000000000000000000000..518d84083edad881714c5d3a01b26984680cf501 --- /dev/null +++ b/pkg/pdfcpu/crypto.go @@ -0,0 +1,1820 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 pdfcpu + +// Functions dealing with PDF encryption. + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/md5" + "crypto/rand" + "crypto/rc4" + "crypto/sha256" + "crypto/sha512" + "encoding/binary" + "encoding/hex" + "fmt" + "io" + "math/big" + "strconv" + "time" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" + + "golang.org/x/text/secure/precis" + "golang.org/x/text/unicode/norm" +) + +var ( + pad = []byte{ + 0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08, + 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A, + } + + nullPad32 = make([]byte, 32) + + // Needed permission bits for pdfcpu commands. + perm = map[model.CommandMode]struct{ extract, modify int }{ + model.VALIDATE: {0, 0}, + model.LISTINFO: {0, 0}, + model.OPTIMIZE: {0, 0}, + model.SPLIT: {1, 0}, + model.SPLITBYPAGENR: {1, 0}, + model.MERGECREATE: {0, 0}, + model.MERGECREATEZIP: {0, 0}, + model.MERGEAPPEND: {0, 0}, + model.EXTRACTIMAGES: {1, 0}, + model.EXTRACTFONTS: {1, 0}, + model.EXTRACTPAGES: {1, 0}, + model.EXTRACTCONTENT: {1, 0}, + model.EXTRACTMETADATA: {1, 0}, + model.TRIM: {0, 1}, + model.LISTATTACHMENTS: {0, 0}, + model.EXTRACTATTACHMENTS: {1, 0}, + model.ADDATTACHMENTS: {0, 1}, + model.ADDATTACHMENTSPORTFOLIO: {0, 1}, + model.REMOVEATTACHMENTS: {0, 1}, + model.LISTPERMISSIONS: {0, 0}, + model.SETPERMISSIONS: {0, 0}, + model.ADDWATERMARKS: {0, 1}, + model.REMOVEWATERMARKS: {0, 1}, + model.IMPORTIMAGES: {0, 1}, + model.INSERTPAGESBEFORE: {0, 1}, + model.INSERTPAGESAFTER: {0, 1}, + model.REMOVEPAGES: {0, 1}, + model.LISTKEYWORDS: {0, 0}, + model.ADDKEYWORDS: {0, 1}, + model.REMOVEKEYWORDS: {0, 1}, + model.LISTPROPERTIES: {0, 0}, + model.ADDPROPERTIES: {0, 1}, + model.REMOVEPROPERTIES: {0, 1}, + model.COLLECT: {1, 0}, + model.CROP: {0, 1}, + model.LISTBOXES: {0, 0}, + model.ADDBOXES: {0, 1}, + model.REMOVEBOXES: {0, 1}, + model.LISTANNOTATIONS: {0, 1}, + model.ADDANNOTATIONS: {0, 1}, + model.REMOVEANNOTATIONS: {0, 1}, + model.ROTATE: {0, 1}, + model.NUP: {0, 1}, + model.BOOKLET: {0, 1}, + model.LISTBOOKMARKS: {0, 0}, + model.ADDBOOKMARKS: {0, 1}, + model.REMOVEBOOKMARKS: {0, 1}, + model.IMPORTBOOKMARKS: {0, 1}, + model.EXPORTBOOKMARKS: {0, 1}, + model.LISTIMAGES: {0, 1}, + model.CREATE: {0, 0}, + model.DUMP: {0, 1}, + model.LISTFORMFIELDS: {0, 0}, + model.REMOVEFORMFIELDS: {0, 1}, + model.LOCKFORMFIELDS: {0, 1}, + model.UNLOCKFORMFIELDS: {0, 1}, + model.RESETFORMFIELDS: {0, 1}, + model.EXPORTFORMFIELDS: {0, 1}, + model.FILLFORMFIELDS: {0, 1}, + model.LISTPAGELAYOUT: {0, 1}, + model.SETPAGELAYOUT: {0, 1}, + model.RESETPAGELAYOUT: {0, 1}, + model.LISTPAGEMODE: {0, 1}, + model.SETPAGEMODE: {0, 1}, + model.RESETPAGEMODE: {0, 1}, + model.LISTVIEWERPREFERENCES: {0, 1}, + model.SETVIEWERPREFERENCES: {0, 1}, + model.RESETVIEWERPREFERENCES: {0, 1}, + } + + ErrUnknownEncryption = errors.New("pdfcpu: unknown encryption") +) + +// NewEncryptDict creates a new EncryptDict using the standard security handler. +func newEncryptDict(v model.Version, needAES bool, keyLength int, permissions int16) types.Dict { + d := types.NewDict() + + d.Insert("Filter", types.Name("Standard")) + + if keyLength >= 128 { + d.Insert("Length", types.Integer(keyLength)) + i := 4 + if keyLength == 256 { + i = 5 + } + d.Insert("V", types.Integer(i)) + if v == model.V20 { + i++ + } + d.Insert("R", types.Integer(i)) + } else { + d.Insert("R", types.Integer(2)) + d.Insert("V", types.Integer(1)) + } + + // Set user access permission flags. + d.Insert("P", types.Integer(permissions)) + + d.Insert("StmF", types.Name("StdCF")) + d.Insert("StrF", types.Name("StdCF")) + + d1 := types.NewDict() + d1.Insert("AuthEvent", types.Name("DocOpen")) + + if needAES { + n := "AESV2" + if keyLength == 256 { + n = "AESV3" + } + d1.Insert("CFM", types.Name(n)) + } else { + d1.Insert("CFM", types.Name("V2")) + } + + d1.Insert("Length", types.Integer(keyLength/8)) + + d2 := types.NewDict() + d2.Insert("StdCF", d1) + + d.Insert("CF", d2) + + if keyLength == 256 { + d.Insert("U", types.NewHexLiteral(make([]byte, 48))) + d.Insert("O", types.NewHexLiteral(make([]byte, 48))) + d.Insert("UE", types.NewHexLiteral(make([]byte, 32))) + d.Insert("OE", types.NewHexLiteral(make([]byte, 32))) + d.Insert("Perms", types.NewHexLiteral(make([]byte, 16))) + } else { + d.Insert("U", types.NewHexLiteral(make([]byte, 32))) + d.Insert("O", types.NewHexLiteral(make([]byte, 32))) + } + + return d +} + +func encKey(userpw string, e *model.Enc) (key []byte) { + // 2a + pw := []byte(userpw) + if len(pw) >= 32 { + pw = pw[:32] + } else { + pw = append(pw, pad[:32-len(pw)]...) + } + + // 2b + h := md5.New() + h.Write(pw) + + // 2c + h.Write(e.O) + + // 2d + var q = uint32(e.P) + h.Write([]byte{byte(q), byte(q >> 8), byte(q >> 16), byte(q >> 24)}) + + // 2e + h.Write(e.ID) + + // 2f + if e.R == 4 && !e.Emd { + h.Write([]byte{0xff, 0xff, 0xff, 0xff}) + } + + // 2g + key = h.Sum(nil) + + // 2h + if e.R >= 3 { + for i := 0; i < 50; i++ { + h.Reset() + h.Write(key[:e.L/8]) + key = h.Sum(nil) + } + } + + // 2i + if e.R >= 3 { + key = key[:e.L/8] + } else { + key = key[:5] + } + + return key +} + +// validateUserPassword validates the user password aka document open password. +func validateUserPassword(ctx *model.Context) (ok bool, err error) { + if ctx.E.R == 5 { + return validateUserPasswordAES256(ctx) + } + + if ctx.E.R == 6 { + return validateUserPasswordAES256Rev6(ctx) + } + + // Alg.4/5 p63 + // 4a/5a create encryption key using Alg.2 p61 + + u, key, err := u(ctx) + if err != nil { + return false, err + } + + ctx.EncKey = key + + switch ctx.E.R { + + case 2: + ok = bytes.Equal(ctx.E.U, u) + + case 3, 4: + ok = bytes.HasPrefix(ctx.E.U, u[:16]) + } + + return ok, nil +} + +func key(ownerpw, userpw string, r, l int) (key []byte) { + // 3a + pw := []byte(ownerpw) + if len(pw) == 0 { + pw = []byte(userpw) + } + if len(pw) >= 32 { + pw = pw[:32] + } else { + pw = append(pw, pad[:32-len(pw)]...) + } + + // 3b + h := md5.New() + h.Write(pw) + key = h.Sum(nil) + + // 3c + if r >= 3 { + for i := 0; i < 50; i++ { + h.Reset() + h.Write(key) + key = h.Sum(nil) + } + } + + // 3d + if r >= 3 { + key = key[:l/8] + } else { + key = key[:5] + } + + return key +} + +// O calculates the owner password digest. +func o(ctx *model.Context) ([]byte, error) { + ownerpw := ctx.OwnerPW + userpw := ctx.UserPW + + e := ctx.E + + // 3a-d + key := key(ownerpw, userpw, e.R, e.L) + + // 3e + o := []byte(userpw) + if len(o) >= 32 { + o = o[:32] + } else { + o = append(o, pad[:32-len(o)]...) + } + + // 3f + c, err := rc4.NewCipher(key) + if err != nil { + return nil, err + } + c.XORKeyStream(o, o) + + // 3g + if e.R >= 3 { + for i := 1; i <= 19; i++ { + keynew := make([]byte, len(key)) + copy(keynew, key) + + for j := range keynew { + keynew[j] ^= byte(i) + } + + c, err := rc4.NewCipher(keynew) + if err != nil { + return nil, err + } + c.XORKeyStream(o, o) + } + } + + return o, nil +} + +// U calculates the user password digest. +func u(ctx *model.Context) (u []byte, key []byte, err error) { + // The PW string is generated from OS codepage characters by first converting the string to PDFDocEncoding. + // If input is Unicode, first convert to a codepage encoding , and then to PDFDocEncoding for backward compatibility. + userpw := ctx.UserPW + //fmt.Printf("U userpw=ctx.UserPW=%s\n", userpw) + + e := ctx.E + + key = encKey(userpw, e) + + c, err := rc4.NewCipher(key) + if err != nil { + return nil, nil, err + } + + switch e.R { + + case 2: + // 4b + u = make([]byte, 32) + copy(u, pad) + c.XORKeyStream(u, u) + + case 3, 4: + // 5b + h := md5.New() + h.Reset() + h.Write(pad) + + // 5c + h.Write(e.ID) + u = h.Sum(nil) + + // 5ds + c.XORKeyStream(u, u) + + // 5e + for i := 1; i <= 19; i++ { + keynew := make([]byte, len(key)) + copy(keynew, key) + + for j := range keynew { + keynew[j] ^= byte(i) + } + + c, err = rc4.NewCipher(keynew) + if err != nil { + return nil, nil, err + } + c.XORKeyStream(u, u) + } + } + + if len(u) < 32 { + u = append(u, nullPad32[:32-len(u)]...) + } + + return u, key, nil +} + +func validationSalt(bb []byte) []byte { + return bb[32:40] +} + +func keySalt(bb []byte) []byte { + return bb[40:] +} + +func decryptOE(ctx *model.Context, opw []byte) error { + b := append(opw, keySalt(ctx.E.O)...) + b = append(b, ctx.E.U...) + key := sha256.Sum256(b) + + cb, err := aes.NewCipher(key[:]) + if err != nil { + return err + } + + iv := make([]byte, 16) + ctx.EncKey = make([]byte, 32) + + mode := cipher.NewCBCDecrypter(cb, iv) + mode.CryptBlocks(ctx.EncKey, ctx.E.OE) + + return nil +} + +func validateOwnerPasswordAES256(ctx *model.Context) (ok bool, err error) { + if len(ctx.OwnerPW) == 0 { + return false, nil + } + + opw, err := processInput(ctx.OwnerPW) + if err != nil { + return false, err + } + + if len(opw) > 127 { + opw = opw[:127] + } + + // Algorithm 3.2a 3. + b := append(opw, validationSalt(ctx.E.O)...) + b = append(b, ctx.E.U...) + s := sha256.Sum256(b) + + if !bytes.HasPrefix(ctx.E.O, s[:]) { + return false, nil + } + + if err := decryptOE(ctx, opw); err != nil { + return false, err + } + + return true, nil +} + +func decryptUE(ctx *model.Context, upw []byte) error { + key := sha256.Sum256(append(upw, keySalt(ctx.E.U)...)) + + cb, err := aes.NewCipher(key[:]) + if err != nil { + return err + } + + iv := make([]byte, 16) + ctx.EncKey = make([]byte, 32) + + mode := cipher.NewCBCDecrypter(cb, iv) + mode.CryptBlocks(ctx.EncKey, ctx.E.UE) + + return nil +} + +func validateUserPasswordAES256(ctx *model.Context) (ok bool, err error) { + upw, err := processInput(ctx.UserPW) + if err != nil { + return false, err + } + + if len(upw) > 127 { + upw = upw[:127] + } + + // Algorithm 3.2a 4, + s := sha256.Sum256(append(upw, validationSalt(ctx.E.U)...)) + + if !bytes.HasPrefix(ctx.E.U, s[:]) { + return false, nil + } + + if err := decryptUE(ctx, upw); err != nil { + return false, err + } + + return true, nil +} + +func processInput(input string) ([]byte, error) { + // Create a new Precis profile for SASLprep + p := precis.NewIdentifier( + precis.BidiRule, + precis.Norm(norm.NFKC), + ) + + output, err := p.String(input) + if err != nil { + return nil, err + } + + return []byte(output), nil +} + +func hashRev6(input, pw, U []byte) ([]byte, int, error) { + // 7.6.4.3.4 Algorithm 2.B returns 32 bytes. + + mod3 := new(big.Int).SetUint64(3) + + k0 := sha256.Sum256(input) + k := k0[:] + + var e []byte + j := 0 + + for ; j < 64 || e[len(e)-1] > byte(j-32); j++ { + var k1 []byte + bb := append(pw, k...) + if len(U) > 0 { + bb = append(bb, U...) + } + for i := 0; i < 64; i++ { + k1 = append(k1, bb...) + } + + cb, err := aes.NewCipher(k[:16]) + if err != nil { + return nil, -1, err + } + + iv := k[16:32] + e = make([]byte, len(k1)) + mode := cipher.NewCBCEncrypter(cb, iv) + mode.CryptBlocks(e, k1) + + num := new(big.Int).SetBytes(e[:16]) + r := (new(big.Int).Mod(num, mod3)).Uint64() + + switch r { + case 0: + k0 := sha256.Sum256(e) + k = k0[:] + case 1: + k0 := sha512.Sum384(e) + k = k0[:] + case 2: + k0 := sha512.Sum512(e) + k = k0[:] + } + + } + + return k[:32], j, nil +} + +func validateOwnerPasswordAES256Rev6(ctx *model.Context) (ok bool, err error) { + if len(ctx.OwnerPW) == 0 { + return false, nil + } + + // Process PW with SASLPrep profile (RFC 4013) of stringprep (RFC 3454). + opw, err := processInput(ctx.OwnerPW) + if err != nil { + return false, err + } + + if len(opw) > 127 { + opw = opw[:127] + } + + // Algorithm 12 + bb := append(opw, validationSalt(ctx.E.O)...) + bb = append(bb, ctx.E.U...) + s, _, err := hashRev6(bb, opw, ctx.E.U) + if err != nil { + return false, err + } + + if !bytes.HasPrefix(ctx.E.O, s[:]) { + return false, nil + } + + bb = append(opw, keySalt(ctx.E.O)...) + bb = append(bb, ctx.E.U...) + key, _, err := hashRev6(bb, opw, ctx.E.U) + if err != nil { + return false, err + } + + cb, err := aes.NewCipher(key[:]) + if err != nil { + return false, err + } + + iv := make([]byte, 16) + ctx.EncKey = make([]byte, 32) + + mode := cipher.NewCBCDecrypter(cb, iv) + mode.CryptBlocks(ctx.EncKey, ctx.E.OE) + + return true, nil +} + +func validateUserPasswordAES256Rev6(ctx *model.Context) (ok bool, err error) { + // Process PW with SASLPrep profile (RFC 4013) of stringprep (RFC 3454). + upw, err := processInput(ctx.UserPW) + if err != nil { + return false, err + } + + if len(upw) > 127 { + upw = upw[:127] + } + + // Algorithm 11 + bb := append(upw, validationSalt(ctx.E.U)...) + s, _, err := hashRev6(bb, upw, nil) + if err != nil { + return false, err + } + + if !bytes.HasPrefix(ctx.E.U, s[:]) { + return false, nil + } + + key, _, err := hashRev6(append(upw, keySalt(ctx.E.U)...), upw, nil) + if err != nil { + return false, err + } + + cb, err := aes.NewCipher(key[:]) + if err != nil { + return false, err + } + + iv := make([]byte, 16) + ctx.EncKey = make([]byte, 32) + + mode := cipher.NewCBCDecrypter(cb, iv) + mode.CryptBlocks(ctx.EncKey, ctx.E.UE) + + return true, nil +} + +// ValidateOwnerPassword validates the owner password aka change permissions password. +func validateOwnerPassword(ctx *model.Context) (ok bool, err error) { + e := ctx.E + + if e.R == 5 { + return validateOwnerPasswordAES256(ctx) + } + + if e.R == 6 { + return validateOwnerPasswordAES256Rev6(ctx) + } + + ownerpw := ctx.OwnerPW + userpw := ctx.UserPW + + // 7a: Alg.3 p62 a-d + key := key(ownerpw, userpw, e.R, e.L) + + // 7b + upw := make([]byte, len(e.O)) + copy(upw, e.O) + + var c *rc4.Cipher + + switch e.R { + + case 2: + c, err = rc4.NewCipher(key) + if err != nil { + return + } + c.XORKeyStream(upw, upw) + + case 3, 4: + for i := 19; i >= 0; i-- { + + keynew := make([]byte, len(key)) + copy(keynew, key) + + for j := range keynew { + keynew[j] ^= byte(i) + } + + c, err = rc4.NewCipher(keynew) + if err != nil { + return false, err + } + + c.XORKeyStream(upw, upw) + } + } + + // Save user pw + upws := ctx.UserPW + + ctx.UserPW = string(upw) + ok, err = validateUserPassword(ctx) + + // Restore user pw + ctx.UserPW = upws + + return ok, err +} + +// SupportedCFEntry returns true if all entries found are supported. +func supportedCFEntry(d types.Dict) (bool, error) { + cfm := d.NameEntry("CFM") + if cfm != nil && *cfm != "V2" && *cfm != "AESV2" && *cfm != "AESV3" { + return false, errors.New("pdfcpu: supportedCFEntry: invalid entry \"CFM\"") + } + + ae := d.NameEntry("AuthEvent") + if ae != nil && *ae != "DocOpen" { + return false, errors.New("pdfcpu: supportedCFEntry: invalid entry \"AuthEvent\"") + } + + l := d.IntEntry("Length") + if l != nil && (*l < 5 || *l > 16) && *l != 32 && *l != 256 { + return false, errors.New("pdfcpu: supportedCFEntry: invalid entry \"Length\"") + } + + return cfm != nil && (*cfm == "AESV2" || *cfm == "AESV3"), nil +} + +func perms(p int) (list []string) { + list = append(list, fmt.Sprintf("permission bits: %012b (x%03X)", uint32(p)&0x0F3C, uint32(p)&0x0F3C)) + list = append(list, fmt.Sprintf("Bit 3: %t (print(rev2), print quality(rev>=3))", p&0x0004 > 0)) + list = append(list, fmt.Sprintf("Bit 4: %t (modify other than controlled by bits 6,9,11)", p&0x0008 > 0)) + list = append(list, fmt.Sprintf("Bit 5: %t (extract(rev2), extract other than controlled by bit 10(rev>=3))", p&0x0010 > 0)) + list = append(list, fmt.Sprintf("Bit 6: %t (add or modify annotations)", p&0x0020 > 0)) + list = append(list, fmt.Sprintf("Bit 9: %t (fill in form fields(rev>=3)", p&0x0100 > 0)) + list = append(list, fmt.Sprintf("Bit 10: %t (extract(rev>=3))", p&0x0200 > 0)) + list = append(list, fmt.Sprintf("Bit 11: %t (modify(rev>=3))", p&0x0400 > 0)) + list = append(list, fmt.Sprintf("Bit 12: %t (print high-level(rev>=3))", p&0x0800 > 0)) + return list +} + +// PermissionsList returns a list of set permissions. +func PermissionsList(p int) (list []string) { + if p == 0 { + return append(list, "Full access") + } + + return perms(p) +} + +// Permissions returns a list of set permissions. +func Permissions(ctx *model.Context) (list []string) { + p := 0 + if ctx.E != nil { + p = ctx.E.P + } + + return PermissionsList(p) +} + +func validatePermissions(ctx *model.Context) (bool, error) { + // Algorithm 3.2a 5. + + if ctx.E.R != 5 && ctx.E.R != 6 { + return true, nil + } + + cb, err := aes.NewCipher(ctx.EncKey[:]) + if err != nil { + return false, err + } + + p := make([]byte, len(ctx.E.Perms)) + cb.Decrypt(p, ctx.E.Perms) + if string(p[9:12]) != "adb" { + return false, nil + } + + b := binary.LittleEndian.Uint32(p[:4]) + return int32(b) == int32(ctx.E.P), nil +} + +func writePermissions(ctx *model.Context, d types.Dict) error { + // Algorithm 3.10 + + if ctx.E.R != 5 && ctx.E.R != 6 { + return nil + } + + b := make([]byte, 16) + binary.LittleEndian.PutUint64(b, uint64(ctx.E.P)) + + b[4] = 0xFF + b[5] = 0xFF + b[6] = 0xFF + b[7] = 0xFF + + var c byte = 'F' + if ctx.E.Emd { + c = 'T' + } + b[8] = c + + b[9] = 'a' + b[10] = 'd' + b[11] = 'b' + + cb, err := aes.NewCipher(ctx.EncKey[:]) + if err != nil { + return err + } + + cb.Encrypt(ctx.E.Perms, b) + d.Update("Perms", types.HexLiteral(hex.EncodeToString(ctx.E.Perms))) + + return nil +} + +func logP(enc *model.Enc) { + if !log.InfoEnabled() { + return + } + for _, s := range perms(enc.P) { + log.Info.Println(s) + } + +} + +func maskExtract(mode model.CommandMode, secHandlerRev int) int { + p, ok := perm[mode] + + // no permissions defined or don't need extract permission + if !ok || p.extract == 0 { + return 0 + } + + // need extract permission + + if secHandlerRev >= 3 { + return 0x0200 // need bit 10 + } + + return 0x0010 // need bit 5 +} + +func maskModify(mode model.CommandMode, secHandlerRev int) int { + p, ok := perm[mode] + + // no permissions defined or don't need modify permission + if !ok || p.modify == 0 { + return 0 + } + + // need modify permission + + if secHandlerRev >= 3 { + return 0x0400 // need bit 11 + } + + return 0x0008 // need bit 4 +} + +// HasNeededPermissions returns true if permissions for pdfcpu processing are present. +func hasNeededPermissions(mode model.CommandMode, enc *model.Enc) bool { + // see 7.6.3.2 + + logP(enc) + + m := maskExtract(mode, enc.R) + if m > 0 { + if enc.P&m == 0 { + return false + } + } + + m = maskModify(mode, enc.R) + if m > 0 { + if enc.P&m == 0 { + return false + } + } + + return true +} + +func getV(ctx *model.Context, d types.Dict, l int) (*int, error) { + v := d.IntEntry("V") + + if v == nil || (*v != 1 && *v != 2 && *v != 4 && *v != 5) { + return nil, errors.Errorf("getV: \"V\" must be one of 1,2,4,5") + } + + if *v == 5 { + if l != 256 { + return nil, errors.Errorf("getV: \"V\" 5 invalid length, must be 256, got %d", l) + } + if ctx.Version() != model.V20 && ctx.XRefTable.ValidationMode == model.ValidationStrict { + return nil, errors.New("getV: 5 valid for PDF 2.0 only") + } + } + + return v, nil +} +func checkStmf(ctx *model.Context, stmf *string, cfDict types.Dict) error { + if stmf != nil && *stmf != "Identity" { + + d := cfDict.DictEntry(*stmf) + if d == nil { + return errors.Errorf("pdfcpu: checkStmf: entry \"%s\" missing in \"CF\"", *stmf) + } + + aes, err := supportedCFEntry(d) + if err != nil { + return errors.Wrapf(err, "pdfcpu: checkStmv: unsupported \"%s\" entry in \"CF\"", *stmf) + } + ctx.AES4Streams = aes + } + + return nil +} + +func checkV(ctx *model.Context, d types.Dict, l int) (*int, error) { + v, err := getV(ctx, d, l) + if err != nil { + return nil, err + } + + // v == 2 implies RC4 + if *v != 4 && *v != 5 { + return v, nil + } + + // CF + cfDict := d.DictEntry("CF") + if cfDict == nil { + return nil, errors.Errorf("pdfcpu: checkV: required entry \"CF\" missing.") + } + + // StmF + stmf := d.NameEntry("StmF") + err = checkStmf(ctx, stmf, cfDict) + if err != nil { + return nil, err + } + + // StrF + strf := d.NameEntry("StrF") + if strf != nil && *strf != "Identity" { + d1 := cfDict.DictEntry(*strf) + if d1 == nil { + return nil, errors.Errorf("pdfcpu: checkV: entry \"%s\" missing in \"CF\"", *strf) + } + aes, err := supportedCFEntry(d1) + if err != nil { + return nil, errors.Wrapf(err, "checkV: unsupported \"%s\" entry in \"CF\"", *strf) + } + ctx.AES4Strings = aes + } + + // EFF + eff := d.NameEntry("EFF") + if eff != nil && *eff != "Identity" { + d := cfDict.DictEntry(*eff) + if d == nil { + return nil, errors.Errorf("pdfcpu: checkV: entry \"%s\" missing in \"CF\"", *eff) + } + aes, err := supportedCFEntry(d) + if err != nil { + return nil, errors.Wrapf(err, "checkV: unsupported \"%s\" entry in \"CF\"", *eff) + } + ctx.AES4EmbeddedStreams = aes + } + + return v, nil +} + +func length(d types.Dict) (int, error) { + l := d.IntEntry("Length") + if l == nil { + return 40, nil + } + + if (*l < 40 || *l > 128 || *l%8 > 0) && *l != 256 { + return 0, errors.Errorf("pdfcpu: length: \"Length\" %d not supported\n", *l) + } + + return *l, nil +} + +func getR(ctx *model.Context, d types.Dict) (int, error) { + maxR := 5 + if ctx.Version() == model.V20 || ctx.XRefTable.ValidationMode == model.ValidationRelaxed { + maxR = 6 + } + + r := d.IntEntry("R") + if r == nil || *r < 2 || *r > maxR { + return 0, ErrUnknownEncryption + } + + return *r, nil +} + +func validateAlgorithm(ctx *model.Context) (ok bool) { + k := ctx.EncryptKeyLength + + if ctx.Version() == model.V20 { + return ctx.EncryptUsingAES && k == 256 + } + + if ctx.EncryptUsingAES { + return k == 40 || k == 128 || k == 256 + } + + return k == 40 || k == 128 +} + +func validateAES256Parameters(d types.Dict) (oe, ue, perms []byte, err error) { + for { + + // OE + oe, err = d.StringEntryBytes("OE") + if err != nil { + break + } + if oe == nil || len(oe) != 32 { + err = errors.New("pdfcpu: unsupported encryption: required entry \"OE\" missing or invalid") + break + } + + // UE + ue, err = d.StringEntryBytes("UE") + if err != nil { + break + } + if ue == nil || len(ue) != 32 { + err = errors.New("pdfcpu: unsupported encryption: required entry \"UE\" missing or invalid") + break + } + + // Perms + perms, err = d.StringEntryBytes("Perms") + if err != nil { + break + } + if perms == nil || len(perms) != 16 { + err = errors.New("pdfcpu: unsupported encryption: required entry \"Perms\" missing or invalid") + } + + break + } + + return oe, ue, perms, err +} + +func validateOAndU(ctx *model.Context, d types.Dict) (o, u []byte, err error) { + for { + + // O + o, err = d.StringEntryBytes("O") + if err != nil { + break + } + l := len(o) + if o == nil || l != 32 && l != 48 { + if ctx.XRefTable.ValidationMode == model.ValidationStrict { + err = errors.New("pdfcpu: unsupported encryption: missing or invalid required entry \"O\"") + break + } + if l < 48 { + err = errors.New("pdfcpu: unsupported encryption: missing or invalid required entry \"O\"") + break + } + o = o[:48] + } + + // U + u, err = d.StringEntryBytes("U") + if err != nil { + break + } + l = len(u) + if u == nil || l != 32 && l != 48 { + if ctx.XRefTable.ValidationMode == model.ValidationStrict { + err = errors.New("pdfcpu: unsupported encryption: missing or invalid required entry \"U\"") + break + } + if l < 48 { + err = errors.New("pdfcpu: unsupported encryption: missing or invalid required entry \"U\"") + break + } + u = u[:48] + } + + break + } + + return o, u, err +} + +// SupportedEncryption returns a pointer to a struct encapsulating used encryption. +func supportedEncryption(ctx *model.Context, d types.Dict) (*model.Enc, error) { + // Filter + filter := d.NameEntry("Filter") + if filter == nil || *filter != "Standard" { + return nil, errors.New("pdfcpu: unsupported encryption: filter must be \"Standard\"") + } + + // SubFilter + if d.NameEntry("SubFilter") != nil { + return nil, errors.New("pdfcpu: unsupported encryption: \"SubFilter\" not supported") + } + + // Length + l, err := length(d) + if err != nil { + return nil, err + } + + // V + v, err := checkV(ctx, d, l) + if err != nil { + return nil, err + } + + // R + r, err := getR(ctx, d) + if err != nil { + return nil, err + } + + o, u, err := validateOAndU(ctx, d) + if err != nil { + return nil, err + } + + var oe, ue, perms []byte + if r == 5 || r == 6 { + oe, ue, perms, err = validateAES256Parameters(d) + if err != nil { + return nil, err + } + } + + // P + p := d.IntEntry("P") + if p == nil { + return nil, errors.New("pdfcpu: unsupported encryption: required entry \"P\" missing") + } + + // EncryptMetadata + encMeta := true + emd := d.BooleanEntry("EncryptMetadata") + if emd != nil { + encMeta = *emd + } + + return &model.Enc{ + O: o, + OE: oe, + U: u, + UE: ue, + L: l, + P: *p, + Perms: perms, + R: r, + V: *v, + Emd: encMeta}, + nil +} + +func decryptKey(objNumber, generation int, key []byte, aes bool) []byte { + m := md5.New() + + nr := uint32(objNumber) + b1 := []byte{byte(nr), byte(nr >> 8), byte(nr >> 16)} + b := append(key, b1...) + + gen := uint16(generation) + b2 := []byte{byte(gen), byte(gen >> 8)} + b = append(b, b2...) + + m.Write(b) + + if aes { + m.Write([]byte("sAlT")) + } + + dk := m.Sum(nil) + + l := len(key) + 5 + if l < 16 { + dk = dk[:l] + } + + return dk +} + +// EncryptBytes encrypts s using RC4 or AES. +func encryptBytes(b []byte, objNr, genNr int, encKey []byte, needAES bool, r int) ([]byte, error) { + if needAES { + k := encKey + if r != 5 { + k = decryptKey(objNr, genNr, encKey, needAES) + } + return encryptAESBytes(b, k) + } + + return applyRC4CipherBytes(b, objNr, genNr, encKey, needAES) +} + +// decryptBytes decrypts bb using RC4 or AES. +func decryptBytes(b []byte, objNr, genNr int, encKey []byte, needAES bool, r int) ([]byte, error) { + if needAES { + k := encKey + if r != 5 { + k = decryptKey(objNr, genNr, encKey, needAES) + } + return decryptAESBytes(b, k) + } + + return applyRC4CipherBytes(b, objNr, genNr, encKey, needAES) +} + +func applyRC4CipherBytes(b []byte, objNr, genNr int, key []byte, needAES bool) ([]byte, error) { + c, err := rc4.NewCipher(decryptKey(objNr, genNr, key, needAES)) + if err != nil { + return nil, err + } + + c.XORKeyStream(b, b) + + return b, nil +} + +func encrypt(m map[string]types.Object, k string, v types.Object, objNr, genNr int, key []byte, needAES bool, r int) error { + s, err := encryptDeepObject(v, objNr, genNr, key, needAES, r) + if err != nil { + return err + } + + if s != nil { + m[k] = s + } + + return nil +} + +func encryptDict(d types.Dict, objNr, genNr int, key []byte, needAES bool, r int) error { + isSig := false + ft := d["FT"] + if ft == nil { + ft = d["Type"] + } + if ft != nil { + if ftv, ok := ft.(types.Name); ok && ftv == "Sig" { + isSig = true + } + } + for k, v := range d { + if isSig && k == "Contents" { + continue + } + err := encrypt(d, k, v, objNr, genNr, key, needAES, r) + if err != nil { + return err + } + } + + return nil +} + +func encryptStringLiteral(sl types.StringLiteral, objNr, genNr int, key []byte, needAES bool, r int) (*types.StringLiteral, error) { + bb, err := types.Unescape(sl.Value()) + if err != nil { + return nil, err + } + + bb, err = encryptBytes(bb, objNr, genNr, key, needAES, r) + if err != nil { + return nil, err + } + + s, err := types.Escape(string(bb)) + if err != nil { + return nil, err + } + + sl = types.StringLiteral(*s) + + return &sl, nil +} + +func decryptStringLiteral(sl types.StringLiteral, objNr, genNr int, key []byte, needAES bool, r int) (*types.StringLiteral, error) { + bb, err := types.Unescape(sl.Value()) + if err != nil { + return nil, err + } + + bb, err = decryptBytes(bb, objNr, genNr, key, needAES, r) + if err != nil { + return nil, err + } + + s, err := types.Escape(string(bb)) + if err != nil { + return nil, err + } + + sl = types.StringLiteral(*s) + + return &sl, nil +} + +func encryptHexLiteral(hl types.HexLiteral, objNr, genNr int, key []byte, needAES bool, r int) (*types.HexLiteral, error) { + bb, err := hl.Bytes() + if err != nil { + return nil, err + } + + bb, err = encryptBytes(bb, objNr, genNr, key, needAES, r) + if err != nil { + return nil, err + } + + hl = types.NewHexLiteral(bb) + + return &hl, nil +} + +func decryptHexLiteral(hl types.HexLiteral, objNr, genNr int, key []byte, needAES bool, r int) (*types.HexLiteral, error) { + bb, err := hl.Bytes() + if err != nil { + return nil, err + } + + bb, err = decryptBytes(bb, objNr, genNr, key, needAES, r) + if err != nil { + return nil, err + } + + hl = types.NewHexLiteral(bb) + + return &hl, nil +} + +// EncryptDeepObject recurses over non trivial PDF objects and encrypts all strings encountered. +func encryptDeepObject(objIn types.Object, objNr, genNr int, key []byte, needAES bool, r int) (types.Object, error) { + _, ok := objIn.(types.IndirectRef) + if ok { + return nil, nil + } + + switch obj := objIn.(type) { + + case types.StreamDict: + err := encryptDict(obj.Dict, objNr, genNr, key, needAES, r) + if err != nil { + return nil, err + } + + case types.Dict: + err := encryptDict(obj, objNr, genNr, key, needAES, r) + if err != nil { + return nil, err + } + + case types.Array: + for i, v := range obj { + s, err := encryptDeepObject(v, objNr, genNr, key, needAES, r) + if err != nil { + return nil, err + } + if s != nil { + obj[i] = s + } + } + + case types.StringLiteral: + sl, err := encryptStringLiteral(obj, objNr, genNr, key, needAES, r) + if err != nil { + return nil, err + } + return *sl, nil + + case types.HexLiteral: + hl, err := encryptHexLiteral(obj, objNr, genNr, key, needAES, r) + if err != nil { + return nil, err + } + return *hl, nil + + default: + + } + + return nil, nil +} + +func decryptDict(d types.Dict, objNr, genNr int, key []byte, needAES bool, r int) error { + isSig := false + ft := d["FT"] + if ft == nil { + ft = d["Type"] + } + if ft != nil { + if ftv, ok := ft.(types.Name); ok && ftv == "Sig" { + isSig = true + } + } + for k, v := range d { + if isSig && k == "Contents" { + continue + } + s, err := decryptDeepObject(v, objNr, genNr, key, needAES, r) + if err != nil { + return err + } + if s != nil { + d[k] = s + } + } + return nil +} + +func decryptDeepObject(objIn types.Object, objNr, genNr int, key []byte, needAES bool, r int) (types.Object, error) { + _, ok := objIn.(types.IndirectRef) + if ok { + return nil, nil + } + + switch obj := objIn.(type) { + + case types.Dict: + if err := decryptDict(obj, objNr, genNr, key, needAES, r); err != nil { + return nil, err + } + + case types.Array: + for i, v := range obj { + s, err := decryptDeepObject(v, objNr, genNr, key, needAES, r) + if err != nil { + return nil, err + } + if s != nil { + obj[i] = s + } + } + + case types.StringLiteral: + sl, err := decryptStringLiteral(obj, objNr, genNr, key, needAES, r) + if err != nil { + return nil, err + } + return *sl, nil + + case types.HexLiteral: + hl, err := decryptHexLiteral(obj, objNr, genNr, key, needAES, r) + if err != nil { + return nil, err + } + return *hl, nil + + default: + + } + + return nil, nil +} + +// EncryptStream encrypts a stream buffer using RC4 or AES. +func encryptStream(buf []byte, objNr, genNr int, encKey []byte, needAES bool, r int) ([]byte, error) { + k := encKey + if r != 5 && r != 6 { + k = decryptKey(objNr, genNr, encKey, needAES) + } + + if needAES { + return encryptAESBytes(buf, k) + } + + return applyRC4Bytes(buf, k) +} + +// decryptStream decrypts a stream buffer using RC4 or AES. +func decryptStream(buf []byte, objNr, genNr int, encKey []byte, needAES bool, r int) ([]byte, error) { + k := encKey + if r != 5 && r != 6 { + k = decryptKey(objNr, genNr, encKey, needAES) + } + + if needAES { + return decryptAESBytes(buf, k) + } + + return applyRC4Bytes(buf, k) +} + +func applyRC4Bytes(buf, key []byte) ([]byte, error) { + c, err := rc4.NewCipher(key) + if err != nil { + return nil, err + } + + var b bytes.Buffer + + r := &cipher.StreamReader{S: c, R: bytes.NewReader(buf)} + + _, err = io.Copy(&b, r) + if err != nil { + return nil, err + } + + return b.Bytes(), nil +} + +func encryptAESBytes(b, key []byte) ([]byte, error) { + // pad b to aes.Blocksize + l := len(b) % aes.BlockSize + c := 0x10 + if l > 0 { + c = aes.BlockSize - l + } + b = append(b, bytes.Repeat([]byte{byte(c)}, aes.BlockSize-l)...) + + if len(b) < aes.BlockSize { + return nil, errors.New("pdfcpu: encryptAESBytes: Ciphertext too short") + } + + if len(b)%aes.BlockSize > 0 { + return nil, errors.New("pdfcpu: encryptAESBytes: Ciphertext not a multiple of block size") + } + + data := make([]byte, aes.BlockSize+len(b)) + iv := data[:aes.BlockSize] + + _, err := io.ReadFull(rand.Reader, iv) + if err != nil { + return nil, err + } + + cb, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + mode := cipher.NewCBCEncrypter(cb, iv) + mode.CryptBlocks(data[aes.BlockSize:], b) + + return data, nil +} + +func decryptAESBytes(b, key []byte) ([]byte, error) { + if len(b) < aes.BlockSize { + return nil, errors.New("pdfcpu: decryptAESBytes: Ciphertext too short") + } + + if len(b)%aes.BlockSize > 0 { + return nil, errors.New("pdfcpu: decryptAESBytes: Ciphertext not a multiple of block size") + } + + cb, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + iv := make([]byte, aes.BlockSize) + copy(iv, b[:aes.BlockSize]) + + data := b[aes.BlockSize:] + mode := cipher.NewCBCDecrypter(cb, iv) + mode.CryptBlocks(data, data) + + // Remove padding. + // Note: For some reason not all AES ciphertexts are padded. + if len(data) > 0 && data[len(data)-1] <= 0x10 { + e := len(data) - int(data[len(data)-1]) + data = data[:e] + } + + return data, nil +} + +func fileID(ctx *model.Context) (types.HexLiteral, error) { + // see also 14.4 File Identifiers. + + // The calculation of the file identifier need not be reproducible; + // all that matters is that the identifier is likely to be unique. + // For example, two implementations of the preceding algorithm might use different formats for the current time, + // causing them to produce different file identifiers for the same file created at the same time, + // but the uniqueness of the identifier is not affected. + + h := md5.New() + + // Current timestamp. + h.Write([]byte(time.Now().String())) + + // File location - ignore, we don't have this. + + // File size. + h.Write([]byte(strconv.Itoa(ctx.Read.ReadFileSize()))) + + // All values of the info dict which is assumed to be there at this point. + if ctx.Version() < model.V20 { + d, err := ctx.DereferenceDict(*ctx.Info) + if err != nil { + return "", err + } + for _, v := range d { + o, err := ctx.Dereference(v) + if err != nil { + return "", err + } + h.Write([]byte(o.String())) + } + } + + m := h.Sum(nil) + + return types.HexLiteral(hex.EncodeToString(m)), nil +} + +func calcFileEncKey(ctx *model.Context) error { + ctx.EncKey = make([]byte, 32) + _, err := io.ReadFull(rand.Reader, ctx.EncKey) + return err +} + +func calcOAndUAES256(ctx *model.Context, d types.Dict) (err error) { + b := make([]byte, 16) + _, err = io.ReadFull(rand.Reader, b) + if err != nil { + return err + } + + u := append(make([]byte, 32), b...) + upw := []byte(ctx.UserPW) + h := sha256.Sum256(append(upw, validationSalt(u)...)) + + ctx.E.U = append(h[:], b...) + d.Update("U", types.HexLiteral(hex.EncodeToString(ctx.E.U))) + + /////////////////////////////////// + + b = make([]byte, 16) + _, err = io.ReadFull(rand.Reader, b) + if err != nil { + return err + } + + o := append(make([]byte, 32), b...) + opw := []byte(ctx.OwnerPW) + c := append(opw, validationSalt(o)...) + h = sha256.Sum256(append(c, ctx.E.U...)) + ctx.E.O = append(h[:], b...) + d.Update("O", types.HexLiteral(hex.EncodeToString(ctx.E.O))) + + ////////////////////////////////// + + if err := calcFileEncKey(ctx); err != nil { + return err + } + + ////////////////////////////////// + + h = sha256.Sum256(append(upw, keySalt(u)...)) + cb, err := aes.NewCipher(h[:]) + if err != nil { + return err + } + + iv := make([]byte, 16) + mode := cipher.NewCBCEncrypter(cb, iv) + mode.CryptBlocks(ctx.E.UE, ctx.EncKey) + d.Update("UE", types.HexLiteral(hex.EncodeToString(ctx.E.UE))) + + ////////////////////////////////// + + c = append(opw, keySalt(o)...) + h = sha256.Sum256(append(c, ctx.E.U...)) + cb, err = aes.NewCipher(h[:]) + if err != nil { + return err + } + + mode = cipher.NewCBCEncrypter(cb, iv) + mode.CryptBlocks(ctx.E.OE, ctx.EncKey) + d.Update("OE", types.HexLiteral(hex.EncodeToString(ctx.E.OE))) + + return nil +} + +func calcOAndUAES256Rev6(ctx *model.Context, d types.Dict) (err error) { + b := make([]byte, 16) + _, err = io.ReadFull(rand.Reader, b) + if err != nil { + return err + } + + u := append(make([]byte, 32), b...) + upw := []byte(ctx.UserPW) + h, _, err := hashRev6(append(upw, validationSalt(u)...), upw, nil) + if err != nil { + return err + } + + ctx.E.U = append(h[:], b...) + d.Update("U", types.HexLiteral(hex.EncodeToString(ctx.E.U))) + + /////////////////////////// + + b = make([]byte, 16) + _, err = io.ReadFull(rand.Reader, b) + if err != nil { + return err + } + + o := append(make([]byte, 32), b...) + opw := []byte(ctx.OwnerPW) + c := append(opw, validationSalt(o)...) + h, _, err = hashRev6(append(c, ctx.E.U...), opw, ctx.E.U) + if err != nil { + return err + } + + ctx.E.O = append(h[:], b...) + d.Update("O", types.HexLiteral(hex.EncodeToString(ctx.E.O))) + + /////////////////////////// + + if err := calcFileEncKey(ctx); err != nil { + return err + } + + /////////////////////////// + + h, _, err = hashRev6(append(upw, keySalt(u)...), upw, nil) + if err != nil { + return err + } + + cb, err := aes.NewCipher(h[:]) + if err != nil { + return err + } + + iv := make([]byte, 16) + mode := cipher.NewCBCEncrypter(cb, iv) + mode.CryptBlocks(ctx.E.UE, ctx.EncKey) + d.Update("UE", types.HexLiteral(hex.EncodeToString(ctx.E.UE))) + + ////////////////////////////// + + c = append(opw, keySalt(o)...) + h, _, err = hashRev6(append(c, ctx.E.U...), opw, ctx.E.U) + if err != nil { + return err + } + + cb, err = aes.NewCipher(h[:]) + if err != nil { + return err + } + + mode = cipher.NewCBCEncrypter(cb, iv) + mode.CryptBlocks(ctx.E.OE, ctx.EncKey) + d.Update("OE", types.HexLiteral(hex.EncodeToString(ctx.E.OE))) + + return nil +} + +func calcOAndU(ctx *model.Context, d types.Dict) (err error) { + if ctx.E.R == 5 { + return calcOAndUAES256(ctx, d) + } + + if ctx.E.R == 6 { + return calcOAndUAES256Rev6(ctx, d) + } + + ctx.E.O, err = o(ctx) + if err != nil { + return err + } + + ctx.E.U, ctx.EncKey, err = u(ctx) + if err != nil { + return err + } + + d.Update("U", types.HexLiteral(hex.EncodeToString(ctx.E.U))) + d.Update("O", types.HexLiteral(hex.EncodeToString(ctx.E.O))) + + return nil +} diff --git a/pkg/pdfcpu/cut.go b/pkg/pdfcpu/cut.go new file mode 100644 index 0000000000000000000000000000000000000000..90f95059dd624342b7c1588dfd64edf64f802e3e --- /dev/null +++ b/pkg/pdfcpu/cut.go @@ -0,0 +1,650 @@ +/* +Copyright 2023 The pdfcpu Authors. + +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 + + 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 pdfcpu + +import ( + "bytes" + "fmt" + "io" + "math" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/matrix" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// ParseCutConfigForPoster parses a Cut command string into an internal structure. +// formsize(=papersize) or dimensions, optionally: scalefactor, border, margin, bgcolor +func ParseCutConfigForPoster(s string, u types.DisplayUnit) (*model.Cut, error) { + + if s == "" { + return nil, errors.New("pdfcpu: missing poster configuration string") + } + + cut := &model.Cut{Unit: u, Scale: 1.0} + + ss := strings.Split(s, ",") + + for _, s := range ss { + + ss1 := strings.Split(s, ":") + if len(ss1) != 2 { + return nil, errors.New("pdfcpu: Invalid poster configuration string. Please consult pdfcpu help poster") + } + + paramPrefix := strings.TrimSpace(ss1[0]) + paramValueStr := strings.TrimSpace(ss1[1]) + + if err := model.CutParamMap.Handle(paramPrefix, paramValueStr, cut); err != nil { + return nil, err + } + } + + return cut, nil +} + +// ParseCutConfigForN parses a NDown command string into an internal structure. +// n, Optionally: border, margin, bgcolor +func ParseCutConfigForN(n int, s string, u types.DisplayUnit) (*model.Cut, error) { + + cut := &model.Cut{Unit: u} + + if !types.IntMemberOf(n, []int{2, 3, 4, 6, 8, 9, 12, 16}) { + return nil, errors.New("pdfcpu: invalid n: Please choose one of 2, 3, 4, 6, 8, 9, 12, 16") + } + + if s == "" { + return cut, nil + } + + ss := strings.Split(s, ",") + + for _, s := range ss { + + ss1 := strings.Split(s, ":") + if len(ss1) != 2 { + return nil, errors.New("pdfcpu: Invalid ndown configuration string. Please consult pdfcpu help ndown") + } + + paramPrefix := strings.TrimSpace(ss1[0]) + paramValueStr := strings.TrimSpace(ss1[1]) + + if err := model.CutParamMap.Handle(paramPrefix, paramValueStr, cut); err != nil { + return nil, err + } + } + + return cut, nil +} + +// ParseCutConfig parses a Cut command string into an internal structure. +// optionally: horizontalCut, verticalCut, bgcolor, border, margin, origin +func ParseCutConfig(s string, u types.DisplayUnit) (*model.Cut, error) { + + if s == "" { + return nil, errors.New("pdfcpu: missing cut configuration string") + } + + cut := &model.Cut{Unit: u} + + ss := strings.Split(s, ",") + + for _, s := range ss { + + ss1 := strings.Split(s, ":") + if len(ss1) != 2 { + return nil, errors.New("pdfcpu: Invalid cut configuration string. Please consult pdfcpu help cut") + } + + paramPrefix := strings.TrimSpace(ss1[0]) + paramValueStr := strings.TrimSpace(ss1[1]) + + if err := model.CutParamMap.Handle(paramPrefix, paramValueStr, cut); err != nil { + return nil, err + } + } + + return cut, nil +} + +func drawOutlineCuts(w io.Writer, cropBox, cb *types.Rectangle, cut *model.Cut) { + for i, f := range cut.Hor { + if i == 0 { + continue + } + y := cropBox.UR.Y - f*cropBox.Height() + draw.DrawLineSimple(w, cb.LL.X, y, cb.UR.X, y) + } + + for i, f := range cut.Vert { + if i == 0 { + continue + } + x := cropBox.LL.X + f*cropBox.Width() + draw.DrawLineSimple(w, x, cb.LL.Y, x, cb.UR.Y) + } +} + +func createOutline( + ctxSrc, ctxDest *model.Context, + pagesIndRef types.IndirectRef, + pagesDict, d types.Dict, + cropBox *types.Rectangle, + migrated map[int]int, + cut *model.Cut) error { + + cb := cropBox.Clone() + + var expCropBox bool + if len(cut.Hor) > 0 && cut.Hor[len(cut.Hor)-1] > 1 { + h := cut.Hor[len(cut.Hor)-1] * cropBox.Height() + cb.LL.Y = cb.UR.Y - h + expCropBox = true + } + + if len(cut.Vert) > 0 && cut.Vert[len(cut.Vert)-1] > 1 { + w := cut.Vert[len(cut.Vert)-1] * cropBox.Width() + cb.UR.X = cb.LL.X + w + expCropBox = true + } + + d1 := d.Clone().(types.Dict) + + var buf bytes.Buffer + + fmt.Fprint(&buf, "[3] 0 d ") + draw.SetStrokeColor(&buf, color.Red) + + // Assumption: origin = top left corner + + drawOutlineCuts(&buf, cropBox, cb, cut) + + bb, err := ctxSrc.PageContent(d1) + if err != nil { + return err + } + + bb = append([]byte("q "), bb...) + bb = append(bb, []byte("Q ")...) + bb = append(bb, buf.Bytes()...) + + sd, _ := ctxSrc.NewStreamDictForBuf(bb) + if err := sd.Encode(); err != nil { + return err + } + + indRef, err := ctxSrc.IndRefForNewObject(*sd) + if err != nil { + return err + } + + d1["Contents"] = *indRef + d1["Parent"] = pagesIndRef + if expCropBox { + d1["MediaBox"] = cb.Array() + d1["CropBox"] = cb.Array() + } + + pageIndRef, err := ctxDest.IndRefForNewObject(d1) + if err != nil { + return err + } + + if err := ctxDest.SetValid(*pageIndRef); err != nil { + return err + } + + if err := migratePageDict(d1, *pageIndRef, ctxSrc, ctxDest, migrated); err != nil { + return err + } + + if err := model.AppendPageTree(pageIndRef, 1, pagesDict); err != nil { + return err + } + + return nil +} + +func prepForCut(ctxSrc *model.Context, i int) ( + *model.Context, + *types.Rectangle, + *types.IndirectRef, + types.Dict, + types.Dict, + *model.InheritedPageAttrs, + error) { + + ctxDest, err := CreateContextWithXRefTable(nil, types.PaperSize["A4"]) + if err != nil { + return nil, nil, nil, nil, nil, nil, err + } + + pagesIndRef, err := ctxDest.Pages() + if err != nil { + return nil, nil, nil, nil, nil, nil, err + } + + pagesDict, err := ctxDest.DereferenceDict(*pagesIndRef) + if err != nil { + return nil, nil, nil, nil, nil, nil, err + } + + d, _, inhPAttrs, err := ctxSrc.PageDict(i, false) + if err != nil { + return nil, nil, nil, nil, nil, nil, err + } + if d == nil { + return nil, nil, nil, nil, nil, nil, errors.Errorf("pdfcpu: unknown page number: %d\n", i) + } + d.Delete("Annots") + + cropBox := inhPAttrs.MediaBox + if inhPAttrs.CropBox != nil { + cropBox = inhPAttrs.CropBox + } + + return ctxDest, cropBox, pagesIndRef, pagesDict, d, inhPAttrs, nil +} + +func internPageRot(ctxSrc *model.Context, rotate int, cropBox *types.Rectangle, d types.Dict, trans []byte) error { + bb, err := ctxSrc.PageContent(d) + if err != nil { + return err + } + + if rotate != 0 { + bbInvRot := append([]byte(" q "), model.ContentBytesForPageRotation(rotate, cropBox.Width(), cropBox.Height())...) + bb = append(bbInvRot, bb...) + bb = append(bb, []byte(" Q ")...) + } + + if len(trans) == 0 { + trans = []byte("q ") + } + bb = append(trans, bb...) + bb = append(bb, []byte("Q ")...) + + sd, _ := ctxSrc.NewStreamDictForBuf(bb) + if err := sd.Encode(); err != nil { + return err + } + + indRef, err := ctxSrc.IndRefForNewObject(*sd) + if err != nil { + return err + } + + d["Contents"] = *indRef + + return nil +} + +func handleCutMargin(ctxSrc *model.Context, d, d1 types.Dict, cropBox, cb *types.Rectangle, i, j int, w, h float64, sc *float64, cut *model.Cut) error { + ar := cb.AspectRatio() + mv := cut.Margin / ar + + // Scale & translate content. + if *sc == 0 { + *sc = (cb.Width() - 2*cut.Margin) / cb.Width() + } + + cbsc := cropBox.Clone() + cbsc.UR.X = cbsc.LL.X + cbsc.Width()**sc + cbsc.UR.Y = cbsc.LL.Y + cbsc.Height()**sc + + llx := cbsc.LL.X + cut.Vert[j]*cbsc.Width() + + lly := cbsc.LL.Y + if i+1 < len(cut.Hor) { + lly = cbsc.UR.Y - cut.Hor[i+1]*cbsc.Height() + } + + cbb := types.RectForWidthAndHeight(llx, lly, w, h) + + d1["MediaBox"] = cbb.Array() + d1["CropBox"] = cbb.Array() + + cb1 := cbb.Clone() + cb1.LL.X += cut.Margin + cb1.LL.Y += mv + cb1.UR.X -= cut.Margin + cb1.UR.Y -= mv + + var buf bytes.Buffer + + c := color.White + if cut.BgColor != nil { + c = *cut.BgColor + } + + w, h = cb1.Width(), mv + r := types.RectForWidthAndHeight(cb1.LL.X, cb1.UR.Y, w, h) + draw.FillRectNoBorder(&buf, r, c) + r = types.RectForWidthAndHeight(cb1.LL.X, cb1.LL.Y-mv, w, h) + draw.FillRectNoBorder(&buf, r, c) + + w, h = cut.Margin, cbb.Height() + r = types.RectForWidthAndHeight(cb1.UR.X, cb1.LL.Y-mv, w, h) + draw.FillRectNoBorder(&buf, r, c) + r = types.RectForWidthAndHeight(cb1.LL.X-cut.Margin, cb1.LL.Y-mv, w, h) + draw.FillRectNoBorder(&buf, r, c) + + if cut.Border { + draw.DrawRect(&buf, cb1, 1, &color.Black, nil) + } + + m := matrix.CalcTransformMatrix(*sc, *sc, 0, 1, cut.Margin, mv) + var trans bytes.Buffer + fmt.Fprintf(&trans, "q %.5f %.5f %.5f %.5f %.5f %.5f cm ", m[0][0], m[0][1], m[1][0], m[1][1], m[2][0], m[2][1]) + + bbOrig, err := ctxSrc.PageContent(d) + if err != nil { + return err + } + + bb := append(trans.Bytes(), bbOrig...) + bb = append(bb, []byte(" Q ")...) + bb = append(bb, buf.Bytes()...) + + sd, _ := ctxSrc.NewStreamDictForBuf(bb) + if err := sd.Encode(); err != nil { + return err + } + + indRef, err := ctxSrc.IndRefForNewObject(*sd) + if err != nil { + return err + } + + d1["Contents"] = *indRef + + return nil +} + +func createTiles( + ctxSrc, ctxDest *model.Context, + pagesIndRef types.IndirectRef, + pagesDict, d types.Dict, + cropBox *types.Rectangle, + inhPAttrs *model.InheritedPageAttrs, + migrated map[int]int, + cut *model.Cut) error { + + var sc float64 + + for i := 0; i < len(cut.Hor); i++ { + ury := cropBox.UR.Y - cut.Hor[i]*cropBox.Height() + if ury < cropBox.LL.Y { + continue + } + lly := cropBox.LL.Y + if i+1 < len(cut.Hor) { + lly = cropBox.UR.Y - cut.Hor[i+1]*cropBox.Height() + } + + h := ury - lly + + for j := 0; j < len(cut.Vert); j++ { + llx := cropBox.LL.X + cut.Vert[j]*cropBox.Width() + if llx > cropBox.UR.X { + continue + } + urx := cropBox.UR.X + if j+1 < len(cut.Vert) { + urx = cropBox.LL.X + cut.Vert[j+1]*cropBox.Width() + } + w := urx - llx + + cb := types.NewRectangle(llx, lly, urx, ury) + + d1 := d.Clone().(types.Dict) + d1["Resources"] = inhPAttrs.Resources.Clone() + d1["Parent"] = pagesIndRef + d1["MediaBox"] = cb.Array() + d1["CropBox"] = cb.Array() + + if cut.Margin > 0 { + if err := handleCutMargin(ctxSrc, d, d1, cropBox, cb, i, j, w, h, &sc, cut); err != nil { + return err + } + } + + pageIndRef, err := ctxDest.IndRefForNewObject(d1) + if err != nil { + return err + } + + if err := ctxDest.SetValid(*pageIndRef); err != nil { + return err + } + + if err := migratePageDict(d1, *pageIndRef, ctxSrc, ctxDest, migrated); err != nil { + return err + } + + if err := model.AppendPageTree(pageIndRef, 1, pagesDict); err != nil { + return err + } + } + } + + return nil +} + +func CutPage(ctxSrc *model.Context, i int, cut *model.Cut) (*model.Context, error) { + + // required: at least one of horizontalCut, verticalCut + // optionally: border, margin, bgcolor + + ctxDest, cropBox, pagesIndRef, pagesDict, d, inhPAttrs, err := prepForCut(ctxSrc, i) + if err != nil { + return nil, err + } + + rotate := inhPAttrs.Rotate + + if types.IntMemberOf(rotate, []int{+90, -90, +270, -270}) { + w := cropBox.Width() + cropBox.UR.X = cropBox.LL.X + cropBox.Height() + cropBox.UR.Y = cropBox.LL.Y + w + d["MediaBox"] = cropBox.Array() + d["CropBox"] = cropBox.Array() + d.Delete("Rotate") + } + + if err := internPageRot(ctxSrc, rotate, cropBox, d, nil); err != nil { + return nil, err + } + + migrated := map[int]int{} + + if err := createOutline(ctxSrc, ctxDest, *pagesIndRef, pagesDict, d, cropBox, migrated, cut); err != nil { + return nil, err + } + + if err := createTiles(ctxSrc, ctxDest, *pagesIndRef, pagesDict, d, cropBox, inhPAttrs, migrated, cut); err != nil { + return nil, err + } + + return ctxDest, nil +} + +func createNDownCuts(n int, cropBox *types.Rectangle, cut *model.Cut) { + var s1, s2 []float64 + + switch n { + case 2: + s1 = append(s1, 0, .5) + s2 = append(s2, 0) + case 3: + s1 = append(s1, 0, .33333, .66666) + s2 = append(s2, 0) + case 4: + s1 = append(s1, 0, .5) + s2 = append(s2, 0, .5) + case 6: + s1 = append(s1, 0, .33333, .66666) + s2 = append(s2, 0, .5) + case 8: + s1 = append(s1, 0, .25, .5, .75) + s2 = append(s2, 0, .5) + case 9: + s1 = append(s1, 0, .33333, .66666) + s2 = append(s2, 0, .33333, .66666) + case 12: + s1 = append(s1, 0, .25, .5, .75) + s2 = append(s2, 0, .33333, .66666) + case 16: + s1 = append(s1, 0, .25, .5, .75) + s2 = append(s2, 0, .25, .5, .75) + } + + if cropBox.Portrait() { + cut.Hor, cut.Vert = s1, s2 + } else { + cut.Hor, cut.Vert = s2, s1 + } +} + +func NDownPage(ctxSrc *model.Context, i, n int, cut *model.Cut) (*model.Context, error) { + + // Optionally: border, margin, bgcolor + + ctxDest, cropBox, pagesIndRef, pagesDict, d, inhPAttrs, err := prepForCut(ctxSrc, i) + if err != nil { + return nil, err + } + + rotate := inhPAttrs.Rotate + + if types.IntMemberOf(rotate, []int{+90, -90, +270, -270}) { + w := cropBox.Width() + cropBox.UR.X = cropBox.LL.X + cropBox.Height() + cropBox.UR.Y = cropBox.LL.Y + w + d["MediaBox"] = cropBox.Array() + d["CropBox"] = cropBox.Array() + d.Delete("Rotate") + } + + if err := internPageRot(ctxSrc, rotate, cropBox, d, nil); err != nil { + return nil, err + } + + createNDownCuts(n, cropBox, cut) + + migrated := map[int]int{} + + if err := createOutline(ctxSrc, ctxDest, *pagesIndRef, pagesDict, d, cropBox, migrated, cut); err != nil { + return nil, err + } + + if err := createTiles(ctxSrc, ctxDest, *pagesIndRef, pagesDict, d, cropBox, inhPAttrs, migrated, cut); err != nil { + return nil, err + } + + return ctxDest, nil +} + +func createPosterCuts(cropBox *types.Rectangle, cut *model.Cut) { + dim := cut.PageDim + + cut.Vert = []float64{0.} + for x := 0.; ; x += dim.Width { + f := (x + dim.Width) / cropBox.Width() + fr := math.Round(f*100) / 100 + if fr != 1 { + cut.Vert = append(cut.Vert, f) + } + if fr >= 1 { + break + } + } + + cut.Hor = []float64{0.} + for y := 0.; ; y += dim.Height { + f := (y + dim.Height) / cropBox.Height() + fr := math.Round(f*100) / 100 + if fr != 1 { + cut.Hor = append(cut.Hor, f) + } + if fr >= 1 { + break + } + } +} + +func PosterPage(ctxSrc *model.Context, i int, cut *model.Cut) (*model.Context, error) { + + // required: formsize(=papersize) or dimensions + // optionally: scalefactor, border, margin, bgcolor + + ctxDest, cropBox, pagesIndRef, pagesDict, d, inhPAttrs, err := prepForCut(ctxSrc, i) + if err != nil { + return nil, err + } + + cropBox.UR.X = cropBox.LL.X + cropBox.Width()*cut.Scale + cropBox.UR.Y = cropBox.LL.Y + cropBox.Height()*cut.Scale + + // Ensure cut.PageDim fits into scaled cropBox. + dim := cut.PageDim + if dim.Width > cropBox.Width() || dim.Height > cropBox.Height() { + return nil, errors.New("pdfcpu: selected poster tile dimensions too big") + } + + rotate := inhPAttrs.Rotate + + if types.IntMemberOf(rotate, []int{+90, -90, +270, -270}) { + w := cropBox.Width() + cropBox.UR.X = cropBox.LL.X + cropBox.Height() + cropBox.UR.Y = cropBox.LL.Y + w + } + + d["MediaBox"] = cropBox.Array() + d["CropBox"] = cropBox.Array() + d.Delete("Rotate") + + // Scale transform + m := matrix.IdentMatrix + m[0][0] = cut.Scale + m[1][1] = cut.Scale + + var trans bytes.Buffer + fmt.Fprintf(&trans, "q %.5f %.5f %.5f %.5f %.5f %.5f cm ", m[0][0], m[0][1], m[1][0], m[1][1], m[2][0], m[2][1]) + + if err := internPageRot(ctxSrc, rotate, cropBox, d, trans.Bytes()); err != nil { + return nil, err + } + + createPosterCuts(cropBox, cut) + + migrated := map[int]int{} + + if err := createOutline(ctxSrc, ctxDest, *pagesIndRef, pagesDict, d, cropBox, migrated, cut); err != nil { + return nil, err + } + + if err := createTiles(ctxSrc, ctxDest, *pagesIndRef, pagesDict, d, cropBox, inhPAttrs, migrated, cut); err != nil { + return nil, err + } + + return ctxDest, nil +} diff --git a/pkg/pdfcpu/doc.go b/pkg/pdfcpu/doc.go new file mode 100644 index 0000000000000000000000000000000000000000..196c65f430cf4f04d19894c3cd3ed28829ef6c2f --- /dev/null +++ b/pkg/pdfcpu/doc.go @@ -0,0 +1,53 @@ +/* +Package pdfcpu is a PDF processing library written in Go supporting encryption. +It provides an API and a command line interface. Supported are all versions up to PDF 1.7 (ISO-32000). + +The commands are: + + annotations list, remove page annotations + attachments list, add, remove, extract embedded file attachments + booklet arrange pages onto larger sheets of paper to make a booklet or zine + bookmarks list, import, export, remove bookmarks + boxes list, add, remove page boundaries for selected pages + changeopw change owner password + changeupw change user password + collect create custom sequence of selected pages + config print configuration + create create PDF content including forms via JSON + crop set cropbox for selected pages + cut custom cut pages horizontally or vertically + decrypt remove password protection + encrypt set password protection + extract extract images, fonts, content, pages or metadata + fonts install, list supported fonts, create cheat sheets + form list, remove fields, lock, unlock, reset, export, fill form via JSON or CSV + grid rearrange pages or images for enhanced browsing experience + images list images for selected pages + import import/convert images to PDF + info print file info + keywords list, add, remove keywords + merge concatenate PDFs + ndown cut selected pages into n pages symmetrically + nup rearrange pages or images for reduced number of pages + optimize optimize PDF by getting rid of redundant page resources + pagelayout list, set, reset page layout for opened document + pagemode list, set, reset page mode for opened document + pages insert, remove selected pages + paper print list of supported paper sizes + permissions list, set user access permissions + portfolio list, add, remove, extract portfolio entries with optional description + poster cut selected pages into poster by paper size or dimensions + properties list, add, remove document properties + resize scale selected pages + rotate rotate selected pages + selectedpages print definition of the -pages flag + split split up a PDF by span or bookmark + stamp add, remove, update Unicode text, image or PDF stamps for selected pages + trim create trimmed version of selected pages + validate validate PDF against PDF 32000-1:2008 (PDF 1.7) + basic PDF 2.0 validation + version print version + viewpref list, set, reset viewer preferences for opened document + watermark add, remove, update Unicode text, image or PDF watermarks for selected pages + zoom zoom in/out of selected pages by magnification factor or corresponding margin +*/ +package pdfcpu diff --git a/pkg/pdfcpu/draw/draw.go b/pkg/pdfcpu/draw/draw.go new file mode 100644 index 0000000000000000000000000000000000000000..959c44b490aa8c5eb1d23e42f56f2d84b1fe5ea3 --- /dev/null +++ b/pkg/pdfcpu/draw/draw.go @@ -0,0 +1,197 @@ +/* +Copyright 2022 The pdfcpu Authors. + +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 + + 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 draw + +import ( + "fmt" + "io" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +// RenderMode represents the text rendering mode (see 9.3.6) +type RenderMode int + +// Render mode +const ( + RMFill RenderMode = iota + RMStroke + RMFillAndStroke +) + +// SetLineJoinStyle sets the line join style for stroking operations. +func SetLineJoinStyle(w io.Writer, s types.LineJoinStyle) { + fmt.Fprintf(w, "%d j ", s) +} + +// SetLineWidth sets line width for stroking operations. +func SetLineWidth(w io.Writer, width float64) { + fmt.Fprintf(w, "%.2f w ", width) +} + +// SetStrokeColor sets the stroke color. +func SetStrokeColor(w io.Writer, c color.SimpleColor) { + fmt.Fprintf(w, "%.2f %.2f %.2f RG ", c.R, c.G, c.B) +} + +// SetFillColor sets the fill color. +func SetFillColor(w io.Writer, c color.SimpleColor) { + fmt.Fprintf(w, "%.2f %.2f %.2f rg ", c.R, c.G, c.B) +} + +// DrawLineSimple draws the path from P to Q. +func DrawLineSimple(w io.Writer, xp, yp, xq, yq float64) { + fmt.Fprintf(w, "%.2f %.2f m %.2f %.2f l s ", xp, yp, xq, yq) +} + +// DrawLine draws the path from P to Q using lineWidth, strokeColor and style. +func DrawLine(w io.Writer, xp, yp, xq, yq float64, lineWidth float64, strokeColor *color.SimpleColor, style *types.LineJoinStyle) { + fmt.Fprintf(w, "q ") + SetLineWidth(w, lineWidth) + if strokeColor != nil { + SetStrokeColor(w, *strokeColor) + } + if style != nil { + SetLineJoinStyle(w, *style) + } + DrawLineSimple(w, xp, yp, xq, yq) + fmt.Fprintf(w, "Q ") +} + +// DrawRectSimple strokes a rectangular path for r. +func DrawRectSimple(w io.Writer, r *types.Rectangle) { + fmt.Fprintf(w, "%.2f %.2f %.2f %.2f re s ", r.LL.X, r.LL.Y, r.Width(), r.Height()) +} + +// DrawRect strokes a rectangular path for r using lineWidth, strokeColor and style. +func DrawRect(w io.Writer, r *types.Rectangle, lineWidth float64, strokeColor *color.SimpleColor, style *types.LineJoinStyle) { + fmt.Fprintf(w, "q ") + SetLineWidth(w, lineWidth) + if strokeColor != nil { + SetStrokeColor(w, *strokeColor) + } + if style != nil { + SetLineJoinStyle(w, *style) + } + DrawRectSimple(w, r) + fmt.Fprintf(w, "Q ") +} + +// FillRect fills a rectangular path for r using lineWidth, strokeCol, fillCol and style. +func FillRect(w io.Writer, r *types.Rectangle, lineWidth float64, strokeCol *color.SimpleColor, fillCol color.SimpleColor, style *types.LineJoinStyle) { + fmt.Fprintf(w, "q ") + SetLineWidth(w, lineWidth) + c := fillCol + if strokeCol != nil { + c = *strokeCol + } + SetStrokeColor(w, c) + SetFillColor(w, fillCol) + if style != nil { + SetLineJoinStyle(w, *style) + } + fmt.Fprintf(w, "%.2f %.2f %.2f %.2f re B ", r.LL.X, r.LL.Y, r.Width(), r.Height()) + fmt.Fprintf(w, "Q ") +} + +// DrawCircle strokes a circle with optional filling. +func DrawCircle(w io.Writer, x, y, r float64, strokeCol color.SimpleColor, fillCol *color.SimpleColor) { + f := .5523 + r1 := r - .1 + + if fillCol != nil { + fmt.Fprintf(w, "q %.2f %.2f %.2f rg 1 0 0 1 %.2f %.2f cm %.2f 0 m ", fillCol.R, fillCol.G, fillCol.B, x, y, r) + fmt.Fprintf(w, "%.3f %.3f %.3f %.3f %.3f %.3f c ", r, f*r, f*r, r, 0., r) + fmt.Fprintf(w, "%.3f %.3f %.3f %.3f %.3f %.3f c ", -f*r, r, -r, f*r, -r, 0.) + fmt.Fprintf(w, "%.3f %.3f %.3f %.3f %.3f %.3f c ", -r, -f*r, -f*r, -r, .0, -r) + fmt.Fprintf(w, "%.3f %.3f %.3f %.3f %.3f %.3f c ", f*r, -r, r, -f*r, r, 0.) + fmt.Fprintf(w, "f Q ") + } + + fmt.Fprintf(w, "q %.2f %.2f %.2f RG 1 0 0 1 %.2f %.2f cm %.2f 0 m ", strokeCol.R, strokeCol.G, strokeCol.B, x, y, r1) + fmt.Fprintf(w, "%.3f %.3f %.3f %.3f %.3f %.3f c ", r1, f*r1, f*r1, r1, 0., r1) + fmt.Fprintf(w, "%.3f %.3f %.3f %.3f %.3f %.3f c ", -f*r1, r1, -r1, f*r1, -r1, 0.) + fmt.Fprintf(w, "%.3f %.3f %.3f %.3f %.3f %.3f c ", -r1, -f*r1, -f*r1, -r1, .0, -r1) + fmt.Fprintf(w, "%.3f %.3f %.3f %.3f %.3f %.3f c ", f*r1, -r1, r1, -f*r1, r1, 0.) + fmt.Fprint(w, "s Q ") +} + +// FillRectNoBorder fills a rectangular path for r using fillCol. +func FillRectNoBorder(w io.Writer, r *types.Rectangle, fillCol color.SimpleColor) { + fmt.Fprintf(w, "q ") + SetStrokeColor(w, fillCol) + SetFillColor(w, fillCol) + fmt.Fprintf(w, "%.2f %.2f %.2f %.2f re B ", r.LL.X, r.LL.Y, r.Width(), r.Height()) + fmt.Fprintf(w, "Q ") +} + +// DrawGrid draws an x * y grid on r using strokeCol and fillCol. +func DrawGrid(w io.Writer, x, y int, r *types.Rectangle, strokeCol color.SimpleColor, fillCol *color.SimpleColor) { + + if fillCol != nil { + FillRectNoBorder(w, r, *fillCol) + } + + s := r.Width() / float64(x) + for i := 0; i <= x; i++ { + x := r.LL.X + float64(i)*s + DrawLine(w, x, r.LL.Y, x, r.UR.Y, 0, &strokeCol, nil) + } + + s = r.Height() / float64(y) + for i := 0; i <= y; i++ { + y := r.LL.Y + float64(i)*s + DrawLine(w, r.LL.X, y, r.UR.X, y, 0, &strokeCol, nil) + } +} + +// DrawHairCross draw a haircross with origin x/y. +func DrawHairCross(w io.Writer, x, y float64, r *types.Rectangle) { + x1, y1 := x, y + if x == 0 { + x1 = r.LL.X + r.Width()/2 + } + if y == 0 { + y1 = r.LL.Y + r.Height()/2 + } + black := color.SimpleColor{} + DrawLine(w, r.LL.X, y1, r.LL.X+r.Width(), y1, 0, &black, nil) // Horizontal line + DrawLine(w, x1, r.LL.Y, x1, r.LL.Y+r.Height(), 0, &black, nil) // Vertical line +} + +// CLI drawing + +const ( + HBar = "\u2501" + VBar = "\u2502" + CrossBar = "\u253f" +) + +// HorSepLine renders a horizontal divider with optional column separators: +// ━━━━━━━━━━┿━━━━━━━━┿━━━━━━━━━━━━━━━━━━━━━━━━┿━━━━━━━┿━━━━━━━━┿━━━━━━━━ +func HorSepLine(ii []int) string { + s := "" + for i, j := range ii { + if i > 0 { + s += CrossBar + } + s += strings.Repeat(HBar, j) + } + return s +} diff --git a/pkg/pdfcpu/extract.go b/pkg/pdfcpu/extract.go new file mode 100644 index 0000000000000000000000000000000000000000..a926e5f6ff41471c68f98afca99b3ae40b51cef1 --- /dev/null +++ b/pkg/pdfcpu/extract.go @@ -0,0 +1,702 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 pdfcpu + +import ( + "bytes" + "fmt" + "io" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/filter" + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// ImageObjNrs returns all image dict objNrs for pageNr. +// Requires an optimized context. +func ImageObjNrs(ctx *model.Context, pageNr int) []int { + // TODO Exclude SMask image objects. + objNrs := []int{} + + if pageNr < 1 { + return objNrs + } + + imgObjNrs := ctx.Optimize.PageImages + if len(imgObjNrs) == 0 { + return objNrs + } + + pageImgObjNrs := imgObjNrs[pageNr-1] + if pageImgObjNrs == nil { + return objNrs + } + + for k, v := range pageImgObjNrs { + if v { + objNrs = append(objNrs, k) + } + } + return objNrs +} + +// StreamLength returns sd's stream length. +func StreamLength(ctx *model.Context, sd *types.StreamDict) (int64, error) { + if val := sd.Int64Entry("Length"); val != nil { + return *val, nil + } + + indRef := sd.IndirectRefEntry("Length") + if indRef == nil { + return 0, nil + } + + i, err := ctx.DereferenceInteger(*indRef) + if err != nil || i == nil { + return 0, err + } + + return int64(*i), nil +} + +// ColorSpaceString returns a string representation for sd's colorspace. +func ColorSpaceString(ctx *model.Context, sd *types.StreamDict) (string, error) { + o, found := sd.Find("ColorSpace") + if !found { + return "", nil + } + + o, err := ctx.Dereference(o) + if err != nil { + return "", err + } + + switch cs := o.(type) { + + case types.Name: + return string(cs), nil + + case types.Array: + return string(cs[0].(types.Name)), nil + } + + return "", nil +} + +func colorSpaceNameComponents(cs types.Name) int { + switch cs { + + case model.DeviceGrayCS: + return 1 + + case model.DeviceRGBCS: + return 3 + + case model.DeviceCMYKCS: + return 4 + } + + return 0 +} + +func indexedColorSpaceComponents(xRefTable *model.XRefTable, cs types.Array) (int, error) { + baseCS, err := xRefTable.Dereference(cs[1]) + if err != nil { + return 0, err + } + + switch cs := baseCS.(type) { + case types.Name: + return colorSpaceNameComponents(cs), nil + + case types.Array: + switch cs[0].(types.Name) { + + case model.CalGrayCS: + return 1, nil + + case model.CalRGBCS: + return 3, nil + + case model.LabCS: + return 3, nil + + case model.ICCBasedCS: + iccProfileStream, _, err := xRefTable.DereferenceStreamDict(cs[1]) + if err != nil { + return 0, err + } + n := iccProfileStream.IntEntry("N") + i := 0 + if n != nil { + i = *n + } + return i, nil + + case model.SeparationCS: + return 1, nil + + case model.DeviceNCS: + return len(cs[1].(types.Array)), nil + } + } + + return 0, nil +} + +// ColorSpaceComponents returns the corresponding number of used color components for sd's colorspace. +func ColorSpaceComponents(xRefTable *model.XRefTable, sd *types.StreamDict) (int, error) { + o, found := sd.Find("ColorSpace") + if !found { + return 0, nil + } + + o, err := xRefTable.Dereference(o) + if err != nil { + return 0, err + } + + switch cs := o.(type) { + case types.Name: + return colorSpaceNameComponents(cs), nil + + case types.Array: + switch cs[0].(types.Name) { + + case model.CalGrayCS: + return 1, nil + + case model.CalRGBCS: + return 3, nil + + case model.LabCS: + return 3, nil + + case model.ICCBasedCS: + iccProfileStream, _, err := xRefTable.DereferenceStreamDict(cs[1]) + if err != nil { + return 0, err + } + n := iccProfileStream.IntEntry("N") + i := 0 + if n != nil { + i = *n + } + return i, nil + + case model.SeparationCS: + return 1, nil + + case model.DeviceNCS: + return len(cs[1].(types.Array)), nil + + case model.IndexedCS: + return indexedColorSpaceComponents(xRefTable, cs) + + } + } + + return 0, nil +} + +func imageWidth(ctx *model.Context, sd *types.StreamDict, objNr int) (int, error) { + obj, ok := sd.Find("Width") + if !ok { + return 0, errors.Errorf("pdfcpu: missing image width obj#%d", objNr) + } + i, err := ctx.DereferenceInteger(obj) + if err != nil { + return 0, err + } + return i.Value(), nil +} + +func imageHeight(ctx *model.Context, sd *types.StreamDict, objNr int) (int, error) { + obj, ok := sd.Find("Height") + if !ok { + return 0, errors.Errorf("pdfcpu: missing image height obj#%d", objNr) + } + i, err := ctx.DereferenceInteger(obj) + if err != nil { + return 0, err + } + return i.Value(), nil +} + +func imageStub( + ctx *model.Context, + sd *types.StreamDict, + resourceId, filters, lastFilter string, + decodeParms types.Dict, + thumb, imgMask bool, + objNr int) (*model.Image, error) { + + w, err := imageWidth(ctx, sd, objNr) + if err != nil { + return nil, err + } + + h, err := imageHeight(ctx, sd, objNr) + if err != nil { + return nil, err + } + + cs, err := ColorSpaceString(ctx, sd) + if err != nil { + return nil, err + } + + comp, err := ColorSpaceComponents(ctx.XRefTable, sd) + if err != nil { + return nil, err + } + if lastFilter == filter.CCITTFax { + comp = 1 + } + + bpc := 0 + if i := sd.IntEntry("BitsPerComponent"); i != nil { + bpc = *i + } + // if jpx, bpc is undefined + if imgMask { + bpc = 1 + } + + var sMask bool + if sm, _ := sd.Find("SMask"); sm != nil { + sMask = true + } + + var mask bool + if sm, _ := sd.Find("Mask"); sm != nil { + mask = true + } + + var interpol bool + if b := sd.BooleanEntry("Interpolate"); b != nil && *b { + interpol = true + } + + size, err := StreamLength(ctx, sd) + if err != nil { + return nil, err + } + + var s string + if decodeParms != nil { + s = decodeParms.String() + } + + img := &model.Image{ + ObjNr: objNr, + Name: resourceId, + Thumb: thumb, + IsImgMask: imgMask, + HasImgMask: mask, + HasSMask: sMask, + Width: w, + Height: h, + Cs: cs, + Comp: comp, + Bpc: bpc, + Interpol: interpol, + Size: size, + Filter: filters, + DecodeParms: s, + } + + return img, nil +} + +func prepareExtractImage(sd *types.StreamDict) (string, string, types.Dict, bool) { + var imgMask bool + if im := sd.BooleanEntry("ImageMask"); im != nil && *im { + imgMask = true + } + + var ( + filters string + lastFilter string + d types.Dict + ) + + fpl := sd.FilterPipeline + if fpl != nil { + var s []string + for _, filter := range fpl { + s = append(s, filter.Name) + lastFilter = filter.Name + if filter.DecodeParms != nil { + d = filter.DecodeParms + } + } + filters = strings.Join(s, ",") + } + + return filters, lastFilter, d, imgMask +} +func decodeImage(ctx *model.Context, sd *types.StreamDict, filters, lastFilter string, objNr int) error { + // CCITTDecoded images / (bit) masks don't have a ColorSpace attribute, but we render image files. + if lastFilter == filter.CCITTFax { + if _, err := ctx.DereferenceDictEntry(sd.Dict, "ColorSpace"); err != nil { + sd.InsertName("ColorSpace", model.DeviceGrayCS) + } + } + + if lastFilter == filter.DCT { + comp, err := ColorSpaceComponents(ctx.XRefTable, sd) + if err != nil { + return err + } + sd.CSComponents = comp + } + + switch lastFilter { + + case filter.DCT, filter.JPX, filter.Flate, filter.CCITTFax, filter.RunLength: + if err := sd.Decode(); err != nil { + return err + } + + default: + msg := fmt.Sprintf("pdfcpu: ExtractImage(obj#%d): skipping img, filter %s unsupported", objNr, filters) + if log.DebugEnabled() { + log.Debug.Println(msg) + } + if log.CLIEnabled() { + log.CLI.Println(msg) + } + return nil + } + + return nil +} + +func img( + ctx *model.Context, + sd *types.StreamDict, + thumb, imgMask bool, + resourceID, filters, lastFilter string, + objNr int) (*model.Image, error) { + + if sd.FilterPipeline == nil { + sd.Content = sd.Raw + } else { + if err := decodeImage(ctx, sd, filters, lastFilter, objNr); err != nil { + return nil, err + } + } + + r, t, err := RenderImage(ctx.XRefTable, sd, thumb, resourceID, objNr) + if err != nil { + return nil, err + } + + img := &model.Image{ + Reader: r, + Name: resourceID, + ObjNr: objNr, + Thumb: thumb, + FileType: t, + } + + return img, nil +} + +// ExtractImage extracts an image from sd. +func ExtractImage(ctx *model.Context, sd *types.StreamDict, thumb bool, resourceID string, objNr int, stub bool) (*model.Image, error) { + if sd == nil { + return nil, nil + } + + filters, lastFilter, decodeParms, imgMask := prepareExtractImage(sd) + + if stub { + return imageStub(ctx, sd, resourceID, filters, lastFilter, decodeParms, thumb, imgMask, objNr) + } + + return img(ctx, sd, thumb, imgMask, resourceID, filters, lastFilter, objNr) +} + +// ExtractPageImages extracts all images used by pageNr. +// Optionally return stubs only. +func ExtractPageImages(ctx *model.Context, pageNr int, stub bool) (map[int]model.Image, error) { + m := map[int]model.Image{} + for _, objNr := range ImageObjNrs(ctx, pageNr) { + imageObj := ctx.Optimize.ImageObjects[objNr] + img, err := ExtractImage(ctx, imageObj.ImageDict, false, imageObj.ResourceNames[0], objNr, stub) + if err != nil { + return nil, err + } + if img != nil { + img.PageNr = pageNr + m[objNr] = *img + } + } + // Extract thumbnail for pageNr + if indRef, ok := ctx.PageThumbs[pageNr]; ok { + objNr := indRef.ObjectNumber.Value() + sd, _, err := ctx.DereferenceStreamDict(indRef) + if err != nil { + return nil, err + } + img, err := ExtractImage(ctx, sd, true, "", objNr, stub) + if err != nil { + return nil, err + } + if img != nil { + img.PageNr = pageNr + m[objNr] = *img + } + } + return m, nil +} + +// Font is a Reader representing an embedded font. +type Font struct { + io.Reader + Name string + Type string +} + +// FontObjNrs returns all font dict objNrs for pageNr. +// Requires an optimized context. +func FontObjNrs(ctx *model.Context, pageNr int) []int { + objNrs := []int{} + + if pageNr < 1 { + return objNrs + } + + fontObjNrs := ctx.Optimize.PageFonts + if len(fontObjNrs) == 0 { + return objNrs + } + + pageFontObjNrs := fontObjNrs[pageNr-1] + if pageFontObjNrs == nil { + return objNrs + } + + for k, v := range pageFontObjNrs { + if v { + objNrs = append(objNrs, k) + } + } + return objNrs +} + +// ExtractFont extracts a font from fontObject. +func ExtractFont(ctx *model.Context, fontObject model.FontObject, objNr int) (*Font, error) { + // Only embedded fonts have binary data. + if !fontObject.Embedded() { + if log.DebugEnabled() { + log.Debug.Printf("ExtractFont: ignoring obj#%d - non embedded font: %s\n", objNr, fontObject.FontName) + } + return nil, nil + } + + d, err := fontDescriptor(ctx.XRefTable, fontObject.FontDict, objNr) + if err != nil { + return nil, err + } + + if d == nil { + if log.DebugEnabled() { + log.Debug.Printf("ExtractFont: ignoring obj#%d - no fontDescriptor available for font: %s\n", objNr, fontObject.FontName) + } + return nil, nil + } + + ir := fontDescriptorFontFileIndirectObjectRef(d) + if ir == nil { + if log.DebugEnabled() { + log.Debug.Printf("ExtractFont: ignoring obj#%d - no font file available for font: %s\n", objNr, fontObject.FontName) + } + return nil, nil + } + + var f *Font + + fontType := fontObject.SubType() + + switch fontType { + + case "TrueType": + // ttf ... true type file + // ttc ... true type collection + sd, _, err := ctx.DereferenceStreamDict(*ir) + if err != nil { + return nil, err + } + if sd == nil { + return nil, errors.Errorf("extractFontData: corrupt font obj#%d for font: %s\n", objNr, fontObject.FontName) + } + + // Decode streamDict if used filter is supported only. + err = sd.Decode() + if err == filter.ErrUnsupportedFilter { + return nil, nil + } + if err != nil { + return nil, err + } + + f = &Font{bytes.NewReader(sd.Content), fontObject.FontName, "ttf"} + + default: + if log.InfoEnabled() { + log.Info.Printf("extractFontData: ignoring obj#%d - unsupported fonttype %s - font: %s\n", objNr, fontType, fontObject.FontName) + } + return nil, nil + } + + return f, nil +} + +// ExtractPageFonts extracts all fonts used by pageNr. +func ExtractPageFonts(ctx *model.Context, pageNr int) ([]Font, error) { + ff := []Font{} + for _, i := range FontObjNrs(ctx, pageNr) { + fontObject := ctx.Optimize.FontObjects[i] + f, err := ExtractFont(ctx, *fontObject, i) + if err != nil { + return nil, err + } + if f != nil { + ff = append(ff, *f) + } + } + return ff, nil +} + +// ExtractPageFonts extracts all form fonts. +func ExtractFormFonts(ctx *model.Context) ([]Font, error) { + ff := []Font{} + for i, fontObject := range ctx.Optimize.FormFontObjects { + f, err := ExtractFont(ctx, *fontObject, i) + if err != nil { + return nil, err + } + if f != nil { + ff = append(ff, *f) + } + } + return ff, nil +} + +// ExtractPages extracts pageNrs into a new single page context. +func ExtractPages(ctx *model.Context, pageNrs []int, usePgCache bool) (*model.Context, error) { + ctxDest, err := CreateContextWithXRefTable(nil, types.PaperSize["A4"]) + if err != nil { + return nil, err + } + + if err := AddPages(ctx, ctxDest, pageNrs, usePgCache); err != nil { + return nil, err + } + + return ctxDest, nil +} + +// ExtractPageContent extracts the consolidated page content stream for pageNr. +func ExtractPageContent(ctx *model.Context, pageNr int) (io.Reader, error) { + consolidateRes := false + d, _, _, err := ctx.PageDict(pageNr, consolidateRes) + if err != nil { + return nil, err + } + bb, err := ctx.PageContent(d) + if err != nil && err != model.ErrNoContent { + return nil, err + } + return bytes.NewReader(bb), nil +} + +// Metadata is a Reader representing a metadata dict. +type Metadata struct { + io.Reader // metadata + ObjNr int // metadata dict objNr + ParentObjNr int // container object number + ParentType string // container dict type +} + +func extractMetadataFromDict(ctx *model.Context, d types.Dict, parentObjNr int) (*Metadata, error) { + o, found := d.Find("Metadata") + if !found || o == nil { + return nil, nil + } + sd, _, err := ctx.DereferenceStreamDict(o) + if err != nil { + return nil, err + } + if sd == nil { + return nil, nil + } + // Get metadata dict object number. + ir, _ := o.(types.IndirectRef) + mdObjNr := ir.ObjectNumber.Value() + // Get container dict type. + dt := "unknown" + if d.Type() != nil { + dt = *d.Type() + } + // Decode streamDict for supported filters only. + if err = sd.Decode(); err == filter.ErrUnsupportedFilter { + return nil, nil + } + if err != nil { + return nil, err + } + return &Metadata{bytes.NewReader(sd.Content), mdObjNr, parentObjNr, dt}, nil +} + +// ExtractMetadata returns all metadata of ctx. +func ExtractMetadata(ctx *model.Context) ([]Metadata, error) { + mm := []Metadata{} + for k, v := range ctx.Table { + if v.Free || v.Compressed { + continue + } + switch d := v.Object.(type) { + case types.Dict: + md, err := extractMetadataFromDict(ctx, d, k) + if err != nil { + return nil, err + } + if md == nil { + continue + } + mm = append(mm, *md) + + case types.StreamDict: + md, err := extractMetadataFromDict(ctx, d.Dict, k) + if err != nil { + return nil, err + } + if md == nil { + continue + } + mm = append(mm, *md) + } + } + return mm, nil +} diff --git a/pkg/pdfcpu/font/fontDict.go b/pkg/pdfcpu/font/fontDict.go new file mode 100644 index 0000000000000000000000000000000000000000..5f4dc4b33b78735fc61e733e63572657bd28118b --- /dev/null +++ b/pkg/pdfcpu/font/fontDict.go @@ -0,0 +1,1107 @@ +/* +Copyright 2022 The pdfcpu Authors. + +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 + + 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 font + +import ( + "bufio" + "bytes" + "encoding/binary" + "encoding/hex" + "fmt" + "math/rand" + "sort" + "strconv" + "strings" + "time" + "unicode/utf16" + + "github.com/pdfcpu/pdfcpu/pkg/font" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +type cjk struct { + encoding string + ordering string + supplement int +} + +// Mapping of supported ISO-15924 font script code keys to corresponding encoding and CIDSystemInfo. +var cjkParms = map[string]cjk{ + // C + "HANS": {"UniGB-UTF16-H", "GB1", 5}, + "HANT": {"UniCNS-UTF16-H", "CNS1", 7}, + // J + "HIRA": {"UniJIS-UTF16-H", "Japan1", 7}, + "KANA": {"UniJIS-UTF16-H", "Japan1", 7}, + "JPAN": {"UniJIS-UTF16-H", "Japan1", 7}, + // K + "HANG": {"UniKS-UTF16-H", "Korea1", 1}, + "KORE": {"UniKS-UTF16-H", "Korea1", 1}, + //"HANG": {"UniKS-UTF16-H", "KR", 9}, + //"KORE": {"UniKS-UTF16-H", "KR", 9}, +} + +func SupportedScript(s string) bool { + return types.MemberOf(s, []string{"HANS", "HANT", "HIRA", "KANA", "JPAN", "HANG", "KORE"}) +} + +// CJKEncodings returns true for supported encodings. +func CJKEncoding(s string) bool { + return types.MemberOf(s, []string{"UniGB-UTF16-H", "UniCNS-UTF16-H", "UniJIS-UTF16-H", "UniKS-UTF16-H"}) +} + +func fontDescriptorIndRefs(fd types.Dict, lang string, font *model.FontResource) error { + if lang != "" { + if s := fd.NameEntry("Lang"); s != nil { + if strings.ToLower(*s) != lang { + return ErrCorruptFontDict + } + } + } + + font.CIDSet = fd.IndirectRefEntry("CIDSet") + if font.CIDSet == nil { + return ErrCorruptFontDict + } + + font.FontFile = fd.IndirectRefEntry("FontFile2") + if font.FontFile == nil { + return ErrCorruptFontDict + } + + return nil +} + +// IndRefsForUserfontUpdate detects used indirect references for a possible user font update. +func IndRefsForUserfontUpdate(xRefTable *model.XRefTable, d types.Dict, lang string, font *model.FontResource) error { + if enc := d.NameEntry("Encoding"); enc == nil || *enc != "Identity-H" { + return ErrCorruptFontDict + } + + // TODO some indRefs may be direct objs => don't reuse userFont. + + font.ToUnicode = d.IndirectRefEntry("ToUnicode") + if font.ToUnicode == nil { + return ErrCorruptFontDict + } + + o, found := d.Find("DescendantFonts") + if !found { + return ErrCorruptFontDict + } + + a, err := xRefTable.DereferenceArray(o) + if err != nil { + return err + } + + if len(a) != 1 { + return ErrCorruptFontDict + } + + df, err := xRefTable.DereferenceDict(a[0]) + if err != nil { + return err + } + + font.W = df.IndirectRefEntry("W") + if font.W == nil { + return ErrCorruptFontDict + } + + o, found = df.Find("FontDescriptor") + if !found { + return ErrCorruptFontDict + } + + fd, err := xRefTable.DereferenceDict(o) + if err != nil { + return err + } + + return fontDescriptorIndRefs(fd, lang, font) +} + +func flateEncodedStreamIndRef(xRefTable *model.XRefTable, data []byte) (*types.IndirectRef, error) { + sd, _ := xRefTable.NewStreamDictForBuf(data) + sd.InsertInt("Length1", len(data)) + if err := sd.Encode(); err != nil { + return nil, err + } + return xRefTable.IndRefForNewObject(*sd) +} + +func ttfFontFile(xRefTable *model.XRefTable, ttf font.TTFLight, fontName string) (*types.IndirectRef, error) { + bb, err := font.Read(fontName) + if err != nil { + return nil, err + } + return flateEncodedStreamIndRef(xRefTable, bb) +} + +func ttfSubFontFile(xRefTable *model.XRefTable, ttf font.TTFLight, fontName string, indRef *types.IndirectRef) (*types.IndirectRef, error) { + bb, err := font.Subset(fontName, xRefTable.UsedGIDs[fontName]) + if err != nil { + return nil, err + } + if indRef == nil { + return flateEncodedStreamIndRef(xRefTable, bb) + } + entry, _ := xRefTable.FindTableEntryForIndRef(indRef) + sd, _ := entry.Object.(types.StreamDict) + sd.Content = bb + sd.InsertInt("Length1", len(bb)) + if err := sd.Encode(); err != nil { + return nil, err + } + entry.Object = sd + return indRef, nil +} + +func PDFDocEncoding(xRefTable *model.XRefTable) (*types.IndirectRef, error) { + arr := types.Array{ + types.Integer(24), + types.Name("breve"), types.Name("caron"), types.Name("circumflex"), types.Name("dotaccent"), + types.Name("hungarumlaut"), types.Name("ogonek"), types.Name("ring"), types.Name("tilde"), + types.Integer(39), + types.Name("quotesingle"), + types.Integer(96), + types.Name("grave"), + types.Integer(128), + types.Name("bullet"), types.Name("dagger"), types.Name("daggerdbl"), types.Name("ellipsis"), types.Name("emdash"), types.Name("endash"), + types.Name("florin"), types.Name("fraction"), types.Name("guilsinglleft"), types.Name("guilsinglright"), types.Name("minus"), types.Name("perthousand"), + types.Name("quotedblbase"), types.Name("quotedblleft"), types.Name("quotedblright"), types.Name("quoteleft"), types.Name("quoteright"), types.Name("quotesinglbase"), + types.Name("trademark"), types.Name("fi"), types.Name("fl"), types.Name("Lslash"), types.Name("OE"), types.Name("Scaron"), types.Name("Ydieresis"), + types.Name("Zcaron"), types.Name("dotlessi"), types.Name("lslash"), types.Name("oe"), types.Name("scaron"), types.Name("zcaron"), + types.Integer(160), + types.Name("Euro"), + types.Integer(164), + types.Name("currency"), + types.Integer(166), + types.Name("brokenbar"), types.Integer(168), types.Name("dieresis"), types.Name("copyright"), types.Name("ordfeminine"), + types.Integer(172), + types.Name("logicalnot"), types.Name(".notdef"), types.Name("registered"), types.Name("macron"), types.Name("degree"), + types.Name("plusminus"), types.Name("twosuperior"), types.Name("threesuperior"), types.Name("acute"), types.Name("mu"), + types.Integer(183), + types.Name("periodcentered"), types.Name("cedilla"), types.Name("onesuperior"), types.Name("ordmasculine"), + types.Integer(188), + types.Name("onequarter"), types.Name("onehalf"), types.Name("threequarters"), + types.Integer(192), + types.Name("Agrave"), types.Name("Aacute"), types.Name("Acircumflex"), types.Name("Atilde"), types.Name("Adieresis"), types.Name("Aring"), types.Name("AE"), + types.Name("Ccedilla"), types.Name("Egrave"), types.Name("Eacute"), types.Name("Ecircumflex"), types.Name("Edieresis"), types.Name("Igrave"), types.Name("Iacute"), + types.Name("Icircumflex"), types.Name("Idieresis"), types.Name("Eth"), types.Name("Ntilde"), types.Name("Ograve"), types.Name("Oacute"), types.Name("Ocircumflex"), + types.Name("Otilde"), types.Name("Odieresis"), types.Name("multiply"), types.Name("Oslash"), types.Name("Ugrave"), types.Name("Uacute"), types.Name("Ucircumflex"), + types.Name("Udieresis"), types.Name("Yacute"), types.Name("Thorn"), types.Name("germandbls"), types.Name("agrave"), types.Name("aacute"), types.Name("acircumflex"), + types.Name("atilde"), types.Name("adieresis"), types.Name("aring"), types.Name("ae"), types.Name("ccedilla"), types.Name("egrave"), types.Name("eacute"), types.Name("ecircumflex"), + types.Name("edieresis"), types.Name("igrave"), types.Name("iacute"), types.Name("icircumflex"), types.Name("idieresis"), types.Name("eth"), types.Name("ntilde"), + types.Name("ograve"), types.Name("oacute"), types.Name("ocircumflex"), types.Name("otilde"), types.Name("odieresis"), types.Name("divide"), types.Name("oslash"), + types.Name("ugrave"), types.Name("uacute"), types.Name("ucircumflex"), types.Name("udieresis"), types.Name("yacute"), types.Name("thorn"), types.Name("ydieresis"), + } + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Encoding"), + "Differences": arr, + }, + ) + + return xRefTable.IndRefForNewObject(d) +} + +func coreFontDict(xRefTable *model.XRefTable, coreFontName string) (*types.IndirectRef, error) { + d := types.NewDict() + d.InsertName("Type", "Font") + d.InsertName("Subtype", "Type1") + d.InsertName("BaseFont", coreFontName) + if coreFontName != "Symbol" && coreFontName != "ZapfDingbats" { + d.InsertName("Encoding", "WinAnsiEncoding") + } + return xRefTable.IndRefForNewObject(d) +} + +// CIDSet computes a CIDSet for used glyphs and updates or returns a new object. +func CIDSet(xRefTable *model.XRefTable, ttf font.TTFLight, fontName string, indRef *types.IndirectRef) (*types.IndirectRef, error) { + bb := make([]byte, ttf.GlyphCount/8+1) + usedGIDs, ok := xRefTable.UsedGIDs[fontName] + if ok { + for gid := range usedGIDs { + bb[gid/8] |= 1 << (7 - (gid % 8)) + } + } + if indRef == nil { + return flateEncodedStreamIndRef(xRefTable, bb) + } + entry, _ := xRefTable.FindTableEntryForIndRef(indRef) + sd, _ := entry.Object.(types.StreamDict) + sd.Content = bb + sd.InsertInt("Length1", len(bb)) + if err := sd.Encode(); err != nil { + return nil, err + } + entry.Object = sd + return indRef, nil +} + +func ttfFontDescriptorFlags(ttf font.TTFLight) uint32 { + // Bits: + // 1 FixedPitch + // 2 Serif + // 3 Symbolic + // 4 Script/cursive + // 6 Nonsymbolic + // 7 Italic + // 17 AllCap + + flags := uint32(0) + + // Bit 1 + //fmt.Printf("fixedPitch: %t\n", ttf.FixedPitch) + if ttf.FixedPitch { + flags |= 0x01 + } + + // Bit 6 Set for non symbolic + // Note: Symbolic fonts are unsupported. + flags |= 0x20 + + // Bit 7 + //fmt.Printf("italicAngle: %f\n", ttf.ItalicAngle) + if ttf.ItalicAngle != 0 { + flags |= 0x40 + } + + //fmt.Printf("flags: %08x\n", flags) + + return flags +} + +// CIDFontFile returns a TrueType font file or subfont file for fontName. +func CIDFontFile(xRefTable *model.XRefTable, ttf font.TTFLight, fontName string, subFont bool) (*types.IndirectRef, error) { + if subFont { + return ttfSubFontFile(xRefTable, ttf, fontName, nil) + } + return ttfFontFile(xRefTable, ttf, fontName) +} + +// CIDFontDescriptor returns a font descriptor describing the CIDFont’s default metrics other than its glyph widths. +func CIDFontDescriptor(xRefTable *model.XRefTable, ttf font.TTFLight, fontName, baseFontName, fontLang string, embed bool) (*types.IndirectRef, error) { + var ( + fontFile *types.IndirectRef + err error + ) + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("FontDescriptor"), + "FontName": types.Name(baseFontName), + "Flags": types.Integer(ttfFontDescriptorFlags(ttf)), + "FontBBox": types.NewNumberArray(ttf.LLx, ttf.LLy, ttf.URx, ttf.URy), + "ItalicAngle": types.Float(ttf.ItalicAngle), + "Ascent": types.Integer(ttf.Ascent), + "Descent": types.Integer(ttf.Descent), + "CapHeight": types.Integer(ttf.CapHeight), + "StemV": types.Integer(70), // Irrelevant for embedded files. + }, + ) + + if embed { + fontFile, err = CIDFontFile(xRefTable, ttf, fontName, true) + if err != nil { + return nil, err + } + d["FontFile2"] = *fontFile + } + + if embed { + // (Optional) + // A stream identifying which CIDs are present in the CIDFont file. If this entry is present, + // the CIDFont shall contain only a subset of the glyphs in the character collection defined by the CIDSystemInfo dictionary. + // If it is absent, the only indication of a CIDFont subset shall be the subset tag in the FontName entry (see 9.6.4, "Font Subsets"). + // The stream’s data shall be organized as a table of bits indexed by CID. + // The bits shall be stored in bytes with the high-order bit first. Each bit shall correspond to a CID. + // The most significant bit of the first byte shall correspond to CID 0, the next bit to CID 1, and so on. + cidSetIndRef, err := CIDSet(xRefTable, ttf, fontName, nil) + if err != nil { + return nil, err + } + d["CIDSet"] = *cidSetIndRef + } + + if fontLang != "" { + d["Lang"] = types.Name(fontLang) + } + + return xRefTable.IndRefForNewObject(d) +} + +// FontDescriptor returns a TrueType font descriptor describing font’s default metrics other than its glyph widths. +func FontDescriptor(xRefTable *model.XRefTable, ttf font.TTFLight, fontName, fontLang string) (*types.IndirectRef, error) { + fontFile, err := ttfFontFile(xRefTable, ttf, fontName) + if err != nil { + return nil, err + } + + d := types.Dict( + map[string]types.Object{ + "Ascent": types.Integer(ttf.Ascent), + "CapHeight": types.Integer(ttf.CapHeight), + "Descent": types.Integer(ttf.Descent), + "Flags": types.Integer(ttfFontDescriptorFlags(ttf)), + "FontBBox": types.NewNumberArray(ttf.LLx, ttf.LLy, ttf.URx, ttf.URy), + "FontFamily": types.StringLiteral(fontName), + "FontFile2": *fontFile, + "FontName": types.Name(fontName), + "ItalicAngle": types.Float(ttf.ItalicAngle), + "StemV": types.Integer(70), // Irrelevant for embedded files. + "Type": types.Name("FontDescriptor"), + }, + ) + + if fontLang != "" { + d["Lang"] = types.Name(fontLang) + } + + return xRefTable.IndRefForNewObject(d) +} + +func wArr(ttf font.TTFLight, from, thru int) types.Array { + a := types.Array{} + for i := from; i <= thru; i++ { + a = append(a, types.Integer(ttf.GlyphWidths[i])) + } + return a +} + +func prepGids(xRefTable *model.XRefTable, ttf font.TTFLight, fontName string, used bool) []int { + gids := ttf.GlyphWidths + if used { + usedGIDs, ok := xRefTable.UsedGIDs[fontName] + if ok { + gids = make([]int, 0, len(usedGIDs)) + for gid := range usedGIDs { + gids = append(gids, int(gid)) + } + sort.Ints(gids) + } + } + return gids +} + +func handleEqualWidths(w, w0, wl, g, g0, gl *int, a *types.Array, skip, equalWidths *bool) { + if *w == 1000 || *w != *wl || *g-*gl > 1 { + // cutoff or switch to non-contiguous width block + *a = append(*a, types.Integer(*g0), types.Integer(*gl), types.Integer(*w0)) // write last contiguous width block + if *w == 1000 { + // cutoff via default + *skip = true + } else { + *g0, *w0 = *g, *w + *gl, *wl = *g0, *w0 + } + *equalWidths = false + } else { + // Remain in contiguous width block + *gl = *g + } +} + +func finalizeWidths(ttf font.TTFLight, w0, g0, gl int, skip, equalWidths bool, a *types.Array) { + if !skip { + if equalWidths { + // write last contiguous width block + *a = append(*a, types.Integer(g0), types.Integer(gl), types.Integer(w0)) + } else { + // write last non-contiguous width block + *a = append(*a, types.Integer(g0)) + a1 := wArr(ttf, g0, gl) + *a = append(*a, a1) + } + } +} + +func calcWidthArray(xRefTable *model.XRefTable, ttf font.TTFLight, fontName string, used bool) types.Array { + gids := prepGids(xRefTable, ttf, fontName, used) + a := types.Array{} + var g0, w0, gl, wl int + start, equalWidths, skip := true, false, false + + for g, w := range gids { + if used { + g = w + w = ttf.GlyphWidths[g] + } + + if start { + start = false + if w == 1000 { + skip = true + continue + } + g0, w0 = g, w + gl, wl = g0, w0 + continue + } + + if skip { + if w != 1000 { + g0, w0 = g, w + gl, wl = g0, w0 + skip, equalWidths = false, false + } + continue + } + + if equalWidths { + handleEqualWidths(&w, &w0, &wl, &g, &g0, &gl, &a, &skip, &equalWidths) + continue + } + + // Non-contiguous + + if w == 1000 { + // cutoff via default + a = append(a, types.Integer(g0)) // write non-contiguous width block + a1 := wArr(ttf, g0, gl) + a = append(a, a1) + skip = true + continue + } + + if g-gl > 1 { + // cutoff via gap for subsets only. + a = append(a, types.Integer(g0)) // write non-contiguous width block + a1 := wArr(ttf, g0, gl) + a = append(a, a1) + g0, w0 = g, w + gl, wl = g0, w0 + continue + } + + if w == wl { + if g-g0 > 1 { + // switch from non equalW to equalW + a = append(a, types.Integer(g0)) // write non-contiguous width block + tru := gl - 1 + if tru < g0 { + tru = g0 + } + a1 := wArr(ttf, g0, tru) + a = append(a, a1) + g0, w0 = gl, wl + } + // just started. + // switch to contiguous width + equalWidths = true + gl = g + continue + } + + // Remain in non-contiguous width block + gl, wl = g, w + } + + finalizeWidths(ttf, w0, g0, gl, skip, equalWidths, &a) + + return a +} + +// CIDWidths returns the value for W in a CIDFontDict. +func CIDWidths(xRefTable *model.XRefTable, ttf font.TTFLight, fontName string, subFont bool, indRef *types.IndirectRef) (*types.IndirectRef, error) { + a := calcWidthArray(xRefTable, ttf, fontName, subFont) + if len(a) == 0 { + return nil, nil + } + + if indRef == nil { + return xRefTable.IndRefForNewObject(a) + } + + entry, _ := xRefTable.FindTableEntryForIndRef(indRef) + entry.Object = a + + return indRef, nil +} + +// Widths returns the value for Widths in a TrueType FontDict. +func Widths(xRefTable *model.XRefTable, ttf font.TTFLight, first, last int) (*types.IndirectRef, error) { + a := types.Array{} + for i := first; i < last; i++ { + pos, ok := ttf.Chars[uint32(i)] + if !ok { + pos = 0 // should be the "invalid char" + } + a = append(a, types.Integer(ttf.GlyphWidths[pos])) + } + return xRefTable.IndRefForNewObject(a) +} + +func bf(b *bytes.Buffer, ttf font.TTFLight, usedGIDs map[uint16]bool, subFont bool) { + var gids []int + if subFont { + gids = make([]int, 0, len(usedGIDs)) + for gid := range usedGIDs { + gids = append(gids, int(gid)) + } + } else { + gids = ttf.Gids() + } + sort.Ints(gids) + + c := 100 + if len(gids) < 100 { + c = len(gids) + } + l := c + + fmt.Fprintf(b, "%d beginbfchar\n", c) + j := 1 + for i := 0; i < l; i++ { + gid := gids[i] + fmt.Fprintf(b, "<%04X> <", gid) + u := ttf.ToUnicode[uint16(gid)] + s := utf16.Encode([]rune{rune(u)}) + for _, v := range s { + fmt.Fprintf(b, "%04X", v) + } + fmt.Fprintf(b, ">\n") + if j%100 == 0 { + b.WriteString("endbfchar\n") + if l-i < 100 { + c = l - i + } + fmt.Fprintf(b, "%d beginbfchar\n", c) + } + j++ + } + b.WriteString("endbfchar\n") +} + +// toUnicodeCMap returns a stream dict containing a CMap file that maps character codes to Unicode values (see 9.10). +func toUnicodeCMap(xRefTable *model.XRefTable, ttf font.TTFLight, fontName string, subFont bool, indRef *types.IndirectRef) (*types.IndirectRef, error) { + // n beginbfchar + // srcCode dstString + // <003A> <0037> : 003a:0037 + // <3A51> : 3a51:d840dc3e + // ... + // endbfchar + + // n beginbfrange + // srcCode1 srcCode2 dstString + // <0000> <005E> <0020> : 0000:0020 0001:0021 0002:0022 ... + // <005F> <0061> [<00660066> <00660069> <00660066006C>] : 005F:00660066 0060:00660069 0061:00660066006C + // endbfrange + + pro := `/CIDInit /ProcSet findresource begin +12 dict begin +begincmap +/CIDSystemInfo << + /Registry (Adobe) + /Ordering (UCS) + /Supplement 0 +>> def +/CMapName /Adobe-Identity-UCS def +/CMapType 2 def +` + + r := `1 begincodespacerange +<0000> +endcodespacerange +` + + epi := `endcmap +CMapName currentdict /CMap defineresource pop +end +end` + + var b bytes.Buffer + b.WriteString(pro) + b.WriteString(r) + usedGIDs := xRefTable.UsedGIDs[fontName] + if usedGIDs == nil { + usedGIDs = map[uint16]bool{} + } + bf(&b, ttf, usedGIDs, subFont) + b.WriteString(epi) + + bb := b.Bytes() + + if indRef == nil { + return flateEncodedStreamIndRef(xRefTable, bb) + } + + entry, _ := xRefTable.FindTableEntryForIndRef(indRef) + sd, _ := entry.Object.(types.StreamDict) + sd.Content = bb + sd.InsertInt("Length1", len(bb)) + if err := sd.Encode(); err != nil { + return nil, err + } + entry.Object = sd + + return indRef, nil +} + +var ( + errCorruptCMap = errors.New("pdfcpu: corrupt CMap") + ErrCorruptFontDict = errors.New("pdfcpu: corrupt fontDict") +) + +func usedGIDsFromCMap(cMap string) ([]uint16, error) { + gids := []uint16{} + i := strings.Index(cMap, "endcodespacerange") + if i < 0 { + return nil, errCorruptCMap + } + scanner := bufio.NewScanner(strings.NewReader(cMap[i+len("endcodespacerange")+1:])) + + // scanLine: %d beginbfchar + scanner.Scan() + s := scanner.Text() + + var lastBlock bool + + for { + ss := strings.Split(s, " ") + i, err := strconv.Atoi(ss[0]) + if err != nil { + return nil, errCorruptCMap + } + + lastBlock = i < 100 + + // scan i lines: + for j := 0; j < i; j++ { + scanner.Scan() + s1 := scanner.Text() + if s1[0] != '<' { + return nil, errCorruptCMap + } + bb, err := hex.DecodeString(s1[1:5]) + if err != nil { + return nil, errCorruptCMap + } + gid := binary.BigEndian.Uint16(bb) + gids = append(gids, gid) + } + + // scanLine: endbfchar + scanner.Scan() + if scanner.Text() != "endbfchar" { + return nil, errCorruptCMap + } + + // scanLine: endcmap => done, or %d beginbfchar + scanner.Scan() + s = scanner.Text() + if s == "endcmap" { + break + } + if lastBlock { + return nil, errCorruptCMap + } + } + + return gids, nil +} + +// UpdateUserfont updates the fontdict for fontName via supplied font resource. +func UpdateUserfont(xRefTable *model.XRefTable, fontName string, f model.FontResource) error { + font.UserFontMetricsLock.RLock() + ttf, ok := font.UserFontMetrics[fontName] + font.UserFontMetricsLock.RUnlock() + + if !ok { + return errors.Errorf("pdfcpu: userfont %s not available", fontName) + } + + if err := usedGIDsFromCMapIndRef(xRefTable, fontName, *f.ToUnicode); err != nil { + return err + } + + if _, err := toUnicodeCMap(xRefTable, ttf, fontName, true, f.ToUnicode); err != nil { + return err + } + + if _, err := ttfSubFontFile(xRefTable, ttf, fontName, f.FontFile); err != nil { + return err + } + + if _, err := CIDWidths(xRefTable, ttf, fontName, true, f.W); err != nil { + return err + } + + if _, err := CIDSet(xRefTable, ttf, fontName, f.CIDSet); err != nil { + return err + } + + return nil +} + +func usedGIDsFromCMapIndRef(xRefTable *model.XRefTable, fontName string, cmapIndRef types.IndirectRef) error { + sd, _, err := xRefTable.DereferenceStreamDict(cmapIndRef) + if err != nil { + return err + } + if err := sd.Decode(); err != nil { + return err + } + gids, err := usedGIDsFromCMap(string(sd.Content)) + if err != nil { + return err + } + m, ok := xRefTable.UsedGIDs[fontName] + if !ok { + m = map[uint16]bool{} + xRefTable.UsedGIDs[fontName] = m + } + for _, gid := range gids { + m[gid] = true + } + return nil +} + +func subFontPrefix() string { + s := "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + var r *rand.Rand = rand.New(rand.NewSource(time.Now().UnixNano())) + bb := make([]byte, 6) + for i := range bb { + bb[i] = s[r.Intn(len(s))] + } + return string(bb) +} + +// CIDFontDict returns the descendant font dict with special encoding for Type0 fonts. +func CIDFontDict(xRefTable *model.XRefTable, ttf font.TTFLight, fontName, baseFontName, lang string, parms *cjk) (*types.IndirectRef, error) { + fdIndRef, err := CIDFontDescriptor(xRefTable, ttf, fontName, baseFontName, lang, parms == nil) + if err != nil { + return nil, err + } + + ordering := "Identity" + if parms != nil { + ordering = parms.ordering + } + + supplement := 0 + if parms != nil { + supplement = parms.supplement + } + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Font"), + "Subtype": types.Name("CIDFontType2"), + "BaseFont": types.Name(baseFontName), + "CIDSystemInfo": types.Dict( + map[string]types.Object{ + "Ordering": types.StringLiteral(ordering), + "Registry": types.StringLiteral("Adobe"), + "Supplement": types.Integer(supplement), + }, + ), + "FontDescriptor": *fdIndRef, + + // (Optional) + // The default width for glyphs in the CIDFont (see 9.7.4.3, "Glyph Metrics in CIDFonts"). + // Default value: 1000 (defined in user units). + // "DW": types.Integer(1000), + + // (Optional) + // A description of the widths for the glyphs in the CIDFont. + // The array’s elements have a variable format that can specify individual widths for consecutive CIDs + // or one width for a range of CIDs (see 9.7.4.3, "Glyph Metrics in CIDFonts"). + // Default value: none (the DW value shall be used for all glyphs). + //"W": *wIndRef, + + // (Optional; applies only to CIDFonts used for vertical writing) + // An array of two numbers specifying the default metrics for vertical writing (see 9.7.4.3, "Glyph Metrics in CIDFonts"). + // Default value: [880 −1000]. + // "DW2": Integer(1000), + + // (Optional; applies only to CIDFonts used for vertical writing) + // A description of the metrics for vertical writing for the glyphs in the CIDFont (see 9.7.4.3, "Glyph Metrics in CIDFonts"). + // Default value: none (the DW2 value shall be used for all glyphs). + // "W2": nil, + }, + ) + + // (Optional; Type 2 CIDFonts only) + // A specification of the mapping from CIDs to glyph indices. + // maps CIDs to the glyph indices for the appropriate glyph descriptions in that font program. + // if stream: the glyph index for a particular CID value c shall be a 2-byte value stored in bytes 2 × c and 2 × c + 1, + // where the first byte shall be the high-order byte.)) + if ordering == "Identity" { + d["CIDToGIDMap"] = types.Name("Identity") + } + + if parms == nil { + wIndRef, err := CIDWidths(xRefTable, ttf, fontName, parms == nil, nil) + if err != nil { + return nil, err + } + if wIndRef != nil { + d["W"] = *wIndRef + } + } + + return xRefTable.IndRefForNewObject(d) +} + +func type0FontDict(xRefTable *model.XRefTable, fontName, lang, script string, indRef *types.IndirectRef) (*types.IndirectRef, error) { + font.UserFontMetricsLock.RLock() + ttf, ok := font.UserFontMetrics[fontName] + font.UserFontMetricsLock.RUnlock() + if !ok { + return nil, errors.Errorf("pdfcpu: font %s not available", fontName) + } + + subFont := script == "" + + // For consecutive pages or if no AP present using this font. + if indRef != nil && subFont && !xRefTable.HasUsedGIDs(fontName) { + if obj, _ := xRefTable.Dereference(*indRef); obj != nil { + return indRef, nil + } + } + + baseFontName := fontName + if subFont { + baseFontName = subFontPrefix() + "+" + fontName + } + + var parms *cjk + if p, ok := cjkParms[script]; ok { + parms = &p + } + + encoding := "Identity-H" + if parms != nil { + encoding = parms.encoding + } + + descendentFontIndRef, err := CIDFontDict(xRefTable, ttf, fontName, baseFontName, lang, parms) + if err != nil { + return nil, err + } + + d := types.NewDict() + d.InsertName("Type", "Font") + d.InsertName("Subtype", "Type0") + d.InsertName("BaseFont", baseFontName) + d.InsertName("Name", fontName) + d.InsertName("Encoding", encoding) + d.Insert("DescendantFonts", types.Array{*descendentFontIndRef}) + + if subFont { + toUnicodeIndRef, err := toUnicodeCMap(xRefTable, ttf, fontName, subFont, nil) + if err != nil { + return nil, err + } + d.Insert("ToUnicode", *toUnicodeIndRef) + } + + if subFont { + // Reset used glyph ids. + delete(xRefTable.UsedGIDs, fontName) + } + + if indRef == nil { + return xRefTable.IndRefForNewObject(d) + } + + entry, _ := xRefTable.FindTableEntryForIndRef(indRef) + entry.Object = d + + return indRef, nil +} + +func trueTypeFontDict(xRefTable *model.XRefTable, fontName, fontLang string) (*types.IndirectRef, error) { + font.UserFontMetricsLock.RLock() + ttf, ok := font.UserFontMetrics[fontName] + font.UserFontMetricsLock.RUnlock() + if !ok { + return nil, errors.Errorf("pdfcpu: font %s not available", fontName) + } + + first, last := 0, 255 + wIndRef, err := Widths(xRefTable, ttf, first, last) + if err != nil { + return nil, err + } + + fdIndRef, err := FontDescriptor(xRefTable, ttf, fontName, fontLang) + if err != nil { + return nil, err + } + + d := types.NewDict() + d.InsertName("Type", "Font") + d.InsertName("Subtype", "TrueType") + d.InsertName("BaseFont", fontName) + d.InsertName("Name", fontName) + d.InsertName("Encoding", "WinAnsiEncoding") + d.InsertInt("FirstChar", first) + d.InsertInt("LastChar", last) + d.Insert("Widths", *wIndRef) + d.Insert("FontDescriptor", *fdIndRef) + + return xRefTable.IndRefForNewObject(d) +} + +// CJK returns true if script and lang imply a CJK font. +func CJK(script, lang string) bool { + if script != "" { + _, ok := cjkParms[script] + return ok + } + return types.MemberOf(lang, []string{"ja", "ko", "zh"}) +} + +// RTL returns true if lang implies a right-to-left script. +func RTL(lang string) bool { + return types.MemberOf(lang, []string{"ar", "fa", "he", "ur"}) +} + +// EnsureFontDict ensures a font dict for fontName, lang, script. +func EnsureFontDict(xRefTable *model.XRefTable, fontName, lang, script string, field bool, indRef *types.IndirectRef) (*types.IndirectRef, error) { + if font.IsCoreFont(fontName) { + if indRef != nil { + return indRef, nil + } + return coreFontDict(xRefTable, fontName) + } + if field && (script == "" || !CJK(script, lang)) { + return trueTypeFontDict(xRefTable, fontName, lang) + } + return type0FontDict(xRefTable, fontName, lang, script, indRef) +} + +// FontResources returns a font resource dict for a font map. +func FontResources(xRefTable *model.XRefTable, fm model.FontMap) (types.Dict, error) { + d := types.Dict{} + + for fontName, font := range fm { + ir, err := EnsureFontDict(xRefTable, fontName, "", "", false, nil) + if err != nil { + return nil, err + } + d.Insert(font.Res.ID, *ir) + } + + return d, nil +} + +// Name evaluates the font name for a given font dict. +func Name(xRefTable *model.XRefTable, fontDict types.Dict, objNumber int) (prefix, fontName string, err error) { + var found bool + var o types.Object + + if *fontDict.Subtype() != "Type3" { + + o, found = fontDict.Find("BaseFont") + if !found { + o, found = fontDict.Find("Name") + if !found { + return "", "", errors.New("pdfcpu: fontName: missing fontDict entries \"BaseFont\" and \"Name\"") + } + } + + } else { + + // Type3 fonts only have Name in V1.0 else use generic name. + + o, found = fontDict.Find("Name") + if !found { + return "", fmt.Sprintf("Type3_%d", objNumber), nil + } + + } + + o, err = xRefTable.Dereference(o) + if err != nil { + return "", "", err + } + + baseFont, ok := o.(types.Name) + if !ok { + return "", "", errors.New("pdfcpu: fontName: corrupt fontDict entry BaseFont") + } + + n := string(baseFont) + + // Isolate Postscript prefix. + var p string + + i := strings.Index(n, "+") + + if i > 0 { + p = n[:i] + n = n[i+1:] + } + + return p, n, nil +} + +// Lang detects the optional language indicator in a font dict. +func Lang(xRefTable *model.XRefTable, d types.Dict) (string, error) { + o, found := d.Find("FontDescriptor") + if found { + fd, err := xRefTable.DereferenceDict(o) + if err != nil { + return "", err + } + var s string + n := fd.NameEntry("Lang") + if n != nil { + s = *n + } + return s, nil + } + + o, found = d.Find("DescendantFonts") + if !found { + return "", ErrCorruptFontDict + } + + arr, err := xRefTable.DereferenceArray(o) + if err != nil { + return "", err + } + + indRef := arr[0].(types.IndirectRef) + d1, err := xRefTable.DereferenceDict(indRef) + if err != nil { + return "", err + } + o, found = d1.Find("FontDescriptor") + if found { + fd, err := xRefTable.DereferenceDict(o) + if err != nil { + return "", err + } + var s string + n := fd.NameEntry("Lang") + if n != nil { + s = *n + } + return s, nil + } + + return "", nil +} diff --git a/pkg/pdfcpu/form/export.go b/pkg/pdfcpu/form/export.go new file mode 100644 index 0000000000000000000000000000000000000000..bd4145aec4636f975945977e1c3b72974ff2236f --- /dev/null +++ b/pkg/pdfcpu/form/export.go @@ -0,0 +1,778 @@ +/* +Copyright 2023 The pdfcpu Authors. + +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 + + 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 form + +import ( + "encoding/json" + "io" + "path/filepath" + "strings" + "time" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// Header represents form meta data. +type Header struct { + Source string `json:"source"` + Version string `json:"version"` + Creation string `json:"creation"` + ID []string `json:"id,omitempty"` + Title string `json:"title,omitempty"` + Author string `json:"author,omitempty"` + Creator string `json:"creator,omitempty"` + Producer string `json:"producer,omitempty"` + Subject string `json:"subject,omitempty"` + Keywords string `json:"keywords,omitempty"` +} + +// TextField represents a form text field. +type TextField struct { + Pages []int `json:"pages"` + ID string `json:"id"` + Name string `json:"name,omitempty"` + Default string `json:"default,omitempty"` + Value string `json:"value"` + Multiline bool `json:"multiline"` + Locked bool `json:"locked"` +} + +// DateField represents an Acroform date field. +type DateField struct { + Pages []int `json:"pages"` + ID string `json:"id"` + Name string `json:"name,omitempty"` + Format string `json:"format"` + Default string `json:"default,omitempty"` + Value string `json:"value"` + Locked bool `json:"locked"` +} + +// RadioButtonGroup represents a form checkbox. +type CheckBox struct { + Pages []int `json:"pages"` + ID string `json:"id"` + Name string `json:"name,omitempty"` + Default bool `json:"default"` + Value bool `json:"value"` + Locked bool `json:"locked"` +} + +// RadioButtonGroup represents a form radio button group. +type RadioButtonGroup struct { + Pages []int `json:"pages"` + ID string `json:"id"` + Name string `json:"name,omitempty"` + Options []string `json:"options"` + Default string `json:"default,omitempty"` + Value string `json:"value"` + Locked bool `json:"locked"` +} + +// ComboBox represents a form combobox. +type ComboBox struct { + Pages []int `json:"pages"` + ID string `json:"id"` + Name string `json:"name,omitempty"` + Editable bool `json:"editable"` + Options []string `json:"options"` + Default string `json:"default,omitempty"` + Value string `json:"value"` + Locked bool `json:"locked"` +} + +// ListBox represents a form listbox. +type ListBox struct { + Pages []int `json:"pages"` + ID string `json:"id"` + Name string `json:"name,omitempty"` + Multi bool `json:"multi"` + Options []string `json:"options"` + Defaults []string `json:"defaults,omitempty"` + Values []string `json:"values,omitempty"` + Locked bool `json:"locked"` +} + +// Page is a container for page imageboxes. +type Page struct { + ImageBoxes []*primitives.ImageBox `json:"image,omitempty"` +} + +// Form represents a PDF form (aka. Acroform). +type Form struct { + TextFields []*TextField `json:"textfield,omitempty"` + DateFields []*DateField `json:"datefield,omitempty"` + CheckBoxes []*CheckBox `json:"checkbox,omitempty"` + RadioButtonGroups []*RadioButtonGroup `json:"radiobuttongroup,omitempty"` + ComboBoxes []*ComboBox `json:"combobox,omitempty"` + ListBoxes []*ListBox `json:"listbox,omitempty"` + Pages map[string]*Page `json:"pages,omitempty"` +} + +// FormGroup represents a JSON struct containing a sequence of form instances. +type FormGroup struct { + Header Header `json:"header"` + Forms []Form `json:"forms"` +} + +func (f Form) textFieldValueAndLock(id, name string) (string, bool, bool) { + for _, tf := range f.TextFields { + if tf.ID == id || tf.Name == name { + return tf.Value, tf.Locked, true + } + } + return "", false, false +} + +func (f Form) dateFieldValueAndLock(id, name string) (string, bool, bool) { + for _, df := range f.DateFields { + if df.ID == id || df.Name == name { + return df.Value, df.Locked, true + } + } + return "", false, false +} + +func (f Form) checkBoxValueAndLock(id, name string) (bool, bool, bool) { + for _, cb := range f.CheckBoxes { + if cb.ID == id || cb.Name == name { + return cb.Value, cb.Locked, true + } + } + return false, false, false +} + +func (f Form) radioButtonGroupValueAndLock(id, name string) (string, bool, bool) { + for _, rbg := range f.RadioButtonGroups { + if rbg.ID == id || rbg.Name == name { + return rbg.Value, rbg.Locked, true + } + } + return "", false, false +} + +func (f Form) comboBoxValueAndLock(id, name string) (string, bool, bool) { + for _, cb := range f.ComboBoxes { + if cb.ID == id || cb.Name == name { + return cb.Value, cb.Locked, true + } + } + return "", false, false +} + +func (f Form) listBoxValuesAndLock(id, name string) ([]string, bool, bool) { + for _, lb := range f.ListBoxes { + if lb.ID == id || lb.Name == name { + return lb.Values, lb.Locked, true + } + } + return nil, false, false +} + +func extractRadioButtonGroupOptions(xRefTable *model.XRefTable, d types.Dict) ([]string, error) { + + var opts []string + p := 0 + + for _, o := range d.ArrayEntry("Kids") { + d, err := xRefTable.DereferenceDict(o) + if err != nil { + return nil, err + } + + indRef := d.IndirectRefEntry("P") + if indRef != nil { + if p == 0 { + p = indRef.ObjectNumber.Value() + } else if p != indRef.ObjectNumber.Value() { + continue + } + } + + d1 := d.DictEntry("AP") + if d1 == nil { + return nil, errors.New("corrupt form field: missing entry AP") + } + d2 := d1.DictEntry("N") + if d2 == nil { + return nil, errors.New("corrupt AP field: missing entry N") + } + for k := range d2 { + k, err := types.DecodeName(k) + if err != nil { + return nil, err + } + if k != "Off" { + for _, opt := range opts { + if opt == k { + continue + } + } + opts = append(opts, k) + } + } + } + + return opts, nil +} + +func extractRadioButtonGroup(xRefTable *model.XRefTable, page int, d types.Dict, id, name string, locked bool) (*RadioButtonGroup, error) { + + rbg := &RadioButtonGroup{Pages: []int{page}, ID: id, Name: name, Locked: locked} + + if s := d.NameEntry("DV"); s != nil { + n, err := types.DecodeName(*s) + if err != nil { + return nil, err + } + rbg.Default = n + } + + if s := d.NameEntry("V"); s != nil { + n, err := types.DecodeName(*s) + if err != nil { + return nil, err + } + if n != "Off" { + rbg.Value = n + } + } + + opts, err := extractRadioButtonGroupOptions(xRefTable, d) + if err != nil { + return nil, err + } + + rbg.Options = opts + + return rbg, nil +} + +func extractCheckBox(page int, d types.Dict, id, name string, locked bool) (*CheckBox, error) { + + cb := &CheckBox{Pages: []int{page}, ID: id, Name: name, Locked: locked} + + if o, ok := d.Find("DV"); ok { + cb.Default = o.(types.Name) != "Off" + } + + if o, ok := d.Find("V"); ok { + cb.Value = o.(types.Name) != "Off" + } + + return cb, nil +} + +func extractComboBox(xRefTable *model.XRefTable, page int, d types.Dict, id, name string, locked bool) (*ComboBox, error) { + + cb := &ComboBox{Pages: []int{page}, ID: id, Name: name, Locked: locked} + + if sl := d.StringLiteralEntry("DV"); sl != nil { + s, err := types.StringLiteralToString(*sl) + if err != nil { + return nil, err + } + cb.Default = strings.TrimSpace(s) + } + + if sl := d.StringLiteralEntry("V"); sl != nil { + s, err := types.StringLiteralToString(*sl) + if err != nil { + return nil, err + } + cb.Value = strings.TrimSpace(s) + } + + opts, err := parseOptions(xRefTable, d) + if err != nil { + return nil, err + } + if len(opts) == 0 { + return nil, errors.New("pdfcpu: combobox missing Opts") + } + + cb.Options = opts + + return cb, nil +} + +func extractDateFormat(d types.Dict) (*primitives.DateFormat, error) { + + d1 := d.DictEntry("AA") + if len(d1) > 0 { + d2 := d1.DictEntry("F") + if len(d2) > 0 { + sl := d2.StringLiteralEntry("JS") + if sl != nil { + s, err := types.StringLiteralToString(*sl) + if err != nil { + return nil, err + } + i := strings.Index(s, "AFDate_FormatEx(\"") + if i >= 0 { + from := i + len("AFDate_FormatEx(\"") + s = s[from : from+10] + } + if df, err := primitives.DateFormatForFmtExt(s); err == nil { + return df, nil + } + } + } + } + + if o, found := d.Find("DV"); found { + sl, err := types.StringOrHexLiteral(o) + if err != nil { + return nil, err + } + s := "" + if sl != nil { + s = *sl + } + if df, err := primitives.DateFormatForDate(s); err == nil { + return df, nil + } + } + + if o, found := d.Find("V"); found { + sl, err := types.StringOrHexLiteral(o) + if err != nil { + return nil, err + } + s := "" + if sl != nil { + s = *sl + } + if df, err := primitives.DateFormatForDate(s); err == nil { + return df, nil + } + } + + return nil, nil +} + +func extractDateField(page int, d types.Dict, id, name string, df *primitives.DateFormat, locked bool) (*DateField, error) { + + dfield := &DateField{Pages: []int{page}, ID: id, Name: name, Format: df.Ext, Locked: locked} + + if o, found := d.Find("DV"); found { + sl, err := types.StringOrHexLiteral(o) + if err != nil { + return nil, err + } + dfield.Default = "" + if sl != nil { + dfield.Default = *sl + } + } + + if o, found := d.Find("V"); found { + sl, err := types.StringOrHexLiteral(o) + if err != nil { + return nil, err + } + dfield.Value = "" + if sl != nil { + dfield.Value = *sl + } + } + + return dfield, nil +} + +func extractTextField(page int, d types.Dict, id, name string, ff *int, locked bool) (*TextField, error) { + + multiLine := ff != nil && uint(primitives.FieldFlags(*ff))&uint(primitives.FieldMultiline) > 0 + + tf := &TextField{Pages: []int{page}, ID: id, Name: name, Multiline: multiLine, Locked: locked} + + if o, found := d.Find("DV"); found { + s, err := types.StringOrHexLiteral(o) + if err != nil { + return nil, err + } + tf.Default = "" + if s != nil { + tf.Default = *s + } + } + + if o, found := d.Find("V"); found { + s, err := types.StringOrHexLiteral(o) + if err != nil { + return nil, err + } + tf.Value = "" + if s != nil { + tf.Value = *s + } + } + + return tf, nil +} + +func extractListBox(xRefTable *model.XRefTable, page int, d types.Dict, id, name string, locked, multi bool) (*ListBox, error) { + + lb := &ListBox{Pages: []int{page}, ID: id, Name: name, Locked: locked, Multi: multi} + + if !multi { + if sl := d.StringLiteralEntry("DV"); sl != nil { + s, err := types.StringLiteralToString(*sl) + if err != nil { + return nil, err + } + lb.Defaults = []string{strings.TrimSpace(s)} + } + if sl := d.StringLiteralEntry("V"); sl != nil { + s, err := types.StringLiteralToString(*sl) + if err != nil { + return nil, err + } + lb.Values = []string{strings.TrimSpace(s)} + } + } else { + ss, err := parseStringLiteralArray(xRefTable, d, "DV") + if err != nil { + return nil, err + } + lb.Defaults = ss + ss, err = parseStringLiteralArray(xRefTable, d, "V") + if err != nil { + return nil, err + } + lb.Values = ss + } + + opts, err := parseOptions(xRefTable, d) + if err != nil { + return nil, err + } + if len(opts) == 0 { + return nil, errors.New("pdfcpu: listbox missing Opts") + } + + lb.Options = opts + + return lb, nil +} + +func header(xRefTable *model.XRefTable, source string) Header { + h := Header{} + h.Source = filepath.Base(source) + h.Version = "pdfcpu " + model.VersionStr + h.Creation = time.Now().Format("2006-01-02 15:04:05 MST") + h.ID = []string{} + h.Title = xRefTable.Title + h.Author = xRefTable.Author + h.Creator = xRefTable.Creator + h.Producer = xRefTable.Producer + h.Subject = xRefTable.Subject + h.Keywords = xRefTable.Keywords + return h +} + +func fieldsForAnnots(xRefTable *model.XRefTable, annots, fields types.Array) (map[string]fieldInfo, error) { + + m := map[string]fieldInfo{} + var prevId string + + for _, v := range annots { + + indRef := v.(types.IndirectRef) + + ok, fi, err := isField(xRefTable, indRef, fields) + if err != nil { + return nil, err + } + if !ok { + continue + } + + if fi.indRef == nil { + fi.indRef = &indRef + } + + if fi.id != prevId { + m[fi.id] = *fi + prevId = fi.id + } + } + + return m, nil +} + +func exportBtn( + xRefTable *model.XRefTable, + i int, + form *Form, + d types.Dict, + id, name string, + locked bool, + ok *bool) error { + + if len(d.ArrayEntry("Kids")) > 1 { + + for _, rb := range form.RadioButtonGroups { + if rb.ID == id && rb.Name == name { + rb.Pages = append(rb.Pages, i) + return nil + } + } + + rbg, err := extractRadioButtonGroup(xRefTable, i, d, id, name, locked) + if err != nil { + return err + } + + form.RadioButtonGroups = append(form.RadioButtonGroups, rbg) + *ok = true + return nil + } + + for _, cb := range form.CheckBoxes { + if cb.Name == name && cb.ID == id { + cb.Pages = append(cb.Pages, i) + return nil + } + } + + cb, err := extractCheckBox(i, d, id, name, locked) + if err != nil { + return err + } + + form.CheckBoxes = append(form.CheckBoxes, cb) + *ok = true + return nil +} + +func exportCh( + xRefTable *model.XRefTable, + i int, + form *Form, + d types.Dict, + id, name string, + locked bool, + ok *bool) error { + + ff := d.IntEntry("Ff") + if ff == nil { + return errors.New("pdfcpu: corrupt form field: missing entry Ff") + } + + if primitives.FieldFlags(*ff)&primitives.FieldCombo > 0 { + + for _, cb := range form.ComboBoxes { + if cb.Name == name && cb.ID == id { + cb.Pages = append(cb.Pages, i) + return nil + } + } + + cb, err := extractComboBox(xRefTable, i, d, id, name, locked) + if err != nil { + return err + } + form.ComboBoxes = append(form.ComboBoxes, cb) + *ok = true + return nil + } + + for _, lb := range form.ListBoxes { + if lb.Name == name && lb.ID == id { + lb.Pages = append(lb.Pages, i) + return nil + } + } + + multi := primitives.FieldFlags(*ff)&primitives.FieldMultiselect > 0 + lb, err := extractListBox(xRefTable, i, d, id, name, locked, multi) + if err != nil { + return err + } + + form.ListBoxes = append(form.ListBoxes, lb) + *ok = true + return nil +} + +func exportTx( + i int, + form *Form, + d types.Dict, + id, name string, + ff *int, + locked bool, + ok *bool) error { + + df, err := extractDateFormat(d) + if err != nil { + return err + } + + if df != nil { + + for _, df := range form.DateFields { + if df.Name == name && df.ID == id { + df.Pages = append(df.Pages, i) + return nil + } + } + + df, err := extractDateField(i, d, id, name, df, locked) + if err != nil { + return err + } + + form.DateFields = append(form.DateFields, df) + *ok = true + return nil + } + + for _, tf := range form.TextFields { + if tf.Name == name && tf.ID == id { + tf.Pages = append(tf.Pages, i) + return nil + } + } + + tf, err := extractTextField(i, d, id, name, ff, locked) + if err != nil { + return err + } + + form.TextFields = append(form.TextFields, tf) + *ok = true + return nil +} + +func exportPageFields(xRefTable *model.XRefTable, i int, form *Form, m map[string]fieldInfo, ok *bool) error { + for id, fi := range m { + + name := fi.name + + d, err := xRefTable.DereferenceDict(*fi.indRef) + if err != nil { + return err + } + if len(d) == 0 { + continue + } + + var locked bool + ff := d.IntEntry("Ff") + if ff != nil { + locked = uint(primitives.FieldFlags(*ff))&uint(primitives.FieldReadOnly) > 0 + } + + ft := fi.ft + if ft == nil { + ft = d.NameEntry("FT") + if ft == nil { + return errors.New("pdfcpu: corrupt form field: missing entry FT") + } + } + + switch *ft { + case "Btn": + if err := exportBtn(xRefTable, i, form, d, id, name, locked, ok); err != nil { + return err + } + + case "Ch": + if err := exportCh(xRefTable, i, form, d, id, name, locked, ok); err != nil { + return err + } + + case "Tx": + if err := exportTx(i, form, d, id, name, ff, locked, ok); err != nil { + return err + } + } + + } + + return nil +} + +// ExportForm extracts form data originating from source from xRefTable. +func ExportForm(xRefTable *model.XRefTable, source string) (*FormGroup, bool, error) { + + fields, err := fields(xRefTable) + if err != nil { + return nil, false, err + } + + formGroup := FormGroup{} + formGroup.Header = header(xRefTable, source) + + form := Form{} + + var ok bool + + for i := 1; i <= xRefTable.PageCount; i++ { + + d, _, _, err := xRefTable.PageDict(i, false) + if err != nil { + return nil, false, err + } + + o, found := d.Find("Annots") + if !found { + continue + } + + arr, err := xRefTable.DereferenceArray(o) + if err != nil { + return nil, false, err + } + + m, err := fieldsForAnnots(xRefTable, arr, fields) + if err != nil { + return nil, false, err + } + + if err := exportPageFields(xRefTable, i, &form, m, &ok); err != nil { + return nil, false, err + } + } + + formGroup.Forms = []Form{form} + + return &formGroup, ok, nil +} + +// ExportFormJSON extracts form data originating from source from xRefTable and writes a JSON representation to w. +func ExportFormJSON(xRefTable *model.XRefTable, source string, w io.Writer) (bool, error) { + + formGroup, ok, err := ExportForm(xRefTable, source) + if err != nil || !ok { + return false, err + } + + bb, err := json.MarshalIndent(formGroup, "", "\t") + if err != nil { + return false, err + } + + _, err = w.Write(bb) + + return ok, err +} diff --git a/pkg/pdfcpu/form/fill.go b/pkg/pdfcpu/form/fill.go new file mode 100644 index 0000000000000000000000000000000000000000..28515f09caa0ecaed74a7e31bda408d66566f04b --- /dev/null +++ b/pkg/pdfcpu/form/fill.go @@ -0,0 +1,1203 @@ +/* +Copyright 2023 The pdfcpu Authors. + +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 + + 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 form + +import ( + "bytes" + "sort" + "strconv" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/font" + pdffont "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/font" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +type DataFormat int + +const ( + CSV DataFormat = iota + JSON +) + +func cacheResIDs(ctx *model.Context, pdf *primitives.PDF) error { + // Iterate over all pages of ctx and prepare a resIds []string for inherited "Font" and "XObject" resources. + for i := 1; i <= ctx.PageCount; i++ { + _, _, inhPA, err := ctx.PageDict(i, true) + if err != nil { + return err + } + if inhPA.Resources["Font"] != nil { + pdf.FontResIDs[i] = inhPA.Resources["Font"].(types.Dict) + } + if inhPA.Resources["XObject"] != nil { + pdf.XObjectResIDs[i] = inhPA.Resources["XObject"].(types.Dict) + } + } + return nil +} + +func addImages(ctx *model.Context, pages map[string]*Page) ([]*model.Page, error) { + + pdf := &primitives.PDF{ + FieldIDs: types.StringSet{}, + Fields: types.Array{}, + FormFonts: map[string]*primitives.FormFont{}, + Pages: map[string]*primitives.PDFPage{}, + FontResIDs: map[int]types.Dict{}, + XObjectResIDs: map[int]types.Dict{}, + Conf: ctx.Configuration, + XRefTable: ctx.XRefTable, + Optimize: ctx.Optimize, + CheckBoxAPs: map[float64]*primitives.AP{}, + RadioBtnAPs: map[float64]*primitives.AP{}, + OldFieldIDs: types.StringSet{}, + Debug: false, + } + + if err := cacheResIDs(ctx, pdf); err != nil { + return nil, err + } + + // What follows is a quirky way of turning a map of pages into a sorted slice of pages + // including entries for pages that are missing in the map. + + var pageNrs []int + + for pageNr := range pages { + nr, err := strconv.Atoi(pageNr) + if err != nil { + return nil, errors.Errorf("pdfcpu: invalid page number: %s", pageNr) + } + pageNrs = append(pageNrs, nr) + } + + sort.Ints(pageNrs) + + pp := []*Page{} + + maxPageNr := pageNrs[len(pageNrs)-1] + for i := 1; i <= maxPageNr; i++ { + pp = append(pp, pages[strconv.Itoa(i)]) + } + + mp := []*model.Page{} + imageMap := model.ImageMap{} + + for i, page := range pp { + + pageNr := i + 1 + + _, _, inhPAttrs, err := ctx.PageDict(pageNr, false) + if err != nil { + return nil, err + } + + p := model.Page{ + MediaBox: inhPAttrs.MediaBox, + CropBox: inhPAttrs.CropBox, + Fm: model.FontMap{}, + Im: model.ImageMap{}, + AnnotTabs: map[int]model.FieldAnnotation{}, + Buf: new(bytes.Buffer), + } + + if page == nil { + if pageNr <= pdf.XRefTable.PageCount { + mp = append(mp, nil) + continue + } + } + + for _, ib := range page.ImageBoxes { + if err := ib.RenderForFill(pdf, &p, pageNr, imageMap); err != nil { + return nil, err + } + } + + mp = append(mp, &p) + } + + return mp, nil +} + +// CSVFieldAttributes represent the value(s) and the lock state for a field. +type CSVFieldAttributes struct { + Values []string + Lock bool +} + +func parsePageNr(s string, ib *primitives.ImageBox) error { + _, err := strconv.Atoi(s) + if err != nil { + return err + } + ib.PageNr = s + return nil +} + +func parseWidth(s string, ib *primitives.ImageBox) error { + f, err := strconv.ParseFloat(s, 64) + if err != nil { + return err + } + ib.Width = f + return nil +} + +func parseHeight(s string, ib *primitives.ImageBox) error { + f, err := strconv.ParseFloat(s, 64) + if err != nil { + return err + } + ib.Height = f + return nil +} + +func parsePositionAnchor(s string, ib *primitives.ImageBox) error { + d := strings.Split(s, " ") + if len(d) < 1 || len(d) > 2 { + return errors.Errorf("pdfcpu: illegal position string: need 1 or 2 values, %s\n", s) + } + if len(d) == 1 { + _, err := types.ParsePositionAnchor(s) + if err != nil { + return err + } + ib.Anchor = s + return nil + } + x, err := strconv.ParseFloat(d[0], 64) + if err != nil { + return err + } + + y, err := strconv.ParseFloat(d[1], 64) + if err != nil { + return err + } + ib.Position = [2]float64{x, y} + return nil +} + +func parsePositionOffset(s string, ib *primitives.ImageBox) error { + d := strings.Split(s, " ") + if len(d) != 2 { + return errors.Errorf("pdfcpu: illegal position offset string: need 2 numeric values, %s\n", s) + } + + f, err := strconv.ParseFloat(d[0], 64) + if err != nil { + return err + } + ib.Dx = f + + f, err = strconv.ParseFloat(d[1], 64) + if err != nil { + return err + } + ib.Dy = f + + return nil +} + +func parseImgBackgroundColor(s string, ib *primitives.ImageBox) error { + ib.BackgroundColor = s + return nil +} + +func parseImgBorder(s string, ib *primitives.ImageBox) error { + + var err error + + b := strings.Split(s, " ") + if len(b) == 0 || len(b) > 5 { + return errors.Errorf("pdfcpu: borders: need between 1 and 5 components, %s\n", s) + } + + ib.Border = &primitives.Border{} + border := ib.Border + + border.Width, err = strconv.Atoi(b[0]) + if err != nil { + return err + } + if border.Width == 0 { + return errors.New("pdfcpu: borders: need width > 0") + } + + if len(b) == 1 { + return nil + } + + st := strings.ToLower(b[1]) + if types.MemberOf(st, []string{"bevel", "miter", "round"}) { + border.Style = st + if len(b) > 2 { + border.Color = strings.Join(b[2:], " ") + } + return nil + } + + border.Color = strings.Join(b[1:], " ") + + return nil +} + +type imageBoxParamMap map[string]func(string, *primitives.ImageBox) error + +var imgParamMap = imageBoxParamMap{ + "bgcolor": parseImgBackgroundColor, + "border": parseImgBorder, + "offset": parsePositionOffset, + "page": parsePageNr, + "position": parsePositionAnchor, + "width": parseWidth, + "height": parseHeight, +} + +func (m imageBoxParamMap) processImageBoxArg(paramPrefix, paramValueStr string, ib *primitives.ImageBox) error { + + var param string + + // Completion support + for k := range m { + if !strings.HasPrefix(k, paramPrefix) { + continue + } + if len(param) > 0 { + return errors.Errorf("pdfcpu: ambiguous parameter prefix \"%s\"", paramPrefix) + } + param = k + } + + if param == "" { + return errors.Errorf("pdfcpu: unknown parameter prefix \"%s\"", paramPrefix) + } + + return m[param](paramValueStr, ib) +} + +func imageBox(s, src, url string) (*primitives.ImageBox, string, error) { + + if !strings.HasPrefix(s, "@img") || len(s) < 6 { + return nil, "", errors.Errorf("pdfcpu: parsing cvs fieldNames: missing @img: <%s>", s) + } + + s = s[4:] + if s[0] != '(' || s[len(s)-1] != ')' { + return nil, "", errors.Errorf("pdfcpu: parsing cvs fieldNames: corrupted @img: <%s>", s) + } + + s = s[1 : len(s)-1] + if len(s) == 0 { + return nil, "", errors.Errorf("pdfcpu: parsing cvs fieldNames: empty @img: <%s>", s) + } + + ib := primitives.ImageBox{Src: src, Dx: 0, Dy: 0, Width: 0, Height: 0} + if url != "" { + ib.Url = url + } + ss := strings.Split(s, ",") + + for _, s := range ss { + ss1 := strings.Split(s, ":") + if len(ss1) != 2 { + return nil, "", errors.Errorf("pdfcpu: parsing cvs fieldNames: corrupted @img: <%s>", s) + } + + paramPrefix := strings.TrimSpace(ss1[0]) + paramValueStr := strings.TrimSpace(ss1[1]) + + if err := imgParamMap.processImageBoxArg(paramPrefix, paramValueStr, &ib); err != nil { + return nil, "", err + } + } + + return &ib, ib.PageNr, nil +} + +// FieldMap returns structures needed to fill a form via CSV. +func FieldMap(fieldNames, formRecord []string) (map[string]CSVFieldAttributes, map[string]*Page, error) { + fm := map[string]CSVFieldAttributes{} + im := map[string]*Page{} + for i, fieldName := range fieldNames { + var lock bool + if fieldName[0] == '*' { + lock = true + fieldName = fieldName[1:] + if fieldName[0] == '@' { + continue + } + } + vv := strings.Split(formRecord[i], ",") + if fieldName[0] != '@' { + v1 := vv[0] + if len(v1) > 1 && v1[0] == '*' { + lock = true + vv[0] = vv[0][1:] + } + fm[fieldName] = CSVFieldAttributes{Values: vv, Lock: lock} + continue + } + + // @img defines a virtual image field by rendering an imageBox. + // For CSV we keep it simple and support the most important imageBox attributes only: + // + // "@img(page:1, pos:40 350, w:290, h:200, bgcol:#F5F5DC, border:5 round LightGray)" + + if len(vv) == 0 || len(vv) > 2 { + // Skip invalid image field + continue + } + + src, url := "", "" + if len(vv) == 1 && vv[0][0] == '(' { + // link only, no image + url = vv[0][1 : len(vv[0])-1] + } else { + src = vv[0] + if len(vv) == 2 { + url = vv[1][1 : len(vv[1])-1] + } + } + + ib, pageNr, err := imageBox(fieldName, src, url) + if err != nil { + return nil, nil, err + } + + if ib == nil { + continue + } + + p, ok := im[pageNr] + if !ok { + p = &Page{} + im[pageNr] = p + } + p.ImageBoxes = append(p.ImageBoxes, ib) + } + + return fm, im, nil +} + +// FillDetails returns a closure that returns new form data provided by CSV or JSON. +func FillDetails(form *Form, fieldMap map[string]CSVFieldAttributes) func(id, name string, fieldType FieldType, format DataFormat) ([]string, bool, bool) { + f := form + fm := fieldMap + + return func(id, name string, fieldType FieldType, format DataFormat) ([]string, bool, bool) { + + if format == CSV { + fa, ok := fm[id] + if ok { + return fa.Values, fa.Lock, true + } + fa, ok = fm[name] + if ok { + return fa.Values, fa.Lock, true + } + return nil, false, false + } + + switch fieldType { + case FTCheckBox: + v, lock, ok := form.checkBoxValueAndLock(id, name) + c := "f" + if v { + c = "t" + } + return []string{c}, lock, ok + + case FTRadioButtonGroup: + v, lock, ok := form.radioButtonGroupValueAndLock(id, name) + return []string{v}, lock, ok + + case FTComboBox: + v, lock, ok := f.comboBoxValueAndLock(id, name) + return []string{v}, lock, ok + + case FTListBox: + return f.listBoxValuesAndLock(id, name) + + case FTDate: + v, lock, ok := f.dateFieldValueAndLock(id, name) + return []string{v}, lock, ok + + case FTText: + v, lock, ok := f.textFieldValueAndLock(id, name) + return []string{v}, lock, ok + } + + return nil, false, false + } +} + +func fillRadioButtons(ctx *model.Context, d types.Dict, vNew string, v types.Name) error { + + for _, o := range d.ArrayEntry("Kids") { + + d, err := ctx.DereferenceDict(o) + if err != nil { + return err + } + + d1 := d.DictEntry("AP") + if d1 == nil { + return errors.New("pdfcpu: corrupt form field: missing entry AP") + } + + d2 := d1.DictEntry("N") + if d2 == nil { + return errors.New("pdfcpu: corrupt AP field: missing entry N") + } + + for k := range d2 { + k, err := types.DecodeName(k) + if err != nil { + return err + } + if k != "Off" { + d["AS"] = types.Name("Off") + if k == vNew { + d["AS"] = v + } + break + } + } + } + + return nil +} + +func fillRadioButtonGroup( + ctx *model.Context, + d types.Dict, + id, name string, + locked bool, + format DataFormat, + fillDetails func(id, name string, fieldType FieldType, format DataFormat) ([]string, bool, bool), + ok *bool) error { + + vv, lock, found := fillDetails(id, name, FTRadioButtonGroup, format) + if !found { + return nil + } + + if locked { + if !lock { + unlockFormField(d) + *ok = true + } + } else { + if lock { + lockFormField(d) + *ok = true + } + } + + vNew := vv[0] + vOld := "" + if s := d.NameEntry("V"); s != nil { + n, err := types.DecodeName(*s) + if err != nil { + return err + } + if n != "Off" { + vOld = n + } + } + if vNew == vOld { + return nil + } + + s := types.EncodeName(vNew) + v := types.Name(s) + d["V"] = v + + if err := fillRadioButtons(ctx, d, vNew, v); err != nil { + return err + } + + *ok = true + + return nil +} + +func fillCheckBoxKid(ctx *model.Context, kids types.Array, off bool) (*types.Name, error) { + d, err := ctx.DereferenceDict(kids[0]) + if err != nil { + return nil, err + } + + d1 := d.DictEntry("AP") + if d1 == nil { + return nil, errors.New("pdfcpu: corrupt form field: missing entry AP") + } + + d2 := d1.DictEntry("N") + if d2 == nil { + return nil, errors.New("pdfcpu: corrupt AP field: missing entry N") + } + + offName, yesName := primitives.CalcCheckBoxASNames(d2) + asName := yesName + if off { + asName = offName + } + + if _, found := d.Find("AS"); found { + d["AS"] = asName + } + + return &asName, nil +} + +func fillCheckBox( + ctx *model.Context, + d types.Dict, + id, name string, + locked bool, + format DataFormat, + fillDetails func(id, name string, fieldType FieldType, format DataFormat) ([]string, bool, bool), + ok *bool) error { + + vv, lock, found := fillDetails(id, name, FTCheckBox, format) + if !found { + return nil + } + + if locked { + if !lock { + unlockFormField(d) + *ok = true + } + } else { + if lock { + lockFormField(d) + *ok = true + } + } + + s := strings.ToLower(vv[0]) + vNew := strings.HasPrefix(s, "t") + vOld := false + if o, found := d.Find("V"); found { + vOld = o.(types.Name) != "Off" + } + if vNew == vOld { + return nil + } + + v := types.Name("Off") + if vNew { + v = types.Name("Yes") + } + + kids := d.ArrayEntry("Kids") + if len(kids) == 1 { + asName, err := fillCheckBoxKid(ctx, kids, v == types.Name("Off")) + if err != nil { + return err + } + d["V"] = *asName + *ok = true + return nil + } + + d["V"] = v + if _, found := d.Find("AS"); found { + offName, yesName := primitives.CalcCheckBoxASNames(d) + //fmt.Printf("off:<%s> yes:<%s>\n", offName, yesName) + asName := yesName + if v == "Off" { + asName = offName + } + d["AS"] = asName + d["V"] = asName + } + *ok = true + return nil +} + +func fillBtn( + ctx *model.Context, + d types.Dict, + id, name string, + locked bool, + format DataFormat, + fillDetails func(id, name string, fieldType FieldType, format DataFormat) ([]string, bool, bool), + ok *bool) error { + + ff := d.IntEntry("Ff") + if ff != nil && primitives.FieldFlags(*ff)&primitives.FieldPushbutton > 0 { + return nil + } + + if len(d.ArrayEntry("Kids")) > 1 { + if err := fillRadioButtonGroup(ctx, d, id, name, locked, format, fillDetails, ok); err != nil { + return err + } + } else { + if err := fillCheckBox(ctx, d, id, name, locked, format, fillDetails, ok); err != nil { + return err + } + } + + return nil +} + +func fillComboBox( + ctx *model.Context, + d types.Dict, + id, name string, + opts []string, + locked bool, + format DataFormat, + fonts map[string]types.IndirectRef, + fillDetails func(id, name string, fieldType FieldType, format DataFormat) ([]string, bool, bool), + ok *bool) error { + + vv, lock, found := fillDetails(id, name, FTComboBox, format) + if !found { + return nil + } + + vNew := vv[0] + if locked { + if !lock { + unlockFormField(d) + d.Delete("AP") + *ok = true + } + } else if lock { + lockFormField(d) + if err := primitives.EnsureComboBoxAP(ctx, d, vNew, fonts); err != nil { + return err + } + *ok = true + } + + vOld := "" + if sl := d.StringLiteralEntry("V"); sl != nil { + s, err := types.StringLiteralToString(*sl) + if err != nil { + return err + } + vOld = s + } + if vNew == vOld { + return nil + } + + s, err := types.EscapeUTF16String(vNew) + if err != nil { + return err + } + + ind := types.Array{} + for i, o := range opts { + if o == vNew { + ind = append(ind, types.Integer(i)) + break + } + } + if len(ind) > 0 { + d["I"] = ind + d["V"] = types.StringLiteral(*s) + } else { + d.Delete("I") + d.Delete("V") + } + *ok = true + + return nil +} + +func updateListBoxValues(multi bool, d types.Dict, opts, vNew []string) (types.Array, error) { + ind := types.Array{} + if multi { + arr := types.Array{} + for _, v := range vNew { + for i, o := range opts { + if o == v { + ind = append(ind, types.Integer(i)) + break + } + } + s, err := types.EscapeUTF16String(v) + if err != nil { + return nil, err + } + arr = append(arr, types.StringLiteral(*s)) + } + if len(vNew) > 0 { + d["I"] = ind + d["V"] = arr + } else { + d.Delete("I") + d.Delete("V") + } + return ind, nil + } + + v := vNew[0] + s, err := types.EscapeUTF16String(v) + if err != nil { + return nil, err + } + for i, o := range opts { + if o == v { + ind = append(ind, types.Integer(i)) + break + } + } + if len(ind) > 0 { + d["I"] = ind + d["V"] = types.StringLiteral(*s) + } else { + d.Delete("I") + d.Delete("V") + } + return ind, nil +} + +func fillListBox( + ctx *model.Context, + d types.Dict, + id, name string, + opts []string, + locked bool, + format DataFormat, + fonts map[string]types.IndirectRef, + fillDetails func(id, name string, fieldType FieldType, format DataFormat) ([]string, bool, bool), + ff *int, + ok *bool) error { + + vNew, lock, found := fillDetails(id, name, FTListBox, format) + if !found { + return nil + } + + var vOld []string + multi := primitives.FieldFlags(*ff)&primitives.FieldMultiselect > 0 + if !multi { + if sl := d.StringLiteralEntry("V"); sl != nil { + s, err := types.StringLiteralToString(*sl) + if err != nil { + return err + } + vOld = []string{s} + } + } else { + ss, err := parseStringLiteralArray(ctx.XRefTable, d, "V") + if err != nil { + return err + } + vOld = ss + } + + if locked { + if !lock { + unlockFormField(d) + *ok = true + } + return nil + } + + if lock { + lockFormField(d) + *ok = true + } + + if types.EqualSlices(vOld, vNew) { + return nil + } + + ind, err := updateListBoxValues(multi, d, opts, vNew) + if err != nil { + return err + } + + if err := primitives.EnsureListBoxAP(ctx, d, opts, ind, fonts); err != nil { + return err + } + + *ok = true + + return nil +} + +func fillCh( + ctx *model.Context, + d types.Dict, + id, name string, + locked bool, + format DataFormat, + fonts map[string]types.IndirectRef, + fillDetails func(id, name string, fieldType FieldType, format DataFormat) ([]string, bool, bool), + ff *int, + ok *bool) error { + + if ff == nil { + return errors.New("pdfcpu: corrupt form field: missing entry Ff") + } + + opts, err := parseOptions(ctx.XRefTable, d) + if err != nil { + return err + } + + if len(opts) == 0 { + return errors.New("pdfcpu: missing Opts") + } + + if primitives.FieldFlags(*ff)&primitives.FieldCombo > 0 { + return fillComboBox(ctx, d, id, name, opts, locked, format, fonts, fillDetails, ok) + } + + return fillListBox(ctx, d, id, name, opts, locked, format, fonts, fillDetails, ff, ok) +} + +func fillDateField( + ctx *model.Context, + d types.Dict, + id, name, vOld string, + locked bool, + format DataFormat, + fonts map[string]types.IndirectRef, + fillDetails func(id, name string, fieldType FieldType, format DataFormat) ([]string, bool, bool), + ok *bool) error { + + vv, lock, found := fillDetails(id, name, FTDate, format) + if !found { + return nil + } + + if locked { + if !lock { + unlockFormField(d) + *ok = true + } + } else { + if lock { + lockFormField(d) + *ok = true + } + } + + vNew := vv[0] + if vNew == vOld { + return nil + } + + s, err := types.EscapeUTF16String(vNew) + if err != nil { + return err + } + + d["V"] = types.StringLiteral(*s) + + if err := primitives.EnsureDateFieldAP(ctx, d, vNew, fonts); err != nil { + return err + } + + *ok = true + + return nil +} + +func fillTextField( + ctx *model.Context, + d types.Dict, + id, name, vOld string, + locked bool, + format DataFormat, + fonts map[string]types.IndirectRef, + fillDetails func(id, name string, fieldType FieldType, format DataFormat) ([]string, bool, bool), + ff *int, + ok *bool) error { + + vv, lock, found := fillDetails(id, name, FTText, format) + if !found { + return nil + } + + if locked { + if !lock { + unlockFormField(d) + *ok = true + } + } else { + if lock { + lockFormField(d) + *ok = true + } + } + + vNew := vv[0] + + if vNew == vOld { + return nil + } + + s, err := types.EscapeUTF16String(vNew) + if err != nil { + return err + } + + d["V"] = types.StringLiteral(*s) + + multiLine := ff != nil && uint(primitives.FieldFlags(*ff))&uint(primitives.FieldMultiline) > 0 + + kids := d.ArrayEntry("Kids") + if len(kids) > 0 { + for _, o := range kids { + + d, err := ctx.DereferenceDict(o) + if err != nil { + return err + } + + if err := primitives.EnsureTextFieldAP(ctx, d, vNew, multiLine, fonts); err != nil { + return err + } + + *ok = true + } + + return nil + } + + if err := primitives.EnsureTextFieldAP(ctx, d, vNew, multiLine, fonts); err != nil { + return err + } + + *ok = true + return nil +} + +func fillTx( + ctx *model.Context, + d types.Dict, + id, name string, + locked bool, + format DataFormat, + fonts map[string]types.IndirectRef, + fillDetails func(id, name string, fieldType FieldType, format DataFormat) ([]string, bool, bool), + ff *int, + ok *bool) error { + + df, err := extractDateFormat(d) + if err != nil { + return err + } + vOld := "" + if o, found := d.Find("V"); found { + s, err := types.StringOrHexLiteral(o) + if err != nil { + return err + } + vOld = "" + if s != nil { + vOld = *s + } + } + + if df != nil { + return fillDateField(ctx, d, id, name, vOld, locked, format, fonts, fillDetails, ok) + } + + return fillTextField(ctx, d, id, name, vOld, locked, format, fonts, fillDetails, ff, ok) +} + +func fillWidgetAnnots( + ctx *model.Context, + fields types.Array, + indRefs map[types.IndirectRef]bool, + wAnnots model.Annot, + format DataFormat, + fonts map[string]types.IndirectRef, + fillDetails func(id, name string, fieldType FieldType, format DataFormat) ([]string, bool, bool), + ok *bool) error { + + for _, indRef := range *(wAnnots.IndRefs) { + + found, fi, err := isField(ctx.XRefTable, indRef, fields) + if err != nil { + return err + } + if !found { + continue + } + + id, name := fi.id, fi.name + + if fi.indRef != nil { + if indRefs[*fi.indRef] { + continue + } + indRefs[*fi.indRef] = true + indRef = *fi.indRef + } + + d, err := ctx.DereferenceDict(indRef) + if err != nil { + return err + } + if len(d) == 0 { + continue + } + + var locked bool + ff := d.IntEntry("Ff") + if ff != nil { + locked = uint(primitives.FieldFlags(*ff))&uint(primitives.FieldReadOnly) > 0 + } + + ft := fi.ft + if ft == nil { + ft = d.NameEntry("FT") + if ft == nil { + return errors.Errorf("pdfcpu: corrupt form field %s: missing entry FT\n%s", id, d) + } + } + + switch *ft { + case "Btn": + err = fillBtn(ctx, d, id, name, locked, format, fillDetails, ok) + + case "Ch": + err = fillCh(ctx, d, id, name, locked, format, fonts, fillDetails, ff, ok) + + case "Tx": + err = fillTx(ctx, d, id, name, locked, format, fonts, fillDetails, ff, ok) + } + + if err != nil { + return err + } + } + + return nil +} + +func setupFillFonts(xRefTable *model.XRefTable) error { + d, err := primitives.FormFontResDict(xRefTable) + if err != nil { + return err + } + + m := xRefTable.FillFonts + + if d == nil { + // setup/reuse Helvetica and add to m + return nil + } + + for k, v := range d { + indRef := v.(types.IndirectRef) + fontName, _, err := primitives.FormFontNameAndLangForID(xRefTable, indRef) + if err != nil { + return err + } + + if font.IsCoreFont(fontName) || font.IsUserFont(fontName) { + m[k] = indRef + } + } + + return nil +} + +// FillForm populates form fields as provided by fillDetails and also supports virtual image fields. +func FillForm( + ctx *model.Context, + fillDetails func(id, name string, fieldType FieldType, format DataFormat) ([]string, bool, bool), + imgs map[string]*Page, + format DataFormat) (bool, []*model.Page, error) { + + xRefTable := ctx.XRefTable + + fields, err := fields(xRefTable) + if err != nil { + return false, nil, err + } + + fonts := map[string]types.IndirectRef{} + indRefs := map[types.IndirectRef]bool{} + + if err := setupFillFonts(xRefTable); err != nil { + return false, nil, err + } + + var ok bool + + for i := 1; i <= xRefTable.PageCount; i++ { + pgAnnots := xRefTable.PageAnnots[i] + if len(pgAnnots) == 0 { + continue + } + wAnnots, found := pgAnnots[model.AnnWidget] + if !found { + continue + } + + if err := fillWidgetAnnots(ctx, fields, indRefs, wAnnots, format, fonts, fillDetails, &ok); err != nil { + return false, nil, err + } + } + + for fName, indRef := range fonts { + if len(ctx.UsedGIDs[fName]) == 0 { + continue + } + // Update user font. + fDict, err := xRefTable.DereferenceDict(indRef) + if err != nil { + return false, nil, err + } + fr := model.FontResource{} + if err := pdffont.IndRefsForUserfontUpdate(xRefTable, fDict, "", &fr); err != nil { + return false, nil, pdffont.ErrCorruptFontDict + } + if err := pdffont.UpdateUserfont(xRefTable, fName, fr); err != nil { + return false, nil, err + } + } + + var pages []*model.Page + + if len(imgs) > 0 { + if pages, err = addImages(ctx, imgs); err != nil { + return false, nil, err + } + } + + // pdfcpu provides all appearance streams for form fields. + // Yet for some files and viewers form fields don't get rendered. + // In these cases you can force the viewer to provide form field appearance streams. + if ctx.NeedAppearances { + xRefTable.Form["NeedAppearances"] = types.Boolean(true) + } + + return ok, pages, nil +} diff --git a/pkg/pdfcpu/form/form.go b/pkg/pdfcpu/form/form.go new file mode 100644 index 0000000000000000000000000000000000000000..2b367eac50131a7acd4be1afd0a7657634581d42 --- /dev/null +++ b/pkg/pdfcpu/form/form.go @@ -0,0 +1,1830 @@ +/* +Copyright 2023 The pdfcpu Authors. + +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 + + 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 form + +import ( + "fmt" + "sort" + "strconv" + "strings" + + "github.com/mattn/go-runewidth" + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw" + pdffont "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/font" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// FieldType represents a form field type. +type FieldType int + +const ( + FTText FieldType = iota + FTDate + FTCheckBox + FTComboBox + FTListBox + FTRadioButtonGroup +) + +func (ft FieldType) String() string { + var s string + switch ft { + case FTText: + s = "Textfield" + case FTDate: + s = "Datefield" + case FTCheckBox: + s = "CheckBox" + case FTComboBox: + s = "ComboBox" + case FTListBox: + s = "ListBox" + case FTRadioButtonGroup: + s = "RadioBGr." + } + return s +} + +// Field represents a form field for s particular page number. +type Field struct { + Pages []int + Locked bool + Typ FieldType + ID string + Name string + Dv string + V string + Opts string +} + +func (f Field) pageString() string { + if len(f.Pages) == 1 { + return strconv.Itoa(f.Pages[0]) + } + sort.Ints(f.Pages) + ss := []string{} + for _, p := range f.Pages { + ss = append(ss, strconv.Itoa(p)) + } + return strings.Join(ss, ",") +} + +type FieldMeta struct { + def, val, opt bool + pageMax, defMax, valMax, idMax, nameMax int +} + +func fields(xRefTable *model.XRefTable) (types.Array, error) { + + if xRefTable.Form == nil { + return nil, errors.New("pdfcpu: no form available") + } + + o, ok := xRefTable.Form.Find("Fields") + if !ok { + return nil, errors.New("pdfcpu: no form fields available") + } + + fields, err := xRefTable.DereferenceArray(o) + if err != nil { + return nil, err + } + + if len(fields) == 0 { + return nil, errors.New("pdfcpu: no form fields available") + } + + return fields, nil +} + +func fullyQualifiedFieldName(xRefTable *model.XRefTable, indRef types.IndirectRef, fields types.Array, id, name *string) (bool, error) { + + d, err := xRefTable.DereferenceDict(indRef) + if err != nil { + return false, err + } + if len(d) == 0 { + return false, errors.Errorf("pdfcpu: corrupt field") + } + + thisID := indRef.ObjectNumber.String() + thisName := "" + s, err := d.StringOrHexLiteralEntry("T") + if err != nil { + return false, err + } + if s != nil { + thisName = *s + } + + pIndRef := d.IndirectRefEntry("Parent") + if pIndRef == nil { + for i := 0; i < len(fields); i++ { + if ir, ok := fields[i].(types.IndirectRef); ok && ir == indRef { + *id = thisID + *name = thisName + return true, nil + } + } + return false, nil + } + + // non-terminal field + + ok, err := fullyQualifiedFieldName(xRefTable, *pIndRef, fields, id, name) + if !ok || err != nil { + return false, err + } + + *id += "." + thisID + if len(*name) > 0 && len(thisName) > 0 { + *name += "." + thisName + } + + return true, nil +} + +type fieldInfo struct { + id string + name string + ft *string + indRef *types.IndirectRef +} + +func isField(xRefTable *model.XRefTable, indRef types.IndirectRef, fields types.Array) (bool, *fieldInfo, error) { + + d, err := xRefTable.DereferenceDict(indRef) + if err != nil { + return false, nil, err + } + if len(d) == 0 { + return false, nil, nil + } + + var ( + id, name string + ft *string + ) + + pIndRef := d.IndirectRefEntry("Parent") + if pIndRef != nil { + dp, err := xRefTable.DereferenceDict(*pIndRef) + if err != nil { + return false, nil, err + } + if len(dp) == 0 { + return false, nil, nil + } + ft = dp.NameEntry("FT") + if ft != nil && (*ft == "Btn" || *ft == "Tx") { + // rbg or text/datefield hierarchy + ok, err := fullyQualifiedFieldName(xRefTable, *pIndRef, fields, &id, &name) + if !ok || err != nil { + return false, nil, err + } + return true, &fieldInfo{id: id, name: name, ft: ft, indRef: pIndRef}, nil + } + } + + ok, err := fullyQualifiedFieldName(xRefTable, indRef, fields, &id, &name) + if !ok || err != nil { + return false, nil, err + } + + if ft == nil { + ft = d.NameEntry("FT") + } + return true, &fieldInfo{id: id, name: name, ft: ft}, nil +} + +func extractStringSlice(a types.Array) ([]string, error) { + var ss []string + for _, o := range a { + sl, ok := o.(types.StringLiteral) + if ok { + s, err := types.StringLiteralToString(sl) + if err != nil { + return nil, err + } + s = strings.TrimSpace(s) + if len(s) > 0 { + ss = append(ss, s) + } + continue + } + arr, ok := o.(types.Array) + if !ok || len(arr) != 2 { + return nil, errors.New("corrupt choice field") + } + sl, ok = arr[1].(types.StringLiteral) + if !ok { + return nil, errors.New("corrupt choice field") + } + s, err := types.StringLiteralToString(sl) + if err != nil { + return nil, err + } + s = strings.TrimSpace(s) + if len(s) > 0 { + ss = append(ss, s) + } + } + return ss, nil +} + +func parseOptions(xRefTable *model.XRefTable, d types.Dict) ([]string, error) { + o, _ := d.Find("Opt") + a, err := xRefTable.DereferenceArray(o) + if err != nil { + return nil, err + } + return extractStringSlice(a) +} + +func parseStringLiteralArray(xRefTable *model.XRefTable, d types.Dict, key string) ([]string, error) { + o, _ := d.Find(key) + if o == nil { + return nil, nil + } + + switch o := o.(type) { + + case types.StringLiteral: + s, err := types.StringLiteralToString(o) + if err != nil { + return nil, err + } + return []string{s}, nil + + case types.Array: + a, err := xRefTable.DereferenceArray(o) + if err != nil { + return nil, err + } + return extractStringSlice(a) + } + + return nil, nil +} + +func collectRadioButtonGroupOptions(xRefTable *model.XRefTable, d types.Dict) (string, error) { + + var vv []string + + for _, o := range d.ArrayEntry("Kids") { + d, err := xRefTable.DereferenceDict(o) + if err != nil { + return "", err + } + d1 := d.DictEntry("AP") + if d1 == nil { + return "", errors.New("corrupt form field: missing entry AP") + } + d2 := d1.DictEntry("N") + if d2 == nil { + return "", errors.New("corrupt AP field: missing entry N") + } + for k := range d2 { + k, err := types.DecodeName(k) + if err != nil { + return "", err + } + if k != "Off" { + found := false + for _, opt := range vv { + if opt == k { + found = true + break + } + } + if !found { + vv = append(vv, k) + } + break + } + } + } + + return strings.Join(vv, ","), nil +} + +func collectRadioButtonGroup(xRefTable *model.XRefTable, d types.Dict, f *Field, fm *FieldMeta) error { + + f.Typ = FTRadioButtonGroup + + if s := d.NameEntry("V"); s != nil { + v, err := types.DecodeName(*s) + if err != nil { + return err + } + if v != "Off" { + if w := runewidth.StringWidth(v); w > fm.valMax { + fm.valMax = w + } + fm.val = true + f.V = v + } + } + + s, err := collectRadioButtonGroupOptions(xRefTable, d) + if err != nil { + return err + } + + f.Opts = s + if len(f.Opts) > 0 { + fm.opt = true + } + + return nil +} + +func collectBtn(xRefTable *model.XRefTable, d types.Dict, f *Field, fm *FieldMeta) error { + + ff := d.IntEntry("Ff") + if ff != nil && primitives.FieldFlags(*ff)&primitives.FieldPushbutton > 0 { + return nil + } + + v := types.Name("Off") + if s, found := d.Find("DV"); found { + v = s.(types.Name) + } + dv, err := types.DecodeName(v.String()) + if err != nil { + return err + } + + if dv != "Off" { + if w := runewidth.StringWidth(dv); w > fm.defMax { + fm.defMax = w + } + fm.def = true + f.Dv = dv + } + + if len(d.ArrayEntry("Kids")) > 1 { + return collectRadioButtonGroup(xRefTable, d, f, fm) + } + + f.Typ = FTCheckBox + if o, found := d.Find("V"); found { + if o.(types.Name) != "Off" { + v := "Yes" + if len(v) > fm.valMax { + fm.valMax = len(v) + } + fm.val = true + f.V = v + } + } + + return nil +} + +func collectComboBox(d types.Dict, f *Field, fm *FieldMeta) error { + f.Typ = FTComboBox + if sl := d.StringLiteralEntry("V"); sl != nil { + v, err := types.StringLiteralToString(*sl) + if err != nil { + return err + } + if w := runewidth.StringWidth(v); w > fm.valMax { + fm.valMax = w + } + fm.val = true + f.V = v + } + if sl := d.StringLiteralEntry("DV"); sl != nil { + dv, err := types.StringLiteralToString(*sl) + if err != nil { + return err + } + if w := runewidth.StringWidth(dv); w > fm.defMax { + fm.defMax = w + } + fm.def = true + f.Dv = dv + } + return nil +} + +func collectListBox(xRefTable *model.XRefTable, multi bool, d types.Dict, f *Field, fm *FieldMeta) error { + f.Typ = FTListBox + if !multi { + if sl := d.StringLiteralEntry("V"); sl != nil { + v, err := types.StringLiteralToString(*sl) + if err != nil { + return err + } + if w := runewidth.StringWidth(v); w > fm.valMax { + fm.valMax = w + } + fm.val = true + f.V = v + } + if sl := d.StringLiteralEntry("DV"); sl != nil { + dv, err := types.StringLiteralToString(*sl) + if err != nil { + return err + } + if w := runewidth.StringWidth(dv); w > fm.defMax { + fm.defMax = w + } + fm.def = true + f.Dv = dv + } + } else { + vv, err := parseStringLiteralArray(xRefTable, d, "V") + if err != nil { + return err + } + if len(vv) > 0 { + v := strings.Join(vv, ",") + if w := runewidth.StringWidth(v); w > fm.valMax { + fm.valMax = w + } + fm.val = true + f.V = v + } + vv, err = parseStringLiteralArray(xRefTable, d, "DV") + if err != nil { + return err + } + if len(vv) > 0 { + dv := strings.Join(vv, ",") + if w := runewidth.StringWidth(dv); w > fm.defMax { + fm.defMax = w + } + fm.def = true + f.Dv = dv + } + } + return nil +} + +func collectCh(xRefTable *model.XRefTable, d types.Dict, f *Field, fm *FieldMeta) error { + ff := d.IntEntry("Ff") + + vv, err := parseOptions(xRefTable, d) + if err != nil { + return err + } + + f.Opts = strings.Join(vv, ",") + if len(f.Opts) > 0 { + fm.opt = true + } + + if ff != nil && primitives.FieldFlags(*ff)&primitives.FieldCombo > 0 { + return collectComboBox(d, f, fm) + } + + multi := ff != nil && (primitives.FieldFlags(*ff)&primitives.FieldMultiselect > 0) + + return collectListBox(xRefTable, multi, d, f, fm) +} + +func collectTx(d types.Dict, f *Field, fm *FieldMeta) error { + if o, found := d.Find("V"); found { + s1, err := types.StringOrHexLiteral(o) + if err != nil { + return err + } + s := "" + if s1 != nil { + s = *s1 + } + v := s + if i := strings.Index(s, "\n"); i >= 0 { + v = s[:i] + v += "\\n" + } + if w := runewidth.StringWidth(v); w > fm.valMax { + fm.valMax = w + } + fm.val = true + f.V = v + } + if o, found := d.Find("DV"); found { + s1, err := types.StringOrHexLiteral(o) + if err != nil { + return err + } + s := "" + if s1 != nil { + s = *s1 + } + dv := s + if i := strings.Index(s, "\n"); i >= 0 { + dv = dv[:i] + dv += "\\n" + } + + if w := runewidth.StringWidth(dv); w > fm.defMax { + fm.defMax = w + } + fm.def = true + f.Dv = dv + } + df, err := extractDateFormat(d) + if err != nil { + return err + } + f.Typ = FTText + if df != nil { + f.Typ = FTDate + } + return nil +} + +func collectPageField( + xRefTable *model.XRefTable, + d types.Dict, + i int, + fi *fieldInfo, + fm *FieldMeta, + fs *[]Field) error { + + exists := false + for j, field := range *fs { + if field.ID == fi.id && field.Name == fi.name { + field.Pages = append(field.Pages, i) + ps := field.pageString() + if len(ps) > fm.pageMax { + fm.pageMax = len(ps) + } + (*fs)[j] = field + exists = true + } + } + + f := Field{Pages: []int{i}} + + f.ID = fi.id + if w := runewidth.StringWidth(fi.id); w > fm.idMax { + fm.idMax = w + } + + f.Name = fi.name + if w := runewidth.StringWidth(fi.name); w > fm.nameMax { + fm.nameMax = w + } + + var locked bool + ff := d.IntEntry("Ff") + if ff != nil { + locked = uint(primitives.FieldFlags(*ff))&uint(primitives.FieldReadOnly) > 0 + } + f.Locked = locked + + ft := fi.ft + if ft == nil { + ft = d.NameEntry("FT") + if ft == nil { + return errors.Errorf("pdfcpu: corrupt form field %s: missing entry FT\n%s", f.ID, d) + } + } + + var err error + + switch *ft { + case "Btn": + err = collectBtn(xRefTable, d, &f, fm) + + case "Ch": + err = collectCh(xRefTable, d, &f, fm) + + case "Tx": + err = collectTx(d, &f, fm) + } + + if err != nil { + return err + } + + if !exists { + *fs = append(*fs, f) + } + + return nil +} + +func collectPageFields( + xRefTable *model.XRefTable, + wAnnots model.Annot, + fields types.Array, + p int, + fm *FieldMeta, + fs *[]Field) error { + + indRefs := map[types.IndirectRef]bool{} + + for _, ir := range *(wAnnots.IndRefs) { + + ok, fi, err := isField(xRefTable, ir, fields) + if err != nil { + return err + } + if !ok { + continue + } + + if fi.indRef != nil { + if indRefs[*fi.indRef] { + continue + } + indRefs[*fi.indRef] = true + ir = *fi.indRef + } + + d, err := xRefTable.DereferenceDict(ir) + if err != nil { + return err + } + if len(d) == 0 { + continue + } + + if err := collectPageField(xRefTable, d, p, fi, fm, fs); err != nil { + return err + } + } + + return nil +} + +func collectFields(xRefTable *model.XRefTable, fields types.Array, fm *FieldMeta) ([]Field, error) { + var fs []Field + + for p := 1; p <= xRefTable.PageCount; p++ { + + pgAnnots := xRefTable.PageAnnots[p] + if len(pgAnnots) == 0 { + continue + } + + wAnnots, ok := pgAnnots[model.AnnWidget] + if !ok { + continue + } + + if err := collectPageFields(xRefTable, wAnnots, fields, p, fm, &fs); err != nil { + return nil, err + } + } + + return fs, nil +} + +func calcListHeader(fm *FieldMeta) (string, []int) { + horSep := []int{} + + s := "Pg " + if fm.pageMax > 2 { + s += strings.Repeat(" ", fm.pageMax-2) + horSep = append(horSep, 15+fm.pageMax-2) + } else { + horSep = append(horSep, 15) + } + + s += "L Field " + draw.VBar + " Id " + if fm.idMax > 3 { + s += strings.Repeat(" ", fm.idMax-3) + horSep = append(horSep, 5+fm.idMax-3) + } else { + horSep = append(horSep, 5) + } + + s += draw.VBar + " Name " + if fm.nameMax > 4 { + s += strings.Repeat(" ", fm.nameMax-4) + horSep = append(horSep, 6+fm.nameMax-4) + } else { + horSep = append(horSep, 6) + } + + if fm.def { + s += draw.VBar + " Default " + if fm.defMax > 7 { + s += strings.Repeat(" ", fm.defMax-7) + horSep = append(horSep, 9+fm.defMax-7) + } else { + horSep = append(horSep, 9) + } + } + if fm.val { + s += draw.VBar + " Value " + if fm.valMax > 5 { + s += strings.Repeat(" ", fm.valMax-5) + horSep = append(horSep, 7+fm.valMax-5) + } else { + horSep = append(horSep, 7) + } + } + if fm.opt { + s += draw.VBar + " Options" + horSep = append(horSep, 8) + } + + return s, horSep +} + +func multiPageFieldsMap(fs []Field) map[string][]Field { + + m := map[string][]Field{} + + for _, f := range fs { + if len(f.Pages) == 1 { + continue + } + ps := f.pageString() + var fields []Field + if fs, ok := m[ps]; ok { + fields = append(fs, f) + } else { + fields = []Field{f} + } + m[ps] = fields + } + + return m +} + +func renderMultiPageFields(m map[string][]Field, fm *FieldMeta) ([]string, error) { + + var ss []string + + s, horSep := calcListHeader(fm) + + ss = append(ss, "Multi page fields:") + ss = append(ss, s) + ss = append(ss, draw.HorSepLine(horSep)) + + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + + p := "" + + for _, k := range keys { + + if p != "" { + ss = append(ss, draw.HorSepLine(horSep)) + } + p = k + + for _, f := range m[k] { + l := " " + if f.Locked { + l = "*" + } + + t := f.Typ.String() + + pageFill := strings.Repeat(" ", fm.pageMax-runewidth.StringWidth(f.pageString())) + idFill := strings.Repeat(" ", fm.idMax-runewidth.StringWidth(f.ID)) + nameFill := strings.Repeat(" ", fm.nameMax-runewidth.StringWidth(f.Name)) + s := fmt.Sprintf("%s%s %s %-9s %s %s%s %s %s%s ", p, pageFill, l, t, draw.VBar, f.ID, idFill, draw.VBar, f.Name, nameFill) + p = strings.Repeat(" ", len(p)) + if fm.def { + dvFill := strings.Repeat(" ", fm.defMax-runewidth.StringWidth(f.Dv)) + s += fmt.Sprintf("%s %s%s ", draw.VBar, f.Dv, dvFill) + } + if fm.val { + vFill := strings.Repeat(" ", fm.valMax-runewidth.StringWidth(f.V)) + s += fmt.Sprintf("%s %s%s ", draw.VBar, f.V, vFill) + } + if fm.opt { + s += fmt.Sprintf("%s %s", draw.VBar, f.Opts) + } + + ss = append(ss, s) + } + } + + ss = append(ss, "") + + return ss, nil +} + +func renderFields(ctx *model.Context, fs []Field, fm *FieldMeta) ([]string, error) { + + ss := []string{} + + m := multiPageFieldsMap(fs) + + if len(m) > 0 { + ss1, err := renderMultiPageFields(m, fm) + if err != nil { + return nil, err + } + ss = ss1 + } + + s, horSep := calcListHeader(fm) + + if ctx.SignatureExist || ctx.AppendOnly { + ss = append(ss, "(signed)") + } + ss = append(ss, s) + ss = append(ss, draw.HorSepLine(horSep)) + + i, needSep := 0, false + for _, f := range fs { + + if len(f.Pages) > 1 { + continue + } + + p := " " + pg := f.Pages[0] + if pg != i { + if pg > 1 && needSep { + ss = append(ss, draw.HorSepLine(horSep)) + } + i += pg - i + p = fmt.Sprintf("%d", i) + needSep = true + } + + l := " " + if f.Locked { + l = "*" + } + + t := f.Typ.String() + + pageFill := strings.Repeat(" ", fm.pageMax-runewidth.StringWidth(f.pageString())) + idFill := strings.Repeat(" ", fm.idMax-runewidth.StringWidth(f.ID)) + nameFill := strings.Repeat(" ", fm.nameMax-runewidth.StringWidth(f.Name)) + s := fmt.Sprintf("%s%s %s %-9s %s %s%s %s %s%s ", p, pageFill, l, t, draw.VBar, f.ID, idFill, draw.VBar, f.Name, nameFill) + if fm.def { + dvFill := strings.Repeat(" ", fm.defMax-runewidth.StringWidth(f.Dv)) + s += fmt.Sprintf("%s %s%s ", draw.VBar, f.Dv, dvFill) + } + if fm.val { + vFill := strings.Repeat(" ", fm.valMax-runewidth.StringWidth(f.V)) + s += fmt.Sprintf("%s %s%s ", draw.VBar, f.V, vFill) + } + if fm.opt { + s += fmt.Sprintf("%s %s", draw.VBar, f.Opts) + } + + ss = append(ss, s) + } + + return ss, nil +} + +// FormFields returns all form fields present in ctx. +func FormFields(ctx *model.Context) ([]Field, *FieldMeta, error) { + + xRefTable := ctx.XRefTable + + fields, err := fields(xRefTable) + if err != nil { + return nil, nil, err + } + + fm := &FieldMeta{pageMax: 2, idMax: 3, nameMax: 4, defMax: 7, valMax: 5} + + fs, err := collectFields(xRefTable, fields, fm) + if err != nil { + return nil, nil, err + } + + return fs, fm, nil +} + +// ListFormFields returns a list of all form fields present in ctx. +func ListFormFields(ctx *model.Context) ([]string, error) { + + // TODO Align output for Bangla, Hindi, Marathi. + + fs, fm, err := FormFields(ctx) + if err != nil { + return nil, err + } + + return renderFields(ctx, fs, fm) +} + +func annotIndRefs(xRefTable *model.XRefTable, fields types.Array) ([]types.IndirectRef, error) { + var indRefs []types.IndirectRef + for _, v := range fields { + indRef := v.(types.IndirectRef) + d, err := xRefTable.DereferenceDict(indRef) + if err != nil { + return nil, err + } + o, ok := d.Find("Kids") + if !ok { + indRefs = append(indRefs, indRef) + continue + } + kids, err := xRefTable.DereferenceArray(o) + if err != nil { + return nil, err + } + if _, ok := d.Find("FT"); ok { + indRefs = append(indRefs, indRef) + continue + } + // Non terminal field + kidIndRefs, err := annotIndRefs(xRefTable, kids) + if err != nil { + return nil, err + } + indRefs = append(indRefs, kidIndRefs...) + } + return indRefs, nil +} + +func annotIndRefSameLevel(xRefTable *model.XRefTable, fields types.Array, fieldIDOrName string) (*types.IndirectRef, error) { + for _, v := range fields { + indRef := v.(types.IndirectRef) + d, err := xRefTable.DereferenceDict(indRef) + if err != nil { + return nil, err + } + _, hasKids := d.Find("Kids") + _, hasFT := d.Find("FT") + if !hasKids || hasFT { + if indRef.ObjectNumber.String() == fieldIDOrName { + return &indRef, nil + } + id, err := d.StringOrHexLiteralEntry("T") + if err != nil { + return nil, err + } + if id != nil && *id == fieldIDOrName { + return &indRef, nil + } + } + } + return nil, nil +} + +func annotIndRefForField(xRefTable *model.XRefTable, fields types.Array, fieldIDOrName string) (*types.IndirectRef, error) { + if strings.IndexByte(fieldIDOrName, '.') < 0 { + // Must be on this level + return annotIndRefSameLevel(xRefTable, fields, fieldIDOrName) + } + + // Must be below + ss := strings.Split(fieldIDOrName, ".") + partialName := ss[0] + for _, v := range fields { + indRef := v.(types.IndirectRef) + d, err := xRefTable.DereferenceDict(indRef) + if err != nil { + return nil, err + } + o, hasKids := d.Find("Kids") + _, hasFT := d.Find("FT") + if !hasKids || hasFT { + continue + } + kids, err := xRefTable.DereferenceArray(o) + if err != nil { + return nil, err + } + if indRef.ObjectNumber.String() == partialName { + return annotIndRefForField(xRefTable, kids, fieldIDOrName[len(partialName)+1:]) + } + id, err := d.StringOrHexLiteralEntry("T") + if err != nil { + return nil, err + } + if id != nil { + if *id == partialName { + return annotIndRefForField(xRefTable, kids, fieldIDOrName[len(partialName)+1:]) + } + } + } + + return nil, nil +} + +func annotIndRefsForFields(xRefTable *model.XRefTable, f []string, fields types.Array) ([]types.IndirectRef, error) { + if len(f) == 0 { + return annotIndRefs(xRefTable, fields) + } + var indRefs []types.IndirectRef + for _, idOrName := range f { + indRef, err := annotIndRefForField(xRefTable, fields, idOrName) + if err != nil { + return nil, err + } + if indRef != nil { + indRefs = append(indRefs, *indRef) + continue + } + if log.CLIEnabled() { + log.CLI.Printf("unable to resolve field id/name: %s\n", idOrName) + } + } + return indRefs, nil +} + +func removeIndRefByIndex(indRefs []types.IndirectRef, i int) []types.IndirectRef { + l := len(indRefs) + lastIndex := l - 1 + if i != lastIndex { + indRefs[i] = indRefs[lastIndex] + } + return indRefs[:lastIndex] +} + +func removeFormFields(xRefTable *model.XRefTable, indRefs *[]types.IndirectRef, fields *types.Array) error { + f := types.Array{} + for _, v := range *fields { + indRef1 := v.(types.IndirectRef) + if len(*indRefs) == 0 { + f = append(f, indRef1) + continue + } + d, err := xRefTable.DereferenceDict(indRef1) + if err != nil { + return err + } + o, hasKids := d.Find("Kids") + _, hasFT := d.Find("FT") + if !hasKids || hasFT { + // terminal field + match := false + for j, indRef2 := range *indRefs { + if indRef1 == indRef2 { + *indRefs = removeIndRefByIndex(*indRefs, j) + match = true + break + } + } + if !match { + f = append(f, indRef1) + } + continue + } + // non terminal fields + kids, err := xRefTable.DereferenceArray(o) + if err != nil { + return err + } + if err := removeFormFields(xRefTable, indRefs, &kids); err != nil { + return err + } + if len(kids) > 0 { + d["Kids"] = kids + f = append(f, indRef1) + } + } + *fields = f + return nil +} + +func deletePageAnnots(xRefTable *model.XRefTable, m map[types.IndirectRef]bool, ok *bool) error { + for i := 1; i <= xRefTable.PageCount && len(m) > 0; i++ { + + d, _, _, err := xRefTable.PageDict(i, false) + if err != nil { + return err + } + + o, found := d.Find("Annots") + if !found { + continue + } + + arr, err := xRefTable.DereferenceArray(o) + if err != nil { + return err + } + + // Delete page annotations for removed form fields. + + for indRef1 := range m { + if len(arr) == 0 { + break + } + for j, v := range arr { + indRef2 := v.(types.IndirectRef) + if indRef1 == indRef2 { + arr = append(arr[:j], arr[j+1:]...) + delete(m, indRef1) + if err := xRefTable.DeleteObject(indRef1); err != nil { + return err + } + *ok = true + break + } + } + } + + if len(arr) == 0 { + d.Delete("Annots") + continue + } + d.Update("Annots", arr) + } + + return nil +} + +// RemoveFormFields deletes all form fields with given ID or name from the form represented by xRefTable. +func RemoveFormFields(ctx *model.Context, fieldIDsOrNames []string) (bool, error) { + + xRefTable := ctx.XRefTable + + fields, err := fields(xRefTable) + if err != nil { + return false, err + } + + indRefs, err := annotIndRefsForFields(xRefTable, fieldIDsOrNames, fields) + if err != nil { + return false, err + } + + indRefsClone := make([]types.IndirectRef, len(indRefs)) + copy(indRefsClone, indRefs) + + // Remove fields from AcroDict. + if err := removeFormFields(xRefTable, &indRefsClone, &fields); err != nil { + return false, err + } + + if len(indRefsClone) > 0 { + return false, errors.New("pdfcpu: Some form fields could not be removed") + } + + if len(fields) == 0 { + ctx.RootDict.Delete("AcroForm") + } else { + xRefTable.Form["Fields"] = fields + } + + var ok bool + + m := map[types.IndirectRef]bool{} + for _, indRef := range indRefs { + d, err := xRefTable.DereferenceDict(indRef) + if err != nil { + return false, err + } + o, ok := d.Find("Kids") + if !ok { + m[indRef] = true + continue + } + kids, err := xRefTable.DereferenceArray(o) + if err != nil { + return false, err + } + for _, indRef := range kids { + m[indRef.(types.IndirectRef)] = true + } + } + + if err := deletePageAnnots(xRefTable, m, &ok); err != nil { + return false, err + } + + if len(m) > 0 { + return false, errors.New("pdfcpu: Some form fields could not be removed") + } + + // pdfcpu provides all appearance streams for form fields. + // Yet for some files and viewers form fields don't get rendered. + // In these cases you can order the viewer to provide form field appearance streams. + if ctx.NeedAppearances { + xRefTable.Form["NeedAppearances"] = types.Boolean(true) + } + + return ok, nil +} + +func resetBtn(xRefTable *model.XRefTable, d types.Dict) error { + + ff := d.IntEntry("Ff") + if ff != nil && primitives.FieldFlags(*ff)&primitives.FieldPushbutton > 0 { + return nil + } + + v := types.Name("Off") + if s, found := d.Find("DV"); found { + v = s.(types.Name) + } + + d["V"] = v + if _, found := d.Find("AS"); found { + // Checkbox + d["AS"] = v + } + + vraw, err := types.DecodeName(v.String()) + if err != nil { + return err + } + + // RadiobuttonGroup + + for _, o := range d.ArrayEntry("Kids") { + d, err := xRefTable.DereferenceDict(o) + if err != nil { + return err + } + d1 := d.DictEntry("AP") + if d1 == nil { + return errors.New("corrupt form field: missing entry AP") + } + d2 := d1.DictEntry("N") + if d2 == nil { + return errors.New("corrupt AP field: missing entry N") + } + for k := range d2 { + k, err := types.DecodeName(k) + if err != nil { + return err + } + if k != "Off" { + d["AS"] = types.Name("Off") + if k == vraw { + d["AS"] = v + } + break + } + } + } + return nil +} + +func resetComboBoxOrRegularListBox(d types.Dict, opts []string, ff *int) (types.Array, error) { + ind := types.Array{} + sl := d.StringLiteralEntry("DV") + if sl == nil { + d.Delete("I") + d.Delete("V") + } else { + dv, err := types.StringLiteralToString(*sl) + if err != nil { + return nil, err + } + // Check if dv is a valid option. + for i, o := range opts { + if o == dv { + ind = append(ind, types.Integer(i)) + break + } + } + if len(ind) > 0 { + d["I"] = ind + d["V"] = *sl + } else { + d.Delete("I") + d.Delete("V") + } + } + if primitives.FieldFlags(*ff)&primitives.FieldCombo > 0 { + d.Delete("AP") + } + return ind, nil +} + +func resetMultiListBox(xRefTable *model.XRefTable, d types.Dict, opts []string) (types.Array, error) { + ind := types.Array{} + defaults, err := parseStringLiteralArray(xRefTable, d, "DV") + if err != nil { + return nil, err + } + for _, dv := range defaults { + for i, o := range opts { + if o == dv { + ind = append(ind, types.Integer(i)) + break + } + } + } + if len(defaults) > 0 { + d["I"] = ind + d["V"] = d["DV"] + } else { + d.Delete("I") + d.Delete("V") + } + + return ind, nil +} + +func resetCh(ctx *model.Context, d types.Dict, fonts map[string]types.IndirectRef) error { + ff := d.IntEntry("Ff") + if ff == nil { + return errors.New("pdfcpu: corrupt form field: missing entry Ff") + } + + opts, err := parseOptions(ctx.XRefTable, d) + if err != nil { + return err + } + if len(opts) == 0 { + return errors.New("pdfcpu: missing Opts") + } + + var ind types.Array + + if primitives.FieldFlags(*ff)&primitives.FieldCombo > 0 || primitives.FieldFlags(*ff)&primitives.FieldMultiselect == 0 { + ind, err = resetComboBoxOrRegularListBox(d, opts, ff) + } else { // primitives.FieldFlags(*ff)&primitives.FieldMultiselect > 0 + ind, err = resetMultiListBox(ctx.XRefTable, d, opts) + } + + if err != nil { + return err + } + + if primitives.FieldFlags(*ff)&primitives.FieldCombo == 0 { + if err := primitives.EnsureListBoxAP(ctx, d, opts, ind, fonts); err != nil { + return err + } + } + + return nil +} + +func resetTx(ctx *model.Context, d types.Dict, fonts map[string]types.IndirectRef) error { + var ( + s string + err error + ) + if o, found := d.Find("DV"); found { + d["V"] = o + sl, _ := o.(types.StringLiteral) + s, err = types.StringLiteralToString(sl) + if err != nil { + return err + } + } else { + if _, found := d["V"]; !found { + return nil + } + d.Delete("V") + } + + isDate := true + if s != "" { + _, err := primitives.DateFormatForDate(s) + isDate = err == nil + } + + if isDate { + err = primitives.EnsureDateFieldAP(ctx, d, s, fonts) + } else { + ff := d.IntEntry("Ff") + multiLine := ff != nil && uint(primitives.FieldFlags(*ff))&uint(primitives.FieldMultiline) > 0 + err = primitives.EnsureTextFieldAP(ctx, d, s, multiLine, fonts) + } + + return err +} + +func matchField(fi *fieldInfo, fieldIDsOrNames []string) bool { + return len(fieldIDsOrNames) == 0 || + types.MemberOf(fi.id, fieldIDsOrNames) || + types.MemberOf(fi.name, fieldIDsOrNames) +} + +func resetPageFields( + ctx *model.Context, + fieldIDsOrNames []string, + wAnnots model.Annot, + fields types.Array, + fonts map[string]types.IndirectRef, + ok *bool) error { + + indRefs := map[types.IndirectRef]bool{} + + for _, ir := range *(wAnnots.IndRefs) { + + found, fi, err := isField(ctx.XRefTable, ir, fields) + if err != nil { + return err + } + if !found { + continue + } + if !matchField(fi, fieldIDsOrNames) { + continue + } + + if fi.indRef != nil { + if indRefs[*fi.indRef] { + continue + } + indRefs[*fi.indRef] = true + ir = *fi.indRef + } + + d, err := ctx.DereferenceDict(ir) + if err != nil { + return err + } + if len(d) == 0 { + continue + } + + ft := fi.ft + if ft == nil { + ft = d.NameEntry("FT") + if ft == nil { + return errors.Errorf("pdfcpu: corrupt form field %s: missing entry FT\n%s", fi.id, d) + } + } + + switch *ft { + case "Btn": + err = resetBtn(ctx.XRefTable, d) + + case "Ch": + err = resetCh(ctx, d, fonts) + + case "Tx": + err = resetTx(ctx, d, fonts) + } + + if err != nil { + return err + } + + *ok = true + } + + return nil +} + +// ResetFormFields clears or resets all form fields contained in fieldIDsOrNames to its default. +func ResetFormFields(ctx *model.Context, fieldIDsOrNames []string) (bool, error) { + + xRefTable := ctx.XRefTable + + fields, err := fields(xRefTable) + if err != nil { + return false, err + } + + var ok bool + fonts := map[string]types.IndirectRef{} + + for i := 1; i <= xRefTable.PageCount; i++ { + + pgAnnots := xRefTable.PageAnnots[i] + if len(pgAnnots) == 0 { + continue + } + + wAnnots, found := pgAnnots[model.AnnWidget] + if !found { + continue + } + + if err := resetPageFields(ctx, fieldIDsOrNames, wAnnots, fields, fonts, &ok); err != nil { + return false, err + } + } + + for fName, indRef := range fonts { + + if len(ctx.UsedGIDs[fName]) == 0 { + continue + } + + fDict, err := xRefTable.DereferenceDict(indRef) + if err != nil { + return false, err + } + + fr := model.FontResource{} + if err := pdffont.IndRefsForUserfontUpdate(xRefTable, fDict, "", &fr); err != nil { + return false, pdffont.ErrCorruptFontDict + } + + if err := pdffont.UpdateUserfont(xRefTable, fName, fr); err != nil { + return false, nil + } + } + + // pdfcpu provides all appearance streams for form fields. + // Yet for some files and viewers form fields don't get rendered. + // In these cases you can order the viewer to provide form field appearance streams. + if ctx.NeedAppearances { + xRefTable.Form["NeedAppearances"] = types.Boolean(true) + } + + return ok, nil +} + +func lockFormField(d types.Dict) { + ff := d.IntEntry("Ff") + i := primitives.FieldFlags(0) + if ff != nil { + i = primitives.FieldFlags(*ff) + } + d["Ff"] = types.Integer(i | primitives.FieldReadOnly) +} + +func ensureAP(ctx *model.Context, d types.Dict, fi *fieldInfo, fonts map[string]types.IndirectRef) error { + ft := fi.ft + if ft == nil { + ft = d.NameEntry("FT") + if ft == nil { + return errors.Errorf("pdfcpu: corrupt form field %s: missing entry FT\n%s", fi.id, d) + } + } + + if *ft == "Ch" { + + ff := d.IntEntry("Ff") + if ff != nil && primitives.FieldFlags(*ff)&primitives.FieldCombo > 0 { + + v := "" + if sl := d.StringLiteralEntry("V"); sl != nil { + s, err := types.StringLiteralToString(*sl) + if err != nil { + return err + } + v = s + } + + if err := primitives.EnsureComboBoxAP(ctx, d, v, fonts); err != nil { + return err + } + + } + } + + return nil +} + +func lockPageFields( + ctx *model.Context, + fieldIDsOrNames []string, + fields types.Array, + wAnnots model.Annot, + fonts map[string]types.IndirectRef, + ok *bool) error { + + indRefs := map[types.IndirectRef]bool{} + + for _, ir := range *(wAnnots.IndRefs) { + + found, fi, err := isField(ctx.XRefTable, ir, fields) + if err != nil { + return err + } + if !found { + continue + } + + if !matchField(fi, fieldIDsOrNames) { + continue + } + + if fi.indRef != nil { + if indRefs[*fi.indRef] { + continue + } + indRefs[*fi.indRef] = true + ir = *fi.indRef + } + + d, err := ctx.DereferenceDict(ir) + if err != nil { + return err + } + if len(d) == 0 { + continue + } + + lockFormField(d) + *ok = true + + for _, o := range d.ArrayEntry("Kids") { + d, err := ctx.DereferenceDict(o) + if err != nil { + return err + } + lockFormField(d) + } + + if err := ensureAP(ctx, d, fi, fonts); err != nil { + return err + } + } + + return nil +} + +// LockFormFields turns all form fields contained in fieldIDsOrNames into read-only. +func LockFormFields(ctx *model.Context, fieldIDsOrNames []string) (bool, error) { + + // Note: Not honoured by Apple Preview for Checkboxes, RadiobuttonGroups and ComboBoxes. + + xRefTable := ctx.XRefTable + + fields, err := fields(xRefTable) + if err != nil { + return false, err + } + + var ok bool + fonts := map[string]types.IndirectRef{} + + for i := 1; i <= xRefTable.PageCount; i++ { + + pgAnnots := xRefTable.PageAnnots[i] + if len(pgAnnots) == 0 { + continue + } + + wAnnots, found := pgAnnots[model.AnnWidget] + if !found { + continue + } + + if err := lockPageFields(ctx, fieldIDsOrNames, fields, wAnnots, fonts, &ok); err != nil { + return false, err + } + } + + for fName, indRef := range fonts { + + if len(ctx.UsedGIDs[fName]) == 0 { + continue + } + + fDict, err := xRefTable.DereferenceDict(indRef) + if err != nil { + return false, err + } + + fr := model.FontResource{} + if err := pdffont.IndRefsForUserfontUpdate(xRefTable, fDict, "", &fr); err != nil { + return false, pdffont.ErrCorruptFontDict + } + + if err := pdffont.UpdateUserfont(xRefTable, fName, fr); err != nil { + return false, nil + } + } + + // pdfcpu provides all appearance streams for form fields. + // Yet for some files and viewers form fields don't get rendered. + // In these cases you can order the viewer to provide form field appearance streams. + if ctx.NeedAppearances { + xRefTable.Form["NeedAppearances"] = types.Boolean(true) + } + + return ok, nil +} + +func unlockFormField(d types.Dict) { + ff := d.IntEntry("Ff") + if ff != nil { + d["Ff"] = types.Integer(uint(primitives.FieldFlags(*ff)) & ^uint(primitives.FieldReadOnly)) + } +} + +func deleteAP(d types.Dict, fi *fieldInfo) error { + ft := fi.ft + if ft == nil { + ft = d.NameEntry("FT") + if ft == nil { + return errors.Errorf("pdfcpu: corrupt form field %s: missing entry FT\n%s", fi.id, d) + } + } + if *ft == "Ch" { + ff := d.IntEntry("Ff") + if ff != nil && primitives.FieldFlags(*ff)&primitives.FieldCombo > 0 { + d.Delete("AP") + } + } + return nil +} + +func unlockPageFields( + xRefTable *model.XRefTable, + fieldIDsOrNames []string, + fields types.Array, + wAnnots model.Annot, + ok *bool) error { + + indRefs := map[types.IndirectRef]bool{} + + for _, ir := range *(wAnnots.IndRefs) { + + found, fi, err := isField(xRefTable, ir, fields) + if err != nil { + return err + } + if !found { + continue + } + + if !matchField(fi, fieldIDsOrNames) { + continue + } + + if fi.indRef != nil { + if indRefs[*fi.indRef] { + continue + } + indRefs[*fi.indRef] = true + ir = *fi.indRef + } + + d, err := xRefTable.DereferenceDict(ir) + if err != nil { + return err + } + if len(d) == 0 { + continue + } + + unlockFormField(d) + + *ok = true + + for _, o := range d.ArrayEntry("Kids") { + d, err := xRefTable.DereferenceDict(o) + if err != nil { + return err + } + unlockFormField(d) + } + + if err := deleteAP(d, fi); err != nil { + return err + } + + } + + return nil +} + +// UnlockFields turns all form fields contained in fieldIDsOrNames writeable. +func UnlockFormFields(ctx *model.Context, fieldIDsOrNames []string) (bool, error) { + + xRefTable := ctx.XRefTable + + fields, err := fields(xRefTable) + if err != nil { + return false, err + } + + var ok bool + + for i := 1; i <= xRefTable.PageCount; i++ { + + pgAnnots := xRefTable.PageAnnots[i] + if len(pgAnnots) == 0 { + continue + } + + wAnnots, found := pgAnnots[model.AnnWidget] + if !found { + continue + } + + if err := unlockPageFields(xRefTable, fieldIDsOrNames, fields, wAnnots, &ok); err != nil { + return false, err + } + } + + // pdfcpu provides all appearance streams for form fields. + // Yet for some files and viewers form fields don't get rendered. + // In these cases you can order the viewer to provide form field appearance streams. + if ctx.NeedAppearances { + xRefTable.Form["NeedAppearances"] = types.Boolean(true) + } + + return ok, nil +} diff --git a/pkg/pdfcpu/format/format.go b/pkg/pdfcpu/format/format.go new file mode 100644 index 0000000000000000000000000000000000000000..0591f35501b819803569755f790b6aca4c36afe8 --- /dev/null +++ b/pkg/pdfcpu/format/format.go @@ -0,0 +1,71 @@ +/* +Copyright 2023 The pdfcpu Authors. + +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 + + 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 format + +import ( + "strconv" + "time" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" +) + +// Text returns a string with resolved place holders for pageNr, pageCount, timestamp or pdfcpu version. +func Text(text, timeStampFormat string, pageNr, pageCount int) (string, bool) { + // replace %p with pageNr + // %P with pageCount + // %t with timestamp + // %v with pdfcpu version + var ( + bb []byte + hasPercent bool + unique bool + ) + for i := 0; i < len(text); i++ { + if text[i] == '%' { + if hasPercent { + bb = append(bb, '%') + } + hasPercent = true + continue + } + if hasPercent { + hasPercent = false + if text[i] == 'p' { + bb = append(bb, strconv.Itoa(pageNr)...) + unique = true + continue + } + if text[i] == 'P' { + bb = append(bb, strconv.Itoa(pageCount)...) + unique = true + continue + } + if text[i] == 't' { + bb = append(bb, time.Now().Format(timeStampFormat)...) + unique = true + continue + } + if text[i] == 'v' { + bb = append(bb, model.VersionStr...) + unique = true + continue + } + } + bb = append(bb, text[i]) + } + return string(bb), unique +} diff --git a/pkg/pdfcpu/iccProfile.go b/pkg/pdfcpu/iccProfile.go new file mode 100644 index 0000000000000000000000000000000000000000..b4d8a2115b3cf58cb7121aa9b09f0b651e71d3ae --- /dev/null +++ b/pkg/pdfcpu/iccProfile.go @@ -0,0 +1,311 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 pdfcpu + +import ( + "encoding/binary" + "encoding/hex" + "fmt" + + "github.com/pkg/errors" +) + +// ICC profiles are not yet supported! +// +// We fall back to the alternate color space and if there is none to whatever color space makes sense. + +// ICC profiles use big endian always. +type iccProfile struct { + b []byte + rX, rY, rZ float32 // redMatrixColumn; the first column in the matrix, which is used in matrix/TRC transforms. + gX, gY, gZ float32 // greenMatrixColumn; the second column in the matrix, which is used in matrix/TRC transforms. + bX, bY, bZ float32 // blueMatrixColumn; the third column in the matrix, which is used in matrix/TRC transforms. + //TRC = tone reproduction curve +} + +// header 128 bytes +// tagcount 4 bytes +// tagtable signature4, offset4, size4(%4=0) +// elements (required, optional, private) + +// dateTimeNumber 12 Bytes +// positionNumber offset 4 Bytes size 4 bytes +// response16Number +// s15Fixed16Number + +// elementdata 4byte boundary padding + +// required: +// profileDescriptionTag +// copyrightTag +// chromaticAdaptationTag + +// BToA0Tag *** +// AToB0Tag + +func (p iccProfile) tag(sig string) (int, int, error) { + + for i, j := 0, 132; i < p.tagCount(); i++ { + s := string(p.b[j : j+4]) + if s != sig { + j += 12 + continue + } + j += 4 + off := binary.BigEndian.Uint32(p.b[j:]) + j += 4 + size := binary.BigEndian.Uint32(p.b[j:]) + return int(off), int(size), nil + } + + return 0, 0, errors.Errorf("tag %s not found", sig) +} + +func (p *iccProfile) matrixCol(sig string) (float32, float32, float32, error) { + + off, size, err := p.tag(sig) + if err != nil { + return 0, 0, 0, err + } + + if size != 20 { + return 0, 0, 0, errors.Errorf("tag %s should have size 20, has:%d", sig, size) + } + + x, y, z := p.xyz(off + 8) + + return x, y, z, nil +} + +func (p *iccProfile) init() error { + + var err error + + p.rX, p.rY, p.rZ, err = p.matrixCol("rXYZ") + if err != nil { + return err + } + + p.gX, p.gY, p.gZ, err = p.matrixCol("gXYZ") + if err != nil { + return err + } + + p.bX, p.bY, p.bZ, err = p.matrixCol("bXYZ") + + return err +} + +func (p iccProfile) size() uint32 { + return binary.BigEndian.Uint32(p.b[0:]) +} + +func (p iccProfile) preferredCMM() string { + return string(p.b[4:8]) +} + +func (p iccProfile) version() string { + major := p.b[8] + minor := p.b[9] >> 4 + bugfix := p.b[9] & 0x0F + return fmt.Sprintf("%d.%d.%d.0", major, minor, bugfix) +} + +func (p iccProfile) class() string { + return string(p.b[12:16]) +} + +func (p iccProfile) dataColorSpace() string { + return string(p.b[16:20]) +} + +func (p iccProfile) pcs() string { + return string(p.b[20:24]) +} + +func (p iccProfile) creationTS() string { + + y := binary.BigEndian.Uint16(p.b[24:]) + m := binary.BigEndian.Uint16(p.b[26:]) + d := binary.BigEndian.Uint16(p.b[28:]) + h := binary.BigEndian.Uint16(p.b[30:]) + min := binary.BigEndian.Uint16(p.b[32:]) + s := binary.BigEndian.Uint16(p.b[34:]) + + return fmt.Sprintf("%4d-%02d-%02d %02d:%02d:%02d", y, m, d, h, min, s) +} + +func (p iccProfile) fileSig() string { + return string(p.b[36:40]) +} + +func (p iccProfile) primaryPlatform() string { + return string(p.b[40:44]) +} + +func (p iccProfile) deviceManufacturer() string { + return string(p.b[48:52]) +} + +func (p iccProfile) deviceModel() string { + return string(p.b[52:56]) +} + +func (p iccProfile) renderingIntent() string { + ri := binary.BigEndian.Uint16(p.b[66:]) + switch ri { + case 0: + return "Perceptual" + case 1: + return "Media-relative colorimetric" + case 2: + return "Saturation" + case 3: + return "ICC-absolute colorimetric" + + } + return "Perceptual" +} + +func (p iccProfile) xyz(i int) (x, y, z float32) { + + x = float32(binary.BigEndian.Uint16(p.b[i:])) + f := float32(binary.BigEndian.Uint16(p.b[i+2:])) / 0x10000 + if x < 0 { + x -= f + } else { + x += f + } + i += 4 + + y = float32(binary.BigEndian.Uint16(p.b[i:])) + f = float32(binary.BigEndian.Uint16(p.b[i+2:])) / 0x10000 + if y < 0 { + y -= f + } else { + y += f + } + i += 4 + + z = float32(binary.BigEndian.Uint16(p.b[i:])) + f = float32(binary.BigEndian.Uint16(p.b[i+2:])) / 0x10000 + if z < 0 { + z -= f + } else { + z += f + } + + return +} + +func (p iccProfile) PCSIlluminant() string { + + x, y, z := p.xyz(68) + + return fmt.Sprintf("X=%4.4f Y=%4.4f Z=%4.4f", x, y, z) +} + +func (p iccProfile) creator() string { + return string(p.b[80:84]) +} + +func (p iccProfile) id() string { + return hex.EncodeToString(p.b[84:100]) +} + +func (p iccProfile) tagCount() int { + return int(binary.BigEndian.Uint32(p.b[128:])) +} + +func (p iccProfile) String() string { + + // profile size: 4 bytes at offset 0 (uintt32) + s := fmt.Sprintf(""+ + " size: %d\n"+ + " preferredCMM: %s\n"+ + " version: %s\n"+ + " class: %s\n"+ + " dataCS: %s\n"+ + " pcs: %s\n"+ + " creationTS: %s\n"+ + " fileSig: %s\n"+ + " primPlatform: %s\n"+ + "deviceManufacturer: %s\n"+ + " deviceModel: %s\n"+ + " rendering intent: %s\n"+ + " PCS illuminant: %s\n"+ + " creator: %s\n"+ + " id: %s\n"+ + " tagCount: %d\n\n", + p.size(), + p.preferredCMM(), + p.version(), + p.class(), + p.dataColorSpace(), + p.pcs(), + p.creationTS(), + p.fileSig(), + p.primaryPlatform(), + p.deviceManufacturer(), + p.deviceModel(), + p.renderingIntent(), + p.PCSIlluminant(), + p.creator(), + p.id(), + p.tagCount(), + ) + + for i, j := 0, 132; i < p.tagCount(); i++ { + sig := string(p.b[j : j+4]) + j += 4 + off := binary.BigEndian.Uint32(p.b[j:]) + j += 4 + size := binary.BigEndian.Uint32(p.b[j:]) + j += 4 + s += fmt.Sprintf("Tag %d: signature:%s offset:%d(#%02x) size:%d(#%02x)\n%s\n", i, sig, off, off, size, size, hex.Dump(p.b[off:off+size])) + //s += fmt.Sprintf("Tag %d: signature:%s offset:%d(#%02x) size:%d(#%02x)\n", i, sig, off, off, size, size) + } + s += fmt.Sprintf("Matrix:\n") + s += fmt.Sprintf("%4.4f %4.4f %4.4f\n", p.rX, p.gX, p.bX) + s += fmt.Sprintf("%4.4f %4.4f %4.4f\n", p.rY, p.gY, p.bY) + s += fmt.Sprintf("%4.4f %4.4f %4.4f\n", p.rZ, p.gZ, p.bZ) + + // cprt copyrightTag multiLocalizedUnicodeType contains the text copyright information for the profile. + // desc profileDescriptionTag multiLocalizedUnicodeType describes the structure containing invariant and localizable versions of the profile description for display. => 10.13 + + // wtpt mediaWhitePointTag XYZType used for generating the ICC-absolute colorimetric intent, specifies the chromatically adapted nCIEXYZ tristimulus values of the media white point. + // bkpt + + // rXYZ XYZType redMatrixColumnTag contains the first column in the matrix used in matrix/TRC transforms. + // gXYZ XYZType greenMatrixColumnTag contains the second column in the matrix used in matrix/TRC transforms. + // bXYZ XYZType blueMatrixColumnTag contains the third column in the matrix used in matrix/TRC transforms. + + // rTRC curveType or parametricCurveType redTRCTag contains the red channel tone reproduction curve. f(device)=linear + // gTRC curveType or parametricCurveType greenTRCTag contains the green channel tone reproduction curve. + // bTRC curveType or parametricCurveType blueTRCTag contains the blue channel tone reproduction curve. + + // dmnd deviceMfgDescTag multiLocalizedUnicodeType describes the structure containing invariant and localizable versions of the device manufacturer for display. => 10.13 + // dmdd deviceModelDescTag multiLocalizedUnicodeType describes the structure containing invariant and localizable versions of the device model for display. => 10.13 + // vued viewingCondDescTag describes the structure containing invariant and localizable versions of the viewing conditions. => 10.13 + + // view viewingConditionsTag viewingConditionsType defines the viewing conditions parameters. => 10.28 + // lumi luminanceTag XYZType contains the absolute luminance of emissive devices in candelas per square metre as described by the Y channel. + // meas measurementTag measurementType describes the alternative measurement specification, such as a D65 illuminant instead of the default D50. + // tech technologyTag signatureType => table 29 + + return s +} diff --git a/pkg/pdfcpu/image.go b/pkg/pdfcpu/image.go new file mode 100644 index 0000000000000000000000000000000000000000..8eb41af281f611e97217d89756bb4cb4518fe9f7 --- /dev/null +++ b/pkg/pdfcpu/image.go @@ -0,0 +1,258 @@ +/* +Copyright 2021 The pdfcpu Authors. + +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 + + 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 pdfcpu + +import ( + "fmt" + "path/filepath" + "sort" + "strconv" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +// Images returns all embedded images of ctx. +func Images(ctx *model.Context, selectedPages types.IntSet) ([]map[int]model.Image, *ImageListMaxLengths, error) { + pageNrs := []int{} + for k, v := range selectedPages { + if !v { + continue + } + pageNrs = append(pageNrs, k) + } + sort.Ints(pageNrs) + + mm := []map[int]model.Image{} + var ( + maxLenObjNr, maxLenID, maxLenSize, maxLenFilters int + ) + + for _, i := range pageNrs { + m, err := ExtractPageImages(ctx, i, true) + if err != nil { + return nil, nil, err + } + if len(m) == 0 { + continue + } + for _, i := range m { + s := strconv.Itoa(i.ObjNr) + if len(s) > maxLenObjNr { + maxLenObjNr = len(s) + } + if len(i.Name) > maxLenID { + maxLenID = len(i.Name) + } + lenSize := len(types.ByteSize(i.Size).String()) + if lenSize > maxLenSize { + maxLenSize = lenSize + } + if len(i.Filter) > maxLenFilters { + maxLenFilters = len(i.Filter) + } + } + mm = append(mm, m) + } + + maxLen := &ImageListMaxLengths{ObjNr: maxLenObjNr, ID: maxLenID, Size: maxLenSize, Filters: maxLenFilters} + + return mm, maxLen, nil +} + +func prepHorSep(horSep *[]int, maxLen *ImageListMaxLengths) string { + s := "Page Obj# " + if maxLen.ObjNr > 4 { + s += strings.Repeat(" ", maxLen.ObjNr-4) + *horSep = append(*horSep, 10+maxLen.ObjNr-4) + } else { + *horSep = append(*horSep, 10) + } + + s += draw.VBar + " Id " + if maxLen.ID > 2 { + s += strings.Repeat(" ", maxLen.ID-2) + *horSep = append(*horSep, 4+maxLen.ID-2) + } else { + *horSep = append(*horSep, 4) + } + + s += draw.VBar + " Type SoftMask ImgMask " + *horSep = append(*horSep, 24) + + s += draw.VBar + " Width " + draw.VBar + " Height " + draw.VBar + " ColorSpace Comp bpc Interp " + *horSep = append(*horSep, 7, 8, 28) + + s += draw.VBar + " " + if maxLen.Size > 4 { + s += strings.Repeat(" ", maxLen.Size-4) + *horSep = append(*horSep, 6+maxLen.Size-4) + } else { + *horSep = append(*horSep, 6) + } + s += "Size " + draw.VBar + " Filters" + if maxLen.Filters > 7 { + *horSep = append(*horSep, 8+maxLen.Filters-7) + } else { + *horSep = append(*horSep, 8) + } + + return s +} + +func sortedObjNrs(ii map[int]model.Image) []int { + objNrs := []int{} + for k := range ii { + objNrs = append(objNrs, k) + } + sort.Ints(objNrs) + return objNrs +} + +func listImages(ctx *model.Context, mm []map[int]model.Image, maxLen *ImageListMaxLengths) ([]string, int, int64, error) { + ss := []string{} + first := true + j, size := 0, int64(0) + m := map[int]bool{} + horSep := []int{} + for _, ii := range mm { + if first { + s := prepHorSep(&horSep, maxLen) + ss = append(ss, s) + first = false + } + ss = append(ss, draw.HorSepLine(horSep)) + + newPage := true + + for _, objNr := range sortedObjNrs(ii) { + img := ii[objNr] + pageNr := "" + if newPage { + pageNr = strconv.Itoa(img.PageNr) + newPage = false + } + t := "image" + if img.IsImgMask { + t = "imask" + } + if img.Thumb { + t = "thumb" + } + + sm := " " + if img.HasSMask { + sm = "*" + } + + im := " " + if img.HasImgMask { + im = "*" + } + + bpc := "-" + if img.Bpc > 0 { + bpc = strconv.Itoa(img.Bpc) + } + + interp := " " + if img.Interpol { + interp = "*" + } + + s := strconv.Itoa(img.ObjNr) + fill1 := strings.Repeat(" ", maxLen.ObjNr-len(s)) + if maxLen.ObjNr < 4 { + fill1 += strings.Repeat(" ", 4-maxLen.ObjNr) + } + + fill2 := strings.Repeat(" ", maxLen.ID-len(img.Name)) + if maxLen.ID < 2 { + fill2 += strings.Repeat(" ", 2-maxLen.ID-len(img.Name)) + } + + sizeStr := types.ByteSize(img.Size).String() + fill3 := strings.Repeat(" ", maxLen.Size-len(sizeStr)) + if maxLen.Size < 4 { + fill3 = strings.Repeat(" ", 4-maxLen.Size) + } + + ss = append(ss, fmt.Sprintf("%4s %s%s %s %s%s %s %s %s %s %s %5d %s %5d %s %10s %d %s %s %s %s%s %s %s", + pageNr, fill1, strconv.Itoa(img.ObjNr), draw.VBar, + fill2, img.Name, draw.VBar, + t, sm, im, draw.VBar, + img.Width, draw.VBar, + img.Height, draw.VBar, + img.Cs, img.Comp, bpc, interp, draw.VBar, + fill3, sizeStr, draw.VBar, img.Filter)) + + if !m[img.ObjNr] { + m[img.ObjNr] = true + j++ + size += img.Size + } + } + } + return ss, j, size, nil +} + +type ImageListMaxLengths struct { + ObjNr, ID, Size, Filters int +} + +// ListImages returns a formatted list of embedded images. +func ListImages(ctx *model.Context, selectedPages types.IntSet) ([]string, error) { + + mm, maxLen, err := Images(ctx, selectedPages) + if err != nil { + return nil, err + } + + ss, j, size, err := listImages(ctx, mm, maxLen) + if err != nil { + return nil, err + } + + return append([]string{fmt.Sprintf("%d images available(%s)", j, types.ByteSize(size))}, ss...), nil +} + +// WriteImageToDisk returns a closure for writing img to disk. +func WriteImageToDisk(outDir, fileName string) func(model.Image, bool, int) error { + return func(img model.Image, singleImgPerPage bool, maxPageDigits int) error { + if img.Reader == nil { + return nil + } + s := "%s_%" + fmt.Sprintf("0%dd", maxPageDigits) + qual := img.Name + if img.Thumb { + qual = "thumb" + } + f := fmt.Sprintf(s+"_%s.%s", fileName, img.PageNr, qual, img.FileType) + // if singleImgPerPage { + // if img.thumb { + // s += "_" + qual + // } + // f = fmt.Sprintf(s+".%s", fileName, img.pageNr, img.FileType) + // } + outFile := filepath.Join(outDir, f) + log.CLI.Printf("writing %s\n", outFile) + return WriteReader(outFile, img) + } +} diff --git a/pkg/pdfcpu/image_test.go b/pkg/pdfcpu/image_test.go new file mode 100644 index 0000000000000000000000000000000000000000..323cec48f2d71f4cecc463857d4c214951b901b7 --- /dev/null +++ b/pkg/pdfcpu/image_test.go @@ -0,0 +1,476 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 pdfcpu + +import ( + "bytes" + "fmt" + "image" + "image/color" + "io" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/filter" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +var inDir, outDir string +var xRefTable *model.XRefTable + +func TestMain(m *testing.M) { + + inDir = filepath.Join("..", "testdata", "resources") + + var err error + + xRefTable, err = CreateXRefTableWithRootDict() + if err != nil { + os.Exit(1) + } + + outDir, err = os.MkdirTemp("", "pdfcpu_imageTests") + if err != nil { + os.Exit(1) + } + + exitCode := m.Run() + + os.Exit(exitCode) +} + +func streamDictForJPGFile(xRefTable *model.XRefTable, fileName string) (*types.StreamDict, error) { + + bb, err := os.ReadFile(fileName) + if err != nil { + return nil, err + } + + c, _, err := image.DecodeConfig(bytes.NewReader(bb)) + if err != nil { + return nil, err + } + + var cs string + + switch c.ColorModel { + + case color.GrayModel: + cs = model.DeviceGrayCS + + case color.YCbCrModel: + cs = model.DeviceRGBCS + + case color.CMYKModel: + cs = model.DeviceCMYKCS + + default: + return nil, errors.New("pdfcpu: unexpected color model for JPEG") + + } + + sd, err := model.CreateDCTImageObject(xRefTable, bb, c.Width, c.Height, 8, cs) + if err != nil { + return nil, err + } + + // Ensure decoded image stream. + if err := sd.Decode(); err != nil { + return nil, err + } + + return sd, nil +} + +func streamDictForImageFile(xRefTable *model.XRefTable, fileName string) (*types.StreamDict, error) { + f, err := os.Open(fileName) + if err != nil { + return nil, err + } + defer f.Close() + + sd, _, _, err := model.CreateImageStreamDict(xRefTable, f, false, false) + return sd, err +} + +func compare(t *testing.T, fn1, fn2 string) { + + f1, err := os.Open(fn1) + if err != nil { + t.Errorf("%s: %v", fn1, err) + return + } + defer f1.Close() + + bb1, err := io.ReadAll(f1) + if err != nil { + t.Errorf("%s: %v", fn1, err) + return + } + + f2, err := os.Open(fn2) + if err != nil { + t.Errorf("%s: %v", fn2, err) + return + } + defer f1.Close() + + bb2, err := io.ReadAll(f2) + if err != nil { + t.Errorf("%s: %v", fn2, err) + return + } + + if len(bb1) != len(bb2) { + t.Errorf("%s <-> %s: length mismatch %d != %d", fn1, fn2, len(bb1), len(bb2)) + return + } + + for i := 0; i < len(bb1); i++ { + if bb1[i] != bb2[i] { + t.Errorf("%s <-> %s: mismatch at %d, 0x%02x != 0x%02x\n", fn1, fn2, i, bb1[i], bb2[i]) + return + } + } + +} + +func printOptionalSMask(t *testing.T, sd *types.StreamDict) { + o := sd.IndirectRefEntry("SMask") + if o != nil { + sm, err := xRefTable.Dereference(*o) + if err != nil { + t.Fatalf("err: %v\n", err) + } + fmt.Printf("SMask %s: %s\n", o, sm) + } +} +func TestReadWritePNGAndWEBP(t *testing.T) { + + for _, filename := range []string{ + "mountain.png", + "mountain.webp", + } { + + // Read a PNG file and create an image object which is a stream dict. + sd, err := streamDictForImageFile(xRefTable, filepath.Join(inDir, filename)) + if err != nil { + t.Fatalf("err: %v\n", err) + } + + // Print the image object. + fmt.Printf("created imageObj: %s\n", sd) + + // Print the optional SMask. + printOptionalSMask(t, sd) + + // The file type and its extension gets decided during the call to WriteImage! + // These testcases all produce PNG files. + fnNoExt := strings.TrimSuffix(filename, filepath.Ext(filename)) + tmpFileName1 := filepath.Join(outDir, fnNoExt) + fmt.Printf("tmpFileName: %s\n", tmpFileName1) + + // Write the image object (as PNG file) to disk. + // fn1 is the resulting fileName path including the suffix (aka filetype extension). + fn1, err := WriteImage(xRefTable, tmpFileName1, sd, false, 0) + if err != nil { + t.Fatalf("err: %v\n", err) + } + + // Since image/png does not write all ancillary chunks (eg. pHYs for dpi) + // we can only compare against a PNG file which resulted from using image/png. + + // Read in a PNG file created by pdfcpu and create an image object. + sd, err = streamDictForImageFile(xRefTable, fn1) + if err != nil { + t.Fatalf("err: %v\n", err) + } + + // Write the image object (as PNG file) to disk.s + fn2, err := WriteImage(xRefTable, tmpFileName1+"2", sd, false, 0) + if err != nil { + t.Fatalf("err: %v\n", err) + } + + // ..and compare each other. + compare(t, fn1, fn2) + } + +} + +// Read in a device gray image stream dump from disk. +func read1BPCDeviceGrayFlateStreamDump(xRefTable *model.XRefTable, fileName string) (*types.StreamDict, error) { + f, err := os.Open(fileName) + if err != nil { + return nil, err + } + defer f.Close() + + // Read in a flate encoded stream. + buf, err := io.ReadAll(f) + if err != nil { + return nil, err + } + + sd := &types.StreamDict{ + Dict: types.Dict( + map[string]types.Object{ + "Type": types.Name("XObject"), + "Subtype": types.Name("Image"), + "Width": types.Integer(1161), + "Height": types.Integer(392), + "BitsPerComponent": types.Integer(1), + "ColorSpace": types.Name(model.DeviceGrayCS), + "Decode": types.NewNumberArray(1, 0), + }, + ), + Raw: buf, + FilterPipeline: []types.PDFFilter{{Name: filter.Flate, DecodeParms: nil}}} + + sd.InsertName("Filter", filter.Flate) + + return sd, sd.Decode() +} + +// Starting out with a DeviceGray color space based image object, write a PNG file then read and write again. +func TestReadDeviceGrayWritePNG(t *testing.T) { + + // Create an image for a flate encoded stream dump file. + filename := "DeviceGray" + path := filepath.Join(inDir, filename+".raw") + + sd, err := read1BPCDeviceGrayFlateStreamDump(xRefTable, path) + if err != nil { + t.Fatalf("err: %v\n", err) + } + + // Print the image object. + fmt.Printf("created imageObj: %s\n", sd) + o := sd.IndirectRefEntry("SMask") + if o != nil { + sm, err := xRefTable.Dereference(*o) + if err != nil { + t.Fatalf("err: %v\n", err) + } + fmt.Printf("SMask %s: %s\n", o, sm) + } + + tmpFile1 := filepath.Join(outDir, filename) + + // Write the image object as PNG file. + fn1, err := WriteImage(xRefTable, tmpFile1, sd, false, 0) + if err != nil { + t.Fatalf("err: %v\n", err) + } + + // Since image/png does not write all ancillary chunks (eg. pHYs for dpi) + // we can only compare against a PNG file which resulted from using image/png. + + // Read in a PNG file created by pdfcpu and create an image object. + sd, err = streamDictForImageFile(xRefTable, fn1) + if err != nil || sd == nil { + t.Fatalf("err: %v\n", err) + } + + fmt.Printf("created another imageObj: %s\n", sd) + + tmpFile2 := filepath.Join(outDir, filename+"2") + + // Write the image object as PNG file. + fn2, err := WriteImage(xRefTable, tmpFile2, sd, false, 0) + if err != nil { + t.Fatalf("err: %v\n", err) + } + + // ..and compare each other. + compare(t, fn1, fn2) +} + +// Read in a device CMYK image stream dump from disk. +func read8BPCDeviceCMYKFlateStreamDump(xRefTable *model.XRefTable, fileName string) (*types.StreamDict, error) { + f, err := os.Open(fileName) + if err != nil { + return nil, err + } + defer f.Close() + + buf, err := io.ReadAll(f) + if err != nil { + return nil, err + } + + decodeParms := types.Dict( + map[string]types.Object{ + "BitsPerComponent": types.Integer(8), + "Colors": types.Integer(4), + "Columns": types.Integer(340), + }, + ) + + sd := &types.StreamDict{ + Dict: types.Dict( + map[string]types.Object{ + "Type": types.Name("XObject"), + "Subtype": types.Name("Image"), + "Width": types.Integer(340), + "Height": types.Integer(216), + "BitsPerComponent": types.Integer(8), + "ColorSpace": types.Name(model.DeviceCMYKCS), + }, + ), + Raw: buf, + FilterPipeline: []types.PDFFilter{{Name: filter.Flate, DecodeParms: decodeParms}}} + + sd.InsertName("Filter", filter.Flate) + + sd.FilterPipeline[0].DecodeParms = decodeParms + + return sd, sd.Decode() +} + +// Starting out with a CMYK color space based image object, write a TIFF file then read and write again. +func TestReadCMYKWriteTIFF(t *testing.T) { + + filename := "DeviceCMYK" + path := filepath.Join(inDir, filename+".raw") + + sd, err := read8BPCDeviceCMYKFlateStreamDump(xRefTable, path) + if err != nil { + t.Errorf("err: %v\n", err) + } + + // Print the image object. + fmt.Printf("created imageObj: %s\n", sd) + + // Print the optional SMask. + printOptionalSMask(t, sd) + + // The file type and its extension gets decided during WriteImage. + // These testcases all produce TIFF files. + tmpFile1 := filepath.Join(outDir, filename) + + // Write the image object as TIFF file. + fn1, err := WriteImage(xRefTable, tmpFile1, sd, false, 0) + if err != nil { + t.Errorf("err: %v\n", err) + } + + // Read in a TIFF file created by pdfcpu and create an image object. + sd, err = streamDictForImageFile(xRefTable, fn1) + if err != nil || sd == nil { + t.Errorf("err: %v\n", err) + } + + tmpFile2 := filepath.Join(outDir, filename+"2") + + // Write the image object as TIFF file. + fn2, err := WriteImage(xRefTable, tmpFile2, sd, false, 0) + if err != nil { + t.Errorf("err: %v\n", err) + } + + // ..and compare each other. + compare(t, fn1, fn2) + +} + +func TestReadTIFFWritePNG(t *testing.T) { + + // TIFF images get read into a Flate encoded image stream like PNGs. + // Any Flate encoded image stream gets written as PNG unless it operates in the Device CMYK color space. + + fileName := "mountain.tif" + + // Read a TIFF file and create an image object which is a stream dict. + sd, err := streamDictForImageFile(xRefTable, filepath.Join(inDir, fileName)) + if err != nil { + t.Fatalf("err: %v\n", err) + } + + // Print the image object. + fmt.Printf("created imageObj: %s\n", sd) + + // Print the optional SMask. + printOptionalSMask(t, sd) + + // The file type and its extension gets decided during the call to WriteImage! + // These testcases all produce PNG files. + fnNoExt := strings.TrimSuffix(fileName, filepath.Ext(fileName)) + tmpFileName1 := filepath.Join(outDir, fnNoExt) + fmt.Printf("tmpFileName: %s\n", tmpFileName1) + + // Write the image object (as PNG file) to disk. + // fn1 is the resulting fileName path including the suffix (aka filetype extension). + fn1, err := WriteImage(xRefTable, tmpFileName1, sd, false, 0) + if err != nil { + t.Fatalf("err: %v\n", err) + } + + // Since image/png does not write all ancillary chunks (eg. pHYs for dpi) + // we can only compare against a PNG file which resulted from using image/png. + + // Read in a PNG file created by pdfcpu and create an image object. + sd, err = streamDictForImageFile(xRefTable, fn1) + if err != nil { + t.Fatalf("err: %v\n", err) + } + + // Write the image object (as PNG file) to disk. + fn2, err := WriteImage(xRefTable, tmpFileName1+"2", sd, false, 0) + if err != nil { + t.Fatalf("err: %v\n", err) + } + + // ..and compare each other. + compare(t, fn1, fn2) +} + +func TestReadWriteJPEG(t *testing.T) { + + fileName := "mountain.jpg" + + // Read a JPEG file and create a stream dict w/o decoding. + sd, err := streamDictForJPGFile(xRefTable, filepath.Join(inDir, fileName)) + if err != nil { + t.Fatalf("err: %v\n", err) + } + + // Print the image object. + fmt.Printf("created imageObj: %s\n", sd) + + // Print the optional SMask. + printOptionalSMask(t, sd) + + // The file type and its extension gets decided during the call to WriteImage! + // These testcases all produce PNG files. + fnNoExt := strings.TrimSuffix(fileName, filepath.Ext(fileName)) + tmpFileName1 := filepath.Join(outDir, fnNoExt) + fmt.Printf("tmpFileName: %s\n", tmpFileName1) + + // Write the image object (as .jpg file) to disk. + // fn is the resulting fileName path including the suffix (aka filetype extension). + fn, err := WriteImage(xRefTable, tmpFileName1, sd, false, 0) + if err != nil { + t.Fatalf("err: %v\n", err) + } + fmt.Printf("fileName: %s\n", fn) + // No comparison since JPG is lossy. +} diff --git a/pkg/pdfcpu/importImage.go b/pkg/pdfcpu/importImage.go new file mode 100644 index 0000000000000000000000000000000000000000..baf74480a6860f6c0ead4b7034e328e6c91b06e7 --- /dev/null +++ b/pkg/pdfcpu/importImage.go @@ -0,0 +1,382 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 pdfcpu + +import ( + "bytes" + "fmt" + "io" + "strconv" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/matrix" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +type importParamMap map[string]func(string, *Import) error + +// Handle applies parameter completion and if successful +// parses the parameter values into import. +func (m importParamMap) Handle(paramPrefix, paramValueStr string, imp *Import) error { + + var param string + + // Completion support + for k := range m { + if !strings.HasPrefix(k, strings.ToLower(paramPrefix)) { + continue + } + if len(param) > 0 { + return errors.Errorf("pdfcpu: ambiguous parameter prefix \"%s\"", paramPrefix) + } + param = k + } + + if param == "" { + return errors.Errorf("pdfcpu: unknown parameter prefix \"%s\"", paramPrefix) + } + + return m[param](paramValueStr, imp) +} + +var impParamMap = importParamMap{ + "dimensions": parseDimensionsImp, + "dpi": parseDPI, + "formsize": parsePageFormatImp, + "papersize": parsePageFormatImp, + "position": parsePositionAnchorImp, + "offset": parsePositionOffsetImp, + "scalefactor": parseScaleFactorImp, + "gray": parseGray, + "sepia": parseSepia, + "backgroundcolor": parseImportBackgroundColor, + "bgcolor": parseImportBackgroundColor, +} + +// Import represents the command details for the command "ImportImage". +type Import struct { + PageDim *types.Dim // page dimensions in display unit. + PageSize string // one of A0,A1,A2,A3,A4(=default),A5,A6,A7,A8,Letter,Legal,Ledger,Tabloid,Executive,ANSIC,ANSID,ANSIE. + UserDim bool // true if one of dimensions or paperSize provided overriding the default. + DPI int // destination resolution to apply in dots per inch. + Pos types.Anchor // position anchor, one of tl,tc,tr,l,c,r,bl,bc,br,full. + Dx, Dy float64 // anchor offset. + Scale float64 // relative scale factor. 0 <= x <= 1 + ScaleAbs bool // true for absolute scaling. + InpUnit types.DisplayUnit // input display unit. + Gray bool // true for rendering in Gray. + Sepia bool + BgColor *color.SimpleColor // background color +} + +// DefaultImportConfig returns the default configuration. +func DefaultImportConfig() *Import { + return &Import{ + PageDim: types.PaperSize["A4"], + PageSize: "A4", + Pos: types.Full, + Scale: 0.5, + InpUnit: types.POINTS, + } +} + +func (imp Import) String() string { + + sc := "relative" + if imp.ScaleAbs { + sc = "absolute" + } + + return fmt.Sprintf("Import conf: %s %s, pos=%s, dx=%f.2, dy=%f.2, scaling: %.1f %s\n", + imp.PageSize, *imp.PageDim, imp.Pos, imp.Dx, imp.Dy, imp.Scale, sc) +} + +func parsePageFormatImp(s string, imp *Import) (err error) { + if imp.UserDim { + return errors.New("pdfcpu: only one of formsize(papersize) or dimensions allowed") + } + imp.PageDim, imp.PageSize, err = types.ParsePageFormat(s) + imp.UserDim = true + return err +} + +func ParsePageDim(v string, u types.DisplayUnit) (*types.Dim, string, error) { + + ss := strings.Split(v, " ") + if len(ss) != 2 { + return nil, v, errors.Errorf("pdfcpu: illegal dimension string: need 2 positive values, %s\n", v) + } + + w, err := strconv.ParseFloat(ss[0], 64) + if err != nil || w <= 0 { + return nil, v, errors.Errorf("pdfcpu: dimension X must be a positiv numeric value: %s\n", ss[0]) + } + + h, err := strconv.ParseFloat(ss[1], 64) + if err != nil || h <= 0 { + return nil, v, errors.Errorf("pdfcpu: dimension Y must be a positiv numeric value: %s\n", ss[1]) + } + + d := types.Dim{Width: types.ToUserSpace(w, u), Height: types.ToUserSpace(h, u)} + + return &d, "", nil +} + +func parseDimensionsImp(s string, imp *Import) (err error) { + if imp.UserDim { + return errors.New("pdfcpu: only one of formsize(papersize) or dimensions allowed") + } + imp.PageDim, imp.PageSize, err = ParsePageDim(s, imp.InpUnit) + imp.UserDim = true + return err +} + +func parsePositionAnchorImp(s string, imp *Import) error { + a, err := types.ParsePositionAnchor(s) + if err != nil { + return err + } + imp.Pos = a + return nil +} + +func parsePositionOffsetImp(s string, imp *Import) error { + + d := strings.Split(s, " ") + if len(d) != 2 { + return errors.Errorf("pdfcpu: illegal position offset string: need 2 numeric values, %s\n", s) + } + + f, err := strconv.ParseFloat(d[0], 64) + if err != nil { + return err + } + imp.Dx = types.ToUserSpace(f, imp.InpUnit) + + f, err = strconv.ParseFloat(d[1], 64) + if err != nil { + return err + } + imp.Dy = types.ToUserSpace(f, imp.InpUnit) + + return nil +} + +func parseScaleFactorImp(s string, imp *Import) (err error) { + imp.Scale, imp.ScaleAbs, err = parseScaleFactor(s) + return err +} + +func parseDPI(s string, imp *Import) (err error) { + imp.DPI, err = strconv.Atoi(s) + return err +} + +func parseGray(s string, imp *Import) error { + switch strings.ToLower(s) { + case "on", "true", "t": + imp.Gray = true + case "off", "false", "f": + imp.Gray = false + default: + return errors.New("pdfcpu: import gray, please provide one of: on/off true/false") + } + + return nil +} + +func parseSepia(s string, imp *Import) error { + switch strings.ToLower(s) { + case "on", "true", "t": + imp.Sepia = true + case "off", "false", "f": + imp.Sepia = false + default: + return errors.New("pdfcpu: import sepia, please provide one of: on/off true/false") + } + + return nil +} + +func parseImportBackgroundColor(s string, imp *Import) error { + c, err := color.ParseColor(s) + if err != nil { + return err + } + imp.BgColor = &c + return nil +} + +// ParseImportDetails parses an Import command string into an internal structure. +func ParseImportDetails(s string, u types.DisplayUnit) (*Import, error) { + + if s == "" { + return nil, nil + } + + imp := DefaultImportConfig() + imp.InpUnit = u + + ss := strings.Split(s, ",") + + for _, s := range ss { + + ss1 := strings.Split(s, ":") + if len(ss1) != 2 { + return nil, errors.New("pdfcpu: Invalid import configuration string. Please consult pdfcpu help import") + } + + paramPrefix := strings.TrimSpace(ss1[0]) + paramValueStr := strings.TrimSpace(ss1[1]) + + if err := impParamMap.Handle(paramPrefix, paramValueStr, imp); err != nil { + return nil, err + } + } + + return imp, nil +} + +func importImagePDFBytes(wr io.Writer, pageDim *types.Dim, imgWidth, imgHeight float64, imp *Import) { + + vpw := float64(pageDim.Width) + vph := float64(pageDim.Height) + vp := types.RectForDim(vpw, vph) + + if imp.BgColor != nil { + draw.FillRectNoBorder(wr, vp, *imp.BgColor) + } + + if imp.Pos == types.Full { + fmt.Fprintf(wr, "q %f 0 0 %f 0 0 cm /Im0 Do Q", vp.Width(), vp.Height()) + return + } + + if imp.DPI > 0 { + // NOTE: We could also set "UserUnit" in the page dict. + imgWidth *= float64(72) / float64(imp.DPI) + imgHeight *= float64(72) / float64(imp.DPI) + } + + bb := types.RectForDim(imgWidth, imgHeight) + ar := bb.AspectRatio() + + if imp.ScaleAbs { + bb.UR.X = imp.Scale * bb.Width() + bb.UR.Y = bb.UR.X / ar + } else { + if ar >= 1 { + if vp.AspectRatio() <= 1 { + bb.UR.X = imp.Scale * vpw + bb.UR.Y = bb.UR.X / ar + } else { + if ar >= vp.AspectRatio() { + bb.UR.X = imp.Scale * vpw + bb.UR.Y = bb.UR.X / ar + } else { + bb.UR.Y = imp.Scale * vph + bb.UR.X = bb.UR.Y * ar + } + } + } else { + if vp.AspectRatio() >= 1 { + bb.UR.Y = imp.Scale * vph + bb.UR.X = bb.UR.Y * ar + } else { + if ar <= vp.AspectRatio() { + bb.UR.Y = imp.Scale * vph + bb.UR.X = bb.UR.Y * ar + } else { + bb.UR.X = imp.Scale * vpw + bb.UR.Y = bb.UR.X / ar + } + } + } + } + + m := matrix.IdentMatrix + + // Scale + m[0][0] = bb.Width() + m[1][1] = bb.Height() + + // Translate + ll := model.LowerLeftCorner(vp, bb.Width(), bb.Height(), imp.Pos) + m[2][0] = ll.X + imp.Dx + m[2][1] = ll.Y + imp.Dy + + fmt.Fprintf(wr, "q %.5f %.5f %.5f %.5f %.5f %.5f cm /Im0 Do Q", + m[0][0], m[0][1], m[1][0], m[1][1], m[2][0], m[2][1]) +} + +// NewPageForImage creates a new page dict in xRefTable for given image reader r. +func NewPageForImage(xRefTable *model.XRefTable, r io.Reader, parentIndRef *types.IndirectRef, imp *Import) (*types.IndirectRef, error) { + + // create image dict. + imgIndRef, w, h, err := model.CreateImageResource(xRefTable, r, imp.Gray, imp.Sepia) + if err != nil { + return nil, err + } + + // create resource dict for XObject. + d := types.Dict( + map[string]types.Object{ + "ProcSet": types.NewNameArray("PDF", "Text", "ImageB", "ImageC", "ImageI"), + "XObject": types.Dict(map[string]types.Object{"Im0": *imgIndRef}), + }, + ) + + resIndRef, err := xRefTable.IndRefForNewObject(d) + if err != nil { + return nil, err + } + + dim := &types.Dim{Width: float64(w), Height: float64(h)} + if imp.Pos != types.Full { + dim = imp.PageDim + } + // mediabox = physical page dimensions + mediaBox := types.RectForDim(dim.Width, dim.Height) + + var buf bytes.Buffer + importImagePDFBytes(&buf, dim, float64(w), float64(h), imp) + sd, _ := xRefTable.NewStreamDictForBuf(buf.Bytes()) + if err = sd.Encode(); err != nil { + return nil, err + } + + contentsIndRef, err := xRefTable.IndRefForNewObject(*sd) + if err != nil { + return nil, err + } + + pageDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("Page"), + "Parent": *parentIndRef, + "MediaBox": mediaBox.Array(), + "Resources": *resIndRef, + "Contents": *contentsIndRef, + }, + ) + + return xRefTable.IndRefForNewObject(pageDict) +} diff --git a/pkg/pdfcpu/info.go b/pkg/pdfcpu/info.go new file mode 100644 index 0000000000000000000000000000000000000000..0c798da96b4eefb2cc4bdc0d313443d97e4e9aeb --- /dev/null +++ b/pkg/pdfcpu/info.go @@ -0,0 +1,610 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 pdfcpu + +import ( + "fmt" + "time" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +func extractAuthor(ctx *model.Context, obj types.Object) (err error) { + // Record for stats. + if ctx.Author, err = ctx.DereferenceText(obj); err != nil { + return err + } + ctx.Author = model.CSVSafeString(ctx.Author) + return nil +} + +func extractCreator(ctx *model.Context, obj types.Object) (err error) { + // Record for stats. + ctx.Creator, err = ctx.DereferenceText(obj) + if err != nil { + return err + } + ctx.Creator = model.CSVSafeString(ctx.Creator) + return nil +} + +func logKey(key string) { + if log.WriteEnabled() { + log.Write.Println("found " + key) + } +} + +// handleInfoDict extracts relevant infoDict fields into the context. +func handleInfoDict(ctx *model.Context, d types.Dict) (err error) { + for key, value := range d { + + switch key { + + case "Title": + logKey(key) + + case "Author": + logKey(key) + if err = extractAuthor(ctx, value); err != nil { + return err + } + + case "Subject": + logKey(key) + + case "Keywords": + logKey(key) + + case "Creator": + logKey(key) + if err = extractCreator(ctx, value); err != nil { + return err + } + + case "Producer", "CreationDate", "ModDate": + // pdfcpu will modify these as direct dict entries. + logKey(key) + if indRef, ok := value.(types.IndirectRef); ok { + // Get rid of these extra objects. + ctx.Optimize.DuplicateInfoObjects[int(indRef.ObjectNumber)] = true + } + + case "Trapped": + logKey("Trapped") + + default: + if log.WriteEnabled() { + log.Write.Printf("handleInfoDict: found out of spec entry %s %v\n", key, value) + } + + } + } + + return nil +} + +func ensureInfoDict(ctx *model.Context) error { + // => 14.3.3 Document Information Dictionary + + // Optional: + // Title - + // Author - + // Subject - + // Keywords - + // Creator - + // Producer modified by pdfcpu + // CreationDate modified by pdfcpu + // ModDate modified by pdfcpu + // Trapped - + + now := types.DateString(time.Now()) + + v := "pdfcpu " + model.VersionStr + + if ctx.Info == nil { + + d := types.NewDict() + d.InsertString("Producer", v) + d.InsertString("CreationDate", now) + d.InsertString("ModDate", now) + + ir, err := ctx.IndRefForNewObject(d) + if err != nil { + return err + } + + ctx.Info = ir + + return nil + } + + d, err := ctx.DereferenceDict(*ctx.Info) + if err != nil || d == nil { + return err + } + + if err = handleInfoDict(ctx, d); err != nil { + return err + } + + d.Update("CreationDate", types.StringLiteral(now)) + d.Update("ModDate", types.StringLiteral(now)) + d.Update("Producer", types.StringLiteral(v)) + + return nil +} + +// Write the document info object for this PDF file. +func writeDocumentInfoDict(ctx *model.Context) error { + if log.WriteEnabled() { + log.Write.Printf("*** writeDocumentInfoDict begin: offset=%d ***\n", ctx.Write.Offset) + } + + // Note: The document info object is optional but pdfcpu ensures one. + + if ctx.Info == nil { + if log.WriteEnabled() { + log.Write.Printf("writeDocumentInfoObject end: No info object present, offset=%d\n", ctx.Write.Offset) + } + return nil + } + + if log.WriteEnabled() { + log.Write.Printf("writeDocumentInfoObject: %s\n", *ctx.Info) + } + + o := *ctx.Info + + d, err := ctx.DereferenceDict(o) + if err != nil || d == nil { + return err + } + + if _, _, err = writeDeepObject(ctx, o); err != nil { + return err + } + + if log.WriteEnabled() { + log.Write.Printf("*** writeDocumentInfoDict end: offset=%d ***\n", ctx.Write.Offset) + } + + return nil +} + +func appendEqualMediaAndCropBoxInfo(ss *[]string, pb model.PageBoundaries, unit string, currUnit types.DisplayUnit) { + mb := pb.MediaBox() + tb := pb.TrimBox() + bb := pb.BleedBox() + ab := pb.ArtBox() + s := " = CropBox" + + if tb == nil || tb.Equals(*mb) { + s += ", TrimBox" + } + if bb == nil || bb.Equals(*mb) { + s += ", BleedBox" + } + if ab == nil || ab.Equals(*mb) { + s += ", ArtBox" + } + + *ss = append(*ss, fmt.Sprintf(" MediaBox (%s) %v %s", unit, mb.Format(currUnit), s)) + + if tb != nil && !tb.Equals(*mb) { + *ss = append(*ss, fmt.Sprintf(" TrimBox (%s) %v", unit, tb.Format(currUnit))) + } + if bb != nil && !bb.Equals(*mb) { + *ss = append(*ss, fmt.Sprintf(" BleedBox (%s) %v", unit, bb.Format(currUnit))) + } + if ab != nil && !ab.Equals(*mb) { + *ss = append(*ss, fmt.Sprintf(" ArtBox (%s) %v", unit, ab.Format(currUnit))) + } +} + +func trimBleedArtBoxString(cb, tb, bb, ab *types.Rectangle) string { + s := "" + if tb == nil || tb.Equals(*cb) { + s += "= TrimBox" + } + if bb == nil || bb.Equals(*cb) { + if len(s) == 0 { + s += "= " + } else { + s += ", " + } + s += "BleedBox" + } + if ab == nil || ab.Equals(*cb) { + if len(s) == 0 { + s += "= " + } else { + s += ", " + } + s += "ArtBox" + } + return s +} + +func appendNotEqualMediaAndCropBoxInfo(ss *[]string, pb model.PageBoundaries, unit string, currUnit types.DisplayUnit) { + mb := pb.MediaBox() + cb := pb.CropBox() + tb := pb.TrimBox() + bb := pb.BleedBox() + ab := pb.ArtBox() + + *ss = append(*ss, fmt.Sprintf(" MediaBox (%s) %v", unit, mb.Format(currUnit))) + + s := trimBleedArtBoxString(cb, tb, bb, ab) + *ss = append(*ss, fmt.Sprintf(" CropBox (%s) %v %s", unit, cb.Format(currUnit), s)) + + if tb != nil && !tb.Equals(*mb) && !tb.Equals(*cb) { + *ss = append(*ss, fmt.Sprintf(" TrimBox (%s) %v", unit, tb.Format(currUnit))) + } + if bb != nil && !bb.Equals(*mb) && !bb.Equals(*cb) { + *ss = append(*ss, fmt.Sprintf(" BleedBox (%s) %v", unit, bb.Format(currUnit))) + } + if ab != nil && !ab.Equals(*mb) && !ab.Equals(*cb) { + *ss = append(*ss, fmt.Sprintf(" ArtBox (%s) %v", unit, ab.Format(currUnit))) + } +} + +func appendPageBoxesInfo(ss *[]string, pb model.PageBoundaries, unit string, currUnit types.DisplayUnit, i int) { + d := pb.CropBox().Dimensions() + if pb.Rot%180 != 0 { + d.Width, d.Height = d.Height, d.Width + } + or := "portrait" + if d.Landscape() { + or = "landscape" + } + s := fmt.Sprintf("rot=%+d orientation:%s", pb.Rot, or) + *ss = append(*ss, fmt.Sprintf("Page %d: %s", i+1, s)) + mb := pb.MediaBox() + cb := pb.CropBox() + if cb == nil || mb != nil && mb.Equals(*cb) { + appendEqualMediaAndCropBoxInfo(ss, pb, unit, currUnit) + return + } + appendNotEqualMediaAndCropBoxInfo(ss, pb, unit, currUnit) +} + +func pageInfo(info *PDFInfo, selectedPages types.IntSet) ([]string, error) { + ss := []string{} + + if len(selectedPages) > 0 { + for i, pb := range info.PageBoundaries { + if _, found := selectedPages[i+1]; !found { + continue + } + appendPageBoxesInfo(&ss, pb, info.UnitString, info.Unit, i) + } + return ss, nil + } + + s := "Page sizes:" + for d := range info.PageDimensions { + dc := d.ConvertToUnit(info.Unit) + ss = append(ss, fmt.Sprintf("%21s %.2f x %.2f %s", s, dc.Width, dc.Height, info.UnitString)) + s = "" + } + return ss, nil +} + +type PDFInfo struct { + FileName string `json:"source,omitempty"` + Version string `json:"version"` + PageCount int `json:"pageCount"` + PageBoundaries []model.PageBoundaries `json:"-"` + Boundaries map[string]model.PageBoundaries `json:"pageBoundaries,omitempty"` + PageDimensions map[types.Dim]bool `json:"-"` + Dimensions []types.Dim `json:"pageSizes,omitempty"` + Title string `json:"title"` + Author string `json:"author"` + Subject string `json:"subject"` + Producer string `json:"producer"` + Creator string `json:"creator"` + CreationDate string `json:"creationDate"` + ModificationDate string `json:"modificationDate"` + PageMode string `json:"pageMode,omitempty"` + PageLayout string `json:"pageLayout,omitempty"` + ViewerPref *model.ViewerPreferences `json:"viewerPreferences,omitempty"` + Keywords []string `json:"keywords"` + Properties map[string]string `json:"properties"` + Tagged bool `json:"tagged"` + Hybrid bool `json:"hybrid"` + Linearized bool `json:"linearized"` + UsingXRefStreams bool `json:"usingXRefStreams"` + UsingObjectStreams bool `json:"usingObjectStreams"` + Watermarked bool `json:"watermarked"` + Thumbnails bool `json:"thumbnails"` + Form bool `json:"form"` + Signatures bool `json:"signatures"` + AppendOnly bool `json:"appendOnly"` + Outlines bool `json:"bookmarks"` + Names bool `json:"names"` + Encrypted bool `json:"encrypted"` + Permissions int `json:"permissions"` + Attachments []model.Attachment `json:"attachments,omitempty"` + Unit types.DisplayUnit `json:"-"` + UnitString string `json:"unit"` +} + +func (info PDFInfo) renderKeywords(ss *[]string) error { + for i, l := range info.Keywords { + if i == 0 { + *ss = append(*ss, fmt.Sprintf("%20s: %s", "Keywords", l)) + continue + } + *ss = append(*ss, fmt.Sprintf("%20s %s", "", l)) + } + return nil +} + +func (info PDFInfo) renderProperties(ss *[]string) error { + first := true + for k, v := range info.Properties { + if first { + *ss = append(*ss, fmt.Sprintf("%20s: %s = %s", "Properties", k, v)) + first = false + continue + } + *ss = append(*ss, fmt.Sprintf("%20s %s = %s", "", k, v)) + } + return nil +} + +func (info PDFInfo) renderFlagsPart1(ss *[]string, separator string) { + *ss = append(*ss, separator) + + s := "No" + if info.Tagged { + s = "Yes" + } + *ss = append(*ss, fmt.Sprintf(" Tagged: %s", s)) + + s = "No" + if info.Hybrid { + s = "Yes" + } + *ss = append(*ss, fmt.Sprintf(" Hybrid: %s", s)) + + s = "No" + if info.Linearized { + s = "Yes" + } + *ss = append(*ss, fmt.Sprintf(" Linearized: %s", s)) + + s = "No" + if info.UsingXRefStreams { + s = "Yes" + } + *ss = append(*ss, fmt.Sprintf(" Using XRef streams: %s", s)) + + s = "No" + if info.UsingObjectStreams { + s = "Yes" + } + *ss = append(*ss, fmt.Sprintf("Using object streams: %s", s)) +} + +func (info PDFInfo) renderFlagsPart2(ss *[]string, separator string) { + s := "No" + if info.Watermarked { + s = "Yes" + } + *ss = append(*ss, fmt.Sprintf(" Watermarked: %s", s)) + + s = "No" + if info.Thumbnails { + s = "Yes" + } + *ss = append(*ss, fmt.Sprintf(" Thumbnails: %s", s)) + + s = "No" + if info.Form { + s = "Yes" + } + *ss = append(*ss, fmt.Sprintf(" Form: %s", s)) + if info.Form { + if info.Signatures || info.AppendOnly { + *ss = append(*ss, " SignaturesExist: Yes") + s = "No" + if info.AppendOnly { + s = "Yes" + } + *ss = append(*ss, fmt.Sprintf(" AppendOnly: %s", s)) + } + } + + s = "No" + if info.Outlines { + s = "Yes" + } + *ss = append(*ss, fmt.Sprintf(" Outlines: %s", s)) + + s = "No" + if info.Names { + s = "Yes" + } + *ss = append(*ss, fmt.Sprintf(" Names: %s", s)) + + *ss = append(*ss, separator) + + s = "No" + if info.Encrypted { + s = "Yes" + } + *ss = append(*ss, fmt.Sprintf("%20s: %s", "Encrypted", s)) +} + +func (info *PDFInfo) renderFlags(ss *[]string, separator string) { + info.renderFlagsPart1(ss, separator) + info.renderFlagsPart2(ss, separator) +} + +func (info *PDFInfo) renderPermissions(ss *[]string) { + l := PermissionsList(info.Permissions) + if len(l) == 1 { + *ss = append(*ss, fmt.Sprintf("%20s: %s", "Permissions", l[0])) + } else { + *ss = append(*ss, fmt.Sprintf("%20s:", "Permissions")) + *ss = append(*ss, l...) + } +} + +func (info *PDFInfo) renderAttachments(ss *[]string) { + for i, a := range info.Attachments { + if i == 0 { + *ss = append(*ss, fmt.Sprintf("%20s: %s", "Attachments", a.FileName)) + continue + } + *ss = append(*ss, fmt.Sprintf("%20s %s", "", a.FileName)) + } +} + +// Info returns info about ctx. +func Info(ctx *model.Context, fileName string, selectedPages types.IntSet) (*PDFInfo, error) { + info := &PDFInfo{FileName: fileName, Unit: ctx.Unit, UnitString: ctx.UnitString()} + + v := ctx.HeaderVersion + if ctx.RootVersion != nil { + v = ctx.RootVersion + } + info.Version = (*v).String() + + info.PageCount = ctx.PageCount + + // PageBoundaries for selected pages. + pbs, err := ctx.PageBoundaries(selectedPages) + if err != nil { + return nil, err + } + info.PageBoundaries = pbs + + // Media box dimensions for all pages. + pd, err := ctx.PageDims() + if err != nil { + return nil, err + } + m := map[types.Dim]bool{} + for _, d := range pd { + m[d] = true + } + info.PageDimensions = m + + info.Title = ctx.Title + info.Author = ctx.Author + info.Subject = ctx.Subject + info.Producer = ctx.Producer + info.Creator = ctx.Creator + info.CreationDate = ctx.CreationDate + info.ModificationDate = ctx.ModDate + + info.PageMode = "" + if ctx.PageMode != nil { + info.PageMode = ctx.PageMode.String() + } + + info.PageLayout = "" + if ctx.PageLayout != nil { + info.PageLayout = ctx.PageLayout.String() + } + + info.ViewerPref = ctx.ViewerPref + + kwl, err := KeywordsList(ctx) + if err != nil { + return nil, err + } + info.Keywords = kwl + + info.Properties = ctx.Properties + info.Tagged = ctx.Tagged + info.Hybrid = ctx.Read.Hybrid + info.Linearized = ctx.Read.Linearized + info.UsingXRefStreams = ctx.Read.UsingXRefStreams + info.UsingObjectStreams = ctx.Read.UsingObjectStreams + info.Watermarked = ctx.Watermarked + info.Thumbnails = len(ctx.PageThumbs) > 0 + info.Form = ctx.Form != nil + info.Outlines = len(ctx.Outlines) > 0 + info.Names = len(ctx.Names) > 0 + + info.Signatures = ctx.SignatureExist + info.AppendOnly = ctx.AppendOnly + info.Encrypted = ctx.Encrypt != nil + + if ctx.E != nil { + info.Permissions = ctx.E.P + } + + aa, err := ctx.ListAttachments() + if err != nil { + return nil, err + } + info.Attachments = aa + + return info, nil +} + +// ListInfo returns formatted info about ctx. +func ListInfo(info *PDFInfo, selectedPages types.IntSet) ([]string, error) { + var separator = draw.HorSepLine([]int{44}) + + var ss []string + + if info.FileName != "" { + ss = append(ss, fmt.Sprintf("%20s: %s", "Source", info.FileName)) + } + ss = append(ss, fmt.Sprintf("%20s: %s", "PDF version", info.Version)) + ss = append(ss, fmt.Sprintf("%20s: %d", "Page count", info.PageCount)) + + pi, err := pageInfo(info, selectedPages) + if err != nil { + return nil, err + } + ss = append(ss, pi...) + + ss = append(ss, fmt.Sprint(separator)) + ss = append(ss, fmt.Sprintf("%20s: %s", "Title", info.Title)) + ss = append(ss, fmt.Sprintf("%20s: %s", "Author", info.Author)) + ss = append(ss, fmt.Sprintf("%20s: %s", "Subject", info.Subject)) + ss = append(ss, fmt.Sprintf("%20s: %s", "PDF Producer", info.Producer)) + ss = append(ss, fmt.Sprintf("%20s: %s", "Content creator", info.Creator)) + ss = append(ss, fmt.Sprintf("%20s: %s", "Creation date", info.CreationDate)) + ss = append(ss, fmt.Sprintf("%20s: %s", "Modification date", info.ModificationDate)) + if info.PageMode != "" { + ss = append(ss, fmt.Sprintf("%20s: %s", "Page mode", info.PageMode)) + } + if info.PageLayout != "" { + ss = append(ss, fmt.Sprintf("%20s: %s", "Page Layout", info.PageLayout)) + } + if info.ViewerPref != nil { + ss = append(ss, fmt.Sprintf("%20s: %s", "Viewer Prefs", info.ViewerPref)) + } + + info.renderKeywords(&ss) + info.renderProperties(&ss) + info.renderFlags(&ss, separator) + info.renderPermissions(&ss) + info.renderAttachments(&ss) + + return ss, nil +} diff --git a/pkg/pdfcpu/keyword.go b/pkg/pdfcpu/keyword.go new file mode 100644 index 0000000000000000000000000000000000000000..e753e9a868db400638545a2cc945e2f4779d65bb --- /dev/null +++ b/pkg/pdfcpu/keyword.go @@ -0,0 +1,143 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 pdfcpu + +import ( + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +// KeywordsList returns a list of keywords as recorded in the document info dict. +func KeywordsList(ctx *model.Context) ([]string, error) { + var ss []string + for keyword, val := range ctx.KeywordList { + if val { + ss = append(ss, keyword) + } + } + return ss, nil +} + +func removeKeywordsFromMetadata(ctx *model.Context) error { + rootDict, err := ctx.Catalog() + if err != nil { + return err + } + + indRef, _ := rootDict["Metadata"].(types.IndirectRef) + entry, _ := ctx.FindTableEntryForIndRef(&indRef) + sd, _ := entry.Object.(types.StreamDict) + + if err = sd.Decode(); err != nil { + return err + } + + if err = model.RemoveKeywords(&sd.Content); err != nil { + return err + } + + //fmt.Println(hex.Dump(sd.Content)) + + if err := sd.Encode(); err != nil { + return err + } + + entry.Object = sd + + return nil +} + +func finalizeKeywords(ctx *model.Context) error { + d, err := ctx.DereferenceDict(*ctx.Info) + if err != nil || d == nil { + return err + } + + ss, err := KeywordsList(ctx) + if err != nil { + return err + } + + s0 := strings.Join(ss, "; ") + + s, err := types.EscapeUTF16String(s0) + if err != nil { + return err + } + + d["Keywords"] = types.StringLiteral(*s) + + if ctx.CatalogXMPMeta != nil { + removeKeywordsFromMetadata(ctx) + } + + return nil +} + +// KeywordsAdd adds keywords to the document info dict. +// Returns true if at least one keyword was added. +func KeywordsAdd(ctx *model.Context, keywords []string) error { + if err := ensureInfoDictAndFileID(ctx); err != nil { + return err + } + + for _, keyword := range keywords { + ctx.KeywordList[strings.TrimSpace(keyword)] = true + } + + return finalizeKeywords(ctx) +} + +// KeywordsRemove deletes keywords from the document info dict. +// Returns true if at least one keyword was removed. +func KeywordsRemove(ctx *model.Context, keywords []string) (bool, error) { + if ctx.Info == nil { + return false, nil + } + + d, err := ctx.DereferenceDict(*ctx.Info) + if err != nil || d == nil { + return false, err + } + + if len(keywords) == 0 { + // Remove all keywords. + delete(d, "Keywords") + + if ctx.CatalogXMPMeta != nil { + removeKeywordsFromMetadata(ctx) + } + + return true, nil + } + + var removed bool + for keyword := range ctx.KeywordList { + if types.MemberOf(keyword, keywords) { + ctx.KeywordList[keyword] = false + removed = true + } + } + + if removed { + err = finalizeKeywords(ctx) + } + + return removed, err +} diff --git a/pkg/pdfcpu/matrix/matrix.go b/pkg/pdfcpu/matrix/matrix.go new file mode 100644 index 0000000000000000000000000000000000000000..5829c4ddbc8535c2346edde97e06f63385775f18 --- /dev/null +++ b/pkg/pdfcpu/matrix/matrix.go @@ -0,0 +1,95 @@ +/* +Copyright 2022 The pdfcpu Authors. + +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 + + 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 matrix + +import ( + "fmt" + "math" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +const ( + DegToRad = math.Pi / 180 + RadToDeg = 180 / math.Pi +) + +type Matrix [3][3]float64 + +var IdentMatrix = Matrix{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}} + +// Multiply calculates the product of two matrices. +func (m Matrix) Multiply(n Matrix) Matrix { + var p Matrix + for i := 0; i < 3; i++ { + for j := 0; j < 3; j++ { + for k := 0; k < 3; k++ { + p[i][j] += m[i][k] * n[k][j] + } + } + } + return p +} + +// Transform applies m to p. +func (m Matrix) Transform(p types.Point) types.Point { + x := p.X*m[0][0] + p.Y*m[1][0] + m[2][0] + y := p.X*m[0][1] + p.Y*m[1][1] + m[2][1] + return types.Point{X: x, Y: y} +} + +func (m Matrix) String() string { + return fmt.Sprintf("%3.2f %3.2f %3.2f\n%3.2f %3.2f %3.2f\n%3.2f %3.2f %3.2f\n", + m[0][0], m[0][1], m[0][2], + m[1][0], m[1][1], m[1][2], + m[2][0], m[2][1], m[2][2]) +} + +// CalcTransformMatrix returns a full transform matrix. +func CalcTransformMatrix(sx, sy, sin, cos, dx, dy float64) Matrix { + // Scale + m1 := IdentMatrix + m1[0][0] = sx + m1[1][1] = sy + // Rotate + m2 := IdentMatrix + m2[0][0] = cos + m2[0][1] = sin + m2[1][0] = -sin + m2[1][1] = cos + // Translate + m3 := IdentMatrix + m3[2][0] = dx + m3[2][1] = dy + return m1.Multiply(m2).Multiply(m3) +} + +// CalcRotateAndTranslateTransformMatrix returns a transform matrix that rotates and translates. +func CalcRotateAndTranslateTransformMatrix(r, dx, dy float64) Matrix { + sin := math.Sin(float64(r) * float64(DegToRad)) + cos := math.Cos(float64(r) * float64(DegToRad)) + return CalcTransformMatrix(1, 1, sin, cos, dx, dy) +} + +// CalcRotateTransformMatrix returns a transform matrix that rotates only. +func CalcRotateTransformMatrix(rot float64, bb *types.Rectangle) Matrix { + sin := math.Sin(float64(rot) * float64(DegToRad)) + cos := math.Cos(float64(rot) * float64(DegToRad)) + dx := bb.LL.X + bb.Width()/2 + sin*(bb.Height()/2) - cos*bb.Width()/2 + dy := bb.LL.Y + bb.Height()/2 - cos*(bb.Height()/2) - sin*bb.Width()/2 + return CalcTransformMatrix(1, 1, sin, cos, dx, dy) +} diff --git a/pkg/pdfcpu/merge.go b/pkg/pdfcpu/merge.go new file mode 100644 index 0000000000000000000000000000000000000000..cd9f05a55b3e3d0336f6ac4a3de857f92bb345b7 --- /dev/null +++ b/pkg/pdfcpu/merge.go @@ -0,0 +1,993 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 pdfcpu + +import ( + "fmt" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +func EnsureOutlines(ctx *model.Context, fName string, append bool) error { + + rootDict, err := ctx.Catalog() + if err != nil { + return err + } + + if err := ctx.LocateNameTree("Dests", true); err != nil { + return err + } + + outlinesDict := types.Dict(map[string]types.Object{"Type": types.Name("Outlines")}) + indRef, err := ctx.IndRefForNewObject(outlinesDict) + if err != nil { + return err + } + + first, last, total, visible, err := createOutlineItemDict(ctx, []Bookmark{{PageFrom: 1, Title: fName}}, indRef, nil) + if err != nil { + return err + } + + outlinesDict["First"] = *first + outlinesDict["Last"] = *last + outlinesDict["Count"] = types.Integer(total + visible) + + if obj, ok := rootDict.Find("Outlines"); ok { + if append { + return nil + } + d, err := ctx.DereferenceDict(obj) + if err != nil { + return err + } + count := d.IntEntry("Count") + c := 0 + f, l := d.IndirectRefEntry("First"), d.IndirectRefEntry("Last") + for ir := f; ir != nil; ir = d.IndirectRefEntry("Next") { + d, err = ctx.DereferenceDict(*ir) + if err != nil { + return err + } + d["Parent"] = *first + c++ + } + d, err = ctx.DereferenceDict(*first) + if err != nil { + return err + } + + d["First"] = *f + d["Last"] = *l + if count != nil && *count != 0 { + c = *count + } + d["Count"] = types.Integer(-c) + } + + rootDict["Outlines"] = *indRef + + return nil +} + +func mergeOutlines(fName string, p int, ctxSrc, ctxDest *model.Context) error { + rootDictDest, _ := ctxDest.Catalog() + indRef := rootDictDest.IndirectRefEntry("Outlines") + outlinesDict, err := ctxDest.DereferenceDict(*indRef) + if err != nil { + return err + } + + first, last, _, _, err := createOutlineItemDict(ctxDest, []Bookmark{{PageFrom: p, Title: fName}}, indRef, nil) + if err != nil { + return err + } + + l := outlinesDict.IndirectRefEntry("Last") + outlinesDict["Last"] = *last + + topCount := 0 + + count := outlinesDict.IntEntry("Count") + if count != nil { + topCount = *count + } + + topCount++ + + d1, err := ctxDest.DereferenceDict(*l) + if err != nil { + return err + } + d1["Next"] = *last + + d2, err := ctxDest.DereferenceDict(*last) + if err != nil { + return err + } + d2["Previous"] = *l + + rootDictSource, err := ctxSrc.Catalog() + if err != nil { + return err + } + + if obj, ok := rootDictSource.Find("Outlines"); ok { + + // Integrate existing outlines from ctxSource. + + d, err := ctxDest.DereferenceDict(obj) + if err != nil { + return err + } + + f, l := d.IndirectRefEntry("First"), d.IndirectRefEntry("Last") + if f == nil && l == nil { + outlinesDict["Count"] = types.Integer(topCount) + return nil + } + + d2["First"] = *f + d2["Last"] = *l + + c := 0 + + // Update parents. + // TODO Collapse outline dicts. + for ir := f; ir != nil; ir = d.IndirectRefEntry("Next") { + d, err = ctxDest.DereferenceDict(*ir) + if err != nil { + return err + } + d["Parent"] = *first + + i := d.IntEntry("Count") + if i != nil && *i > 0 { + c += *i + } + + c++ + } + + d2["Count"] = types.Integer(c) + topCount += c + } + + outlinesDict["Count"] = types.Integer(topCount) + return nil +} + +func handleNeedAppearances(ctxSrc *model.Context, dSrc, dDest types.Dict) error { + o, found := dSrc.Find("NeedAppearances") + if !found || o == nil { + return nil + } + b, err := ctxSrc.DereferenceBoolean(o, model.V10) + if err != nil { + return err + } + if b != nil && *b { + dDest["NeedAppearances"] = types.Boolean(true) + } + return nil +} + +func handleCO(ctxSrc, ctxDest *model.Context, dSrc, dDest types.Dict) error { + o, found := dSrc.Find("CO") + if !found { + return nil + } + arrSrc, err := ctxSrc.DereferenceArray(o) + if err != nil { + return err + } + o, found = dDest.Find("CO") + if !found { + dDest["CO"] = arrSrc + return nil + } + arrDest, err := ctxDest.DereferenceArray(o) + if err != nil { + return err + } + if len(arrDest) == 0 { + dDest["CO"] = arrSrc + } else { + arrDest = append(arrDest, arrSrc...) + dDest["CO"] = arrDest + } + return nil +} + +func handleDR(ctxSrc, ctxDest *model.Context, dSrc, dDest types.Dict) error { + o, found := dSrc.Find("DR") + if !found { + return nil + } + dSrc, err := ctxSrc.DereferenceDict(o) + if err != nil { + return err + } + if len(dSrc) == 0 { + return nil + } + _, found = dDest.Find("DR") + if !found { + dDest["DR"] = dSrc + } + return nil +} + +func handleDA(ctxSrc *model.Context, dSrc, dDest types.Dict, arrFieldsSrc types.Array) error { + // (for each with field type /FT /Tx w/o DA, set DA to default DA) + // TODO Walk field tree and inspect terminal fields. + + sSrc := dSrc.StringEntry("DA") + if sSrc == nil || len(*sSrc) == 0 { + return nil + } + sDest := dDest.StringEntry("DA") + if sDest == nil { + dDest["DA"] = types.StringLiteral(*sSrc) + return nil + } + // Push sSrc down to all top level fields of dSource + for _, o := range arrFieldsSrc { + d, err := ctxSrc.DereferenceDict(o) + if err != nil { + return err + } + n := d.NameEntry("FT") + if n != nil && *n == "Tx" { + _, found := d.Find("DA") + if !found { + d["DA"] = types.StringLiteral(*sSrc) + } + } + } + return nil +} + +func handleQ(ctxSrc *model.Context, dSrc, dDest types.Dict, arrFieldsSrc types.Array) error { + // (for each with field type /FT /Tx w/o Q, set Q to default Q) + // TODO Walk field tree and inspect terminal fields. + + iSrc := dSrc.IntEntry("Q") + if iSrc == nil { + return nil + } + iDest := dDest.IntEntry("Q") + if iDest == nil { + dDest["Q"] = types.Integer(*iSrc) + return nil + } + // Push iSrc down to all top level fields of dSource + for _, o := range arrFieldsSrc { + d, err := ctxSrc.DereferenceDict(o) + if err != nil { + return err + } + n := d.NameEntry("FT") + if n != nil && *n == "Tx" { + _, found := d.Find("Q") + if !found { + d["Q"] = types.Integer(*iSrc) + } + } + } + return nil +} + +func handleFormAttributes(ctxSrc, ctxDest *model.Context, dSrc, dDest types.Dict, arrFieldsSrc types.Array) error { + // NeedAppearances: try: set to true only + if err := handleNeedAppearances(ctxSrc, dSrc, dDest); err != nil { + return err + } + + // SigFlags: set bit 1 to true only (SignaturesExist) + // set bit 2 to true only (AppendOnly) + dDest.Delete("SigFields") + + // CO: add all indrefs + if err := handleCO(ctxSrc, ctxDest, dSrc, dDest); err != nil { + return err + } + + // DR: default resource dict + if err := handleDR(ctxSrc, ctxDest, dSrc, dDest); err != nil { + return err + } + + // DA: default appearance streams for variable text fields + if err := handleDA(ctxSrc, dSrc, dDest, arrFieldsSrc); err != nil { + return err + } + + // Q: left, center, right for variable text fields + if err := handleQ(ctxSrc, dSrc, dDest, arrFieldsSrc); err != nil { + return err + } + + // XFA: ignore + delete(dDest, "XFA") + + return nil +} + +func rootDicts(ctxSrc, ctxDest *model.Context) (types.Dict, types.Dict, error) { + rootDictSource, err := ctxSrc.Catalog() + if err != nil { + return nil, nil, err + } + + rootDictDest, err := ctxDest.Catalog() + if err != nil { + return nil, nil, err + } + + return rootDictSource, rootDictDest, nil +} + +func mergeInFields(ctxDest *model.Context, arrFieldsSrc, arrFieldsDest types.Array, dDest types.Dict) error { + parentDict := + types.Dict(map[string]types.Object{ + "Kids": arrFieldsSrc, + "T": types.StringLiteral(fmt.Sprintf("%d", len(arrFieldsDest))), + }) + + ir, err := ctxDest.IndRefForNewObject(parentDict) + if err != nil { + return err + } + + for _, ir1 := range arrFieldsSrc { + d, err := ctxDest.DereferenceDict(ir1) + if err != nil { + return err + } + if len(d) == 0 { + continue + } + d["Parent"] = *ir + } + + dDest["Fields"] = append(arrFieldsDest, *ir) + + return nil +} + +func mergeDests(ctxSource, ctxDest *model.Context) error { + rootDictSource, rootDictDest, err := rootDicts(ctxSource, ctxDest) + if err != nil { + return err + } + + o1, found := rootDictSource.Find("Dests") + if !found { + return nil + } + + o2, found := rootDictDest.Find("Dests") + if !found { + rootDictDest["Dests"] = o1 + return nil + } + + destsSrc, err := ctxSource.DereferenceDict(o1) + if err != nil { + return err + } + + destsDest, err := ctxDest.DereferenceDict(o2) + if err != nil { + return err + } + + // Note: We ignore duplicate keys + for k, v := range destsSrc { + destsDest[k] = v + } + + return nil +} + +func mergeNames(ctxSrc, ctxDest *model.Context) error { + + rootDictSrc, rootDictDest, err := rootDicts(ctxSrc, ctxDest) + if err != nil { + return err + } + + _, found := rootDictSrc.Find("Names") + if !found { + // Nothing to merge in. + return nil + } + + if _, found := rootDictDest.Find("Names"); !found { + ctxDest.Names = ctxSrc.Names + return nil + } + + // We need to merge src Names into dest Names. + + for id, namesSrc := range ctxSrc.Names { + if namesDest, ok := ctxDest.Names[id]; ok { + // Merge src tree into dest tree including collision detection. + if err := namesDest.AddTree(ctxDest.XRefTable, namesSrc, ctxSrc.NameRefs[id], []string{"D", "Dest"}); err != nil { + return err + } + continue + } + + // Name tree missing in dest ctx => copy over names from src ctx + ctxDest.Names[id] = namesSrc + } + + return nil +} + +func mergeForms(ctxSrc, ctxDest *model.Context) error { + + rootDictSource, rootDictDest, err := rootDicts(ctxSrc, ctxDest) + if err != nil { + return err + } + + o, found := rootDictSource.Find("AcroForm") + if !found { + return nil + } + + dSrc, err := ctxSrc.DereferenceDict(o) + if err != nil || len(dSrc) == 0 { + return err + } + + // Retrieve ctxSrc Form Fields + o, found = dSrc.Find("Fields") + if !found { + return nil + } + arrFieldsSrc, err := ctxSrc.DereferenceArray(o) + if err != nil { + return err + } + if len(arrFieldsSrc) == 0 { + return nil + } + + // We have a ctxSrc.Form with fields. + + o, found = rootDictDest.Find("AcroForm") + if !found { + rootDictDest["AcroForm"] = dSrc + return nil + } + + dDest, err := ctxDest.DereferenceDict(o) + if err != nil { + return err + } + + if len(dDest) == 0 { + rootDictDest["AcroForm"] = dSrc + return nil + } + + // Retrieve ctxDest AcroForm Fields + o, found = dDest.Find("Fields") + if !found { + rootDictDest["AcroForm"] = dSrc + return nil + } + arrFieldsDest, err := ctxDest.DereferenceArray(o) + if err != nil { + return err + } + if len(arrFieldsDest) == 0 { + rootDictDest["AcroForm"] = dSrc + return nil + } + + if err := mergeInFields(ctxDest, arrFieldsSrc, arrFieldsDest, dDest); err != nil { + return err + } + + return handleFormAttributes(ctxSrc, ctxDest, dSrc, dDest, arrFieldsSrc) +} + +func patchIndRef(ir *types.IndirectRef, lookup map[int]int) { + i := ir.ObjectNumber.Value() + ir.ObjectNumber = types.Integer(lookup[i]) +} + +func patchObject(o types.Object, lookup map[int]int) types.Object { + if log.TraceEnabled() { + log.Trace.Printf("patchObject before: %v\n", o) + } + + var ob types.Object + + switch obj := o.(type) { + + case types.IndirectRef: + patchIndRef(&obj, lookup) + ob = obj + + case types.Dict: + patchDict(obj, lookup) + ob = obj + + case types.StreamDict: + patchDict(obj.Dict, lookup) + ob = obj + + case types.ObjectStreamDict: + patchDict(obj.Dict, lookup) + ob = obj + + case types.XRefStreamDict: + patchDict(obj.Dict, lookup) + ob = obj + + case types.Array: + patchArray(&obj, lookup) + ob = obj + + } + + if log.TraceEnabled() { + log.Trace.Printf("patchObject end: %v\n", ob) + } + + return ob +} + +func patchDict(d types.Dict, lookup map[int]int) { + if log.TraceEnabled() { + log.Trace.Printf("patchDict before: %v\n", d) + } + + for k, obj := range d { + o := patchObject(obj, lookup) + if o != nil { + d[k] = o + } + } + + if log.TraceEnabled() { + log.Trace.Printf("patchDict after: %v\n", d) + } +} + +func patchArray(a *types.Array, lookup map[int]int) { + if log.TraceEnabled() { + log.Trace.Printf("patchArray begin: %v\n", *a) + } + + for i, obj := range *a { + o := patchObject(obj, lookup) + if o != nil { + (*a)[i] = o + } + } + + if log.TraceEnabled() { + log.Trace.Printf("patchArray end: %v\n", a) + } +} + +func objNrsIntSet(ctx *model.Context) types.IntSet { + objNrs := types.IntSet{} + + for k := range ctx.Table { + if k == 0 { + // obj#0 is always the head of the freelist. + continue + } + objNrs[k] = true + } + + return objNrs +} + +func lookupTable(keys types.IntSet, i int) map[int]int { + m := map[int]int{} + + for k := range keys { + m[k] = i + i++ + } + + return m +} + +// Patch an IntSet of objNrs using lookup. +func patchObjects(s types.IntSet, lookup map[int]int) types.IntSet { + t := types.IntSet{} + + for k, v := range s { + if v { + t[lookup[k]] = v + } + } + + return t +} + +func patchNameTree(n *model.Node, lookup map[int]int) error { + + patchValues := func(xRefTable *model.XRefTable, k string, v *types.Object) error { + *v = patchObject(*v, lookup) + return nil + } + + return n.Process(nil, patchValues) +} + +func patchSourceObjectNumbers(ctxSrc, ctxDest *model.Context) { + if log.DebugEnabled() { + log.Debug.Printf("patchSourceObjectNumbers: ctxSrc: xRefTableSize:%d trailer.Size:%d - %s\n", len(ctxSrc.Table), *ctxSrc.Size, ctxSrc.Read.FileName) + log.Debug.Printf("patchSourceObjectNumbers: ctxDest: xRefTableSize:%d trailer.Size:%d - %s\n", len(ctxDest.Table), *ctxDest.Size, ctxDest.Read.FileName) + } + + // Patch source xref tables obj numbers which are essentially the keys. + //logInfoMerge.Printf("Source XRefTable before:\n%s\n", ctxSource) + + objNrs := objNrsIntSet(ctxSrc) + + // Create lookup table for object numbers. + // The first number is the successor of the last number in ctxDest. + lookup := lookupTable(objNrs, *ctxDest.Size) + + // Patch pointer to root object + patchIndRef(ctxSrc.Root, lookup) + + // Patch pointer to info object + if ctxSrc.Info != nil { + patchIndRef(ctxSrc.Info, lookup) + } + + // Patch free object zero + entry := ctxSrc.Table[0] + off := int(*entry.Offset) + if off != 0 { + i := int64(lookup[off]) + entry.Offset = &i + } + + // Patch all indRefs for xref table entries. + for k := range objNrs { + + //logDebugMerge.Printf("patching obj #%d\n", k) + + entry := ctxSrc.Table[k] + + if entry.Free { + if log.DebugEnabled() { + log.Debug.Printf("patch free entry: old offset:%d\n", *entry.Offset) + } + off := int(*entry.Offset) + if off == 0 { + continue + } + i := int64(lookup[off]) + entry.Offset = &i + if log.DebugEnabled() { + log.Debug.Printf("patch free entry: new offset:%d\n", *entry.Offset) + } + continue + } + + patchObject(entry.Object, lookup) + } + + // Patch xref entry object numbers. + m := make(map[int]*model.XRefTableEntry, *ctxSrc.Size) + for k, v := range lookup { + m[v] = ctxSrc.Table[k] + } + m[0] = ctxSrc.Table[0] + ctxSrc.Table = m + + // Patch DuplicateInfo object numbers. + ctxSrc.Optimize.DuplicateInfoObjects = patchObjects(ctxSrc.Optimize.DuplicateInfoObjects, lookup) + + // Patch Linearization object numbers. + ctxSrc.LinearizationObjs = patchObjects(ctxSrc.LinearizationObjs, lookup) + + // Patch XRefStream objects numbers. + ctxSrc.Read.XRefStreams = patchObjects(ctxSrc.Read.XRefStreams, lookup) + + // Patch object stream object numbers. + ctxSrc.Read.ObjectStreams = patchObjects(ctxSrc.Read.ObjectStreams, lookup) + + // Patch cached name trees. + for _, v := range ctxSrc.Names { + patchNameTree(v, lookup) + } + + if log.DebugEnabled() { + log.Debug.Printf("patchSourceObjectNumbers end") + } +} + +func createDividerPagesDict(ctx *model.Context, parentIndRef types.IndirectRef) (*types.IndirectRef, error) { + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Pages"), + "Parent": parentIndRef, + "Count": types.Integer(1), + }, + ) + + indRef, err := ctx.IndRefForNewObject(d) + if err != nil { + return nil, err + } + + dims, err := ctx.XRefTable.PageDims() + if err != nil { + return nil, err + } + + last := len(dims) - 1 + mediaBox := types.NewRectangle(0, 0, dims[last].Width, dims[last].Height) + + indRefPageDict, err := ctx.EmptyPage(indRef, mediaBox) + if err != nil { + return nil, err + } + ctx.SetValid(*indRefPageDict) + + d.Insert("Kids", types.Array{*indRefPageDict}) + + return indRef, nil +} + +func appendSourcePageTreeToDestPageTree(ctxSrc, ctxDest *model.Context, dividerPage bool) error { + if log.DebugEnabled() { + log.Debug.Println("appendSourcePageTreeToDestPageTree begin") + } + + indRefPageTreeRootDictDest, err := ctxDest.Pages() + if err != nil { + return err + } + + pageTreeRootDictDest, err := ctxDest.XRefTable.DereferenceDict(*indRefPageTreeRootDictDest) + if err != nil { + return err + } + + pageCountDest := pageTreeRootDictDest.IntEntry("Count") + if pageCountDest == nil || *pageCountDest != ctxDest.PageCount { + return errors.Errorf("pdfcpu: corrupt page node at obj #%d\n", indRefPageTreeRootDictDest.ObjectNumber) + } + + c := ctxDest.PageCount + + d := types.NewDict() + d.InsertName("Type", "Pages") + kids := types.Array{*indRefPageTreeRootDictDest} + + indRef, err := ctxDest.IndRefForNewObject(d) + if err != nil { + return err + } + + if dividerPage { + dividerPagesNodeIndRef, err := createDividerPagesDict(ctxDest, *indRef) + if err != nil { + return err + } + kids = append(kids, *dividerPagesNodeIndRef) + c++ + } + + pageTreeRootDictDest["Parent"] = *indRef + + indRefPageTreeRootDictSource, err := ctxSrc.Pages() + if err != nil { + return err + } + + d.Insert("Kids", append(kids, *indRefPageTreeRootDictSource)) + + pageTreeRootDictSource, err := ctxSrc.XRefTable.DereferenceDict(*indRefPageTreeRootDictSource) + if err != nil { + return err + } + + pageTreeRootDictSource["Parent"] = *indRef + + pageCountSource := pageTreeRootDictSource.IntEntry("Count") + if pageCountSource == nil || *pageCountSource != ctxSrc.PageCount { + return errors.Errorf("pdfcpu: corrupt page node at obj #%d\n", indRefPageTreeRootDictSource.ObjectNumber) + } + + c += ctxSrc.PageCount + d.InsertInt("Count", c) + ctxDest.PageCount = c + + rootDict, err := ctxDest.Catalog() + if err != nil { + return err + } + + rootDict["Pages"] = *indRef + + if log.DebugEnabled() { + log.Debug.Println("appendSourcePageTreeToDestPageTree end") + } + + return nil +} + +func zipSourcePageTreeIntoDestPageTree(ctxSrc, ctxDest *model.Context) error { + if log.DebugEnabled() { + log.Debug.Println("zipSourcePageTreeIntoDestPageTree begin") + } + + appendFromPageNr := 0 + if ctxSrc.PageCount > ctxDest.PageCount { + appendFromPageNr = ctxDest.PageCount + 1 + } + + rootPageIndRef, err := ctxDest.Pages() + if err != nil { + return err + } + + // Process dest page tree recursively and weave in src pages + p := 0 + if ctxDest.PageCount, err = ctxDest.InsertPages(rootPageIndRef, &p, ctxSrc); err != nil { + return err + } + + if appendFromPageNr > 0 { + // append remaining src pages + if ctxDest.PageCount, err = ctxDest.AppendPages(rootPageIndRef, appendFromPageNr, ctxSrc); err != nil { + return err + } + } + + if log.DebugEnabled() { + log.Debug.Println("zipSourcePageTreeIntoDestPageTree end") + } + + return nil +} + +func appendSourceObjectsToDest(ctxSrc, ctxDest *model.Context) { + if log.DebugEnabled() { + log.Debug.Println("appendSourceObjectsToDest begin") + } + + for objNr, entry := range ctxSrc.Table { + + // Do not copy free list head. + if objNr == 0 { + continue + } + + if log.DebugEnabled() { + log.Debug.Printf("adding obj %d from src to dest\n", objNr) + } + + ctxDest.Table[objNr] = entry + + *ctxDest.Size++ + + } + + if log.DebugEnabled() { + log.Debug.Println("appendSourceObjectsToDest end") + } +} + +// merge two disjunct IntSets +func mergeIntSets(src, dest types.IntSet) { + for k := range src { + dest[k] = true + } +} + +func mergeDuplicateObjNumberIntSets(ctxSrc, ctxDest *model.Context) { + if log.DebugEnabled() { + log.Debug.Println("mergeDuplicateObjNumberIntSets begin") + } + + mergeIntSets(ctxSrc.Optimize.DuplicateInfoObjects, ctxDest.Optimize.DuplicateInfoObjects) + mergeIntSets(ctxSrc.LinearizationObjs, ctxDest.LinearizationObjs) + mergeIntSets(ctxSrc.Read.XRefStreams, ctxDest.Read.XRefStreams) + mergeIntSets(ctxSrc.Read.ObjectStreams, ctxDest.Read.ObjectStreams) + + if log.DebugEnabled() { + log.Debug.Println("mergeDuplicateObjNumberIntSets end") + } +} + +// MergeXRefTables merges Context ctxSrc into ctxDest by appending its page tree. +// zip ... zip 2 files together (eg. 1A,1B,2A,2B,3A,3B...) +// dividerPage ... insert blank page between merged files (not applicable for zipping) +func MergeXRefTables(fName string, ctxSrc, ctxDest *model.Context, zip, dividerPage bool) (err error) { + + patchSourceObjectNumbers(ctxSrc, ctxDest) + + appendSourceObjectsToDest(ctxSrc, ctxDest) + + origDestPageCount := ctxDest.PageCount + if dividerPage { + origDestPageCount++ + } + + if zip { + err = zipSourcePageTreeIntoDestPageTree(ctxSrc, ctxDest) + } else { + err = appendSourcePageTreeToDestPageTree(ctxSrc, ctxDest, dividerPage) + } + + if err != nil { + return nil + } + + if err = mergeForms(ctxSrc, ctxDest); err != nil { + return err + } + + if err = mergeDests(ctxSrc, ctxDest); err != nil { + return err + } + + if err = mergeNames(ctxSrc, ctxDest); err != nil { + return err + } + + if !zip && ctxDest.Configuration.CreateBookmarks { + if err = mergeOutlines(fName, origDestPageCount+1, ctxSrc, ctxDest); err != nil { + return err + } + } + + // Mark src's root object as free. + if err = ctxDest.FreeObject(int(ctxSrc.Root.ObjectNumber)); err != nil { + return + } + + // Mark source's info object as free. + // Note: Any indRefs this info object depends on are missed. + if ctxSrc.Info != nil { + if err = ctxDest.FreeObject(int(ctxSrc.Info.ObjectNumber)); err != nil { + return + } + } + + // Merge all IntSets containing redundant object numbers. + mergeDuplicateObjNumberIntSets(ctxSrc, ctxDest) + + if log.InfoEnabled() { + log.Info.Printf("Dest XRefTable after merge:\n%s\n", ctxDest) + } + + return nil +} diff --git a/pkg/pdfcpu/migrate.go b/pkg/pdfcpu/migrate.go new file mode 100644 index 0000000000000000000000000000000000000000..2b3d51fd26fe3c04c9066a1314de7c67581d0798 --- /dev/null +++ b/pkg/pdfcpu/migrate.go @@ -0,0 +1,291 @@ +/* +Copyright 2023 The pdfcpu Authors. + +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 + + 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 pdfcpu + +import ( + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +func migrateIndRef(ir *types.IndirectRef, ctxSource, ctxDest *model.Context, migrated map[int]int) (types.Object, error) { + o, err := ctxSource.Dereference(*ir) + if err != nil { + return nil, err + } + + if o != nil { + o = o.Clone() + } + + objNrNew, err := ctxDest.InsertObject(o) + if err != nil { + return nil, err + } + + objNr := ir.ObjectNumber.Value() + migrated[objNr] = objNrNew + ir.ObjectNumber = types.Integer(objNrNew) + return o, nil +} + +func migrateObject(o types.Object, ctxSource, ctxDest *model.Context, migrated map[int]int) (types.Object, error) { + var err error + switch o := o.(type) { + case types.IndirectRef: + objNr := o.ObjectNumber.Value() + if migrated[objNr] > 0 { + o.ObjectNumber = types.Integer(migrated[objNr]) + return o, nil + } + o1, err := migrateIndRef(&o, ctxSource, ctxDest, migrated) + if err != nil { + return nil, err + } + if _, err := migrateObject(o1, ctxSource, ctxDest, migrated); err != nil { + return nil, err + } + return o, nil + + case types.Dict: + for k, v := range o { + if o[k], err = migrateObject(v, ctxSource, ctxDest, migrated); err != nil { + return nil, err + } + } + return o, nil + + case types.StreamDict: + for k, v := range o.Dict { + if o.Dict[k], err = migrateObject(v, ctxSource, ctxDest, migrated); err != nil { + return nil, err + } + } + return o, nil + + case types.Array: + for k, v := range o { + if o[k], err = migrateObject(v, ctxSource, ctxDest, migrated); err != nil { + return nil, err + } + } + return o, nil + } + + return o, nil +} + +func migrateAnnots(o types.Object, pageIndRef types.IndirectRef, ctxSrc, ctxDest *model.Context, migrated map[int]int) (types.Object, error) { + arr := o.(types.Array) + for i, v := range o.(types.Array) { + var d types.Dict + o, ok := v.(types.IndirectRef) + if ok { + objNr := o.ObjectNumber.Value() + if migrated[objNr] > 0 { + o.ObjectNumber = types.Integer(migrated[objNr]) + arr[i] = o + continue + } + o1, err := migrateIndRef(&o, ctxSrc, ctxDest, migrated) + if err != nil { + return nil, err + } + arr[i] = o + d = o1.(types.Dict) + } else { + d = v.(types.Dict) + } + for k, v := range d { + if k == "P" { + d["P"] = pageIndRef + continue + } + if k == "Parent" { + pDict, err := ctxSrc.DereferenceDict(v) + if err != nil { + return nil, err + } + ft := pDict.NameEntry("FT") + if ft == nil || *ft != "Btn" { + d.Delete("Parent") + continue + } + pDict.Delete("Parent") + } + o1, err := migrateObject(v, ctxSrc, ctxDest, migrated) + if err != nil { + return nil, err + } + d[k] = o1 + } + } + + return arr, nil +} + +func migratePageDict(d types.Dict, pageIndRef types.IndirectRef, ctxSrc, ctxDest *model.Context, migrated map[int]int) error { + var err error + for k, v := range d { + if k == "Parent" { + continue + } + if k == "Annots" { + o, ok := d[k].(types.IndirectRef) + if ok { + objNr := o.ObjectNumber.Value() + if migrated[objNr] > 0 { + o.ObjectNumber = types.Integer(migrated[objNr]) + d[k] = o + continue + } + v, err = migrateIndRef(&o, ctxSrc, ctxDest, migrated) + if err != nil { + return err + } + d[k] = o + if _, err = migrateAnnots(v, pageIndRef, ctxSrc, ctxDest, migrated); err != nil { + return err + } + continue + } + if d[k], err = migrateAnnots(v, pageIndRef, ctxSrc, ctxDest, migrated); err != nil { + return err + } + continue + } + if d[k], err = migrateObject(v, ctxSrc, ctxDest, migrated); err != nil { + return err + } + } + return nil +} + +func migrateAnnot(indRef *types.IndirectRef, fieldsSrc, fieldsDest *types.Array, ctxSrc *model.Context, migrated map[int]int) error { + for _, v := range *fieldsSrc { + ir, ok := v.(types.IndirectRef) + if !ok { + continue + } + objNr := ir.ObjectNumber.Value() + if migrated[objNr] == indRef.ObjectNumber.Value() { + *fieldsDest = append(*fieldsDest, indRef) + break + } + d, err := ctxSrc.DereferenceDict(ir) + if err != nil { + return err + } + o, ok := d.Find("Kids") + if !ok { + continue + } + kids, err := ctxSrc.DereferenceArray(o) + if err != nil { + return err + } + if ok, err = detectMigratedAnnot(ctxSrc, indRef, kids, migrated); err != nil { + return err + } + if ok { + *fieldsDest = append(*fieldsDest, *indRef) + } + } + + return nil +} + +func migrateFields(d types.Dict, fieldsSrc, fieldsDest *types.Array, ctxSrc, ctxDest *model.Context, migrated map[int]int) error { + o, _ := d.Find("Annots") + annots, err := ctxDest.DereferenceArray(o) + if err != nil { + return err + } + for _, v := range annots { + indRef, ok := v.(types.IndirectRef) + if !ok { + continue + } + d, err := ctxDest.DereferenceDict(indRef) + if err != nil { + return err + } + if pIndRef := d.IndirectRefEntry("Parent"); pIndRef != nil { + indRef = *pIndRef + } + var found bool + for _, v := range *fieldsDest { + if v.(types.IndirectRef) == indRef { + found = true + break + } + } + if found { + continue + } + if err := migrateAnnot(&indRef, fieldsSrc, fieldsDest, ctxSrc, migrated); err != nil { + return err + } + } + + return nil +} + +func migrateFormDict(d types.Dict, fields types.Array, ctxSrc, ctxDest *model.Context, migrated map[int]int) error { + var err error + for k, v := range d { + if k == "Fields" { + d[k] = fields + continue + } + if d[k], err = migrateObject(v, ctxSrc, ctxDest, migrated); err != nil { + return err + } + } + return nil +} + +func detectMigratedAnnot(ctxSrc *model.Context, indRef *types.IndirectRef, kids types.Array, migrated map[int]int) (bool, error) { + for _, v := range kids { + ir, ok := v.(types.IndirectRef) + if !ok { + continue + } + objNr := ir.ObjectNumber.Value() + if migrated[objNr] == indRef.ObjectNumber.Value() { + return true, nil + } + d, err := ctxSrc.DereferenceDict(ir) + if err != nil { + return false, err + } + o, ok := d.Find("Kids") + if !ok { + continue + } + kids, err := ctxSrc.DereferenceArray(o) + if err != nil { + return false, err + } + if ok, err = detectMigratedAnnot(ctxSrc, indRef, kids, migrated); err != nil { + return false, err + } + if ok { + return true, nil + } + } + return false, nil +} diff --git a/pkg/pdfcpu/model/annotation.go b/pkg/pdfcpu/model/annotation.go new file mode 100644 index 0000000000000000000000000000000000000000..b6c25b714f13eae673418b9241624ca5cfd56715 --- /dev/null +++ b/pkg/pdfcpu/model/annotation.go @@ -0,0 +1,1666 @@ +/* +Copyright 2021 The pdfcpu Authors. + +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 + + 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 model + +import ( + "fmt" + "time" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// AnnotationFlags represents the PDF annotation flags. +type AnnotationFlags int + +const ( // See table 165 + AnnInvisible AnnotationFlags = 1 << iota + AnnHidden + AnnPrint + AnnNoZoom + AnnNoRotate + AnnNoView + AnnReadOnly + AnnLocked + AnnToggleNoView + AnnLockedContents +) + +// AnnotationType represents the various PDF annotation types. +type AnnotationType int + +const ( + AnnText AnnotationType = iota + AnnLink + AnnFreeText + AnnLine + AnnSquare + AnnCircle + AnnPolygon + AnnPolyLine + AnnHighLight + AnnUnderline + AnnSquiggly + AnnStrikeOut + AnnStamp + AnnCaret + AnnInk + AnnPopup + AnnFileAttachment + AnnSound + AnnMovie + AnnWidget + AnnScreen + AnnPrinterMark + AnnTrapNet + AnnWatermark + Ann3D + AnnRedact +) + +var AnnotTypes = map[string]AnnotationType{ + "Text": AnnText, + "Link": AnnLink, + "FreeText": AnnFreeText, + "Line": AnnLine, + "Square": AnnSquare, + "Circle": AnnCircle, + "Polygon": AnnPolygon, + "PolyLine": AnnPolyLine, + "Highlight": AnnHighLight, + "Underline": AnnUnderline, + "Squiggly": AnnSquiggly, + "StrikeOut": AnnStrikeOut, + "Stamp": AnnStamp, + "Caret": AnnCaret, + "Ink": AnnInk, + "Popup": AnnPopup, + "FileAttachment": AnnFileAttachment, + "Sound": AnnSound, + "Movie": AnnMovie, + "Widget": AnnWidget, + "Screen": AnnScreen, + "PrinterMark": AnnPrinterMark, + "TrapNet": AnnTrapNet, + "Watermark": AnnWatermark, + "3D": Ann3D, + "Redact": AnnRedact, +} + +// AnnotTypeStrings manages string representations for annotation types. +var AnnotTypeStrings = map[AnnotationType]string{ + AnnText: "Text", + AnnLink: "Link", + AnnFreeText: "FreeText", + AnnLine: "Line", + AnnSquare: "Square", + AnnCircle: "Circle", + AnnPolygon: "Polygon", + AnnPolyLine: "PolyLine", + AnnHighLight: "Highlight", + AnnUnderline: "Underline", + AnnSquiggly: "Squiggly", + AnnStrikeOut: "StrikeOut", + AnnStamp: "Stamp", + AnnCaret: "Caret", + AnnInk: "Ink", + AnnPopup: "Popup", + AnnFileAttachment: "FileAttachment", + AnnSound: "Sound", + AnnMovie: "Movie", + AnnWidget: "Widget", + AnnScreen: "Screen", + AnnPrinterMark: "PrinterMark", + AnnTrapNet: "TrapNet", + AnnWatermark: "Watermark", + Ann3D: "3D", + AnnRedact: "Redact", +} + +// BorderStyle (see table 168) +type BorderStyle int + +const ( + BSSolid BorderStyle = iota + BSDashed + BSBeveled + BSInset + BSUnderline +) + +func borderStyleDict(width float64, style BorderStyle) types.Dict { + d := types.Dict(map[string]types.Object{ + "Type": types.Name("Border"), + "W": types.Float(width), + }) + + var s string + + switch style { + case BSSolid: + s = "S" + case BSDashed: + s = "D" + case BSBeveled: + s = "B" + case BSInset: + s = "I" + case BSUnderline: + s = "U" + } + + d["S"] = types.Name(s) + + return d +} + +func borderEffectDict(cloudyBorder bool, intensity int) types.Dict { + s := "S" + if cloudyBorder { + s = "C" + } + + return types.Dict(map[string]types.Object{ + "S": types.Name(s), + "I": types.Integer(intensity), + }) +} + +func borderArray(rx, ry, width float64) types.Array { + return types.NewNumberArray(rx, ry, width) +} + +// LineEndingStyle (see table 179) +type LineEndingStyle int + +const ( + LESquare LineEndingStyle = iota + LECircle + LEDiamond + LEOpenArrow + LEClosedArrow + LENone + LEButt + LEROpenArrow + LERClosedArrow + LESlash +) + +func LineEndingStyleName(les LineEndingStyle) string { + var s string + switch les { + case LESquare: + s = "Square" + case LECircle: + s = "Circle" + case LEDiamond: + s = "Diamond" + case LEOpenArrow: + s = "OpenArrow" + case LEClosedArrow: + s = "ClosedArrow" + case LENone: + s = "None" + case LEButt: + s = "Butt" + case LEROpenArrow: + s = "ROpenArrow" + case LERClosedArrow: + s = "RClosedArrow" + case LESlash: + s = "Slash" + } + return s +} + +// AnnotationRenderer is the interface for PDF annotations. +type AnnotationRenderer interface { + RenderDict(xRefTable *XRefTable, pageIndRef *types.IndirectRef) (types.Dict, error) + Type() AnnotationType + RectString() string + ID() string + ContentString() string +} + +// Annotation represents a PDF annnotation. +type Annotation struct { + SubType AnnotationType // The type of annotation that this dictionary describes. + Rect types.Rectangle // The annotation rectangle, defining the location of the annotation on the page in default user space units. + Contents string // Text that shall be displayed for the annotation. + NM string // (Since V1.4) The annotation name, a text string uniquely identifying it among all the annotations on its page. + ModificationDate string // M - The date and time when the annotation was most recently modified. + P *types.IndirectRef // An indirect reference to the page object with which this annotation is associated. + F AnnotationFlags // A set of flags specifying various characteristics of the annotation. + C *color.SimpleColor // The background color of the annotation’s icon when closed, pop up title bar color, link ann border color. + BorderRadX float64 // Border radius X + BorderRadY float64 // Border radius Y + BorderWidth float64 // Border width + // StructParent int + // OC types.dict +} + +// NewAnnotation returns a new annotation. +func NewAnnotation( + typ AnnotationType, + rect types.Rectangle, + contents, id string, + modDate string, + f AnnotationFlags, + col *color.SimpleColor, + borderRadX float64, + borderRadY float64, + borderWidth float64) Annotation { + + return Annotation{ + SubType: typ, + Rect: rect, + Contents: contents, + NM: id, + ModificationDate: modDate, + F: f, + C: col, + BorderRadX: borderRadX, + BorderRadY: borderRadY, + BorderWidth: borderWidth, + } +} + +// NewAnnotationForRawType returns a new annotation of a specific type. +func NewAnnotationForRawType( + typ string, + rect types.Rectangle, + contents, id string, + modDate string, + f AnnotationFlags, + + col *color.SimpleColor, + borderRadX float64, + borderRadY float64, + borderWidth float64) Annotation { + return NewAnnotation(AnnotTypes[typ], rect, contents, id, modDate, f, col, borderRadX, borderRadY, borderWidth) +} + +// ID returns the annotation id. +func (ann Annotation) ID() string { + return ann.NM +} + +// ContentString returns a string representation of ann's contents. +func (ann Annotation) ContentString() string { + return ann.Contents +} + +// RectString returns ann's positioning rectangle. +func (ann Annotation) RectString() string { + return ann.Rect.ShortString() +} + +// Type returns ann's type. +func (ann Annotation) Type() AnnotationType { + return ann.SubType +} + +// TypeString returns a string representation of ann's type. +func (ann Annotation) TypeString() string { + return AnnotTypeStrings[ann.SubType] +} + +func (ann Annotation) RenderDict(xRefTable *XRefTable, pageIndRef *types.IndirectRef) (types.Dict, error) { + d := types.Dict(map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name(ann.TypeString()), + "Rect": ann.Rect.Array(), + }) + + if pageIndRef != nil { + d["P"] = *pageIndRef + } + + if ann.Contents != "" { + s, err := types.EscapeUTF16String(ann.Contents) + if err != nil { + return nil, err + } + d.InsertString("Contents", *s) + } + + if ann.NM != "" { + d.InsertString("NM", ann.NM) + } + + modDate := types.DateString(time.Now()) + if ann.ModificationDate != "" { + _, ok := types.DateTime(ann.ModificationDate, xRefTable.ValidationMode == ValidationRelaxed) + if !ok { + return nil, errors.Errorf("pdfcpu: annotation renderDict - validateDateEntry: <%s> invalid date", ann.ModificationDate) + } + modDate = ann.ModificationDate + } + d.InsertString("ModDate", modDate) + + if ann.F != 0 { + d["F"] = types.Integer(ann.F) + } + + if ann.C != nil { + d["C"] = ann.C.Array() + } + + if ann.BorderWidth > 0 { + d["Border"] = borderArray(ann.BorderRadX, ann.BorderRadY, ann.BorderWidth) + } + + return d, nil +} + +// PopupAnnotation represents PDF Popup annotations. +type PopupAnnotation struct { + Annotation + ParentIndRef *types.IndirectRef // The optional parent markup annotation with which this pop-up annotation shall be associated. + Open bool // A flag specifying whether the annotation shall initially be displayed open. +} + +// NewPopupAnnotation returns a new popup annotation. +func NewPopupAnnotation( + rect types.Rectangle, + contents, id string, + modDate string, + f AnnotationFlags, + col *color.SimpleColor, + borderRadX float64, + borderRadY float64, + borderWidth float64, + + parentIndRef *types.IndirectRef, + displayOpen bool) PopupAnnotation { + + ann := NewAnnotation(AnnPopup, rect, contents, id, modDate, f, col, borderRadX, borderRadY, borderWidth) + + return PopupAnnotation{ + Annotation: ann, + ParentIndRef: parentIndRef, + Open: displayOpen, + } +} + +// ContentString returns a string representation of ann's content. +func (ann PopupAnnotation) ContentString() string { + s := "\"" + ann.Contents + "\"" + if ann.ParentIndRef != nil { + s = "-> #" + ann.ParentIndRef.ObjectNumber.String() + } + return s +} + +func (ann PopupAnnotation) RenderDict(xRefTable *XRefTable, pageIndRef *types.IndirectRef) (types.Dict, error) { + d, err := ann.Annotation.RenderDict(xRefTable, pageIndRef) + if err != nil { + return nil, err + } + + if ann.ParentIndRef != nil { + d["Parent"] = *ann.ParentIndRef + } + + d["Open"] = types.Boolean(ann.Open) + + return d, nil +} + +// LinkAnnotation represents a PDF link annotation. +type LinkAnnotation struct { + Annotation + Dest *Destination // internal link + URI string // external link + Quad types.QuadPoints // shall be ignored if any coordinate lies outside the region specified by Rect. + Border bool // render border using borderColor. + BorderWidth float64 + BorderStyle BorderStyle +} + +// NewLinkAnnotation returns a new link annotation. +func NewLinkAnnotation( + rect types.Rectangle, + contents, id string, + modDate string, + f AnnotationFlags, + borderCol *color.SimpleColor, + + dest *Destination, // supply dest or uri, dest takes precedence + uri string, + quad types.QuadPoints, + border bool, + borderWidth float64, + borderStyle BorderStyle) LinkAnnotation { + + ann := NewAnnotation(AnnLink, rect, contents, id, modDate, f, borderCol, 0, 0, 0) + + return LinkAnnotation{ + Annotation: ann, + Dest: dest, + URI: uri, + Quad: quad, + Border: border, + BorderWidth: borderWidth, + BorderStyle: borderStyle, + } +} + +// ContentString returns a string representation of ann's content. +func (ann LinkAnnotation) ContentString() string { + if len(ann.URI) > 0 { + return ann.URI + } + if ann.Dest != nil { + // eg. page /XYZ left top zoom + return fmt.Sprintf("Page %d %s", ann.Dest.PageNr, ann.Dest) + } + return "internal link" +} + +// RenderDict renders ann into a page annotation dict. +func (ann LinkAnnotation) RenderDict(xRefTable *XRefTable, pageIndRef *types.IndirectRef) (types.Dict, error) { + d, err := ann.Annotation.RenderDict(xRefTable, pageIndRef) + if err != nil { + return nil, err + } + + if ann.Dest != nil { + dest := ann.Dest + if dest.Zoom == 0 { + dest.Zoom = 1 + } + _, indRef, pAttr, err := xRefTable.PageDict(dest.PageNr, false) + if err != nil { + return nil, err + } + if dest.Typ == DestXYZ && dest.Left < 0 && dest.Top < 0 { + // Show top left corner of destination page. + dest.Left = int(pAttr.MediaBox.LL.X) + dest.Top = int(pAttr.MediaBox.UR.Y) + if pAttr.CropBox != nil { + dest.Left = int(pAttr.CropBox.LL.X) + dest.Top = int(pAttr.CropBox.UR.Y) + } + } + d["Dest"] = dest.Array(*indRef) + } else { + actionDict := types.Dict(map[string]types.Object{ + "Type": types.Name("Action"), + "S": types.Name("URI"), + "URI": types.StringLiteral(ann.URI), + }) + d["A"] = actionDict + } + + if ann.Quad != nil { + d.Insert("QuadPoints", ann.Quad.Array()) + } + + if !ann.Border { + d["Border"] = types.NewIntegerArray(0, 0, 0) + } else { + if ann.C != nil { + d["C"] = ann.C.Array() + } + } + + d["BS"] = borderStyleDict(ann.BorderWidth, ann.BorderStyle) + + return d, nil +} + +// MarkupAnnotation represents a PDF markup annotation. +type MarkupAnnotation struct { + Annotation + T string // The text label that shall be displayed in the title bar of the annotation’s pop-up window when open and active. This entry shall identify the user who added the annotation. + PopupIndRef *types.IndirectRef // An indirect reference to a pop-up annotation for entering or editing the text associated with this annotation. + CA *float64 // (Default: 1.0) The constant opacity value that shall be used in painting the annotation. + RC string // A rich text string that shall be displayed in the pop-up window when the annotation is opened. + CreationDate string // The date and time when the annotation was created. + Subj string // Text representing a short description of the subject being addressed by the annotation. +} + +// NewMarkupAnnotation returns a new markup annotation. +func NewMarkupAnnotation( + subType AnnotationType, + rect types.Rectangle, + contents, id string, + modDate string, + f AnnotationFlags, + col *color.SimpleColor, + borderRadX float64, + borderRadY float64, + borderWidth float64, + + title string, + popupIndRef *types.IndirectRef, + ca *float64, + rc, subject string) MarkupAnnotation { + + ann := NewAnnotation(subType, rect, contents, id, modDate, f, col, borderRadX, borderRadY, borderWidth) + + return MarkupAnnotation{ + Annotation: ann, + T: title, + PopupIndRef: popupIndRef, + CA: ca, + RC: rc, + CreationDate: types.DateString(time.Now()), + Subj: subject} +} + +// ContentString returns a string representation of ann's content. +func (ann MarkupAnnotation) ContentString() string { + s := "\"" + ann.Contents + "\"" + if ann.PopupIndRef != nil { + s += "-> #" + ann.PopupIndRef.ObjectNumber.String() + } + return s +} + +func (ann MarkupAnnotation) RenderDict(xRefTable *XRefTable, pageIndRef *types.IndirectRef) (types.Dict, error) { + d, err := ann.Annotation.RenderDict(xRefTable, pageIndRef) + if err != nil { + return nil, err + } + + if ann.T != "" { + s, err := types.EscapeUTF16String(ann.T) + if err != nil { + return nil, err + } + d.InsertString("T", *s) + } + + if ann.PopupIndRef != nil { + d.Insert("Popup", *ann.PopupIndRef) + } + + if ann.CA != nil { + d.Insert("CA", types.Float(*ann.CA)) + } + + if ann.RC != "" { + s, err := types.EscapeUTF16String(ann.RC) + if err != nil { + return nil, err + } + d.InsertString("RC", *s) + } + + d.InsertString("CreationDate", ann.CreationDate) + + if ann.Subj != "" { + s, err := types.EscapeUTF16String(ann.Subj) + if err != nil { + return nil, err + } + d.InsertString("Subj", *s) + } + + return d, nil +} + +// TextAnnotation represents a PDF text annotation aka "Sticky Note". +type TextAnnotation struct { + MarkupAnnotation + Open bool // A flag specifying whether the annotation shall initially be displayed open. + Name string // The name of an icon that shall be used in displaying the annotation. Comment, Key, (Note), Help, NewParagraph, Paragraph, Insert +} + +// NewTextAnnotation returns a new text annotation. +func NewTextAnnotation( + rect types.Rectangle, + contents, id string, + modDate string, + f AnnotationFlags, + col *color.SimpleColor, + title string, + popupIndRef *types.IndirectRef, + ca *float64, + rc, subject string, + borderRadX float64, + borderRadY float64, + borderWidth float64, + + displayOpen bool, + name string) TextAnnotation { + + ma := NewMarkupAnnotation(AnnText, rect, contents, id, modDate, f, col, borderRadX, borderRadY, borderWidth, title, popupIndRef, ca, rc, subject) + + return TextAnnotation{ + MarkupAnnotation: ma, + Open: displayOpen, + Name: name, + } +} + +// RenderDict renders ann into a PDF annotation dict. +func (ann TextAnnotation) RenderDict(xRefTable *XRefTable, pageIndRef *types.IndirectRef) (types.Dict, error) { + d, err := ann.MarkupAnnotation.RenderDict(xRefTable, pageIndRef) + if err != nil { + return nil, err + } + + d["Open"] = types.Boolean(ann.Open) + + if ann.Name != "" { + d.InsertName("Name", ann.Name) + } + + return d, nil +} + +// FreeTextIntent represents the various free text annotation intents. +type FreeTextIntent int + +const ( + IntentFreeText FreeTextIntent = 1 << iota + IntentFreeTextCallout + IntentFreeTextTypeWriter +) + +func FreeTextIntentName(fti FreeTextIntent) string { + var s string + switch fti { + case IntentFreeText: + s = "FreeText" + case IntentFreeTextCallout: + s = "FreeTextCallout" + case IntentFreeTextTypeWriter: + s = "FreeTextTypeWriter" + } + return s +} + +// FreeText Annotation displays text directly on the page. +type FreeTextAnnotation struct { + MarkupAnnotation + Text string // Rich text string, see XFA 3.3 + HAlign types.HAlignment // Code specifying the form of quadding (justification) + FontName string // font name + FontSize int // font size + FontCol *color.SimpleColor // font color + DS string // Default style string + Intent string // Description of the intent of the free text annotation + CallOutLine types.Array // if intent is FreeTextCallout + CallOutLineEndingStyle string + Margins types.Array + BorderWidth float64 + BorderStyle BorderStyle + CloudyBorder bool + CloudyBorderIntensity int // 0,1,2 +} + +// XFA conform rich text string examples: +// The second and fourth words are bold. +// The second and fourth words are italicized. +// For more information see this web site. + +// NewFreeTextAnnotation returns a new free text annotation. +func NewFreeTextAnnotation( + rect types.Rectangle, + contents, id string, + modDate string, + f AnnotationFlags, + col *color.SimpleColor, + title string, + popupIndRef *types.IndirectRef, + ca *float64, + rc, subject string, + + text string, + hAlign types.HAlignment, + fontName string, + fontSize int, + fontCol *color.SimpleColor, + ds string, + intent *FreeTextIntent, + callOutLine types.Array, + callOutLineEndingStyle *LineEndingStyle, + MLeft, MTop, MRight, MBot float64, + borderWidth float64, + borderStyle BorderStyle, + cloudyBorder bool, + cloudyBorderIntensity int) FreeTextAnnotation { + + // validate required DA, DS + + // validate callOutline: 2 or 3 points => array of 4 or 6 numbers. + + ma := NewMarkupAnnotation(AnnFreeText, rect, contents, id, modDate, f, col, 0, 0, 0, title, popupIndRef, ca, rc, subject) + + if cloudyBorderIntensity < 0 || cloudyBorderIntensity > 2 { + cloudyBorderIntensity = 0 + } + + freeTextIntent := "" + if intent != nil { + freeTextIntent = FreeTextIntentName(*intent) + } + + leStyle := "" + if callOutLineEndingStyle != nil { + leStyle = LineEndingStyleName(*callOutLineEndingStyle) + } + + freeTextAnn := FreeTextAnnotation{ + MarkupAnnotation: ma, + Text: text, + HAlign: hAlign, + FontName: fontName, + FontSize: fontSize, + FontCol: fontCol, + DS: ds, + Intent: freeTextIntent, + CallOutLine: callOutLine, + CallOutLineEndingStyle: leStyle, + BorderWidth: borderWidth, + BorderStyle: borderStyle, + CloudyBorder: cloudyBorder, + CloudyBorderIntensity: cloudyBorderIntensity, + } + + if MLeft > 0 || MTop > 0 || MRight > 0 || MBot > 0 { + freeTextAnn.Margins = types.NewNumberArray(MLeft, MTop, MRight, MBot) + } + + return freeTextAnn +} + +// RenderDict renders ann into a PDF annotation dict. +func (ann FreeTextAnnotation) RenderDict(xRefTable *XRefTable, pageIndRef *types.IndirectRef) (types.Dict, error) { + d, err := ann.MarkupAnnotation.RenderDict(xRefTable, pageIndRef) + if err != nil { + return nil, err + } + + da := "" + + // TODO Implement Tf operator + + // fontID, err := xRefTable.EnsureFont(ann.FontName) // in root page Resources? + // if err != nil { + // return nil, err + // } + + // da := fmt.Sprintf("/%s %d Tf", fontID, ann.FontSize) + + if ann.FontCol != nil { + da += fmt.Sprintf(" %.2f %.2f %.2f rg", ann.FontCol.R, ann.FontCol.G, ann.FontCol.B) + } + d["DA"] = types.StringLiteral(da) + + d.InsertInt("Q", int(ann.HAlign)) + + if ann.Text == "" { + if ann.Contents == "" { + return nil, errors.New("pdfcpu: FreeTextAnnotation missing \"text\"") + } + ann.Text = ann.Contents + } + s, err := types.EscapeUTF16String(ann.Text) + if err != nil { + return nil, err + } + d.InsertString("RC", *s) + + if ann.DS != "" { + d.InsertString("DS", ann.DS) + } + + if ann.Intent != "" { + d.InsertName("IT", ann.Intent) + if ann.Intent == "FreeTextCallout" { + if len(ann.CallOutLine) > 0 { + d["CL"] = ann.CallOutLine + d.InsertName("LE", ann.CallOutLineEndingStyle) + } + } + } + + if ann.Margins != nil { + d["RD"] = ann.Margins + } + + if ann.BorderWidth > 0 { + d["BS"] = borderStyleDict(ann.BorderWidth, ann.BorderStyle) + } + + if ann.CloudyBorder && ann.CloudyBorderIntensity > 0 { + d["BE"] = borderEffectDict(ann.CloudyBorder, ann.CloudyBorderIntensity) + } + + return d, nil +} + +// LineIntent represents the various line annotation intents. +type LineIntent int + +const ( + IntentLineArrow LineIntent = 1 << iota + IntentLineDimension +) + +func LineIntentName(li LineIntent) string { + var s string + switch li { + case IntentLineArrow: + s = "LineArrow" + case IntentLineDimension: + s = "LineDimension" + } + return s +} + +// LineAnnotation represents a line annotation. +type LineAnnotation struct { + MarkupAnnotation + P1, P2 types.Point // Two points in default user space. + LineEndings types.Array // Optional array of two names that shall specify the line ending styles. + LeaderLineLength float64 // Length of leader lines in default user space that extend from each endpoint of the line perpendicular to the line itself. + LeaderLineOffset float64 // Non-negative number that shall represent the length of the leader line offset, which is the amount of empty space between the endpoints of the annotation and the beginning of the leader lines. + LeaderLineExtensionLength float64 // Non-negative number that shall represents the length of leader line extensions that extend from the line proper 180 degrees from the leader lines, + Intent string // Optional description of the intent of the line annotation. + Measure types.Dict // Optional measure dictionary that shall specify the scale and units that apply to the line annotation. + Caption bool // Use text specified by "Contents" or "RC" as caption. + CaptionPositionTop bool // if true the caption shall be on top of the line else caption shall be centred inside the line. + CaptionOffsetX float64 + CaptionOffsetY float64 + FillCol *color.SimpleColor + BorderWidth float64 + BorderStyle BorderStyle +} + +// NewLineAnnotation returns a new line annotation. +func NewLineAnnotation( + rect types.Rectangle, + contents, id string, + modDate string, + f AnnotationFlags, + col *color.SimpleColor, + title string, + popupIndRef *types.IndirectRef, + ca *float64, + rc, subject string, + + p1, p2 types.Point, + beginLineEndingStyle *LineEndingStyle, + endLineEndingStyle *LineEndingStyle, + leaderLineLength float64, + leaderLineOffset float64, + leaderLineExtensionLength float64, + intent *LineIntent, + measure types.Dict, + caption bool, + captionPosTop bool, + captionOffsetX float64, + captionOffsetY float64, + fillCol *color.SimpleColor, + borderWidth float64, + borderStyle BorderStyle) LineAnnotation { + + ma := NewMarkupAnnotation(AnnLine, rect, contents, id, modDate, f, col, 0, 0, 0, title, popupIndRef, ca, rc, subject) + + lineIntent := "" + if intent != nil { + lineIntent = LineIntentName(*intent) + } + + lineAnn := LineAnnotation{ + MarkupAnnotation: ma, + P1: p1, + P2: p2, + LeaderLineLength: leaderLineLength, + LeaderLineOffset: leaderLineOffset, + LeaderLineExtensionLength: leaderLineExtensionLength, + Intent: lineIntent, + Measure: measure, + Caption: caption, + CaptionPositionTop: captionPosTop, + CaptionOffsetX: captionOffsetX, + CaptionOffsetY: captionOffsetY, + FillCol: fillCol, + BorderWidth: borderWidth, + BorderStyle: borderStyle, + } + + if beginLineEndingStyle != nil && endLineEndingStyle != nil { + lineAnn.LineEndings = + types.NewNameArray( + LineEndingStyleName(*beginLineEndingStyle), + LineEndingStyleName(*endLineEndingStyle), + ) + } + + return lineAnn +} + +// RenderDict renders ann into a PDF annotation dict. +func (ann LineAnnotation) RenderDict(xRefTable *XRefTable, pageIndRef *types.IndirectRef) (types.Dict, error) { + + d, err := ann.MarkupAnnotation.RenderDict(xRefTable, pageIndRef) + if err != nil { + return nil, err + } + + if ann.LeaderLineExtensionLength < 0 { + return nil, errors.New("pdfcpu: LineAnnotation leader line extension length must not be negative.") + } + + if ann.LeaderLineExtensionLength > 0 && ann.LeaderLineLength == 0 { + return nil, errors.New("pdfcpu: LineAnnotation leader line length missing.") + } + + if ann.LeaderLineOffset < 0 { + return nil, errors.New("pdfcpu: LineAnnotation leader line offset must not be negative.") + } + + d["L"] = types.NewNumberArray(ann.P1.X, ann.P1.Y, ann.P2.X, ann.P2.Y) + + if ann.LeaderLineExtensionLength > 0 { + d["LLE"] = types.Float(ann.LeaderLineExtensionLength) + } + + if ann.LeaderLineLength > 0 { + d["LL"] = types.Float(ann.LeaderLineLength) + if ann.LeaderLineOffset > 0 { + d["LLO"] = types.Float(ann.LeaderLineOffset) + } + } + + if len(ann.Measure) > 0 { + d["Measure"] = ann.Measure + } + + if ann.Intent != "" { + d.InsertName("IT", ann.Intent) + + } + + d["Cap"] = types.Boolean(ann.Caption) + if ann.Caption { + if ann.CaptionPositionTop { + d["CP"] = types.Name("Top") + } + d["CO"] = types.NewNumberArray(ann.CaptionOffsetX, ann.CaptionOffsetY) + } + + if ann.FillCol != nil { + d["IC"] = ann.FillCol.Array() + } + + if ann.BorderWidth > 0 { + d["BS"] = borderStyleDict(ann.BorderWidth, ann.BorderStyle) + } + + if len(ann.LineEndings) == 2 { + d["LE"] = ann.LineEndings + } + + return d, nil +} + +// SquareAnnotation represents a square annotation. +type SquareAnnotation struct { + MarkupAnnotation + FillCol *color.SimpleColor + Margins types.Array + BorderWidth float64 + BorderStyle BorderStyle + CloudyBorder bool + CloudyBorderIntensity int // 0,1,2 +} + +// NewSquareAnnotation returns a new square annotation. +func NewSquareAnnotation( + rect types.Rectangle, + contents, id string, + modDate string, + f AnnotationFlags, + col *color.SimpleColor, + title string, + popupIndRef *types.IndirectRef, + ca *float64, + rc, subject string, + + fillCol *color.SimpleColor, + MLeft, MTop, MRight, MBot float64, + borderWidth float64, + borderStyle BorderStyle, + cloudyBorder bool, + cloudyBorderIntensity int) SquareAnnotation { + + ma := NewMarkupAnnotation(AnnSquare, rect, contents, id, modDate, f, col, 0, 0, 0, title, popupIndRef, ca, rc, subject) + + if cloudyBorderIntensity < 0 || cloudyBorderIntensity > 2 { + cloudyBorderIntensity = 0 + } + + squareAnn := SquareAnnotation{ + MarkupAnnotation: ma, + FillCol: fillCol, + BorderWidth: borderWidth, + BorderStyle: borderStyle, + CloudyBorder: cloudyBorder, + CloudyBorderIntensity: cloudyBorderIntensity, + } + + if MLeft > 0 || MTop > 0 || MRight > 0 || MBot > 0 { + squareAnn.Margins = types.NewNumberArray(MLeft, MTop, MRight, MBot) + } + + return squareAnn +} + +// RenderDict renders ann into a page annotation dict. +func (ann SquareAnnotation) RenderDict(xRefTable *XRefTable, pageIndRef *types.IndirectRef) (types.Dict, error) { + d, err := ann.MarkupAnnotation.RenderDict(xRefTable, pageIndRef) + if err != nil { + return nil, err + } + + if ann.FillCol != nil { + d["IC"] = ann.FillCol.Array() + } + + if ann.Margins != nil { + d["RD"] = ann.Margins + } + + if ann.BorderWidth > 0 { + d["BS"] = borderStyleDict(ann.BorderWidth, ann.BorderStyle) + } + + if ann.CloudyBorder && ann.CloudyBorderIntensity > 0 { + d["BE"] = borderEffectDict(ann.CloudyBorder, ann.CloudyBorderIntensity) + } + + return d, nil +} + +// CircleAnnotation represents a square annotation. +type CircleAnnotation struct { + MarkupAnnotation + FillCol *color.SimpleColor + Margins types.Array + BorderWidth float64 + BorderStyle BorderStyle + CloudyBorder bool + CloudyBorderIntensity int // 0,1,2 +} + +// NewCircleAnnotation returns a new circle annotation. +func NewCircleAnnotation( + rect types.Rectangle, + contents, id string, + modDate string, + f AnnotationFlags, + col *color.SimpleColor, + title string, + popupIndRef *types.IndirectRef, + ca *float64, + rc, subject string, + + fillCol *color.SimpleColor, + MLeft, MTop, MRight, MBot float64, + borderWidth float64, + borderStyle BorderStyle, + cloudyBorder bool, + cloudyBorderIntensity int) CircleAnnotation { + + ma := NewMarkupAnnotation(AnnCircle, rect, contents, id, modDate, f, col, 0, 0, 0, title, popupIndRef, ca, rc, subject) + + if cloudyBorderIntensity < 0 || cloudyBorderIntensity > 2 { + cloudyBorderIntensity = 0 + } + + circleAnn := CircleAnnotation{ + MarkupAnnotation: ma, + FillCol: fillCol, + BorderWidth: borderWidth, + BorderStyle: borderStyle, + CloudyBorder: cloudyBorder, + CloudyBorderIntensity: cloudyBorderIntensity, + } + + if MLeft > 0 || MTop > 0 || MRight > 0 || MBot > 0 { + circleAnn.Margins = types.NewNumberArray(MLeft, MTop, MRight, MBot) + } + + return circleAnn +} + +// RenderDict renders ann into a page annotation dict. +func (ann CircleAnnotation) RenderDict(xRefTable *XRefTable, pageIndRef *types.IndirectRef) (types.Dict, error) { + d, err := ann.MarkupAnnotation.RenderDict(xRefTable, pageIndRef) + if err != nil { + return nil, err + } + + if ann.FillCol != nil { + d["IC"] = ann.FillCol.Array() + } + + if ann.Margins != nil { + d["RD"] = ann.Margins + } + + if ann.BorderWidth > 0 { + d["BS"] = borderStyleDict(ann.BorderWidth, ann.BorderStyle) + } + + if ann.CloudyBorder && ann.CloudyBorderIntensity > 0 { + d["BE"] = borderEffectDict(ann.CloudyBorder, ann.CloudyBorderIntensity) + } + + return d, nil +} + +// PolygonIntent represents the various polygon annotation intents. +type PolygonIntent int + +const ( + IntentPolygonCloud PolygonIntent = 1 << iota + IntentPolygonDimension +) + +func PolygonIntentName(pi PolygonIntent) string { + var s string + switch pi { + case IntentPolygonCloud: + s = "PolygonCloud" + case IntentPolygonDimension: + s = "PolygonDimension" + } + return s +} + +// PolygonAnnotation represents a polygon annotation. +type PolygonAnnotation struct { + MarkupAnnotation + Vertices types.Array // Array of numbers specifying the alternating horizontal and vertical coordinates, respectively, of each vertex, in default user space. + Path types.Array // Array of n arrays, each supplying the operands for a path building operator (m, l or c). + Intent string // Optional description of the intent of the polygon annotation. + Measure types.Dict // Optional measure dictionary that shall specify the scale and units that apply to the annotation. + FillCol *color.SimpleColor + BorderWidth float64 + BorderStyle BorderStyle + CloudyBorder bool + CloudyBorderIntensity int // 0,1,2 +} + +// NewPolygonAnnotation returns a new polygon annotation. +func NewPolygonAnnotation( + rect types.Rectangle, + contents, id string, + modDate string, + f AnnotationFlags, + col *color.SimpleColor, + title string, + popupIndRef *types.IndirectRef, + ca *float64, + rc, subject string, + + vertices types.Array, + path types.Array, + intent *PolygonIntent, + measure types.Dict, + fillCol *color.SimpleColor, + borderWidth float64, + borderStyle BorderStyle, + cloudyBorder bool, + cloudyBorderIntensity int) PolygonAnnotation { + + ma := NewMarkupAnnotation(AnnPolygon, rect, contents, id, modDate, f, col, 0, 0, 0, title, popupIndRef, ca, rc, subject) + + polygonIntent := "" + if intent != nil { + polygonIntent = PolygonIntentName(*intent) + } + + if cloudyBorderIntensity < 0 || cloudyBorderIntensity > 2 { + cloudyBorderIntensity = 0 + } + + polygonAnn := PolygonAnnotation{ + MarkupAnnotation: ma, + Vertices: vertices, + Path: path, + Intent: polygonIntent, + Measure: measure, + FillCol: fillCol, + BorderWidth: borderWidth, + BorderStyle: borderStyle, + CloudyBorder: cloudyBorder, + CloudyBorderIntensity: cloudyBorderIntensity, + } + + return polygonAnn +} + +// RenderDict renders ann into a PDF annotation dict. +func (ann PolygonAnnotation) RenderDict(xRefTable *XRefTable, pageIndRef *types.IndirectRef) (types.Dict, error) { + + d, err := ann.MarkupAnnotation.RenderDict(xRefTable, pageIndRef) + if err != nil { + return nil, err + } + + if len(ann.Measure) > 0 { + d["Measure"] = ann.Measure + } + + if len(ann.Vertices) > 0 && len(ann.Path) > 0 { + return nil, errors.New("pdfcpu: PolygonAnnotation supports \"Vertices\" or \"Path\" only") + } + + if len(ann.Vertices) > 0 { + d["Vertices"] = ann.Vertices + } else { + d["Path"] = ann.Path + } + + if ann.Intent != "" { + d.InsertName("IT", ann.Intent) + + } + + if ann.FillCol != nil { + d["IC"] = ann.FillCol.Array() + } + + if ann.BorderWidth > 0 { + d["BS"] = borderStyleDict(ann.BorderWidth, ann.BorderStyle) + } + + if ann.CloudyBorder && ann.CloudyBorderIntensity > 0 { + d["BE"] = borderEffectDict(ann.CloudyBorder, ann.CloudyBorderIntensity) + } + + return d, nil +} + +// PolyLineIntent represents the various polyline annotation intents. +type PolyLineIntent int + +const ( + IntentPolyLinePolygonCloud PolyLineIntent = 1 << iota + IntentPolyLineDimension +) + +func PolyLineIntentName(pi PolyLineIntent) string { + var s string + switch pi { + case IntentPolyLineDimension: + s = "PolyLineDimension" + } + return s +} + +type PolyLineAnnotation struct { + MarkupAnnotation + Vertices types.Array // Array of numbers specifying the alternating horizontal and vertical coordinates, respectively, of each vertex, in default user space. + Path types.Array // Array of n arrays, each supplying the operands for a path building operator (m, l or c). + Intent string // Optional description of the intent of the polyline annotation. + Measure types.Dict // Optional measure dictionary that shall specify the scale and units that apply to the annotation. + FillCol *color.SimpleColor + BorderWidth float64 + BorderStyle BorderStyle + LineEndings types.Array // Optional array of two names that shall specify the line ending styles. +} + +// NewPolyLineAnnotation returns a new polyline annotation. +func NewPolyLineAnnotation( + rect types.Rectangle, + contents, id string, + modDate string, + f AnnotationFlags, + col *color.SimpleColor, + title string, + popupIndRef *types.IndirectRef, + ca *float64, + rc, subject string, + + vertices types.Array, + path types.Array, + intent *PolyLineIntent, + measure types.Dict, + fillCol *color.SimpleColor, + borderWidth float64, + borderStyle BorderStyle, + beginLineEndingStyle *LineEndingStyle, + endLineEndingStyle *LineEndingStyle) PolyLineAnnotation { + + ma := NewMarkupAnnotation(AnnPolyLine, rect, contents, id, modDate, f, col, 0, 0, 0, title, popupIndRef, ca, rc, subject) + + polyLineIntent := "" + if intent != nil { + polyLineIntent = PolyLineIntentName(*intent) + } + + polyLineAnn := PolyLineAnnotation{ + MarkupAnnotation: ma, + Vertices: vertices, + Path: path, + Intent: polyLineIntent, + Measure: measure, + FillCol: fillCol, + BorderWidth: borderWidth, + BorderStyle: borderStyle, + } + + if beginLineEndingStyle != nil && endLineEndingStyle != nil { + polyLineAnn.LineEndings = + types.NewNameArray( + LineEndingStyleName(*beginLineEndingStyle), + LineEndingStyleName(*endLineEndingStyle), + ) + } + + return polyLineAnn +} + +// RenderDict renders ann into a PDF annotation dict. +func (ann PolyLineAnnotation) RenderDict(xRefTable *XRefTable, pageIndRef *types.IndirectRef) (types.Dict, error) { + + d, err := ann.MarkupAnnotation.RenderDict(xRefTable, pageIndRef) + if err != nil { + return nil, err + } + + if len(ann.Measure) > 0 { + d["Measure"] = ann.Measure + } + + if len(ann.Vertices) > 0 && len(ann.Path) > 0 { + return nil, errors.New("pdfcpu: PolyLineAnnotation supports \"Vertices\" or \"Path\" only") + } + + if len(ann.Vertices) > 0 { + d["Vertices"] = ann.Vertices + } else { + d["Path"] = ann.Path + } + + if ann.Intent != "" { + d.InsertName("IT", ann.Intent) + + } + + if ann.FillCol != nil { + d["IC"] = ann.FillCol.Array() + } + + if ann.BorderWidth > 0 { + d["BS"] = borderStyleDict(ann.BorderWidth, ann.BorderStyle) + } + + if len(ann.LineEndings) == 2 { + d["LE"] = ann.LineEndings + } + + return d, nil +} + +type TextMarkupAnnotation struct { + MarkupAnnotation + Quad types.QuadPoints +} + +func NewTextMarkupAnnotation( + subType AnnotationType, + rect types.Rectangle, + contents, id string, + modDate string, + f AnnotationFlags, + col *color.SimpleColor, + borderRadX float64, + borderRadY float64, + borderWidth float64, + title string, + popupIndRef *types.IndirectRef, + ca *float64, + rc, subject string, + + quad types.QuadPoints) TextMarkupAnnotation { + + ma := NewMarkupAnnotation(subType, rect, contents, id, modDate, f, col, borderRadX, borderRadY, borderWidth, title, popupIndRef, ca, rc, subject) + + return TextMarkupAnnotation{ + MarkupAnnotation: ma, + Quad: quad, + } +} + +func (ann TextMarkupAnnotation) RenderDict(xRefTable *XRefTable, pageIndRef *types.IndirectRef) (types.Dict, error) { + d, err := ann.MarkupAnnotation.RenderDict(xRefTable, pageIndRef) + if err != nil { + return nil, err + } + + if ann.Quad != nil { + d.Insert("QuadPoints", ann.Quad.Array()) + } + + return d, nil +} + +type HighlightAnnotation struct { + TextMarkupAnnotation +} + +func NewHighlightAnnotation( + rect types.Rectangle, + contents, id string, + modDate string, + f AnnotationFlags, + col *color.SimpleColor, + borderRadX float64, + borderRadY float64, + borderWidth float64, + title string, + popupIndRef *types.IndirectRef, + ca *float64, + rc, subject string, + + quad types.QuadPoints) HighlightAnnotation { + + return HighlightAnnotation{ + NewTextMarkupAnnotation(AnnHighLight, rect, contents, id, modDate, f, col, borderRadX, borderRadY, borderWidth, title, popupIndRef, ca, rc, subject, quad), + } +} + +type UnderlineAnnotation struct { + TextMarkupAnnotation +} + +func NewUnderlineAnnotation( + rect types.Rectangle, + contents, id string, + modDate string, + f AnnotationFlags, + col *color.SimpleColor, + borderRadX float64, + borderRadY float64, + borderWidth float64, + title string, + popupIndRef *types.IndirectRef, + ca *float64, + rc, subject string, + + quad types.QuadPoints) UnderlineAnnotation { + + return UnderlineAnnotation{ + NewTextMarkupAnnotation(AnnUnderline, rect, contents, id, modDate, f, col, borderRadX, borderRadY, borderWidth, title, popupIndRef, ca, rc, subject, quad), + } +} + +type SquigglyAnnotation struct { + TextMarkupAnnotation +} + +func NewSquigglyAnnotation( + rect types.Rectangle, + contents, id string, + modDate string, + f AnnotationFlags, + col *color.SimpleColor, + borderRadX float64, + borderRadY float64, + borderWidth float64, + title string, + popupIndRef *types.IndirectRef, + ca *float64, + rc, subject string, + + quad types.QuadPoints) SquigglyAnnotation { + + return SquigglyAnnotation{ + NewTextMarkupAnnotation(AnnSquiggly, rect, contents, id, modDate, f, col, borderRadX, borderRadY, borderWidth, title, popupIndRef, ca, rc, subject, quad), + } +} + +type StrikeOutAnnotation struct { + TextMarkupAnnotation +} + +func NewStrikeOutAnnotation( + rect types.Rectangle, + contents, id string, + modDate string, + f AnnotationFlags, + col *color.SimpleColor, + borderRadX float64, + borderRadY float64, + borderWidth float64, + title string, + popupIndRef *types.IndirectRef, + ca *float64, + rc, subject string, + + quad types.QuadPoints) StrikeOutAnnotation { + + return StrikeOutAnnotation{ + NewTextMarkupAnnotation(AnnStrikeOut, rect, contents, id, modDate, f, col, borderRadX, borderRadY, borderWidth, title, popupIndRef, ca, rc, subject, quad), + } +} + +type CaretAnnotation struct { + MarkupAnnotation + RD *types.Rectangle // A set of four numbers that shall describe the numerical differences between two rectangles: the Rect entry of the annotation and the actual boundaries of the underlying caret. + Paragraph bool // A new paragraph symbol (¶) shall be associated with the caret. +} + +func NewCaretAnnotation( + rect types.Rectangle, + contents, id string, + modDate string, + f AnnotationFlags, + col *color.SimpleColor, + borderRadX float64, + borderRadY float64, + borderWidth float64, + title string, + popupIndRef *types.IndirectRef, + ca *float64, + rc, subject string, + + rd *types.Rectangle, + paragraph bool) CaretAnnotation { + + ma := NewMarkupAnnotation(AnnCaret, rect, contents, id, modDate, f, col, borderRadX, borderRadY, borderWidth, title, popupIndRef, ca, rc, subject) + + return CaretAnnotation{ + MarkupAnnotation: ma, + RD: rd, + Paragraph: paragraph, + } +} + +func (ann CaretAnnotation) RenderDict(xRefTable *XRefTable, pageIndRef *types.IndirectRef) (types.Dict, error) { + d, err := ann.MarkupAnnotation.RenderDict(xRefTable, pageIndRef) + if err != nil { + return nil, err + } + + if ann.RD != nil { + d["RD"] = ann.RD.Array() + } + + if ann.Paragraph { + d["Sy"] = types.Name("P") + } + + return d, nil +} + +// A series of alternating x and y coordinates in PDF user space, specifying points along the path. +type InkPath []float64 + +type InkAnnotation struct { + MarkupAnnotation + InkList []InkPath // Array of n arrays, each representing a stroked path of points in user space. + BorderWidth float64 + BorderStyle BorderStyle +} + +func NewInkAnnotation( + rect types.Rectangle, + contents, id string, + modDate string, + f AnnotationFlags, + col *color.SimpleColor, + title string, + popupIndRef *types.IndirectRef, + ca *float64, + rc, subject string, + + ink []InkPath, + borderWidth float64, + borderStyle BorderStyle) InkAnnotation { + + ma := NewMarkupAnnotation(AnnInk, rect, contents, id, modDate, f, col, 0, 0, 0, title, popupIndRef, ca, rc, subject) + + return InkAnnotation{ + MarkupAnnotation: ma, + InkList: ink, + BorderWidth: borderWidth, + BorderStyle: borderStyle, + } +} + +func (ann InkAnnotation) RenderDict(xRefTable *XRefTable, pageIndRef *types.IndirectRef) (types.Dict, error) { + d, err := ann.MarkupAnnotation.RenderDict(xRefTable, pageIndRef) + if err != nil { + return nil, err + } + + ink := types.Array{} + for i := range ann.InkList { + ink = append(ink, types.NewNumberArray(ann.InkList[i]...)) + } + d["InkList"] = ink + + if ann.BorderWidth > 0 { + d["BS"] = borderStyleDict(ann.BorderWidth, ann.BorderStyle) + } + + return d, nil +} diff --git a/pkg/pdfcpu/model/attach.go b/pkg/pdfcpu/model/attach.go new file mode 100644 index 0000000000000000000000000000000000000000..96de0aff42a90033635ba3dcb9f92480a9ffd771 --- /dev/null +++ b/pkg/pdfcpu/model/attach.go @@ -0,0 +1,447 @@ +/* +Copyright 2021 The pdfcpu Authors. + +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 + + 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 model + +import ( + "bytes" + "fmt" + "io" + "sort" + "time" + + "github.com/pdfcpu/pdfcpu/pkg/filter" + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// Attachment is a Reader representing a PDF attachment. +type Attachment struct { + io.Reader // attachment data + ID string // id + FileName string // filename + Desc string // description + ModTime *time.Time // time of last modification (optional) +} + +func (a Attachment) String() string { + return fmt.Sprintf("Attachment: id:%s desc:%s modTime:%s", a.ID, a.Desc, a.ModTime) +} + +func decodeFileSpecStreamDict(sd *types.StreamDict, id string) error { + fpl := sd.FilterPipeline + + if fpl == nil { + sd.Content = sd.Raw + return nil + } + + // Ignore filter chains with length > 1 + if len(fpl) > 1 { + if log.DebugEnabled() { + log.Debug.Printf("decodedFileSpecStreamDict: ignore %s, more than 1 filter.\n", id) + } + return nil + } + + // Only FlateDecode supported. + if fpl[0].Name != filter.Flate { + if log.DebugEnabled() { + log.Debug.Printf("decodedFileSpecStreamDict: ignore %s, %s filter unsupported.\n", id, fpl[0].Name) + } + return nil + } + + // Decode streamDict for supported filters only. + return sd.Decode() +} + +func fileSpecStreamFileName(xRefTable *XRefTable, d types.Dict) (string, error) { + o, found := d.Find("UF") + if found { + return xRefTable.DereferenceStringOrHexLiteral(o, V10, nil) + } + + o, found = d.Find("F") + if found { + return xRefTable.DereferenceStringOrHexLiteral(o, V10, nil) + } + + return "", errors.New("fileSpecStream missing \"UF\",\"F\"") +} + +func fileSpecStreamDict(xRefTable *XRefTable, d types.Dict) (*types.StreamDict, error) { + // Entry EF is a dict holding a stream dict in entry F. + o, found := d.Find("EF") + if !found || o == nil { + return nil, nil + } + + d, err := xRefTable.DereferenceDict(o) + if err != nil || o == nil { + return nil, err + } + + // Entry F holds the embedded file's data. + o, found = d.Find("F") + if !found || o == nil { + return nil, nil + } + + sd, _, err := xRefTable.DereferenceStreamDict(o) + return sd, err +} + +// NewFileSpectDictForAttachment returns a fileSpecDict for a. +func (xRefTable *XRefTable) NewFileSpecDictForAttachment(a Attachment) (types.Dict, error) { + modTime := time.Now() + if a.ModTime != nil { + modTime = *a.ModTime + } + sd, err := xRefTable.NewEmbeddedStreamDict(a, modTime) + if err != nil { + return nil, err + } + + // TODO insert (escaped) reverse solidus before solidus between file name components. + + return xRefTable.NewFileSpecDict(a.ID, a.ID, a.Desc, *sd) +} + +func fileSpecStreamDictInfo(xRefTable *XRefTable, id string, o types.Object, decode bool) (*types.StreamDict, string, string, *time.Time, error) { + d, err := xRefTable.DereferenceDict(o) + if err != nil { + return nil, "", "", nil, err + } + + var desc string + o, found := d.Find("Desc") + if found { + desc, err = xRefTable.DereferenceStringOrHexLiteral(o, V10, nil) + if err != nil { + return nil, "", "", nil, err + } + } + + fileName, err := fileSpecStreamFileName(xRefTable, d) + if err != nil { + return nil, "", "", nil, err + } + + sd, err := fileSpecStreamDict(xRefTable, d) + if err != nil { + return nil, "", "", nil, err + } + + var modDate *time.Time + if d = sd.DictEntry("Params"); d != nil { + if s := d.StringEntry("ModDate"); s != nil { + dt, ok := types.DateTime(*s, xRefTable.ValidationMode == ValidationRelaxed) + if !ok { + return nil, desc, "", nil, errors.New("pdfcpu: invalid date ModDate") + } + modDate = &dt + } + } + + err = decodeFileSpecStreamDict(sd, id) + + return sd, desc, fileName, modDate, err +} + +// ListAttachments returns a slice of attachment stubs (attachment w/o data). +func (ctx *Context) ListAttachments() ([]Attachment, error) { + xRefTable := ctx.XRefTable + if !xRefTable.Valid { + if err := xRefTable.LocateNameTree("EmbeddedFiles", false); err != nil { + return nil, err + } + } + if xRefTable.Names["EmbeddedFiles"] == nil { + return nil, nil + } + + aa := []Attachment{} + + createAttachmentStub := func(xRefTable *XRefTable, id string, o *types.Object) error { + decode := false + _, desc, fileName, modTime, err := fileSpecStreamDictInfo(xRefTable, id, *o, decode) + if err != nil { + return err + } + aa = append(aa, Attachment{nil, id, fileName, desc, modTime}) + return nil + } + + // Extract stub info. + if err := ctx.Names["EmbeddedFiles"].Process(xRefTable, createAttachmentStub); err != nil { + return nil, err + } + + return aa, nil +} + +// AddAttachment adds a. +func (ctx *Context) AddAttachment(a Attachment, useCollection bool) error { + xRefTable := ctx.XRefTable + if err := xRefTable.LocateNameTree("EmbeddedFiles", true); err != nil { + return err + } + + if useCollection { + // Ensure a Collection entry in the catalog. + if err := xRefTable.EnsureCollection(); err != nil { + return err + } + } + + d, err := xRefTable.NewFileSpecDictForAttachment(a) + if err != nil { + return err + } + + ir, err := xRefTable.IndRefForNewObject(d) + if err != nil { + return err + } + + m := NameMap{a.ID: []types.Dict{d}} + + return xRefTable.Names["EmbeddedFiles"].Add(xRefTable, a.ID, *ir, m, []string{"F", "UF"}) +} + +var errContentMatch = errors.New("name tree content match") + +// SearchEmbeddedFilesNameTreeNodeByContent tries to identify a name tree by content. +func (ctx *Context) SearchEmbeddedFilesNameTreeNodeByContent(s string) (*string, types.Object, error) { + + var ( + k *string + v types.Object + ) + + identifyAttachmentStub := func(xRefTable *XRefTable, id string, o *types.Object) error { + decode := false + _, desc, fileName, _, err := fileSpecStreamDictInfo(xRefTable, id, *o, decode) + if err != nil { + return err + } + if s == fileName || s == desc { + k = &id + v = *o + return errContentMatch + } + return nil + } + + if err := ctx.Names["EmbeddedFiles"].Process(ctx.XRefTable, identifyAttachmentStub); err != nil { + if err != errContentMatch { + return nil, nil, err + } + // Node identified. + return k, v, nil + } + + return nil, nil, nil +} + +func (ctx *Context) removeAttachment(id string) (bool, error) { + if log.CLIEnabled() { + log.CLI.Printf("removing %s\n", id) + } + xRefTable := ctx.XRefTable + // EmbeddedFiles name tree containing at least one key value pair. + empty, ok, err := xRefTable.Names["EmbeddedFiles"].Remove(xRefTable, id) + if err != nil { + return false, err + } + if empty { + // Delete name tree root object. + if err := xRefTable.RemoveEmbeddedFilesNameTree(); err != nil { + return false, err + } + } + if !ok { + // Try to identify name tree node by content. + k, _, err := ctx.SearchEmbeddedFilesNameTreeNodeByContent(id) + if err != nil { + return false, err + } + if k == nil { + if log.CLIEnabled() { + log.CLI.Printf("attachment %s not found", id) + } + return false, nil + } + empty, _, err = xRefTable.Names["EmbeddedFiles"].Remove(xRefTable, *k) + if err != nil { + return false, err + } + if empty { + // Delete name tree root object. + if err := xRefTable.RemoveEmbeddedFilesNameTree(); err != nil { + return false, err + } + } + } + return true, nil +} + +// RemoveAttachments removes attachments with given id and returns true if anything removed. +func (ctx *Context) RemoveAttachments(ids []string) (bool, error) { + // Note: Any remove operation may be deleting the only key value pair of this name tree. + xRefTable := ctx.XRefTable + if !xRefTable.Valid { + if err := xRefTable.LocateNameTree("EmbeddedFiles", false); err != nil { + return false, err + } + } + if xRefTable.Names["EmbeddedFiles"] == nil { + return false, errors.Errorf("no attachments available.") + } + + if len(ids) == 0 { + // Remove all attachments - delete name tree root object. + if log.CLIEnabled() { + log.CLI.Println("removing all attachments") + } + if err := xRefTable.RemoveEmbeddedFilesNameTree(); err != nil { + return false, err + } + return true, nil + } + + for _, id := range ids { + found, err := ctx.removeAttachment(id) + if err != nil { + return false, err + } + if !found { + return false, nil + } + } + + return true, nil +} + +// RemoveAttachment removes a and returns true on success. +func (ctx *Context) RemoveAttachment(a Attachment) (bool, error) { + return ctx.RemoveAttachments([]string{a.ID}) +} + +// ExtractAttachments extracts attachments with id. +func (ctx *Context) ExtractAttachments(ids []string) ([]Attachment, error) { + xRefTable := ctx.XRefTable + if !xRefTable.Valid { + if err := xRefTable.LocateNameTree("EmbeddedFiles", false); err != nil { + return nil, err + } + } + if xRefTable.Names["EmbeddedFiles"] == nil { + return nil, errors.Errorf("no attachments available.") + } + + aa := []Attachment{} + + createAttachment := func(xRefTable *XRefTable, id string, o *types.Object) error { + decode := true + sd, desc, fileName, modTime, err := fileSpecStreamDictInfo(xRefTable, id, *o, decode) + if err != nil { + return err + } + a := Attachment{Reader: bytes.NewReader(sd.Content), ID: id, FileName: fileName, Desc: desc, ModTime: modTime} + aa = append(aa, a) + return nil + } + + // Search with UF,F,Desc + if len(ids) > 0 { + for _, id := range ids { + v, ok := ctx.Names["EmbeddedFiles"].Value(id) + if !ok { + // Try to identify name tree node by content. + k, o, err := ctx.SearchEmbeddedFilesNameTreeNodeByContent(id) + if err != nil { + return nil, err + } + if k == nil { + if log.CLIEnabled() { + log.CLI.Printf("attachment %s not found", id) + } + if log.InfoEnabled() { + log.Info.Printf("pdfcpu: extractAttachments: %s not found", id) + } + continue + } + v = o + } + if err := createAttachment(ctx.XRefTable, id, &v); err != nil { + return nil, err + } + } + return aa, nil + } + + // Extract all files. + if err := ctx.Names["EmbeddedFiles"].Process(ctx.XRefTable, createAttachment); err != nil { + return nil, err + } + + return aa, nil +} + +// ExtractAttachment extracts a fully populated attachment. +func (ctx *Context) ExtractAttachment(a Attachment) (*Attachment, error) { + aa, err := ctx.ExtractAttachments([]string{a.ID}) + if err != nil || len(aa) == 0 { + return nil, err + } + if len(aa) > 1 { + return nil, errors.Errorf("pdfcpu: unexpected number of attachments: %d", len(aa)) + } + return &aa[0], nil +} + +func (ctx *Context) AddAttachmentsToInfoDigest(ss *[]string) error { + aa, err := ctx.ListAttachments() + if err != nil { + return err + } + if len(aa) == 0 { + return nil + } + + var list []string + for _, a := range aa { + s := a.FileName + if a.Desc != "" { + s = fmt.Sprintf("%s (%s)", s, a.Desc) + } + list = append(list, s) + } + sort.Strings(list) + + for i, s := range list { + if i == 0 { + *ss = append(*ss, fmt.Sprintf("%20s: %s", "Attachments", s)) + continue + } + *ss = append(*ss, fmt.Sprintf("%20s %s,", "", s)) + } + + return nil +} diff --git a/pkg/pdfcpu/model/booklet.go b/pkg/pdfcpu/model/booklet.go new file mode 100644 index 0000000000000000000000000000000000000000..9d428f1381704eabcb82300a7d6f2befa7105070 --- /dev/null +++ b/pkg/pdfcpu/model/booklet.go @@ -0,0 +1,224 @@ +/* + Copyright 2020 The pdfcpu Authors. + + 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 + + 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 model + +import ( + "fmt" + + "io" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +type BookletType int + +// These are the types of booklet layouts. +const ( + Booklet BookletType = iota + BookletAdvanced + BookletPerfectBound +) + +func (b BookletType) String() string { + switch b { + case Booklet: + return "booklet" + case BookletAdvanced: + return "booklet advanced" + case BookletPerfectBound: + return "booklet perfect bound" + } + return "" +} + +type BookletBinding int + +const ( + LongEdge BookletBinding = iota + ShortEdge +) + +func (b BookletBinding) String() string { + switch b { + case ShortEdge: + return "short-edge" + case LongEdge: + return "long-edge" + } + return "" +} + +type BookletPage struct { + Number int + Rotate bool +} + +func drawGuideLineLabel(w io.Writer, x, y float64, s string, mb *types.Rectangle, fm FontMap, rot int) { + fontName := "Helvetica" + td := TextDescriptor{ + FontName: fontName, + FontKey: fm.EnsureKey(fontName), + FontSize: 9, + Scale: 1.0, + ScaleAbs: true, + StrokeCol: color.Black, + FillCol: color.Black, + X: x, + Y: y, + Rotation: float64(rot), + Text: s, + } + WriteMultiLine(nil, w, mb, nil, td) +} + +func drawScissors(w io.Writer, isVerticalCut bool, horzCutYpos float64, mb *types.Rectangle, fm FontMap) { + x := 0. + y := horzCutYpos - 4 + rot := 0. + if isVerticalCut { + // TODO: if we ever have multiple vertical cuts, would need to change this. + x = mb.Width()/2 - 12 + y = 12 + rot = 90 + } + fontName := "ZapfDingbats" + td := TextDescriptor{ + FontName: fontName, + FontKey: fm.EnsureKey(fontName), + FontSize: 12, + Scale: 1.0, + ScaleAbs: true, + StrokeCol: color.Black, + FillCol: color.Black, + X: x, + Y: y, + Rotation: rot, + Text: string([]byte{byte(34)}), + } + WriteMultiLine(nil, w, mb, nil, td) +} + +type cutOrFold int + +const ( + none cutOrFold = iota + cut + fold +) + +func (c cutOrFold) String(nup *NUp) string { + if c == cut { + if nup.BookletType == BookletAdvanced { + return "Fold & Cut here" + } + return "Cut here" + } + if c == fold { + return "Fold here" + } + return "" +} + +func getCutFolds(nup *NUp) (horizontal cutOrFold, vertical cutOrFold) { + var getCutOrFold = func(nup *NUp) (cutOrFold, cutOrFold) { + switch nup.N() { + case 2: + return fold, none + case 4: + if nup.BookletBinding == LongEdge { + return cut, fold + } else { + return fold, cut + } + case 6: + // Really, it has two horizontal cuts. + return cut, fold + case 8: + // Also has a horizontal cut in the center. + return fold, cut + } + return none, none + } + horizontal, vertical = getCutOrFold(nup) + if nup.BookletType == BookletPerfectBound { + // All folds turn into cuts for perfect binding. + if horizontal == fold { + horizontal = cut + } + if vertical == fold { + vertical = cut + } + } + if nup.N() == 4 && nup.PageDim.Landscape() { + // The logic above is for a portrait sheet, so swap the outputs. + return vertical, horizontal + } + return horizontal, vertical +} + +func drawGuideHorizontal(w io.Writer, y, width float64, cutOrFold cutOrFold, nup *NUp, mb *types.Rectangle, fm FontMap) { + fmt.Fprint(w, "[3] 0 d ") + draw.SetLineWidth(w, 0) + draw.SetStrokeColor(w, color.Gray) + draw.DrawLineSimple(w, 0, y, width, y) + drawGuideLineLabel(w, width-46, y+2, cutOrFold.String(nup), mb, fm, 0) + if cutOrFold == cut { + drawScissors(w, false, y, mb, fm) + } +} + +func drawGuideVertical(w io.Writer, x, height float64, cutOrFold cutOrFold, nup *NUp, mb *types.Rectangle, fm FontMap) { + fmt.Fprint(w, "[3] 0 d ") + draw.SetLineWidth(w, 0) + draw.SetStrokeColor(w, color.Gray) + draw.DrawLineSimple(w, x, 0, x, height) + drawGuideLineLabel(w, x-23, height-32, cutOrFold.String(nup), mb, fm, 90) + if cutOrFold == cut { + drawScissors(w, true, height/2, mb, fm) + } +} + +// DrawBookletGuides draws guides according to corresponding nup value. +func DrawBookletGuides(nup *NUp, w io.Writer) FontMap { + width := nup.PageDim.Width + height := nup.PageDim.Height + var fm FontMap = FontMap{} + mb := types.RectForDim(width, height) + + horz, vert := getCutFolds(nup) + if horz != none { + switch nup.N() { + case 2, 4: + drawGuideHorizontal(w, height/2, width, horz, nup, mb, fm) + case 6: + // 6up: two cuts + drawGuideHorizontal(w, height*1/3, width, horz, nup, mb, fm) + drawGuideHorizontal(w, height*2/3, width, horz, nup, mb, fm) + case 8: + // 8up: middle cut and 1/4,3/4 folds + drawGuideHorizontal(w, height/2, width, cut, nup, mb, fm) + drawGuideHorizontal(w, height*1/4, width, fold, nup, mb, fm) + drawGuideHorizontal(w, height*3/4, width, fold, nup, mb, fm) + } + } + if vert != none { + drawGuideVertical(w, width/2, height, vert, nup, mb, fm) + } + return fm +} diff --git a/pkg/pdfcpu/model/box.go b/pkg/pdfcpu/model/box.go new file mode 100644 index 0000000000000000000000000000000000000000..1e0d042ff9a47067963c538f18fbfd8374b689d9 --- /dev/null +++ b/pkg/pdfcpu/model/box.go @@ -0,0 +1,1257 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 model + +import ( + "fmt" + "strconv" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// Box is a rectangular region in user space +// expressed either explicitly via Rect +// or implicitly via margins applied to the containing parent box. +// Media box serves as parent box for crop box. +// Crop box serves as parent box for trim, bleed and art box. +type Box struct { + Rect *types.Rectangle `json:"rect"` // Rectangle in user space. + Inherited bool `json:"-"` // Media box and Crop box may be inherited. + RefBox string `json:"-"` // Use position of another box, + // Margins to parent box in points. + // Relative to parent box if 0 < x < 0.5 + MLeft, MRight float64 `json:"-"` + MTop, MBot float64 `json:"-"` + // Relative position within parent box + Dim *types.Dim `json:"-"` // dimensions + Pos types.Anchor `json:"-"` // position anchor within parent box, one of tl,tc,tr,l,c,r,bl,bc,br. + Dx, Dy int `json:"-"` // anchor offset +} + +// PageBoundaries represent the defined PDF page boundaries. +type PageBoundaries struct { + Media *Box `json:"mediaBox,omitempty"` + Crop *Box `json:"cropBox,omitempty"` + Trim *Box `json:"trimBox,omitempty"` + Bleed *Box `json:"bleedBox,omitempty"` + Art *Box `json:"artBox,omitempty"` + Rot int `json:"rot"` // The effective page rotation. + Orientation string `json:"orient"` +} + +// SelectAll selects all page boundaries. +func (pb *PageBoundaries) SelectAll() { + b := &Box{} + pb.Media, pb.Crop, pb.Trim, pb.Bleed, pb.Art = b, b, b, b, b +} + +func (pb PageBoundaries) String() string { + ss := []string{} + if pb.Media != nil { + ss = append(ss, "mediaBox") + } + if pb.Crop != nil { + ss = append(ss, "cropBox") + } + if pb.Trim != nil { + ss = append(ss, "trimBox") + } + if pb.Bleed != nil { + ss = append(ss, "bleedBox") + } + if pb.Art != nil { + ss = append(ss, "artBox") + } + return strings.Join(ss, ", ") +} + +// MediaBox returns the effective mediabox for pb. +func (pb PageBoundaries) MediaBox() *types.Rectangle { + if pb.Media == nil { + return nil + } + return pb.Media.Rect +} + +// CropBox returns the effective cropbox for pb. +func (pb PageBoundaries) CropBox() *types.Rectangle { + if pb.Crop == nil || pb.Crop.Rect == nil { + return pb.MediaBox() + } + return pb.Crop.Rect +} + +// TrimBox returns the effective trimbox for pb. +func (pb PageBoundaries) TrimBox() *types.Rectangle { + if pb.Trim == nil || pb.Trim.Rect == nil { + return pb.CropBox() + } + return pb.Trim.Rect +} + +// BleedBox returns the effective bleedbox for pb. +func (pb PageBoundaries) BleedBox() *types.Rectangle { + if pb.Bleed == nil || pb.Bleed.Rect == nil { + return pb.CropBox() + } + return pb.Bleed.Rect +} + +// ArtBox returns the effective artbox for pb. +func (pb PageBoundaries) ArtBox() *types.Rectangle { + if pb.Art == nil || pb.Art.Rect == nil { + return pb.CropBox() + } + return pb.Art.Rect +} + +// ResolveBox resolves s and tries to assign an empty page boundary. +func (pb *PageBoundaries) ResolveBox(s string) error { + for _, k := range []string{"media", "crop", "trim", "bleed", "art"} { + b := &Box{} + if strings.HasPrefix(k, s) { + switch k { + case "media": + pb.Media = b + case "crop": + pb.Crop = b + case "trim": + pb.Trim = b + case "bleed": + pb.Bleed = b + case "art": + pb.Art = b + } + return nil + } + } + return errors.Errorf("pdfcpu: invalid box prefix: %s", s) +} + +// ParseBoxList parses a list of box +func ParseBoxList(s string) (*PageBoundaries, error) { + // A comma separated, unsorted list of values: + // + // m(edia), c(rop), t(rim), b(leed), a(rt) + + s = strings.TrimSpace(s) + if len(s) == 0 { + return nil, nil + } + pb := &PageBoundaries{} + for _, s := range strings.Split(s, ",") { + if err := pb.ResolveBox(strings.TrimSpace(s)); err != nil { + return nil, err + } + } + return pb, nil +} + +func resolveBoxType(s string) (string, error) { + for _, k := range []string{"media", "crop", "trim", "bleed", "art"} { + if strings.HasPrefix(k, s) { + return k, nil + } + } + return "", errors.Errorf("pdfcpu: invalid box type: %s", s) +} + +func processBox(b **Box, boxID, paramValueStr string, unit types.DisplayUnit) error { + var err error + if *b != nil { + return errors.Errorf("pdfcpu: duplicate box definition: %s", boxID) + } + // process box assignment + boxVal, err := resolveBoxType(paramValueStr) + if err == nil { + if boxVal == boxID { + return errors.Errorf("pdfcpu: invalid box self assigment: %s", boxID) + } + *b = &Box{RefBox: boxVal} + return nil + } + // process box definition + *b, err = ParseBox(paramValueStr, unit) + return err +} + +// ParsePageBoundaries parses a list of box definitions and assignments. +func ParsePageBoundaries(s string, unit types.DisplayUnit) (*PageBoundaries, error) { + // A sequence of box definitions/assignments: + // + // m(edia): {box} + // c(rop): {box} + // a(rt): {box} | b(leed) | c(rop) | m(edia) | t(rim) + // b(leed): {box} | a(rt) | c(rop) | m(edia) | t(rim) + // t(rim): {box} | a(rt) | b(leed) | c(rop) | m(edia) + + s = strings.TrimSpace(s) + if len(s) == 0 { + return nil, errors.New("pdfcpu: missing page boundaries in the form of box definitions/assignments") + } + pb := &PageBoundaries{} + for _, s := range strings.Split(s, ",") { + + s1 := strings.Split(s, ":") + if len(s1) != 2 { + return nil, errors.New("pdfcpu: invalid box assignment") + } + + paramPrefix := strings.TrimSpace(s1[0]) + paramValueStr := strings.TrimSpace(s1[1]) + + boxKey, err := resolveBoxType(paramPrefix) + if err != nil { + return nil, errors.New("pdfcpu: invalid box type") + } + + // process box definition + switch boxKey { + case "media": + if pb.Media != nil { + return nil, errors.New("pdfcpu: duplicate box definition: media") + } + // process media box definition + pb.Media, err = ParseBox(paramValueStr, unit) + + case "crop": + if pb.Crop != nil { + return nil, errors.New("pdfcpu: duplicate box definition: crop") + } + // process crop box definition + pb.Crop, err = ParseBox(paramValueStr, unit) + + case "trim": + err = processBox(&pb.Trim, "trim", paramValueStr, unit) + + case "bleed": + err = processBox(&pb.Bleed, "bleed", paramValueStr, unit) + + case "art": + err = processBox(&pb.Art, "art", paramValueStr, unit) + + } + + if err != nil { + return nil, err + } + } + return pb, nil +} + +func parseBoxByRectangle(s string, u types.DisplayUnit) (*Box, error) { + ss := strings.Fields(s) + if len(ss) != 4 { + return nil, errors.Errorf("pdfcpu: invalid box definition: %s", s) + } + f, err := strconv.ParseFloat(ss[0], 64) + if err != nil { + return nil, err + } + xmin := types.ToUserSpace(f, u) + + f, err = strconv.ParseFloat(ss[1], 64) + if err != nil { + return nil, err + } + ymin := types.ToUserSpace(f, u) + + f, err = strconv.ParseFloat(ss[2], 64) + if err != nil { + return nil, err + } + xmax := types.ToUserSpace(f, u) + + f, err = strconv.ParseFloat(ss[3], 64) + if err != nil { + return nil, err + } + ymax := types.ToUserSpace(f, u) + + if xmax < xmin { + xmin, xmax = xmax, xmin + } + + if ymax < ymin { + ymin, ymax = ymax, ymin + } + + return &Box{Rect: types.NewRectangle(xmin, ymin, xmax, ymax)}, nil +} + +func parseBoxPercentage(s string) (float64, error) { + pct, err := strconv.ParseFloat(s, 64) + if err != nil { + return 0, err + } + if pct <= -50 || pct >= 50 { + return 0, errors.Errorf("pdfcpu: invalid margin percentage: %s must be < 50%%", s) + } + return pct / 100, nil +} + +func parseBoxBySingleMarginVal(s, s1 string, abs bool, u types.DisplayUnit) (*Box, error) { + if s1[len(s1)-1] == '%' { + // margin percentage + // 10.5% + // % has higher precedence than abs/rel. + s1 = s1[:len(s1)-1] + if len(s1) == 0 { + return nil, errors.Errorf("pdfcpu: invalid box definition: %s", s) + } + m, err := parseBoxPercentage(s1) + if err != nil { + return nil, err + } + return &Box{MLeft: m, MRight: m, MTop: m, MBot: m}, nil + } + m, err := strconv.ParseFloat(s1, 64) + if err != nil { + return nil, err + } + if !abs { + // 0.25 rel (=25%) + if m <= 0 || m >= .5 { + return nil, errors.Errorf("pdfcpu: invalid relative box margin: %f must be positive < 0.5", m) + } + return &Box{MLeft: m, MRight: m, MTop: m, MBot: m}, nil + } + // 10 + // 10 abs + // .5 + // .5 abs + m = types.ToUserSpace(m, u) + return &Box{MLeft: m, MRight: m, MTop: m, MBot: m}, nil +} + +func parseBoxBy2Percentages(s, s1, s2 string) (*Box, error) { + // 10% 40% + // Parse vert margin. + s1 = s1[:len(s1)-1] + if len(s1) == 0 { + return nil, errors.Errorf("pdfcpu: invalid box definition: %s", s) + } + vm, err := parseBoxPercentage(s1) + if err != nil { + return nil, err + } + + if s2[len(s2)-1] != '%' { + return nil, errors.Errorf("pdfcpu: invalid box definition: %s", s) + } + // Parse hor margin. + s2 = s2[:len(s2)-1] + if len(s2) == 0 { + return nil, errors.Errorf("pdfcpu: invalid box definition: %s", s) + } + hm, err := parseBoxPercentage(s2) + if err != nil { + return nil, err + } + return &Box{MLeft: hm, MRight: hm, MTop: vm, MBot: vm}, nil +} + +func parseBoxBy2MarginVals(s, s1, s2 string, abs bool, u types.DisplayUnit) (*Box, error) { + if s1[len(s1)-1] == '%' { + return parseBoxBy2Percentages(s, s1, s2) + } + + // 10 5 + // 10 5 abs + // .1 .5 + // .1 .5 abs + // .1 .4 rel + vm, err := strconv.ParseFloat(s1, 64) + if err != nil { + return nil, err + } + if !abs { + // eg 0.25 rel (=25%) + if vm <= 0 || vm >= .5 { + return nil, errors.Errorf("pdfcpu: invalid relative vertical box margin: %f must be positive < 0.5", vm) + } + } + hm, err := strconv.ParseFloat(s2, 64) + if err != nil { + return nil, err + } + if !abs { + // eg 0.25 rel (=25%) + if hm <= 0 || hm >= .5 { + return nil, errors.Errorf("pdfcpu: invalid relative horizontal box margin: %f must be positive < 0.5", hm) + } + } + if abs { + vm = types.ToUserSpace(vm, u) + hm = types.ToUserSpace(hm, u) + } + return &Box{MLeft: hm, MRight: hm, MTop: vm, MBot: vm}, nil +} + +func parseBoxBy3Percentages(s, s1, s2, s3 string) (*Box, error) { + // 10% 15.5% 10% + // Parse top margin. + s1 = s1[:len(s1)-1] + if len(s1) == 0 { + return nil, errors.Errorf("pdfcpu: invalid box definition: %s", s) + } + pct, err := strconv.ParseFloat(s1, 64) + if err != nil { + return nil, err + } + tm := pct / 100 + + if s2[len(s2)-1] != '%' { + return nil, errors.Errorf("pdfcpu: invalid box definition: %s", s) + } + // Parse hor margin. + s2 = s2[:len(s2)-1] + if len(s2) == 0 { + return nil, errors.Errorf("pdfcpu: invalid box definition: %s", s) + } + hm, err := parseBoxPercentage(s2) + if err != nil { + return nil, err + } + + if s3[len(s3)-1] != '%' { + return nil, errors.Errorf("pdfcpu: invalid box definition: %s", s) + } + // Parse bottom margin. + s3 = s3[:len(s3)-1] + if len(s3) == 0 { + return nil, errors.Errorf("pdfcpu: invalid box definition: %s", s) + } + pct, err = strconv.ParseFloat(s3, 64) + if err != nil { + return nil, err + } + bm := pct / 100 + if tm+bm >= 1 { + return nil, errors.Errorf("pdfcpu: vertical margin overflow: %s", s) + } + + return &Box{MLeft: hm, MRight: hm, MTop: tm, MBot: bm}, nil +} + +func parseBoxBy3MarginVals(s, s1, s2, s3 string, abs bool, u types.DisplayUnit) (*Box, error) { + if s1[len(s1)-1] == '%' { + return parseBoxBy3Percentages(s, s1, s2, s3) + } + + // 10 5 15 ... absolute, top:10 left,right:5 bottom:15 + // 10 5 15 abs ... absolute, top:10 left,right:5 bottom:15 + // .1 .155 .1 ... absolute, top:.1 left,right:.155 bottom:.1 + // .1 .155 .1 abs ... absolute, top:.1 left,right:.155 bottom:.1 + // .1 .155 .1 rel ... relative, top:.1 left,right:.155 bottom:.1 + tm, err := strconv.ParseFloat(s1, 64) + if err != nil { + return nil, err + } + + hm, err := strconv.ParseFloat(s2, 64) + if err != nil { + return nil, err + } + if !abs { + // eg 0.25 rel (=25%) + if hm <= 0 || hm >= .5 { + return nil, errors.Errorf("pdfcpu: invalid relative horizontal box margin: %f must be positive < 0.5", hm) + } + } + + bm, err := strconv.ParseFloat(s3, 64) + if err != nil { + return nil, err + } + if !abs && (tm+bm >= 1) { + return nil, errors.Errorf("pdfcpu: vertical margin overflow: %s", s) + } + + if abs { + tm = types.ToUserSpace(tm, u) + hm = types.ToUserSpace(hm, u) + bm = types.ToUserSpace(bm, u) + } + return &Box{MLeft: hm, MRight: hm, MTop: tm, MBot: bm}, nil +} + +func parseBoxBy4Percentages(s, s1, s2, s3, s4 string) (*Box, error) { + // 10% 15% 15% 10% + // Parse top margin. + s1 = s1[:len(s1)-1] + if len(s1) == 0 { + return nil, errors.Errorf("pdfcpu: invalid box definition: %s", s) + } + pct, err := strconv.ParseFloat(s1, 64) + if err != nil { + return nil, err + } + tm := pct / 100 + + // Parse right margin. + if s2[len(s2)-1] != '%' { + return nil, errors.Errorf("pdfcpu: invalid box definition: %s", s) + } + s2 = s2[:len(s2)-1] + if len(s2) == 0 { + return nil, errors.Errorf("pdfcpu: invalid box definition: %s", s) + } + pct, err = strconv.ParseFloat(s1, 64) + if err != nil { + return nil, err + } + rm := pct / 100 + + // Parse bottom margin. + if s3[len(s3)-1] != '%' { + return nil, errors.Errorf("pdfcpu: invalid box definition: %s", s) + } + s3 = s3[:len(s3)-1] + if len(s3) == 0 { + return nil, errors.Errorf("pdfcpu: invalid box definition: %s", s) + } + pct, err = strconv.ParseFloat(s3, 64) + if err != nil { + return nil, err + } + bm := pct / 100 + + // Parse left margin. + if s4[len(s4)-1] != '%' { + return nil, errors.Errorf("pdfcpu: invalid box definition: %s", s) + } + s4 = s4[:len(s4)-1] + if len(s4) == 0 { + return nil, errors.Errorf("pdfcpu: invalid box definition: %s", s) + } + pct, err = strconv.ParseFloat(s3, 64) + if err != nil { + return nil, err + } + lm := pct / 100 + + if tm+bm >= 1 { + return nil, errors.Errorf("pdfcpu: vertical margin overflow: %s", s) + } + if rm+lm >= 1 { + return nil, errors.Errorf("pdfcpu: horizontal margin overflow: %s", s) + } + + return &Box{MLeft: lm, MRight: rm, MTop: tm, MBot: bm}, nil +} + +func parseBoxBy4MarginVals(s, s1, s2, s3, s4 string, abs bool, u types.DisplayUnit) (*Box, error) { + if s1[len(s1)-1] == '%' { + return parseBoxBy4Percentages(s, s1, s2, s3, s4) + } + + // 0.4 0.4 20 20 ... absolute, top:.4 right:.4 bottom:20 left:20 + // 0.4 0.4 .1 .1 ... absolute, top:.4 right:.4 bottom:.1 left:.1 + // 0.4 0.4 .1 .1 abs ... absolute, top:.4 right:.4 bottom:.1 left:.1 + // 0.4 0.4 .1 .1 rel ... relative, top:.4 right:.4 bottom:.1 left:.1 + + // Parse top margin. + tm, err := strconv.ParseFloat(s1, 64) + if err != nil { + return nil, err + } + + // Parse right margin. + rm, err := strconv.ParseFloat(s2, 64) + if err != nil { + return nil, err + } + + // Parse bottom margin. + bm, err := strconv.ParseFloat(s3, 64) + if err != nil { + return nil, err + } + + // Parse left margin. + lm, err := strconv.ParseFloat(s4, 64) + if err != nil { + return nil, err + } + if !abs { + if tm+bm >= 1 { + return nil, errors.Errorf("pdfcpu: vertical margin overflow: %s", s) + } + if lm+rm >= 1 { + return nil, errors.Errorf("pdfcpu: horizontal margin overflow: %s", s) + } + } + + if abs { + tm = types.ToUserSpace(tm, u) + rm = types.ToUserSpace(rm, u) + bm = types.ToUserSpace(bm, u) + lm = types.ToUserSpace(lm, u) + } + return &Box{MLeft: lm, MRight: rm, MTop: tm, MBot: bm}, nil +} + +func parseBoxOffset(s string, b *Box, u types.DisplayUnit) error { + d := strings.Split(s, " ") + if len(d) != 2 { + return errors.Errorf("pdfcpu: illegal position offset string: need 2 numeric values, %s\n", s) + } + + f, err := strconv.ParseFloat(d[0], 64) + if err != nil { + return err + } + b.Dx = int(types.ToUserSpace(f, u)) + + f, err = strconv.ParseFloat(d[1], 64) + if err != nil { + return err + } + b.Dy = int(types.ToUserSpace(f, u)) + + return nil +} + +func parseBoxDimByPercentage(s, s1, s2 string, b *Box) error { + // 10% 40% + // Parse width. + s1 = s1[:len(s1)-1] + if len(s1) == 0 { + return errors.Errorf("pdfcpu: invalid box definition: %s", s) + } + pct, err := strconv.ParseFloat(s1, 64) + if err != nil { + return err + } + if pct <= 0 || pct > 100 { + return errors.Errorf("pdfcpu: invalid percentage: %s", s) + } + w := pct / 100 + + if s2[len(s2)-1] != '%' { + return errors.Errorf("pdfcpu: invalid box definition: %s", s) + } + // Parse height. + s2 = s2[:len(s2)-1] + if len(s2) == 0 { + return errors.Errorf("pdfcpu: invalid box definition: %s", s) + } + pct, err = strconv.ParseFloat(s2, 64) + if err != nil { + return err + } + if pct <= 0 || pct > 100 { + return errors.Errorf("pdfcpu: invalid percentage: %s", s) + } + h := pct / 100 + b.Dim = &types.Dim{Width: w, Height: h} + return nil +} + +func parseBoxDimWidthAndHeight(s1, s2 string, abs bool) (float64, float64, error) { + var ( + w, h float64 + err error + ) + + w, err = strconv.ParseFloat(s1, 64) + if err != nil { + return w, h, err + } + if !abs { + // eg 0.25 rel (=25%) + if w <= 0 || w > 1 { + return w, h, errors.Errorf("pdfcpu: invalid relative box width: %f must be positive <= 1", w) + } + } + + h, err = strconv.ParseFloat(s2, 64) + if err != nil { + return w, h, err + } + if !abs { + // eg 0.25 rel (=25%) + if h <= 0 || h > 1 { + return w, h, errors.Errorf("pdfcpu: invalid relative box height: %f must be positive <= 1", h) + } + } + + return w, h, nil +} + +func parseBoxDim(s string, b *Box, u types.DisplayUnit) error { + ss := strings.Fields(s) + if len(ss) != 2 && len(ss) != 3 { + return errors.Errorf("pdfcpu: illegal dimension string: need 2 positive numeric values, %s\n", s) + } + abs := true + if len(ss) == 3 { + s1 := ss[2] + if s1 != "rel" && s1 != "abs" { + return errors.New("pdfcpu: illegal dimension string") + } + abs = s1 == "abs" + } + + s1, s2 := ss[0], ss[1] + if s1[len(s1)-1] == '%' { + return parseBoxDimByPercentage(s, s1, s2, b) + } + + w, h, err := parseBoxDimWidthAndHeight(s1, s2, abs) + if err != nil { + return err + } + + if abs { + w = types.ToUserSpace(w, u) + h = types.ToUserSpace(h, u) + } + b.Dim = &types.Dim{Width: w, Height: h} + return nil +} + +func parseBoxByPosWithinParent(ss []string, u types.DisplayUnit) (*Box, error) { + b := &Box{Pos: types.Center} + for _, s := range ss { + + ss1 := strings.Split(s, ":") + if len(ss1) != 2 { + return nil, errors.Errorf("pdfcpu: invalid box definition: %s", s) + } + + paramPrefix := strings.TrimSpace(ss1[0]) + paramValueStr := strings.TrimSpace(ss1[1]) + + switch paramPrefix { + case "dim": + if err := parseBoxDim(paramValueStr, b, u); err != nil { + return nil, err + } + + case "pos": + a, err := types.ParsePositionAnchor(paramValueStr) + if err != nil { + return nil, err + } + b.Pos = a + + case "off": + if err := parseBoxOffset(paramValueStr, b, u); err != nil { + return nil, err + } + + default: + return nil, errors.Errorf("pdfcpu: invalid box definition: %s", s) + } + } + if b.Dim == nil { + return nil, errors.New("pdfcpu: missing box definition attr dim") + } + return b, nil +} + +func parseBoxByMarginVals(ss []string, s string, abs bool, u types.DisplayUnit) (*Box, error) { + switch len(ss) { + case 1: + return parseBoxBySingleMarginVal(s, ss[0], abs, u) + case 2: + return parseBoxBy2MarginVals(s, ss[0], ss[1], abs, u) + case 3: + return parseBoxBy3MarginVals(s, ss[0], ss[1], ss[2], abs, u) + case 4: + return parseBoxBy4MarginVals(s, ss[0], ss[1], ss[2], ss[3], abs, u) + case 5: + return nil, errors.Errorf("pdfcpu: invalid box definition: %s", s) + } + return nil, nil +} + +// ParseBox parses a box definition. +func ParseBox(s string, u types.DisplayUnit) (*Box, error) { + // A rectangular region in userspace expressed in terms of + // a rectangle or margins relative to its parent box. + // Media box serves as parent/default for crop box. + // Crop box serves as parent/default for trim, bleed and art box: + + // [0 10 200 150] ... rectangle + + // 0.5 0.5 20 20 ... absolute, top:.5 right:.5 bottom:20 left:20 + // 0.5 0.5 .1 .1 abs ... absolute, top:.5 right:.5 bottom:.1 left:.1 + // 0.5 0.5 .1 .1 rel ... relative, top:.5 right:.5 bottom:20 left:20 + // 10 ... absolute, top,right,bottom,left:10 + // 10 5 ... absolute, top,bottom:10 left,right:5 + // 10 5 15 ... absolute, top:10 left,right:5 bottom:15 + // 5% <50% ... relative, top,right,bottom,left:5% of parent box width/height + // .1 .5 ... absolute, top,bottom:.1 left,right:.5 + // .1 .3 rel ... relative, top,bottom:.1=10% left,right:.3=30% + // -10 ... absolute, top,right,bottom,left enlarging the parent box as well + + // dim:30 30 ... 30 x 30 display units, anchored at center of parent box + // dim:30 30 abs ... 30 x 30 display units, anchored at center of parent box + // dim:.3 .3 rel ... 0.3 x 0.3 relative width/height of parent box, anchored at center of parent box + // dim:30% 30% ... 0.3 x 0.3 relative width/height of parent box, anchored at center of parent box + // pos:tl, dim:30 30 ... 0.3 x 0.3 relative width/height of parent box, anchored at top left corner of parent box + // pos:bl, off: 5 5, dim:30 30 ...30 x 30 display units with offset 5/5, anchored at bottom left corner of parent box + // pos:bl, off: -5 -5, dim:.3 .3 rel ...0.3 x 0.3 relative width/height and anchored at bottom left corner of parent box + + s = strings.TrimSpace(s) + if len(s) == 0 { + return nil, nil + } + + if s[0] == '[' && s[len(s)-1] == ']' { + // Rectangle in PDF Array notation. + return parseBoxByRectangle(s[1:len(s)-1], u) + } + + // Via relative position within parent box. + ss := strings.Split(s, ",") + if len(ss) > 3 { + return nil, errors.Errorf("pdfcpu: invalid box definition: %s", s) + } + if len(ss) > 1 || strings.HasPrefix(ss[0], "dim") { + return parseBoxByPosWithinParent(ss, u) + } + + // Via margins relative to parent box. + ss = strings.Fields(s) + if len(ss) > 5 { + return nil, errors.Errorf("pdfcpu: invalid box definition: %s", s) + } + if len(ss) == 1 && (ss[0] == "abs" || ss[0] == "rel") { + return nil, errors.Errorf("pdfcpu: invalid box definition: %s", s) + } + + abs := true + l := len(ss) - 1 + s1 := ss[l] + if s1 == "rel" || s1 == "abs" { + abs = s1 == "abs" + ss = ss[:l] + } + + return parseBoxByMarginVals(ss, s, abs, u) +} + +func (ctx *Context) addPageBoundaryString(i int, pb PageBoundaries, wantPB *PageBoundaries) []string { + unit := ctx.UnitString() + ss := []string{} + d := pb.CropBox().Dimensions() + if pb.Rot%180 != 0 { + d.Width, d.Height = d.Height, d.Width + } + or := "portrait" + if d.Landscape() { + or = "landscape" + } + + s := fmt.Sprintf("rot=%+d orientation:%s", pb.Rot, or) + ss = append(ss, fmt.Sprintf("Page %d: %s", i+1, s)) + if wantPB.Media != nil { + s := "" + if pb.Media.Inherited { + s = "(inherited)" + } + ss = append(ss, fmt.Sprintf(" MediaBox (%s) %v %s", unit, pb.MediaBox().Format(ctx.Unit), s)) + } + if wantPB.Crop != nil { + s := "" + if pb.Crop == nil { + s = "(default)" + } else if pb.Crop.Inherited { + s = "(inherited)" + } + ss = append(ss, fmt.Sprintf(" CropBox (%s) %v %s", unit, pb.CropBox().Format(ctx.Unit), s)) + } + if wantPB.Trim != nil { + s := "" + if pb.Trim == nil { + s = "(default)" + } + ss = append(ss, fmt.Sprintf(" TrimBox (%s) %v %s", unit, pb.TrimBox().Format(ctx.Unit), s)) + } + if wantPB.Bleed != nil { + s := "" + if pb.Bleed == nil { + s = "(default)" + } + ss = append(ss, fmt.Sprintf(" BleedBox (%s) %v %s", unit, pb.BleedBox().Format(ctx.Unit), s)) + } + if wantPB.Art != nil { + s := "" + if pb.Art == nil { + s = "(default)" + } + ss = append(ss, fmt.Sprintf(" ArtBox (%s) %v %s", unit, pb.ArtBox().Format(ctx.Unit), s)) + } + return append(ss, "") +} + +// ListPageBoundaries lists page boundaries specified in wantPB for selected pages. +func (ctx *Context) ListPageBoundaries(selectedPages types.IntSet, wantPB *PageBoundaries) ([]string, error) { + pbs, err := ctx.PageBoundaries(selectedPages) + if err != nil { + return nil, err + } + ss := []string{} + for i, pb := range pbs { + if _, found := selectedPages[i+1]; !found { + continue + } + ss = append(ss, ctx.addPageBoundaryString(i, pb, wantPB)...) + } + + return ss, nil +} + +// RemovePageBoundaries removes page boundaries specified by pb for selected pages. +// The media box is mandatory (inherited or not) and can't be removed. +// A removed crop box defaults to the media box. +// Removed trim/bleed/art boxes default to the crop box. +func (ctx *Context) RemovePageBoundaries(selectedPages types.IntSet, pb *PageBoundaries) error { + for k, v := range selectedPages { + if !v { + continue + } + d, _, inhPAttrs, err := ctx.PageDict(k, false) + if err != nil { + return err + } + if pb.Crop != nil { + if oldVal := d.Delete("CropBox"); oldVal == nil { + d.Insert("CropBox", inhPAttrs.MediaBox.Array()) + } + } + if pb.Trim != nil { + d.Delete("TrimBox") + } + if pb.Bleed != nil { + d.Delete("BleedBox") + } + if pb.Art != nil { + d.Delete("ArtBox") + } + } + return nil +} + +func boxLowerLeftCorner(r *types.Rectangle, w, h float64, a types.Anchor) types.Point { + var p types.Point + + switch a { + + case types.TopLeft: + p.X = r.LL.X + p.Y = r.UR.Y - h + + case types.TopCenter: + p.X = r.UR.X - r.Width()/2 - w/2 + p.Y = r.UR.Y - h + + case types.TopRight: + p.X = r.UR.X - w + p.Y = r.UR.Y - h + + case types.Left: + p.X = r.LL.X + p.Y = r.UR.Y - r.Height()/2 - h/2 + + case types.Center: + p.X = r.UR.X - r.Width()/2 - w/2 + p.Y = r.UR.Y - r.Height()/2 - h/2 + + case types.Right: + p.X = r.UR.X - w + p.Y = r.UR.Y - r.Height()/2 - h/2 + + case types.BottomLeft: + p.X = r.LL.X + p.Y = r.LL.Y + + case types.BottomCenter: + p.X = r.UR.X - r.Width()/2 - w/2 + p.Y = r.LL.Y + + case types.BottomRight: + p.X = r.UR.X - w + p.Y = r.LL.Y + } + + return p +} + +func boxByDim(boxName string, b *Box, d types.Dict, parent *types.Rectangle) *types.Rectangle { + w := b.Dim.Width + if w <= 1 { + w *= parent.Width() + } + h := b.Dim.Height + if h <= 1 { + h *= parent.Height() + } + ll := boxLowerLeftCorner(parent, w, h, b.Pos) + r := types.RectForWidthAndHeight(ll.X+float64(b.Dx), ll.Y+float64(b.Dy), w, h) + if d != nil { + d.Update(boxName, r.Array()) + } + return r +} + +func ensureCropBoxWithinMediaBox(xmin, xmax, ymin, ymax float64, d types.Dict, parent *types.Rectangle) { + if xmin < parent.LL.X || ymin < parent.LL.Y || xmax > parent.UR.X || ymax > parent.UR.Y { + // Expand media box. + if xmin < parent.LL.X { + parent.LL.X = xmin + } + if xmax > parent.UR.X { + parent.UR.X = xmax + } + if ymin < parent.LL.Y { + parent.LL.Y = ymin + } + if ymax > parent.UR.Y { + parent.UR.Y = ymax + } + if d != nil { + d.Update("MediaBox", parent.Array()) + } + } +} + +func ApplyBox(boxName string, b *Box, d types.Dict, parent *types.Rectangle) *types.Rectangle { + if b.Rect != nil { + if d != nil { + d.Update(boxName, b.Rect.Array()) + } + return b.Rect + } + + if b.Dim != nil { + return boxByDim(boxName, b, d, parent) + } + + mLeft, mRight, mTop, mBot := b.MLeft, b.MRight, b.MTop, b.MBot + if b.MLeft != 0 && -1 < b.MLeft && b.MLeft < 1 { + // Margins relative to media box + mLeft *= parent.Width() + mRight *= parent.Width() + mBot *= parent.Height() + mTop *= parent.Height() + } + xmin := parent.LL.X + mLeft + ymin := parent.LL.Y + mBot + xmax := parent.UR.X - mRight + ymax := parent.UR.Y - mTop + r := types.NewRectangle(xmin, ymin, xmax, ymax) + if d != nil { + d.Update(boxName, r.Array()) + } + if boxName == "CropBox" { + ensureCropBoxWithinMediaBox(xmin, xmax, ymin, ymax, d, parent) + } + return r +} + +type boxes struct { + mediaBox, cropBox, trimBox, bleedBox, artBox *types.Rectangle +} + +func applyBoxDefinitions(d types.Dict, pb *PageBoundaries, b *boxes) { + parentBox := b.mediaBox + if pb.Media != nil { + //fmt.Println("add mb") + b.mediaBox = ApplyBox("MediaBox", pb.Media, d, parentBox) + } + + if pb.Crop != nil { + //fmt.Println("add cb") + b.cropBox = ApplyBox("CropBox", pb.Crop, d, parentBox) + } + + if b.cropBox != nil { + parentBox = b.cropBox + } + if pb.Trim != nil && pb.Trim.RefBox == "" { + //fmt.Println("add tb") + b.trimBox = ApplyBox("TrimBox", pb.Trim, d, parentBox) + } + + if pb.Bleed != nil && pb.Bleed.RefBox == "" { + //fmt.Println("add bb") + b.bleedBox = ApplyBox("BleedBox", pb.Bleed, d, parentBox) + } + + if pb.Art != nil && pb.Art.RefBox == "" { + //fmt.Println("add ab") + b.artBox = ApplyBox("ArtBox", pb.Art, d, parentBox) + } +} + +func updateTrimBox(d types.Dict, trimBox *Box, b *boxes) { + var r *types.Rectangle + switch trimBox.RefBox { + case "media": + r = b.mediaBox + case "crop": + r = b.cropBox + case "bleed": + r = b.bleedBox + if r == nil { + r = b.cropBox + } + case "art": + r = b.artBox + if r == nil { + r = b.cropBox + } + } + d.Update("TrimBox", r.Array()) + b.trimBox = r +} + +func updateBleedBox(d types.Dict, bleedBox *Box, b *boxes) { + var r *types.Rectangle + switch bleedBox.RefBox { + case "media": + r = b.mediaBox + case "crop": + r = b.cropBox + case "trim": + r = b.trimBox + if r == nil { + r = b.cropBox + } + case "art": + r = b.artBox + if r == nil { + r = b.cropBox + } + } + d.Update("BleedBox", r.Array()) + b.bleedBox = r +} + +func updateArtBox(d types.Dict, artBox *Box, b *boxes) { + var r *types.Rectangle + switch artBox.RefBox { + case "media": + r = b.mediaBox + case "crop": + r = b.cropBox + case "trim": + r = b.trimBox + if r == nil { + r = b.cropBox + } + case "bleed": + r = b.bleedBox + if r == nil { + r = b.cropBox + } + } + d.Update("ArtBox", r.Array()) + b.artBox = r +} + +func applyBoxAssignments(d types.Dict, pb *PageBoundaries, b *boxes) { + if pb.Trim != nil && pb.Trim.RefBox != "" { + updateTrimBox(d, pb.Trim, b) + } + + if pb.Bleed != nil && pb.Bleed.RefBox != "" { + updateBleedBox(d, pb.Bleed, b) + } + + if pb.Art != nil && pb.Art.RefBox != "" { + updateArtBox(d, pb.Art, b) + } +} + +// AddPageBoundaries adds page boundaries specified by pb for selected pages. +func (ctx *Context) AddPageBoundaries(selectedPages types.IntSet, pb *PageBoundaries) error { + for k, v := range selectedPages { + if !v { + continue + } + d, _, inhPAttrs, err := ctx.PageDict(k, false) + if err != nil { + return err + } + mediaBox := inhPAttrs.MediaBox + cropBox := inhPAttrs.CropBox + + var trimBox *types.Rectangle + obj, found := d.Find("TrimBox") + if found { + a, err := ctx.DereferenceArray(obj) + if err != nil { + return err + } + if trimBox, err = rect(ctx.XRefTable, a); err != nil { + return err + } + } + + var bleedBox *types.Rectangle + obj, found = d.Find("BleedBox") + if found { + a, err := ctx.DereferenceArray(obj) + if err != nil { + return err + } + if bleedBox, err = rect(ctx.XRefTable, a); err != nil { + return err + } + } + + var artBox *types.Rectangle + obj, found = d.Find("ArtBox") + if found { + a, err := ctx.DereferenceArray(obj) + if err != nil { + return err + } + if artBox, err = rect(ctx.XRefTable, a); err != nil { + return err + } + } + + boxes := &boxes{mediaBox: mediaBox, cropBox: cropBox, trimBox: trimBox, bleedBox: bleedBox, artBox: artBox} + applyBoxDefinitions(d, pb, boxes) + applyBoxAssignments(d, pb, boxes) + } + return nil +} + +// Crop sets crop box for selected pages to b. +func (ctx *Context) Crop(selectedPages types.IntSet, b *Box) error { + for k, v := range selectedPages { + if !v { + continue + } + d, _, inhPAttrs, err := ctx.PageDict(k, false) + if err != nil { + return err + } + ApplyBox("CropBox", b, d, inhPAttrs.MediaBox) + } + return nil +} diff --git a/pkg/pdfcpu/model/colorSpace.go b/pkg/pdfcpu/model/colorSpace.go new file mode 100644 index 0000000000000000000000000000000000000000..62e5fba4e7e159d99af08a82168e86020cf3c16b --- /dev/null +++ b/pkg/pdfcpu/model/colorSpace.go @@ -0,0 +1,32 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 model + +// PDF defines the following Color Spaces: +const ( + DeviceGrayCS = "DeviceGray" + DeviceRGBCS = "DeviceRGB" + DeviceCMYKCS = "DeviceCMYK" + CalGrayCS = "CalGray" + CalRGBCS = "CalRGB" + LabCS = "Lab" + ICCBasedCS = "ICCBased" + IndexedCS = "Indexed" + PatternCS = "Pattern" + SeparationCS = "Separation" + DeviceNCS = "DeviceN" +) diff --git a/pkg/pdfcpu/model/configuration.go b/pkg/pdfcpu/model/configuration.go new file mode 100644 index 0000000000000000000000000000000000000000..04c60de6d82083a1bbb61c5aeea55dbaa6f48a0f --- /dev/null +++ b/pkg/pdfcpu/model/configuration.go @@ -0,0 +1,498 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 model + +import ( + _ "embed" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/pdfcpu/pdfcpu/pkg/font" + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +const ( + // ValidationStrict ensures 100% compliance with the spec (PDF 32000-1:2008). + ValidationStrict int = iota + + // ValidationRelaxed ensures PDF compliance based on frequently encountered validation errors. + ValidationRelaxed +) + +// See table 22 - User access permissions +type PermissionFlags int + +const ( + UnusedFlag1 PermissionFlags = 1 << iota // Bit 1: unused + UnusedFlag2 // Bit 2: unused + PermissionPrintRev2 // Bit 3: Print (security handlers rev.2), draft print (security handlers >= rev.3) + PermissionModify // Bit 4: Modify contents by operations other than controlled by bits 6, 9, 11. + PermissionExtract // Bit 5: Copy, extract text & graphics + PermissionModAnnFillForm // Bit 6: Add or modify annotations, fill form fields, in conjunction with bit 4 create/mod form fields. + UnusedFlag7 // Bit 7: unused + UnusedFlag8 // Bit 8: unused + PermissionFillRev3 // Bit 9: Fill form fields (security handlers >= rev.3) + PermissionExtractRev3 // Bit 10: Copy, extract text & graphics (security handlers >= rev.3) (unused since PDF 2.0) + PermissionAssembleRev3 // Bit 11: Assemble document (security handlers >= rev.3) + PermissionPrintRev3 // Bit 12: Print (security handlers >= rev.3) +) + +const ( + PermissionsNone = PermissionFlags(0xF0C3) + PermissionsPrint = PermissionsNone + PermissionPrintRev2 + PermissionPrintRev3 + PermissionsAll = PermissionFlags(0xFFFF) +) + +const ( + + // StatsFileNameDefault is the standard stats filename. + StatsFileNameDefault = "stats.csv" +) + +// CommandMode specifies the operation being executed. +type CommandMode int + +// The available commands. +const ( + VALIDATE CommandMode = iota + LISTINFO + OPTIMIZE + SPLIT + SPLITBYPAGENR + MERGECREATE + MERGECREATEZIP + MERGEAPPEND + EXTRACTIMAGES + EXTRACTFONTS + EXTRACTPAGES + EXTRACTCONTENT + EXTRACTMETADATA + TRIM + LISTATTACHMENTS + EXTRACTATTACHMENTS + ADDATTACHMENTS + ADDATTACHMENTSPORTFOLIO + REMOVEATTACHMENTS + LISTPERMISSIONS + SETPERMISSIONS + ADDWATERMARKS + REMOVEWATERMARKS + IMPORTIMAGES + INSERTPAGESBEFORE + INSERTPAGESAFTER + REMOVEPAGES + LISTKEYWORDS + ADDKEYWORDS + REMOVEKEYWORDS + LISTPROPERTIES + ADDPROPERTIES + REMOVEPROPERTIES + COLLECT + CROP + LISTBOXES + ADDBOXES + REMOVEBOXES + LISTANNOTATIONS + ADDANNOTATIONS + REMOVEANNOTATIONS + ROTATE + NUP + BOOKLET + LISTBOOKMARKS + ADDBOOKMARKS + REMOVEBOOKMARKS + IMPORTBOOKMARKS + EXPORTBOOKMARKS + LISTIMAGES + CREATE + DUMP + LISTFORMFIELDS + REMOVEFORMFIELDS + LOCKFORMFIELDS + UNLOCKFORMFIELDS + RESETFORMFIELDS + EXPORTFORMFIELDS + FILLFORMFIELDS + MULTIFILLFORMFIELDS + ENCRYPT + DECRYPT + CHANGEUPW + CHANGEOPW + CHEATSHEETSFONTS + INSTALLFONTS + LISTFONTS + RESIZE + POSTER + NDOWN + CUT + LISTPAGELAYOUT + SETPAGELAYOUT + RESETPAGELAYOUT + LISTPAGEMODE + SETPAGEMODE + RESETPAGEMODE + LISTVIEWERPREFERENCES + SETVIEWERPREFERENCES + RESETVIEWERPREFERENCES + ZOOM +) + +// Configuration of a Context. +type Configuration struct { + // Location of corresponding config.yml + Path string + + // Check filename extensions. + CheckFileNameExt bool + + // Enables PDF V1.5 compatible processing of object streams, xref streams, hybrid PDF files. + Reader15 bool + + // Enables decoding of all streams (fontfiles, images..) for logging purposes. + DecodeAllStreams bool + + // Validate against ISO-32000: strict or relaxed. + ValidationMode int + + // Enable validation right before writing. + PostProcessValidate bool + + // Check for broken links in LinkedAnnotations/URIActions. + ValidateLinks bool + + // End of line char sequence for writing. + Eol string + + // Turns on object stream generation. + // A signal for compressing any new non-stream-object into an object stream. + // true enforces WriteXRefStream to true. + // false does not prevent xRefStream generation. + WriteObjectStream bool + + // Switches between xRefSection (<=V1.4) and objectStream/xRefStream (>=V1.5) writing. + WriteXRefStream bool + + // Turns on stats collection. + // TODO Decision - unused. + CollectStats bool + + // A CSV-filename holding the statistics. + StatsFileName string + + // Supplied user password. + UserPW string + UserPWNew *string + + // Supplied owner password. + OwnerPW string + OwnerPWNew *string + + // EncryptUsingAES ensures AES encryption. + // true: AES encryption + // false: RC4 encryption. + EncryptUsingAES bool + + // AES:40,128,256 RC4:40,128 + EncryptKeyLength int + + // Supplied user access permissions, see Table 22. + Permissions PermissionFlags // int16 + + // Command being executed. + Cmd CommandMode + + // Display unit in effect. + Unit types.DisplayUnit + + // Timestamp format. + TimestampFormat string + + // Date format. + DateFormat string + + // Optimize. + Optimize bool + + // Optimize page resources via content stream analysis. + OptimizeResourceDicts bool + + // Optimize duplicate content streams across pages. + OptimizeDuplicateContentStreams bool + + // Merge creates bookmarks + CreateBookmarks bool + + // PDF Viewer is expected to supply appearance streams for form fields. + NeedAppearances bool +} + +// ConfigPath defines the location of pdfcpu's configuration directory. +// If set to a file path, pdfcpu will ensure the config dir at this location. +// Other possible values: +// +// default: Ensure config dir at default location +// disable: Disable config dir usage +// +// If you want to disable config dir usage in a multi threaded environment +// you are encouraged to use api.DisableConfigDir(). +var ConfigPath string = "default" + +var loadedDefaultConfig *Configuration + +//go:embed resources/config.yml +var configFileBytes []byte + +//go:embed resources/Roboto-Regular.ttf +var robotoFontFileBytes []byte + +func ensureConfigFileAt(path string) error { + f, err := os.Open(path) + if err != nil { + f.Close() + s := fmt.Sprintf("#############################\n# pdfcpu %s #\n# Created: %s #\n", VersionStr, time.Now().Format("2006-01-02 15:04")) + bb := append([]byte(s), configFileBytes...) + if err := os.WriteFile(path, bb, os.ModePerm); err != nil { + return err + } + f, err = os.Open(path) + if err != nil { + return err + } + } + defer f.Close() + // Load configuration into loadedDefaultConfig. + return parseConfigFile(f, path) +} + +// EnsureDefaultConfigAt tries to load the default configuration from path. +// If path/pdfcpu/config.yaml is not found, it will be created. +func EnsureDefaultConfigAt(path string) error { + configDir := filepath.Join(path, "pdfcpu") + font.UserFontDir = filepath.Join(configDir, "fonts") + if err := os.MkdirAll(font.UserFontDir, os.ModePerm); err != nil { + return err + } + if err := ensureConfigFileAt(filepath.Join(configDir, "config.yml")); err != nil { + return err + } + //fmt.Println(loadedDefaultConfig) + + files, err := os.ReadDir(font.UserFontDir) + if err != nil { + return err + } + + if len(files) == 0 { + // Ensure Roboto font for form filling. + fn := "Roboto-Regular" + if log.CLIEnabled() { + log.CLI.Printf("installing user font:") + } + if err := font.InstallFontFromBytes(font.UserFontDir, fn, robotoFontFileBytes); err != nil { + if log.CLIEnabled() { + log.CLI.Printf("%v", err) + } + } + } + + return font.LoadUserFonts() +} + +func newDefaultConfiguration() *Configuration { + // NOTE: Needs to stay in sync with config.yml + // + // Takes effect whenever the installed config.yml is disabled: + // cli: supply -conf disable + // api: call api.DisableConfigDir() + return &Configuration{ + CheckFileNameExt: true, + Reader15: true, + DecodeAllStreams: false, + ValidationMode: ValidationRelaxed, + ValidateLinks: false, + Eol: types.EolLF, + WriteObjectStream: true, + WriteXRefStream: true, + EncryptUsingAES: true, + EncryptKeyLength: 256, + Permissions: PermissionsPrint, + TimestampFormat: "2006-01-02 15:04", + DateFormat: "2006-01-02", + Optimize: true, + OptimizeResourceDicts: true, + OptimizeDuplicateContentStreams: false, + CreateBookmarks: true, + NeedAppearances: false, + } +} + +// NewDefaultConfiguration returns the default pdfcpu configuration. +func NewDefaultConfiguration() *Configuration { + if loadedDefaultConfig != nil { + c := *loadedDefaultConfig + return &c + } + if ConfigPath != "disable" { + path, err := os.UserConfigDir() + if err != nil { + path = os.TempDir() + } + if err = EnsureDefaultConfigAt(path); err == nil { + c := *loadedDefaultConfig + return &c + } + fmt.Fprintf(os.Stderr, "pdfcpu: config dir problem: %v\n", err) + os.Exit(1) + } + // Bypass config.yml + return newDefaultConfiguration() +} + +// NewAESConfiguration returns a default configuration for AES encryption. +func NewAESConfiguration(userPW, ownerPW string, keyLength int) *Configuration { + c := NewDefaultConfiguration() + c.UserPW = userPW + c.OwnerPW = ownerPW + c.EncryptUsingAES = true + c.EncryptKeyLength = keyLength + return c +} + +// NewRC4Configuration returns a default configuration for RC4 encryption. +func NewRC4Configuration(userPW, ownerPW string, keyLength int) *Configuration { + c := NewDefaultConfiguration() + c.UserPW = userPW + c.OwnerPW = ownerPW + c.EncryptUsingAES = false + c.EncryptKeyLength = keyLength + return c +} + +func (c Configuration) String() string { + path := "default" + if len(c.Path) > 0 { + path = c.Path + } + return fmt.Sprintf("pdfcpu configuration:\n"+ + "Path: %s\n"+ + "CheckFileNameExt: %t\n"+ + "Reader15: %t\n"+ + "DecodeAllStreams: %t\n"+ + "ValidationMode: %s\n"+ + "PostProcessValidate: %t\n"+ + "ValidateLinks: %t\n"+ + "Eol: %s\n"+ + "WriteObjectStream: %t\n"+ + "WriteXrefStream: %t\n"+ + "EncryptUsingAES: %t\n"+ + "EncryptKeyLength: %d\n"+ + "Permissions: %d\n"+ + "Unit : %s\n"+ + "TimestampFormat: %s\n"+ + "DateFormat:  %s\n"+ + "Optimize %t\n"+ + "OptimizeResourceDicts %t\n"+ + "OptimizeDuplicateContentStreams %t\n"+ + "CreateBookmarks %t\n"+ + "NeedAppearances %t\n", + path, + c.CheckFileNameExt, + c.Reader15, + c.DecodeAllStreams, + c.ValidationModeString(), + c.PostProcessValidate, + c.ValidateLinks, + c.EolString(), + c.WriteObjectStream, + c.WriteXRefStream, + c.EncryptUsingAES, + c.EncryptKeyLength, + c.Permissions, + c.UnitString(), + c.TimestampFormat, + c.DateFormat, + c.Optimize, + c.OptimizeResourceDicts, + c.OptimizeDuplicateContentStreams, + c.CreateBookmarks, + c.NeedAppearances, + ) +} + +// EolString returns a string rep for the eol in effect. +func (c *Configuration) EolString() string { + var s string + switch c.Eol { + case types.EolLF: + s = "EolLF" + case types.EolCR: + s = "EolCR" + case types.EolCRLF: + s = "EolCRLF" + } + return s +} + +// ValidationModeString returns a string rep for the validation mode in effect. +func (c *Configuration) ValidationModeString() string { + if c.ValidationMode == ValidationStrict { + return "strict" + } + return "relaxed" +} + +// UnitString returns a string rep for the display unit in effect. +func (c *Configuration) UnitString() string { + var s string + switch c.Unit { + case types.POINTS: + s = "points" + case types.INCHES: + s = "inches" + case types.CENTIMETRES: + s = "cm" + case types.MILLIMETRES: + s = "mm" + } + return s +} + +// SetUnit configures the display unit. +func (c *Configuration) SetUnit(s string) { + switch s { + case "points": + c.Unit = types.POINTS + case "inches": + c.Unit = types.INCHES + case "cm": + c.Unit = types.CENTIMETRES + case "mm": + c.Unit = types.MILLIMETRES + } +} + +// ApplyReducedFeatureSet returns true if complex entries like annotations shall not be written. +func (c *Configuration) ApplyReducedFeatureSet() bool { + switch c.Cmd { + case SPLIT, TRIM, EXTRACTPAGES, IMPORTIMAGES: + return true + } + return false +} diff --git a/pkg/pdfcpu/model/context.go b/pkg/pdfcpu/model/context.go new file mode 100644 index 0000000000000000000000000000000000000000..73f43d8ac13d0b86632b8d9229485eccd12f0177 --- /dev/null +++ b/pkg/pdfcpu/model/context.go @@ -0,0 +1,645 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 model + +import ( + "bufio" + "fmt" + "io" + "os" + "sort" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +// Context represents an environment for processing PDF files. +type Context struct { + *Configuration + *XRefTable + Read *ReadContext + Optimize *OptimizationContext + Write *WriteContext + WritingPages bool // true, when writing page dicts. + Dest bool // true when writing a destination within a page. +} + +// NewContext initializes a new Context. +func NewContext(rs io.ReadSeeker, conf *Configuration) (*Context, error) { + + if conf == nil { + conf = NewDefaultConfiguration() + } + + rdCtx, err := newReadContext(rs) + if err != nil { + return nil, err + } + + ctx := &Context{ + conf, + newXRefTable(conf), + rdCtx, + newOptimizationContext(), + NewWriteContext(conf.Eol), + false, + false, + } + + return ctx, nil +} + +// ResetWriteContext prepares an existing WriteContext for a new file to be written. +func (ctx *Context) ResetWriteContext() { + ctx.Write = NewWriteContext(ctx.Write.Eol) +} + +func (rc *ReadContext) logReadContext(logStr *[]string) { + if rc.UsingObjectStreams { + *logStr = append(*logStr, "using object streams\n") + } + if rc.UsingXRefStreams { + *logStr = append(*logStr, "using xref streams\n") + } + if rc.Linearized { + *logStr = append(*logStr, "is linearized file\n") + } + if rc.Hybrid { + *logStr = append(*logStr, "is hybrid reference file\n") + } +} + +func (ctx *Context) String() string { + + var logStr []string + + logStr = append(logStr, "*************************************************************************************************\n") + logStr = append(logStr, fmt.Sprintf("HeaderVersion: %s\n", ctx.HeaderVersion)) + + if ctx.RootVersion != nil { + logStr = append(logStr, fmt.Sprintf("RootVersion: %s\n", ctx.RootVersion)) + } + + logStr = append(logStr, fmt.Sprintf("has %d pages\n", ctx.PageCount)) + + ctx.Read.logReadContext(&logStr) + + if ctx.Tagged { + logStr = append(logStr, "is tagged file\n") + } + + logStr = append(logStr, "XRefTable:\n") + logStr = append(logStr, fmt.Sprintf(" Size: %d\n", *ctx.XRefTable.Size)) + logStr = append(logStr, fmt.Sprintf(" Root object: %s\n", *ctx.Root)) + + if ctx.Info != nil { + logStr = append(logStr, fmt.Sprintf(" Info object: %s\n", *ctx.Info)) + } + + if ctx.ID != nil { + logStr = append(logStr, fmt.Sprintf(" ID object: %s\n", ctx.ID)) + } + + if ctx.Encrypt != nil { + logStr = append(logStr, fmt.Sprintf(" Encrypt object: %s\n", *ctx.Encrypt)) + } + + if ctx.AdditionalStreams != nil && len(*ctx.AdditionalStreams) > 0 { + + var objectNumbers []string + for _, k := range *ctx.AdditionalStreams { + indRef, _ := k.(types.IndirectRef) + objectNumbers = append(objectNumbers, fmt.Sprintf("%d", int(indRef.ObjectNumber))) + } + sort.Strings(objectNumbers) + + logStr = append(logStr, fmt.Sprintf(" AdditionalStreams: %s\n\n", strings.Join(objectNumbers, ","))) + } + + logStr = append(logStr, fmt.Sprintf("XRefTable with %d entries:\n", len(ctx.Table))) + + // Print sorted object list. + logStr = ctx.list(logStr) + + // Print free list. + logStr, err := ctx.freeList(logStr) + if err != nil { + if log.InfoEnabled() { + log.Info.Fatalln(err) + } + } + + // Print list of any missing objects. + if len(ctx.XRefTable.Table) < *ctx.XRefTable.Size { + if count, mstr := ctx.MissingObjects(); count > 0 { + logStr = append(logStr, fmt.Sprintf("%d missing objects: %s\n", count, *mstr)) + } + } + + logStr = append(logStr, fmt.Sprintf("\nTotal pages: %d\n", ctx.PageCount)) + logStr = ctx.Optimize.collectFontInfo(logStr) + logStr = ctx.Optimize.collectImageInfo(logStr) + logStr = append(logStr, "\n") + + return strings.Join(logStr, "") +} + +func (ctx *Context) UnitString() string { + u := "points" + switch ctx.Unit { + case types.INCHES: + u = "inches" + case types.CENTIMETRES: + u = "cm" + case types.MILLIMETRES: + u = "mm" + } + return u +} + +// ConvertToUnit converts dimensions in point to inches,cm,mm +func (ctx *Context) ConvertToUnit(d types.Dim) types.Dim { + return d.ConvertToUnit(ctx.Unit) +} + +// ReadContext represents the context for reading a PDF file. +type ReadContext struct { + FileName string // Input PDF-File. + FileSize int64 // Input file size. + RS io.ReadSeeker // Input read seeker. + EolCount int // 1 or 2 characters used for eol. + BinaryTotalSize int64 // total stream data + BinaryImageSize int64 // total image stream data + BinaryFontSize int64 // total font stream data (fontfiles) + BinaryImageDuplSize int64 // total obsolet image stream data after optimization + BinaryFontDuplSize int64 // total obsolet font stream data after optimization + Linearized bool // File is linearized. + Hybrid bool // File is a hybrid PDF file. + UsingObjectStreams bool // File is using object streams. + ObjectStreams types.IntSet // All object numbers of any object streams found which need to be decoded. + UsingXRefStreams bool // File is using xref streams. + XRefStreams types.IntSet // All object numbers of any xref streams found. +} + +func newReadContext(rs io.ReadSeeker) (*ReadContext, error) { + + rdCtx := &ReadContext{ + RS: rs, + ObjectStreams: types.IntSet{}, + XRefStreams: types.IntSet{}, + } + + fileSize, err := rs.Seek(0, io.SeekEnd) + if err != nil { + return nil, err + } + rdCtx.FileSize = fileSize + + return rdCtx, nil +} + +// IsObjectStreamObject returns true if object i is a an object stream. +// All compressed objects are object streams. +func (rc *ReadContext) IsObjectStreamObject(i int) bool { + return rc.ObjectStreams[i] +} + +// ObjectStreamsString returns a formatted string and the number of object stream objects. +func (rc *ReadContext) ObjectStreamsString() (int, string) { + + var objs []int + for k := range rc.ObjectStreams { + if rc.ObjectStreams[k] { + objs = append(objs, k) + } + } + sort.Ints(objs) + + var objStreams []string + for _, i := range objs { + objStreams = append(objStreams, fmt.Sprintf("%d", i)) + } + + return len(objStreams), strings.Join(objStreams, ",") +} + +// LogStats logs stats for read file. +func (rc *ReadContext) LogStats(optimized bool) { + if !log.StatsEnabled() { + return + } + + textSize := rc.FileSize - rc.BinaryTotalSize // = non binary content = non stream data + + log.Stats.Println("Original:") + log.Stats.Printf("File size : %s (%d bytes)\n", types.ByteSize(rc.FileSize), rc.FileSize) + log.Stats.Printf("Total binary data : %s (%d bytes) %4.1f%%\n", types.ByteSize(rc.BinaryTotalSize), rc.BinaryTotalSize, float32(rc.BinaryTotalSize)/float32(rc.FileSize)*100) + log.Stats.Printf("Total other data : %s (%d bytes) %4.1f%%\n\n", types.ByteSize(textSize), textSize, float32(textSize)/float32(rc.FileSize)*100) + + // Only when optimizing we get details about resource data usage. + if optimized { + + // Image stream data of original file. + binaryImageSize := rc.BinaryImageSize + rc.BinaryImageDuplSize + + // Font stream data of original file. (just font files) + binaryFontSize := rc.BinaryFontSize + rc.BinaryFontDuplSize + + // Content stream data, other font related stream data. + binaryOtherSize := rc.BinaryTotalSize - binaryImageSize - binaryFontSize + + log.Stats.Println("Breakup of binary data:") + log.Stats.Printf("images : %s (%d bytes) %4.1f%%\n", types.ByteSize(binaryImageSize), binaryImageSize, float32(binaryImageSize)/float32(rc.BinaryTotalSize)*100) + log.Stats.Printf("fonts : %s (%d bytes) %4.1f%%\n", types.ByteSize(binaryFontSize), binaryFontSize, float32(binaryFontSize)/float32(rc.BinaryTotalSize)*100) + log.Stats.Printf("other : %s (%d bytes) %4.1f%%\n\n", types.ByteSize(binaryOtherSize), binaryOtherSize, float32(binaryOtherSize)/float32(rc.BinaryTotalSize)*100) + } +} + +// ReadFileSize returns the size of the input file, if there is one. +func (rc *ReadContext) ReadFileSize() int { + if rc == nil { + return 0 + } + return int(rc.FileSize) +} + +// OptimizationContext represents the context for the optimiziation of a PDF file. +type OptimizationContext struct { + + // Font section + PageFonts []types.IntSet // For each page a registry of font object numbers. + FontObjects map[int]*FontObject // FontObject lookup table by font object number. + FormFontObjects map[int]*FontObject // FormFontObject lookup table by font object number. + Fonts map[string][]int // All font object numbers registered for a font name. + DuplicateFonts map[int]types.Dict // Registry of duplicate font dicts. + DuplicateFontObjs types.IntSet // The set of objects that represents the union of the object graphs of all duplicate font dicts. + + // Image section + PageImages []types.IntSet // For each page a registry of image object numbers. + ImageObjects map[int]*ImageObject // ImageObject lookup table by image object number. + DuplicateImages map[int]*types.StreamDict // Registry of duplicate image dicts. + DuplicateImageObjs types.IntSet // The set of objects that represents the union of the object graphs of all duplicate image dicts. + + ContentStreamCache map[int]*types.StreamDict + FormStreamCache map[int]*types.StreamDict + + DuplicateInfoObjects types.IntSet // Possible result of manual info dict modification. + NonReferencedObjs []int // Objects that are not referenced. + + Cache map[int]bool // For visited objects during optimization. + NullObjNr *int // objNr of a regular null object, to be used for fixing references to free objects. +} + +func newOptimizationContext() *OptimizationContext { + return &OptimizationContext{ + FontObjects: map[int]*FontObject{}, + FormFontObjects: map[int]*FontObject{}, + Fonts: map[string][]int{}, + DuplicateFonts: map[int]types.Dict{}, + DuplicateFontObjs: types.IntSet{}, + ImageObjects: map[int]*ImageObject{}, + DuplicateImages: map[int]*types.StreamDict{}, + DuplicateImageObjs: types.IntSet{}, + DuplicateInfoObjects: types.IntSet{}, + ContentStreamCache: map[int]*types.StreamDict{}, + FormStreamCache: map[int]*types.StreamDict{}, + Cache: map[int]bool{}, + } +} + +// IsDuplicateFontObject returns true if object #i is a duplicate font object. +func (oc *OptimizationContext) IsDuplicateFontObject(i int) bool { + return oc.DuplicateFontObjs[i] +} + +// DuplicateFontObjectsString returns a formatted string and the number of objs. +func (oc *OptimizationContext) DuplicateFontObjectsString() (int, string) { + + var objs []int + for k := range oc.DuplicateFontObjs { + if oc.DuplicateFontObjs[k] { + objs = append(objs, k) + } + } + sort.Ints(objs) + + var dupFonts []string + for _, i := range objs { + dupFonts = append(dupFonts, fmt.Sprintf("%d", i)) + } + + return len(dupFonts), strings.Join(dupFonts, ",") +} + +// IsDuplicateImageObject returns true if object #i is a duplicate image object. +func (oc *OptimizationContext) IsDuplicateImageObject(i int) bool { + return oc.DuplicateImageObjs[i] +} + +// DuplicateImageObjectsString returns a formatted string and the number of objs. +func (oc *OptimizationContext) DuplicateImageObjectsString() (int, string) { + + var objs []int + for k := range oc.DuplicateImageObjs { + if oc.DuplicateImageObjs[k] { + objs = append(objs, k) + } + } + sort.Ints(objs) + + var dupImages []string + for _, i := range objs { + dupImages = append(dupImages, fmt.Sprintf("%d", i)) + } + + return len(dupImages), strings.Join(dupImages, ",") +} + +// IsDuplicateInfoObject returns true if object #i is a duplicate info object. +func (oc *OptimizationContext) IsDuplicateInfoObject(i int) bool { + return oc.DuplicateInfoObjects[i] +} + +// DuplicateInfoObjectsString returns a formatted string and the number of objs. +func (oc *OptimizationContext) DuplicateInfoObjectsString() (int, string) { + + var objs []int + for k := range oc.DuplicateInfoObjects { + if oc.DuplicateInfoObjects[k] { + objs = append(objs, k) + } + } + sort.Ints(objs) + + var dupInfos []string + for _, i := range objs { + dupInfos = append(dupInfos, fmt.Sprintf("%d", i)) + } + + return len(dupInfos), strings.Join(dupInfos, ",") +} + +// NonReferencedObjsString returns a formatted string and the number of objs. +func (oc *OptimizationContext) NonReferencedObjsString() (int, string) { + + var s []string + for _, o := range oc.NonReferencedObjs { + s = append(s, fmt.Sprintf("%d", o)) + } + + return len(oc.NonReferencedObjs), strings.Join(s, ",") +} + +// Prepare info gathered about font usage in form of a string array. +func (oc *OptimizationContext) collectFontInfo(logStr []string) []string { + + // Print available font info. + if len(oc.Fonts) == 0 || len(oc.PageFonts) == 0 { + return append(logStr, "No font info available.\n") + } + + fontHeader := "obj prefix Fontname Subtype Encoding Embedded ResourceIds\n" + + // Log fonts usage per page. + for i, fontObjectNumbers := range oc.PageFonts { + + if len(fontObjectNumbers) == 0 { + continue + } + + logStr = append(logStr, fmt.Sprintf("\nFonts for page %d:\n", i+1)) + logStr = append(logStr, fontHeader) + + var objectNumbers []int + for k := range fontObjectNumbers { + objectNumbers = append(objectNumbers, k) + } + sort.Ints(objectNumbers) + + for _, objectNumber := range objectNumbers { + fontObject := oc.FontObjects[objectNumber] + logStr = append(logStr, fmt.Sprintf("#%-6d %s", objectNumber, fontObject)) + } + } + + // Log all fonts sorted by object number. + logStr = append(logStr, "\nFontobjects:\n") + logStr = append(logStr, fontHeader) + + var objectNumbers []int + for k := range oc.FontObjects { + objectNumbers = append(objectNumbers, k) + } + sort.Ints(objectNumbers) + + for _, objectNumber := range objectNumbers { + fontObject := oc.FontObjects[objectNumber] + logStr = append(logStr, fmt.Sprintf("#%-6d %s", objectNumber, fontObject)) + } + + // Log all fonts sorted by fontname. + logStr = append(logStr, "\nFonts:\n") + logStr = append(logStr, fontHeader) + + var fontNames []string + for k := range oc.Fonts { + fontNames = append(fontNames, k) + } + sort.Strings(fontNames) + + for _, fontName := range fontNames { + for _, objectNumber := range oc.Fonts[fontName] { + fontObject := oc.FontObjects[objectNumber] + logStr = append(logStr, fmt.Sprintf("#%-6d %s", objectNumber, fontObject)) + } + } + + logStr = append(logStr, "\nDuplicate Fonts:\n") + + // Log any duplicate fonts. + if len(oc.DuplicateFonts) > 0 { + + var objectNumbers []int + for k := range oc.DuplicateFonts { + objectNumbers = append(objectNumbers, k) + } + sort.Ints(objectNumbers) + + var f []string + + for _, i := range objectNumbers { + f = append(f, fmt.Sprintf("%d", i)) + } + + logStr = append(logStr, strings.Join(f, ",")) + } + + return append(logStr, "\n") +} + +// Prepare info gathered about image usage in form of a string array. +func (oc *OptimizationContext) collectImageInfo(logStr []string) []string { + + // Print available image info. + if len(oc.ImageObjects) == 0 { + return append(logStr, "\nNo image info available.\n") + } + + imageHeader := "obj ResourceIds\n" + + // Log images per page. + for i, imageObjectNumbers := range oc.PageImages { + + if len(imageObjectNumbers) == 0 { + continue + } + + logStr = append(logStr, fmt.Sprintf("\nImages for page %d:\n", i+1)) + logStr = append(logStr, imageHeader) + + var objectNumbers []int + for k := range imageObjectNumbers { + objectNumbers = append(objectNumbers, k) + } + sort.Ints(objectNumbers) + + for _, objectNumber := range objectNumbers { + imageObject := oc.ImageObjects[objectNumber] + logStr = append(logStr, fmt.Sprintf("#%-6d %s\n", objectNumber, imageObject.ResourceNamesString())) + } + } + + // Log all images sorted by object number. + logStr = append(logStr, "\nImageobjects:\n") + logStr = append(logStr, imageHeader) + + var objectNumbers []int + for k := range oc.ImageObjects { + objectNumbers = append(objectNumbers, k) + } + sort.Ints(objectNumbers) + + for _, objectNumber := range objectNumbers { + imageObject := oc.ImageObjects[objectNumber] + logStr = append(logStr, fmt.Sprintf("#%-6d %s\n", objectNumber, imageObject.ResourceNamesString())) + } + + logStr = append(logStr, "\nDuplicate Images:\n") + + // Log any duplicate images. + if len(oc.DuplicateImages) > 0 { + + var objectNumbers []int + for k := range oc.DuplicateImages { + objectNumbers = append(objectNumbers, k) + } + sort.Ints(objectNumbers) + + var f []string + + for _, i := range objectNumbers { + f = append(f, fmt.Sprintf("%d", i)) + } + + logStr = append(logStr, strings.Join(f, ",")) + } + + return logStr +} + +// WriteContext represents the context for writing a PDF file. +type WriteContext struct { + + // The PDF-File which gets generated. + *bufio.Writer // A writer associated with Fp. + Fp *os.File // A file pointer needed for detecting FileSize. + FileSize int64 // The size of the written file. + DirName string // The output directory. + FileName string // The output file name. + SelectedPages types.IntSet // For split, trim and extract. + BinaryTotalSize int64 // total stream data, counts 100% all stream data written. + BinaryImageSize int64 // total image stream data written = Read.BinaryImageSize. + BinaryFontSize int64 // total font stream data (fontfiles) = copy of Read.BinaryFontSize. + Table map[int]int64 // object write offsets + Offset int64 // current write offset + WriteToObjectStream bool // if true start to embed objects into object streams and obey ObjectStreamMaxObjects. + CurrentObjStream *int // if not nil, any new non-stream-object gets added to the object stream with this object number. + Eol string // end of line char sequence + Increment bool // Write context as PDF increment. + ObjNrs []int // Increment candidate object numbers. + OffsetPrevXRef *int64 // Increment trailer entry "Prev". +} + +// NewWriteContext returns a new WriteContext. +func NewWriteContext(eol string) *WriteContext { + return &WriteContext{SelectedPages: types.IntSet{}, Table: map[int]int64{}, Eol: eol, ObjNrs: []int{}} +} + +// SetWriteOffset saves the current write offset to the PDFDestination. +func (wc *WriteContext) SetWriteOffset(objNumber int) { + wc.Table[objNumber] = wc.Offset +} + +// HasWriteOffset returns true if an object has already been written to PDFDestination. +func (wc *WriteContext) HasWriteOffset(objNumber int) bool { + _, found := wc.Table[objNumber] + return found +} + +// LogStats logs stats for written file. +func (wc *WriteContext) LogStats() { + if !log.StatsEnabled() { + return + } + + fileSize := wc.FileSize + binaryTotalSize := wc.BinaryTotalSize // stream data + textSize := fileSize - binaryTotalSize // non stream data + + binaryImageSize := wc.BinaryImageSize + binaryFontSize := wc.BinaryFontSize + binaryOtherSize := binaryTotalSize - binaryImageSize - binaryFontSize // content streams + + log.Stats.Println("Optimized:") + log.Stats.Printf("File size : %s (%d bytes)\n", types.ByteSize(fileSize), fileSize) + log.Stats.Printf("Total binary data : %s (%d bytes) %4.1f%%\n", types.ByteSize(binaryTotalSize), binaryTotalSize, float32(binaryTotalSize)/float32(fileSize)*100) + log.Stats.Printf("Total other data : %s (%d bytes) %4.1f%%\n\n", types.ByteSize(textSize), textSize, float32(textSize)/float32(fileSize)*100) + + log.Stats.Println("Breakup of binary data:") + log.Stats.Printf("images : %s (%d bytes) %4.1f%%\n", types.ByteSize(binaryImageSize), binaryImageSize, float32(binaryImageSize)/float32(binaryTotalSize)*100) + log.Stats.Printf("fonts : %s (%d bytes) %4.1f%%\n", types.ByteSize(binaryFontSize), binaryFontSize, float32(binaryFontSize)/float32(binaryTotalSize)*100) + log.Stats.Printf("other : %s (%d bytes) %4.1f%%\n\n", types.ByteSize(binaryOtherSize), binaryOtherSize, float32(binaryOtherSize)/float32(binaryTotalSize)*100) +} + +// WriteEol writes an end of line sequence. +func (wc *WriteContext) WriteEol() error { + + _, err := wc.WriteString(wc.Eol) + + return err +} + +// IncrementWithObjNr adds obj# i to wc for writing. +func (wc *WriteContext) IncrementWithObjNr(i int) { + for _, objNr := range wc.ObjNrs { + if objNr == i { + return + } + } + wc.ObjNrs = append(wc.ObjNrs, i) +} diff --git a/pkg/pdfcpu/model/cut.go b/pkg/pdfcpu/model/cut.go new file mode 100644 index 0000000000000000000000000000000000000000..78690d28a76513a75fbd90554bfc73f8dc168b5c --- /dev/null +++ b/pkg/pdfcpu/model/cut.go @@ -0,0 +1,225 @@ +/* +Copyright 2023 The pdfcpu Authors. + +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 + + 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 model + +import ( + "strconv" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +type Cut struct { + Hor []float64 // Horizontal cut points + Vert []float64 // Vertical cut points + Scale float64 // scale factor x > 1 (poster) + PageSize string // paper/form size eg. A2,A3,A4,Legal,Ledger,... + PageDim *types.Dim // page dimensions in display unit + Unit types.DisplayUnit // display unit + UserDim bool // true if dimensions set by dim rather than formsize + Border bool // true to render crop box + Margin float64 // glue area in display unit + BgColor *color.SimpleColor // background color + Origin types.Corner // one of 4 page corners, default = UpperLeft +} + +type cutParameterMap map[string]func(string, *Cut) error + +func parseHorCut(v string, cut *Cut) (err error) { + + for _, s := range strings.Split(v, " ") { + f, err := strconv.ParseFloat(s, 64) + if err != nil { + return errors.Errorf("pdfcpu: cut position must be a float value: %s\n", s) + } + if f <= 0 || f >= 1 { + return errors.Errorf("pdfcpu: invalid cut poistion %.2f: 0 < i < 1.0\n", f) + } + cut.Hor = append(cut.Hor, f) + } + + return nil +} + +func parseVertCut(v string, cut *Cut) (err error) { + + for _, s := range strings.Split(v, " ") { + f, err := strconv.ParseFloat(s, 64) + if err != nil { + return errors.Errorf("pdfcpu: cut position must be a float value: %s\n", s) + } + if f <= 0 || f >= 1 { + return errors.Errorf("pdfcpu: invalid cut poistion %.2f: 0 < i < 1.0\n", f) + } + cut.Vert = append(cut.Vert, f) + } + + return nil +} + +func parsePageDimCut(v string, u types.DisplayUnit) (*types.Dim, string, error) { + + ss := strings.Split(v, " ") + if len(ss) != 2 { + return nil, v, errors.Errorf("pdfcpu: illegal dimension string: need 2 values one may be 0, %s\n", v) + } + + w, err := strconv.ParseFloat(ss[0], 64) + if err != nil || w < 0 { + return nil, v, errors.Errorf("pdfcpu: dimension width must be >= 0: %s\n", ss[0]) + } + + h, err := strconv.ParseFloat(ss[1], 64) + if err != nil || h < 0 { + return nil, v, errors.Errorf("pdfcpu: dimension height must >= 0: %s\n", ss[1]) + } + + d := types.Dim{Width: types.ToUserSpace(w, u), Height: types.ToUserSpace(h, u)} + + return &d, "", nil +} + +func parseDimensionsCut(s string, cut *Cut) (err error) { + cut.PageDim, _, err = parsePageDimCut(s, cut.Unit) + if err != nil { + return err + } + cut.UserDim = true + return nil +} + +func parsePageFormatCut(s string, cut *Cut) error { + + // Optional: appended last letter L indicates landscape mode. + // Optional: appended last letter P indicates portrait mode. + // eg. A4L means A4 in landscape mode whereas A4 defaults to A4P + // The default mode is defined implicitly via PaperSize dimensions. + + var landscape, portrait bool + + v := s + if strings.HasSuffix(v, "L") { + v = v[:len(v)-1] + landscape = true + } else if strings.HasSuffix(v, "P") { + v = v[:len(v)-1] + portrait = true + } + + d, ok := types.PaperSize[v] + if !ok { + return errors.Errorf("pdfcpu: page format %s is unsupported.\n", v) + } + + if (d.Portrait() && landscape) || (d.Landscape() && portrait) { + d.Width, d.Height = d.Height, d.Width + } + + cut.PageDim = d + cut.PageSize = v + + return nil +} + +func parseScaleFactorCut(s string, cut *Cut) (err error) { + + sc, err := strconv.ParseFloat(s, 64) + if err != nil { + return errors.Errorf("pdfcpu: scale factor must be a float value: %s\n", s) + } + + if sc < 1 { + return errors.Errorf("pdfcpu: invalid scale factor %.2f: i >= 1.0\n", sc) + } + + cut.Scale = sc + return nil +} + +func parseBackgroundColorCut(s string, cut *Cut) error { + c, err := color.ParseColor(s) + if err != nil { + return err + } + cut.BgColor = &c + return nil +} + +func parseBorderCut(s string, cut *Cut) error { + switch strings.ToLower(s) { + case "on", "true", "t": + cut.Border = true + case "off", "false", "f": + cut.Border = false + default: + return errors.New("pdfcpu: cut border, please provide one of: on/off true/false t/f") + } + + return nil +} + +func parseMarginCut(s string, cut *Cut) error { + f, err := strconv.ParseFloat(s, 64) + if err != nil { + return err + } + + if f < 0 { + return errors.New("pdfcpu: cut margin, Please provide a positive value") + } + + cut.Margin = types.ToUserSpace(f, cut.Unit) + + return nil +} + +var CutParamMap = cutParameterMap{ + "horizontalCut": parseHorCut, + "verticalCut": parseVertCut, + "dimensions": parseDimensionsCut, + "formsize": parsePageFormatCut, + "papersize": parsePageFormatCut, + "scalefactor": parseScaleFactorCut, + "border": parseBorderCut, + "margin": parseMarginCut, + "bgcolor": parseBackgroundColorCut, +} + +// Handle applies parameter completion and on success parse parameter values into resize. +func (m cutParameterMap) Handle(paramPrefix, paramValueStr string, cut *Cut) error { + + var param string + + // Completion support + for k := range m { + if !strings.HasPrefix(k, strings.ToLower(paramPrefix)) { + continue + } + if len(param) > 0 { + return errors.Errorf("pdfcpu: ambiguous parameter prefix \"%s\"", paramPrefix) + } + param = k + } + + if param == "" { + return errors.Errorf("pdfcpu: unknown parameter prefix \"%s\"", paramPrefix) + } + + return m[param](paramValueStr, cut) +} diff --git a/pkg/pdfcpu/model/dereference.go b/pkg/pdfcpu/model/dereference.go new file mode 100644 index 0000000000000000000000000000000000000000..bf7e7a601df1a3e71b43eb4b5a60e5ce75660433 --- /dev/null +++ b/pkg/pdfcpu/model/dereference.go @@ -0,0 +1,471 @@ +/* +Copyright 2021 The pdfcpu Authors. + +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 + + 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 model + +import ( + "context" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +func processDictRefCounts(xRefTable *XRefTable, d types.Dict) { + for _, e := range d { + switch o1 := e.(type) { + case types.IndirectRef: + xRefTable.IncrementRefCount(&o1) + case types.Dict: + ProcessRefCounts(xRefTable, o1) + case types.Array: + ProcessRefCounts(xRefTable, o1) + } + } +} + +func processArrayRefCounts(xRefTable *XRefTable, a types.Array) { + for _, e := range a { + switch o1 := e.(type) { + case types.IndirectRef: + xRefTable.IncrementRefCount(&o1) + case types.Dict: + ProcessRefCounts(xRefTable, o1) + case types.Array: + ProcessRefCounts(xRefTable, o1) + } + } +} + +func ProcessRefCounts(xRefTable *XRefTable, o types.Object) { + switch o := o.(type) { + case types.Dict: + processDictRefCounts(xRefTable, o) + case types.StreamDict: + processDictRefCounts(xRefTable, o.Dict) + case types.Array: + processArrayRefCounts(xRefTable, o) + } +} + +func (xRefTable *XRefTable) indRefToObject(ir *types.IndirectRef, decodeLazy bool) (types.Object, error) { + if ir == nil { + return nil, errors.New("pdfcpu: indRefToObject: input argument is nil") + } + + // 7.3.10 + // An indirect reference to an undefined object shall not be considered an error by a conforming reader; + // it shall be treated as a reference to the null object. + entry, found := xRefTable.FindTableEntryForIndRef(ir) + if !found || entry.Free { + return nil, nil + } + + xRefTable.CurObj = int(ir.ObjectNumber) + + if l, ok := entry.Object.(types.LazyObjectStreamObject); ok && decodeLazy { + ob, err := l.DecodedObject(context.TODO()) + if err != nil { + return nil, err + } + + ProcessRefCounts(xRefTable, ob) + entry.Object = ob + } + + // return dereferenced object + return entry.Object, nil +} + +// Dereference resolves an indirect object and returns the resulting PDF object. +func (xRefTable *XRefTable) Dereference(o types.Object) (types.Object, error) { + ir, ok := o.(types.IndirectRef) + if !ok { + // Nothing do dereference. + return o, nil + } + + return xRefTable.indRefToObject(&ir, true) +} + +func (xRefTable *XRefTable) DereferenceForWrite(o types.Object) (types.Object, error) { + ir, ok := o.(types.IndirectRef) + if !ok { + // Nothing do dereference. + return o, nil + } + + return xRefTable.indRefToObject(&ir, false) +} + +// DereferenceBoolean resolves and validates a boolean object, which may be an indirect reference. +func (xRefTable *XRefTable) DereferenceBoolean(o types.Object, sinceVersion Version) (*types.Boolean, error) { + + o, err := xRefTable.Dereference(o) + if err != nil || o == nil { + return nil, err + } + + b, ok := o.(types.Boolean) + if !ok { + return nil, errors.Errorf("pdfcpu: dereferenceBoolean: wrong type <%v>", o) + } + + // Version check + if err = xRefTable.ValidateVersion("DereferenceBoolean", sinceVersion); err != nil { + return nil, err + } + + return &b, nil +} + +// DereferenceInteger resolves and validates an integer object, which may be an indirect reference. +func (xRefTable *XRefTable) DereferenceInteger(o types.Object) (*types.Integer, error) { + + o, err := xRefTable.Dereference(o) + if err != nil || o == nil { + return nil, err + } + + i, ok := o.(types.Integer) + if !ok { + return nil, errors.Errorf("pdfcpu: dereferenceInteger: wrong type <%v>", o) + } + + return &i, nil +} + +// DereferenceNumber resolves a number object, which may be an indirect reference and returns a float64. +func (xRefTable *XRefTable) DereferenceNumber(o types.Object) (float64, error) { + + var ( + f float64 + err error + ) + + o, _ = xRefTable.Dereference(o) + + switch o := o.(type) { + + case types.Integer: + f = float64(o.Value()) + + case types.Float: + f = o.Value() + + default: + err = errors.Errorf("pdfcpu: dereferenceNumber: wrong type <%v>", o) + + } + + return f, err +} + +// DereferenceName resolves and validates a name object, which may be an indirect reference. +func (xRefTable *XRefTable) DereferenceName(o types.Object, sinceVersion Version, validate func(string) bool) (n types.Name, err error) { + + o, err = xRefTable.Dereference(o) + if err != nil || o == nil { + return n, err + } + + n, ok := o.(types.Name) + if !ok { + return n, errors.Errorf("pdfcpu: dereferenceName: wrong type <%v>", o) + } + + // Version check + if err = xRefTable.ValidateVersion("DereferenceName", sinceVersion); err != nil { + return n, err + } + + // Validation + if validate != nil && !validate(n.Value()) { + return n, errors.Errorf("pdfcpu: dereferenceName: invalid <%s>", n.Value()) + } + + return n, nil +} + +// DereferenceStringLiteral resolves and validates a string literal object, which may be an indirect reference. +func (xRefTable *XRefTable) DereferenceStringLiteral(o types.Object, sinceVersion Version, validate func(string) bool) (s types.StringLiteral, err error) { + + o, err = xRefTable.Dereference(o) + if err != nil || o == nil { + return s, err + } + + s, ok := o.(types.StringLiteral) + if !ok { + return s, errors.Errorf("pdfcpu: dereferenceStringLiteral: wrong type <%v>", o) + } + + // Ensure UTF16 correctness. + s1, err := types.StringLiteralToString(s) + if err != nil { + return s, err + } + + // Version check + if err = xRefTable.ValidateVersion("DereferenceStringLiteral", sinceVersion); err != nil { + return s, err + } + + // Validation + if validate != nil && !validate(s1) { + return s, errors.Errorf("pdfcpu: dereferenceStringLiteral: invalid <%s>", s1) + } + + return s, nil +} + +// DereferenceStringOrHexLiteral resolves and validates a string or hex literal object, which may be an indirect reference. +func (xRefTable *XRefTable) DereferenceStringOrHexLiteral(obj types.Object, sinceVersion Version, validate func(string) bool) (s string, err error) { + + o, err := xRefTable.Dereference(obj) + if err != nil || o == nil { + return "", err + } + + switch str := o.(type) { + + case types.StringLiteral: + // Ensure UTF16 correctness. + if s, err = types.StringLiteralToString(str); err != nil { + return "", err + } + + case types.HexLiteral: + // Ensure UTF16 correctness. + if s, err = types.HexLiteralToString(str); err != nil { + return "", err + } + + default: + return "", errors.Errorf("pdfcpu: dereferenceStringOrHexLiteral: wrong type %T", obj) + + } + + // Version check + if err = xRefTable.ValidateVersion("DereferenceStringOrHexLiteral", sinceVersion); err != nil { + return "", err + } + + // Validation + if validate != nil && !validate(s) { + return "", errors.Errorf("pdfcpu: dereferenceStringOrHexLiteral: invalid <%s>", s) + } + + return s, nil +} + +// Text returns a string based representation for String and Hexliterals. +func Text(o types.Object) (string, error) { + switch obj := o.(type) { + case types.StringLiteral: + return types.StringLiteralToString(obj) + case types.HexLiteral: + return types.HexLiteralToString(obj) + default: + return "", errors.Errorf("pdfcpu: corrupt text: %v\n", obj) + } +} + +// DereferenceText resolves and validates a string or hex literal object to a string. +func (xRefTable *XRefTable) DereferenceText(o types.Object) (string, error) { + o, err := xRefTable.Dereference(o) + if err != nil { + return "", err + } + return Text(o) +} + +func CSVSafeString(s string) string { + return strings.Replace(s, ";", ",", -1) +} + +// DereferenceCSVSafeText resolves and validates a string or hex literal object to a string. +func (xRefTable *XRefTable) DereferenceCSVSafeText(o types.Object) (string, error) { + s, err := xRefTable.DereferenceText(o) + if err != nil { + return "", err + } + return CSVSafeString(s), nil +} + +// DereferenceArray resolves and validates an array object, which may be an indirect reference. +func (xRefTable *XRefTable) DereferenceArray(o types.Object) (types.Array, error) { + + o, err := xRefTable.Dereference(o) + if err != nil || o == nil { + return nil, err + } + + a, ok := o.(types.Array) + if !ok { + return nil, errors.Errorf("pdfcpu: dereferenceArray: wrong type %T <%v>", o, o) + } + + return a, nil +} + +// DereferenceDict resolves and validates a dictionary object, which may be an indirect reference. +func (xRefTable *XRefTable) DereferenceDict(o types.Object) (types.Dict, error) { + + o, err := xRefTable.Dereference(o) + if err != nil || o == nil { + return nil, err + } + + d, ok := o.(types.Dict) + if !ok { + //return nil, errors.Errorf("pdfcpu: dereferenceDict: wrong type %T <%v>", o, o) + return nil, nil + } + + return d, nil +} + +// DereferenceFontDict returns the font dict referenced by indRef. +func (xRefTable *XRefTable) DereferenceFontDict(indRef types.IndirectRef) (types.Dict, error) { + d, err := xRefTable.DereferenceDict(indRef) + if err != nil { + return nil, err + } + if d == nil { + return nil, nil + } + + if d.Type() == nil { + return nil, errors.Errorf("pdfcpu: DereferenceFontDict: missing dict type %s\n", indRef) + } + + if *d.Type() != "Font" { + return nil, errors.Errorf("pdfcpu: DereferenceFontDict: expected Type=Font, unexpected Type: %s", *d.Type()) + } + + return d, nil +} + +// DereferencePageNodeDict returns the page node dict referenced by indRef. +func (xRefTable *XRefTable) DereferencePageNodeDict(indRef types.IndirectRef) (types.Dict, error) { + d, err := xRefTable.DereferenceDict(indRef) + if err != nil { + return nil, err + } + if d == nil { + return nil, nil + } + + dictType := d.Type() + if dictType == nil { + return nil, errors.New("pdfcpu: DereferencePageNodeDict: Missing dict type") + } + + if *dictType != "Pages" && *dictType != "Page" { + return nil, errors.Errorf("pdfcpu: DereferencePageNodeDict: unexpected Type: %s", *dictType) + } + + return d, nil +} + +func (xRefTable *XRefTable) dereferenceDestArray(o types.Object) (types.Array, error) { + o, err := xRefTable.Dereference(o) + if err != nil || o == nil { + return nil, err + } + switch o := o.(type) { + case types.Array: + return o, nil + case types.Dict: + o1, err := xRefTable.DereferenceDictEntry(o, "D") + if err != nil { + return nil, err + } + arr, ok := o1.(types.Array) + if !ok { + errors.Errorf("pdfcpu: corrupted dest array:\n%s\n", o) + } + return arr, nil + } + + return nil, errors.Errorf("pdfcpu: corrupted dest array:\n%s\n", o) +} + +// DereferenceDestArray resolves the destination for key. +func (xRefTable *XRefTable) DereferenceDestArray(key string) (types.Array, error) { + o, ok := xRefTable.Names["Dests"].Value(key) + if !ok { + return nil, errors.Errorf("pdfcpu: corrupted named destination for: %s", key) + } + return xRefTable.dereferenceDestArray(o) +} + +// DereferenceDictEntry returns a dereferenced dict entry. +func (xRefTable *XRefTable) DereferenceDictEntry(d types.Dict, key string) (types.Object, error) { + o, found := d.Find(key) + if !found || o == nil { + return nil, errors.Errorf("pdfcpu: dict=%s entry=%s missing.", d, key) + } + return xRefTable.Dereference(o) +} + +// DereferenceStringEntryBytes returns the bytes of a string entry of d. +func (xRefTable *XRefTable) DereferenceStringEntryBytes(d types.Dict, key string) ([]byte, error) { + o, found := d.Find(key) + if !found || o == nil { + return nil, nil + } + o, err := xRefTable.Dereference(o) + if err != nil { + return nil, nil + } + + switch o := o.(type) { + case types.StringLiteral: + bb, err := types.Unescape(o.Value()) + if err != nil { + return nil, err + } + return bb, nil + + case types.HexLiteral: + return o.Bytes() + + } + + return nil, errors.Errorf("pdfcpu: DereferenceStringEntryBytes dict=%s entry=%s, wrong type %T <%v>", d, key, o, o) +} + +func (xRefTable *XRefTable) DestName(obj types.Object) (string, error) { + dest, err := xRefTable.Dereference(obj) + if err != nil { + return "", err + } + + var s string + + switch d := dest.(type) { + case types.Name: + s = d.Value() + case types.StringLiteral: + s, err = types.StringLiteralToString(d) + case types.HexLiteral: + s, err = types.HexLiteralToString(d) + } + + return s, err +} diff --git a/pkg/pdfcpu/model/destination.go b/pkg/pdfcpu/model/destination.go new file mode 100644 index 0000000000000000000000000000000000000000..405bcf20713dbc76ffa6e3f190995834f912bb10 --- /dev/null +++ b/pkg/pdfcpu/model/destination.go @@ -0,0 +1,81 @@ +/* +Copyright 2023 The pdfcpu Authors. + +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 + + 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 model + +import "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + +// DestinationType represents the various PDF destination types. +type DestinationType int + +// See table 151 +const ( + DestXYZ DestinationType = iota // [page /XYZ left top zoom] + DestFit // [page /Fit] + DestFitH // [page /FitH top] + DestFitV // [page /FitV left] + DestFitR // [page /FitR left bottom right top] + DestFitB // [page /FitB] + DestFitBH // [page /FitBH top] + DestFitBV // [page /FitBV left] +) + +// DestinationTypeStrings manages string representations for destination types. +var DestinationTypeStrings = map[DestinationType]string{ + DestXYZ: "XYZ", // Position (left, top) at upper-left corner of window. + DestFit: "Fit", // Fit entire page within window. + DestFitH: "FitH", // Position with (top) at top edge of window. + DestFitV: "FitV", // Position with (left) positioned at left edge of window. + DestFitR: "FitR", // Fit (left, bottom, right, top) entirely within window. + DestFitB: "FitB", // Magnify content just enough to fit its bounding box entirely within window. + DestFitBH: "FitBH", // Position with (top) at top edge of window and contents fit bounding box width within window. + DestFitBV: "FitBV", // Position with (left) at left edge of window and contents fit bounding box height within window. +} + +// Destination represents a PDF destination. +type Destination struct { + Typ DestinationType + PageNr int + Left, Bottom, Right, Top int + Zoom float32 +} + +func (dest Destination) String() string { + return DestinationTypeStrings[dest.Typ] +} + +func (dest Destination) Name() types.Name { + return types.Name(DestinationTypeStrings[dest.Typ]) +} + +func (dest Destination) Array(indRef types.IndirectRef) types.Array { + arr := types.Array{indRef, dest.Name()} + switch dest.Typ { + case DestXYZ: + arr = append(arr, types.Integer(dest.Left), types.Integer(dest.Top), types.Float(dest.Zoom)) + case DestFitH: + arr = append(arr, types.Integer(dest.Top)) + case DestFitV: + arr = append(arr, types.Integer(dest.Left)) + case DestFitR: + arr = append(arr, types.Integer(dest.Left), types.Integer(dest.Bottom), types.Integer(dest.Right), types.Integer(dest.Top)) + case DestFitBH: + arr = append(arr, types.Integer(dest.Top)) + case DestFitBV: + arr = append(arr, types.Integer(dest.Left)) + } + return arr +} diff --git a/pkg/pdfcpu/model/document.go b/pkg/pdfcpu/model/document.go new file mode 100644 index 0000000000000000000000000000000000000000..a0e74dbfe74028c308949411a49a8fadf1beb24e --- /dev/null +++ b/pkg/pdfcpu/model/document.go @@ -0,0 +1,884 @@ +/* +Copyright 2023 The pdfcpu Authors. + +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 + + 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 model + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +type PageMode int + +const ( + PageModeUseNone PageMode = iota + PageModeUseOutlines + PageModeUseThumbs + PageModeFullScreen + PageModeUseOC + PageModeUseAttachments +) + +func PageModeFor(s string) *PageMode { + if s == "" { + return nil + } + var pm PageMode + switch strings.ToLower(s) { + case "usenone": + pm = PageModeUseNone + case "useoutlines": + pm = PageModeUseOutlines + case "usethumbs": + pm = PageModeUseThumbs + case "fullscreen": + pm = PageModeFullScreen + case "useoc": + pm = PageModeUseOC + case "useattachments": + pm = PageModeUseAttachments + default: + return nil + } + return &pm +} + +func (pm *PageMode) String() string { + if pm == nil { + return "" + } + switch *pm { + case PageModeUseNone: + return "UseNone" // = default + case PageModeUseOutlines: + return "UseOutlines" + case PageModeUseThumbs: + return "UseThumbs" + case PageModeFullScreen: + return "FullScreen" + case PageModeUseOC: + return "UseOC" + case PageModeUseAttachments: + return "UseAttachments" + default: + return "?" + } +} + +type PageLayout int + +const ( + PageLayoutSinglePage PageLayout = iota + PageLayoutTwoColumnLeft + PageLayoutTwoColumnRight + PageLayoutTwoPageLeft + PageLayoutTwoPageRight +) + +func PageLayoutFor(s string) *PageLayout { + if s == "" { + return nil + } + var pl PageLayout + switch strings.ToLower(s) { + case "singlepage": + pl = PageLayoutSinglePage + case "twocolumnleft": + pl = PageLayoutTwoColumnLeft + case "twocolumnright": + pl = PageLayoutTwoColumnRight + case "twopageleft": + pl = PageLayoutTwoPageLeft + case "twopageright": + pl = PageLayoutTwoPageRight + default: + return nil + } + return &pl +} + +func (pl *PageLayout) String() string { + if pl == nil { + return "" + } + switch *pl { + case PageLayoutSinglePage: + return "SinglePage" // = default + case PageLayoutTwoColumnLeft: + return "TwoColumnLeft" + case PageLayoutTwoColumnRight: + return "TwoColumnRight" + case PageLayoutTwoPageLeft: + return "TwoPageLeft" + case PageLayoutTwoPageRight: + return "TwoPageRight" + default: + return "?" + } +} + +type NonFullScreenPageMode PageMode + +const ( + NFSPageModeUseNone NonFullScreenPageMode = iota + NFSPageModeUseOutlines + NFSPageModeUseThumb + NFSPageModeUseOC +) + +type PageBoundary int + +const ( + MediaBox PageBoundary = iota + CropBox + TrimBox + BleedBox + ArtBox +) + +func PageBoundaryFor(s string) *PageBoundary { + if s == "" { + return nil + } + var pb PageBoundary + switch strings.ToLower(s) { + case "mediabox": + pb = MediaBox + case "cropbox": + pb = CropBox + case "trimbox": + pb = TrimBox + case "bleedbox": + pb = BleedBox + case "artbox": + pb = ArtBox + default: + return nil + } + return &pb +} + +func (pb *PageBoundary) String() string { + if pb == nil { + return "" + } + switch *pb { + case MediaBox: + return "MediaBox" + case CropBox: + return "CropBox" + case TrimBox: + return "TrimBox" + case BleedBox: + return "BleedBox" + case ArtBox: + return "ArtBox" + default: + return "?" + } +} + +type PrintScaling int + +const ( + PrintScalingNone PrintScaling = iota + PrintScalingAppDefault +) + +func PrintScalingFor(s string) *PrintScaling { + if s == "" { + return nil + } + var ps PrintScaling + switch strings.ToLower(s) { + case "none": + ps = PrintScalingNone + case "appdefault": + ps = PrintScalingAppDefault + default: + return nil + } + return &ps +} + +func (ps *PrintScaling) String() string { + if ps == nil { + return "" + } + switch *ps { + case PrintScalingNone: + return "None" + case PrintScalingAppDefault: + return "AppDefault" + default: + return "?" + } +} + +type Direction int + +const ( + L2R Direction = iota + R2L +) + +func DirectionFor(s string) *Direction { + if s == "" { + return nil + } + var d Direction + switch strings.ToLower(s) { + case "l2r": + d = L2R + case "r2l": + d = R2L + default: + return nil + } + return &d +} + +func (d *Direction) String() string { + if d == nil { + return "" + } + switch *d { + case L2R: + return "L2R" + case R2L: + return "R2L" + default: + return "?" + } +} + +type PaperHandling int + +const ( + Simplex PaperHandling = iota + DuplexFlipShortEdge + DuplexFlipLongEdge +) + +func PaperHandlingFor(s string) *PaperHandling { + if s == "" { + return nil + } + var ph PaperHandling + switch strings.ToLower(s) { + case "simplex": + ph = Simplex + case "duplexflipshortedge": + ph = DuplexFlipShortEdge + case "duplexfliplongedge": + ph = DuplexFlipLongEdge + default: + return nil + } + return &ph +} + +func (ph *PaperHandling) String() string { + if ph == nil { + return "" + } + switch *ph { + case Simplex: + return "Simplex" + case DuplexFlipShortEdge: + return "DuplexFlipShortEdge" + case DuplexFlipLongEdge: + return "DuplexFlipLongEdge" + default: + return "?" + } +} + +// ViewerPreferences see 12.2 Table 147 +type ViewerPreferences struct { + HideToolbar *bool + HideMenubar *bool + HideWindowUI *bool + FitWindow *bool + CenterWindow *bool + DisplayDocTitle *bool // since 1.4 + NonFullScreenPageMode *NonFullScreenPageMode + Direction *Direction // since 1.3 + ViewArea *PageBoundary // since 1.4 to 1.7 + ViewClip *PageBoundary // since 1.4 to 1.7 + PrintArea *PageBoundary // since 1.4 to 1.7 + PrintClip *PageBoundary // since 1.4 to 1.7 + PrintScaling *PrintScaling // since 1.6 + Duplex *PaperHandling // since 1.7 + PickTrayByPDFSize *bool // since 1.7 + PrintPageRange types.Array // since 1.7 + NumCopies *types.Integer // since 1.7 + Enforce types.Array // since 2.0 +} + +func (vp *ViewerPreferences) validatePrinterPreferences(version Version) error { + if vp.PrintScaling != nil && version < V16 { + return errors.Errorf("pdfcpu: invalid viewer preference \"PrintScaling\" - since PDF 1.6, got: %v\n", version) + } + if vp.Duplex != nil && version < V17 { + return errors.Errorf("pdfcpu: invalid viewer preference \"Duplex\" - since PDF 1.7, got: %v\n", version) + } + if vp.PickTrayByPDFSize != nil && version < V17 { + return errors.Errorf("pdfcpu: invalid viewer preference \"PickTrayByPDFSize\" - since PDF 1.7, got: %v\n", version) + } + if len(vp.PrintPageRange) > 0 && version < V17 { + return errors.Errorf("pdfcpu: invalid viewer preference \"PrintPageRange\" - since PDF 1.7, got: %v\n", version) + } + if vp.NumCopies != nil && version < V17 { + return errors.Errorf("pdfcpu: invalid viewer preference \"NumCopies\" - since PDF 1.7, got: %v\n", version) + } + if len(vp.Enforce) > 0 && version < V20 { + return errors.Errorf("pdfcpu: invalid viewer preference \"Enforce\" - since PDF 2.0, got: %v\n", version) + } + + return nil +} + +func (vp *ViewerPreferences) Validate(version Version) error { + if vp.Direction != nil && version < V13 { + return errors.Errorf("pdfcpu: invalid viewer preference \"Direction\" - since PDF 1.3, got: %v\n", version) + } + if vp.ViewArea != nil && (version < V14 || version > V17) { + return errors.Errorf("pdfcpu: invalid viewer preference \"ViewArea\" - since PDF 1.4 until PDF 1.7, got: %v\n", version) + } + if vp.ViewClip != nil && (version < V14 || version > V17) { + return errors.Errorf("pdfcpu: invalid viewer preference \"ViewClip\" - since PDF 1.4 until PDF 1.7, got: %v\n", version) + } + if vp.PrintArea != nil && (version < V14 || version > V17) { + return errors.Errorf("pdfcpu: invalid viewer preference \"PrintArea\" - since PDF 1.4 until PDF 1.7, got: %v\n", version) + } + if vp.PrintClip != nil && (version < V14 || version > V17) { + return errors.Errorf("pdfcpu: invalid viewer preference \"PrintClip\" - since PDF 1.4 until PDF 1.7, got: %v\n", version) + } + + return vp.validatePrinterPreferences(version) +} + +func (vp *ViewerPreferences) SetHideToolBar(val bool) { + vp.HideToolbar = &val +} + +func (vp *ViewerPreferences) SetHideMenuBar(val bool) { + vp.HideMenubar = &val +} + +func (vp *ViewerPreferences) SetHideWindowUI(val bool) { + vp.HideWindowUI = &val +} + +func (vp *ViewerPreferences) SetFitWindow(val bool) { + vp.FitWindow = &val +} + +func (vp *ViewerPreferences) SetCenterWindow(val bool) { + vp.CenterWindow = &val +} + +func (vp *ViewerPreferences) SetDisplayDocTitle(val bool) { + vp.DisplayDocTitle = &val +} + +func (vp *ViewerPreferences) SetPickTrayByPDFSize(val bool) { + vp.PickTrayByPDFSize = &val +} + +func (vp *ViewerPreferences) SetNumCopies(i int) { + vp.NumCopies = (*types.Integer)(&i) +} + +func (vp *ViewerPreferences) populatePrinterPreferences(vp1 *ViewerPreferences) { + if vp1.PrintArea != nil { + vp.PrintArea = vp1.PrintArea + } + if vp1.PrintClip != nil { + vp.PrintClip = vp1.PrintClip + } + if vp1.PrintScaling != nil { + vp.PrintScaling = vp1.PrintScaling + } + if vp1.Duplex != nil { + vp.Duplex = vp1.Duplex + } + if vp1.PickTrayByPDFSize != nil { + vp.PickTrayByPDFSize = vp1.PickTrayByPDFSize + } + if len(vp1.PrintPageRange) > 0 { + vp.PrintPageRange = vp1.PrintPageRange + } + if vp1.NumCopies != nil { + vp.NumCopies = vp1.NumCopies + } + if len(vp1.Enforce) > 0 { + vp.Enforce = vp1.Enforce + } +} + +func (vp *ViewerPreferences) Populate(vp1 *ViewerPreferences) { + if vp1.HideToolbar != nil { + vp.HideToolbar = vp1.HideToolbar + } + if vp1.HideMenubar != nil { + vp.HideMenubar = vp1.HideMenubar + } + if vp1.HideWindowUI != nil { + vp.HideWindowUI = vp1.HideWindowUI + } + if vp1.FitWindow != nil { + vp.FitWindow = vp1.FitWindow + } + if vp1.CenterWindow != nil { + vp.CenterWindow = vp1.CenterWindow + } + if vp1.DisplayDocTitle != nil { + vp.DisplayDocTitle = vp1.DisplayDocTitle + } + if vp1.NonFullScreenPageMode != nil { + vp.NonFullScreenPageMode = vp1.NonFullScreenPageMode + } + if vp1.Direction != nil { + vp.Direction = vp1.Direction + } + if vp1.ViewArea != nil { + vp.ViewArea = vp1.ViewArea + } + if vp1.ViewClip != nil { + vp.ViewClip = vp1.ViewClip + } + + vp.populatePrinterPreferences(vp1) +} + +func DefaultViewerPreferences(version Version) *ViewerPreferences { + vp := ViewerPreferences{} + vp.SetHideToolBar(false) + vp.SetHideMenuBar(false) + vp.SetHideWindowUI(false) + vp.SetFitWindow(false) + vp.SetCenterWindow(false) + if version >= V14 { + vp.SetDisplayDocTitle(false) + } + vp.NonFullScreenPageMode = (*NonFullScreenPageMode)(PageModeFor("UseNone")) + if version >= V13 { + vp.Direction = DirectionFor("L2R") + } + if version >= V14 && version < V20 { + vp.ViewArea = PageBoundaryFor("CropBox") + vp.ViewClip = PageBoundaryFor("CropBox") + vp.PrintArea = PageBoundaryFor("CropBox") + vp.PrintClip = PageBoundaryFor("CropBox") + } + if version >= V16 { + vp.PrintScaling = PrintScalingFor("AppDefault") + } + if version >= V17 { + vp.SetNumCopies(1) + } + + return &vp +} + +func ViewerPreferencesWithDefaults(vp *ViewerPreferences, version Version) (*ViewerPreferences, error) { + vp1 := DefaultViewerPreferences(version) + + if vp == nil { + return vp1, nil + } + + vp1.Populate(vp) + + return vp1, nil +} + +type ViewerPrefJSON struct { + HideToolbar *bool `json:"hideToolbar,omitempty"` + HideMenubar *bool `json:"hideMenubar,omitempty"` + HideWindowUI *bool `json:"hideWindowUI,omitempty"` + FitWindow *bool `json:"fitWindow,omitempty"` + CenterWindow *bool `json:"centerWindow,omitempty"` + DisplayDocTitle *bool `json:"displayDocTitle,omitempty"` + NonFullScreenPageMode string `json:"nonFullScreenPageMode,omitempty"` + Direction string `json:"direction,omitempty"` + ViewArea string `json:"viewArea,omitempty"` + ViewClip string `json:"viewClip,omitempty"` + PrintArea string `json:"printArea,omitempty"` + PrintClip string `json:"printClip,omitempty"` + PrintScaling string `json:"printScaling,omitempty"` + Duplex string `json:"duplex,omitempty"` + PickTrayByPDFSize *bool `json:"pickTrayByPDFSize,omitempty"` + PrintPageRange []int `json:"printPageRange,omitempty"` + NumCopies *int `json:"numCopies,omitempty"` + Enforce []string `json:"enforce,omitempty"` +} + +func (vp *ViewerPreferences) MarshalJSON() ([]byte, error) { + vpJSON := ViewerPrefJSON{ + HideToolbar: vp.HideToolbar, + HideMenubar: vp.HideMenubar, + HideWindowUI: vp.HideWindowUI, + FitWindow: vp.FitWindow, + CenterWindow: vp.CenterWindow, + DisplayDocTitle: vp.DisplayDocTitle, + NonFullScreenPageMode: (*PageMode)(vp.NonFullScreenPageMode).String(), + Direction: vp.Direction.String(), + ViewArea: vp.ViewArea.String(), + ViewClip: vp.ViewClip.String(), + PrintArea: vp.PrintArea.String(), + PrintClip: vp.PrintClip.String(), + PrintScaling: vp.PrintScaling.String(), + Duplex: vp.Duplex.String(), + PickTrayByPDFSize: vp.PickTrayByPDFSize, + NumCopies: (*int)(vp.NumCopies), + } + + if len(vp.PrintPageRange) > 0 { + var ii []int + for _, v := range vp.PrintPageRange { + ii = append(ii, v.(types.Integer).Value()) + } + vpJSON.PrintPageRange = ii + } + + if len(vp.Enforce) > 0 { + var ss []string + for _, v := range vp.Enforce { + ss = append(ss, v.(types.Name).Value()) + } + vpJSON.Enforce = ss + } + + return json.Marshal(&vpJSON) +} + +func (vp *ViewerPreferences) unmarshalPrintPageRange(vpJSON ViewerPrefJSON) error { + if len(vpJSON.PrintPageRange) > 0 { + arr := vpJSON.PrintPageRange + if len(arr)%2 > 0 { + return errors.New("pdfcpu: invalid \"PrintPageRange\" - expecting pairs of ascending page numbers\n") + } + for i := 0; i < len(arr); i += 2 { + if arr[i] >= arr[i+1] { + // TODO validate ascending, non overlapping int intervals. + return errors.New("pdfcpu: invalid \"PrintPageRange\" - expecting pairs of ascending page numbers\n") + } + } + vp.PrintPageRange = types.NewIntegerArray(arr...) + } + + return nil +} + +func (vp *ViewerPreferences) unmarshalPrinterPreferences(vpJSON ViewerPrefJSON) error { + vp.PrintArea = PageBoundaryFor(vpJSON.PrintArea) + if vpJSON.PrintArea != "" && vp.PrintArea == nil { + return errors.Errorf("pdfcpu: unknown \"PrintArea\", got: %s want one of: MediaBox, CropBox, TrimBox, BleedBox, ArtBox\n", vpJSON.PrintArea) + } + + vp.PrintClip = PageBoundaryFor(vpJSON.PrintClip) + if vpJSON.PrintClip != "" && vp.PrintClip == nil { + return errors.Errorf("pdfcpu: unknown \"PrintClip\", got: %s want one of: MediaBox, CropBox, TrimBox, BleedBox, ArtBox\n", vpJSON.PrintClip) + } + + vp.PrintScaling = PrintScalingFor(vpJSON.PrintScaling) + if vpJSON.PrintScaling != "" && vp.PrintScaling == nil { + return errors.Errorf("pdfcpu: unknown \"PrintScaling\", got: %s, want one of: None, AppDefault", vpJSON.PrintScaling) + } + + vp.Duplex = PaperHandlingFor(vpJSON.Duplex) + if vpJSON.Duplex != "" && vp.Duplex == nil { + return errors.Errorf("pdfcpu: unknown \"Duplex\", got: %s, want one of: Simplex, DuplexFlipShortEdge, DuplexFlipLongEdge", vpJSON.Duplex) + } + + if err := vp.unmarshalPrintPageRange(vpJSON); err != nil { + return err + } + + if len(vpJSON.Enforce) > 1 { + return errors.New("pdfcpu: \"Enforce\" must be array with one element: \"PrintScaling\"\n") + } + + if len(vpJSON.Enforce) > 0 { + if vpJSON.Enforce[0] != "PrintScaling" { + return errors.New("pdfcpu: \"Enforce\" must be array with one element: \"PrintScaling\"\n") + } + vp.Enforce = types.NewNameArray("PrintScaling") + } + + return nil +} + +func (vp *ViewerPreferences) UnmarshalJSON(data []byte) error { + + vpJSON := ViewerPrefJSON{} + + if err := json.Unmarshal(data, &vpJSON); err != nil { + return err + } + + *vp = ViewerPreferences{ + HideToolbar: vpJSON.HideToolbar, + HideMenubar: vpJSON.HideMenubar, + HideWindowUI: vpJSON.HideWindowUI, + FitWindow: vpJSON.FitWindow, + CenterWindow: vpJSON.CenterWindow, + DisplayDocTitle: vpJSON.DisplayDocTitle, + PickTrayByPDFSize: vpJSON.PickTrayByPDFSize, + NumCopies: (*types.Integer)(vpJSON.NumCopies), + } + + if vp.NumCopies != nil && *vp.NumCopies < 1 { + return errors.Errorf("pdfcpu: invalid \"NumCopies\", got: %d, want a numerical value > 0", *vp.NumCopies) + } + + vp.NonFullScreenPageMode = (*NonFullScreenPageMode)(PageModeFor(vpJSON.NonFullScreenPageMode)) + if vpJSON.NonFullScreenPageMode != "" { + if vp.NonFullScreenPageMode == nil { + return errors.Errorf("pdfcpu: unknown \"NonFullScreenPageMode\", got: %s want one of: UseNone, UseOutlines, UseThumbs, UseOC\n", vpJSON.NonFullScreenPageMode) + } + pm := (PageMode)(*vp.NonFullScreenPageMode) + if pm == PageModeFullScreen || pm == PageModeUseAttachments { + return errors.Errorf("pdfcpu: unknown \"NonFullScreenPageMode\", got: %s want one of: UseNone, UseOutlines, UseThumbs, UseOC\n", vpJSON.NonFullScreenPageMode) + } + } + + vp.Direction = DirectionFor(vpJSON.Direction) + if vpJSON.Direction != "" && vp.Direction == nil { + return errors.Errorf("pdfcpu: unknown \"Direction\", got: %s want one of: L2R, R2L\n", vpJSON.Direction) + } + + vp.ViewArea = PageBoundaryFor(vpJSON.ViewArea) + if vpJSON.ViewArea != "" && vp.ViewArea == nil { + return errors.Errorf("pdfcpu: unknown \"ViewArea\", got: %s want one of: MediaBox, CropBox, TrimBox, BleedBox, ArtBox\n", vpJSON.ViewArea) + } + + vp.ViewClip = PageBoundaryFor(vpJSON.ViewClip) + if vpJSON.ViewClip != "" && vp.ViewClip == nil { + return errors.Errorf("pdfcpu: unknown \"ViewClip\", got: %s want one of: MediaBox, CropBox, TrimBox, BleedBox, ArtBox\n", vpJSON.ViewClip) + } + + return vp.unmarshalPrinterPreferences(vpJSON) +} + +func renderViewerFlags(vp ViewerPreferences, ss *[]string) { + if vp.HideToolbar != nil { + *ss = append(*ss, fmt.Sprintf("%22s%s = %t", "", "HideToolbar", *vp.HideToolbar)) + } + + if vp.HideMenubar != nil { + *ss = append(*ss, fmt.Sprintf("%22s%s = %t", "", "HideMenubar", *vp.HideMenubar)) + } + + if vp.HideWindowUI != nil { + *ss = append(*ss, fmt.Sprintf("%22s%s = %t", "", "HideWindowUI", *vp.HideWindowUI)) + } + + if vp.FitWindow != nil { + *ss = append(*ss, fmt.Sprintf("%22s%s = %t", "", "FitWindow", *vp.FitWindow)) + } + + if vp.CenterWindow != nil { + *ss = append(*ss, fmt.Sprintf("%22s%s = %t", "", "CenterWindow", *vp.CenterWindow)) + } + + if vp.DisplayDocTitle != nil { + *ss = append(*ss, fmt.Sprintf("%22s%s = %t", "", "DisplayDocTitle", *vp.DisplayDocTitle)) + } + + if vp.NonFullScreenPageMode != nil { + pm := PageMode(*vp.NonFullScreenPageMode) + *ss = append(*ss, fmt.Sprintf("%22s%s = %s", "", "NonFullScreenPageMode", pm.String())) + } +} + +func listViewerFlags(vp ViewerPreferences, ss *[]string) { + if vp.HideToolbar != nil { + *ss = append(*ss, fmt.Sprintf("%s = %t", "HideToolbar", *vp.HideToolbar)) + } + + if vp.HideMenubar != nil { + *ss = append(*ss, fmt.Sprintf("%s = %t", "HideMenubar", *vp.HideMenubar)) + } + + if vp.HideWindowUI != nil { + *ss = append(*ss, fmt.Sprintf("%s = %t", "HideWindowUI", *vp.HideWindowUI)) + } + + if vp.FitWindow != nil { + *ss = append(*ss, fmt.Sprintf("%s = %t", "FitWindow", *vp.FitWindow)) + } + + if vp.CenterWindow != nil { + *ss = append(*ss, fmt.Sprintf("%s = %t", "CenterWindow", *vp.CenterWindow)) + } + + if vp.DisplayDocTitle != nil { + *ss = append(*ss, fmt.Sprintf("%s = %t", "DisplayDocTitle", *vp.DisplayDocTitle)) + } + + if vp.NonFullScreenPageMode != nil { + pm := PageMode(*vp.NonFullScreenPageMode) + *ss = append(*ss, fmt.Sprintf("%s = %s", "NonFullScreenPageMode", pm.String())) + } +} + +func (vp ViewerPreferences) listPrinterPreferences() []string { + var ss []string + + if vp.PrintArea != nil { + ss = append(ss, fmt.Sprintf("%s = %s", "PrintArea", vp.PrintArea)) + } + + if vp.PrintClip != nil { + ss = append(ss, fmt.Sprintf("%s = %s", "PrintClip", vp.PrintClip)) + } + + if vp.PrintScaling != nil { + ss = append(ss, fmt.Sprintf("%s = %s", "PrintScaling", vp.PrintScaling)) + } + + if vp.Duplex != nil { + ss = append(ss, fmt.Sprintf("%s = %s", "Duplex", vp.Duplex)) + } + + if vp.PickTrayByPDFSize != nil { + ss = append(ss, fmt.Sprintf("%s = %t", "PickTrayByPDFSize", *vp.PickTrayByPDFSize)) + } + + if len(vp.PrintPageRange) > 0 { + var ss1 []string + for i := 0; i < len(vp.PrintPageRange); i += 2 { + ss1 = append(ss1, fmt.Sprintf("%d-%d", vp.PrintPageRange[i].(types.Integer), vp.PrintPageRange[i+1].(types.Integer))) + } + ss = append(ss, fmt.Sprintf("%s = %s", "PrintPageRange", strings.Join(ss1, ","))) + } + + if vp.NumCopies != nil { + ss = append(ss, fmt.Sprintf("%s = %d", "NumCopies", *vp.NumCopies)) + } + + if len(vp.Enforce) > 0 { + var ss1 []string + for _, v := range vp.Enforce { + ss1 = append(ss1, v.String()) + } + ss = append(ss, fmt.Sprintf("%s = %s", "Enforce", strings.Join(ss1, ","))) + } + + return ss +} + +// List generates output for the viewer pref command. +func (vp ViewerPreferences) List() []string { + var ss []string + + listViewerFlags(vp, &ss) + + if vp.Direction != nil { + ss = append(ss, fmt.Sprintf("%s = %s", "Direction", vp.Direction)) + } + + if vp.ViewArea != nil { + ss = append(ss, fmt.Sprintf("%s = %s", "ViewArea", vp.ViewArea)) + } + + if vp.ViewClip != nil { + ss = append(ss, fmt.Sprintf("%s = %s", "ViewClip", vp.ViewClip)) + } + + ss = append(ss, vp.listPrinterPreferences()...) + + if len(ss) > 0 { + ss1 := []string{"Viewer preferences:"} + for _, s := range ss { + ss1 = append(ss1, " "+s) + } + ss = ss1 + } else { + ss = append(ss, "No viewer preferences available.") + } + + return ss +} + +// String generates output for the info command. +func (vp ViewerPreferences) String() string { + var ss []string + + renderViewerFlags(vp, &ss) + + if vp.Direction != nil { + ss = append(ss, fmt.Sprintf("%22s%s = %s", "", "Direction", vp.Direction)) + } + + if vp.ViewArea != nil { + ss = append(ss, fmt.Sprintf("%22s%s = %s", "", "ViewArea", vp.ViewArea)) + } + + if vp.ViewClip != nil { + ss = append(ss, fmt.Sprintf("%22s%s = %s", "", "ViewClip", vp.ViewClip)) + } + + if vp.PrintArea != nil { + ss = append(ss, fmt.Sprintf("%22s%s = %s", "", "PrintArea", vp.PrintArea)) + } + + if vp.PrintClip != nil { + ss = append(ss, fmt.Sprintf("%22s%s = %s", "", "PrintClip", vp.PrintClip)) + } + + if vp.PrintScaling != nil { + ss = append(ss, fmt.Sprintf("%22s%s = %s", "", "PrintScaling", vp.PrintScaling)) + } + + if vp.Duplex != nil { + ss = append(ss, fmt.Sprintf("%22s%s = %s", "", "Duplex", vp.Duplex)) + } + + if vp.PickTrayByPDFSize != nil { + ss = append(ss, fmt.Sprintf("%22s%s = %t", "", "PickTrayByPDFSize", *vp.PickTrayByPDFSize)) + } + + if len(vp.PrintPageRange) > 0 { + var ss1 []string + for i := 0; i < len(vp.PrintPageRange); i += 2 { + ss1 = append(ss1, fmt.Sprintf("%d-%d", vp.PrintPageRange[i].(types.Integer), vp.PrintPageRange[i+1].(types.Integer))) + } + ss = append(ss, fmt.Sprintf("%22s%s = %s", "", "PrintPageRange", strings.Join(ss1, ","))) + } + + if vp.NumCopies != nil { + ss = append(ss, fmt.Sprintf("%22s%s = %d", "", "NumCopies", *vp.NumCopies)) + } + + if len(vp.Enforce) > 0 { + var ss1 []string + for _, v := range vp.Enforce { + ss1 = append(ss1, v.String()) + } + ss = append(ss, fmt.Sprintf("%22s%s = %s", "", "Enforce", strings.Join(ss1, ","))) + } + + return strings.TrimSpace(strings.Join(ss, "\n")) +} diff --git a/pkg/pdfcpu/model/equal.go b/pkg/pdfcpu/model/equal.go new file mode 100644 index 0000000000000000000000000000000000000000..1cb70e68b61e94a5caf7e1d2f341b8e9294562db --- /dev/null +++ b/pkg/pdfcpu/model/equal.go @@ -0,0 +1,239 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 model + +import ( + "bytes" + "fmt" + "strings" + + "github.com/pkg/errors" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +// EqualObjects returns true if two objects are equal in the context of given xrefTable. +// Some object and an indirect reference to it are treated as equal. +// Objects may in fact be object trees. +func EqualObjects(o1, o2 types.Object, xRefTable *XRefTable) (ok bool, err error) { + + //log.Debug.Printf("equalObjects: comparing %T with %T \n", o1, o2) + + ir1, ok := o1.(types.IndirectRef) + if ok { + ir2, ok := o2.(types.IndirectRef) + if ok && ir1 == ir2 { + return true, nil + } + } + + o1, err = xRefTable.Dereference(o1) + if err != nil { + return false, err + } + + o2, err = xRefTable.Dereference(o2) + if err != nil { + return false, err + } + + if o1 == nil { + return o2 != nil, nil + } + + o1Type := fmt.Sprintf("%T", o1) + o2Type := fmt.Sprintf("%T", o2) + //log.Debug.Printf("equalObjects: comparing dereferenced %s with %s \n", o1Type, o2Type) + + if o1Type != o2Type { + return false, nil + } + + switch o1.(type) { + + case types.Name, types.StringLiteral, types.HexLiteral, + types.Integer, types.Float, types.Boolean: + ok = o1 == o2 + + case types.Dict: + ok, err = equalDicts(o1.(types.Dict), o2.(types.Dict), xRefTable) + + case types.StreamDict: + sd1 := o1.(types.StreamDict) + sd2 := o2.(types.StreamDict) + ok, err = EqualStreamDicts(&sd1, &sd2, xRefTable) + + case types.Array: + ok, err = equalArrays(o1.(types.Array), o2.(types.Array), xRefTable) + + default: + err = errors.Errorf("equalObjects: unhandled compare for type %s\n", o1Type) + } + + return ok, err +} + +func equalArrays(a1, a2 types.Array, xRefTable *XRefTable) (bool, error) { + + if len(a1) != len(a2) { + return false, nil + } + + for i, o1 := range a1 { + + ok, err := EqualObjects(o1, a2[i], xRefTable) + if err != nil { + return false, err + } + + if !ok { + return false, nil + } + } + + return true, nil +} + +// EqualStreamDicts returns true if two stream dicts are equal and contain the same bytes. +func EqualStreamDicts(sd1, sd2 *types.StreamDict, xRefTable *XRefTable) (bool, error) { + + ok, err := equalDicts(sd1.Dict, sd2.Dict, xRefTable) + if err != nil { + return false, err + } + + if !ok { + return false, nil + } + + if sd1.Raw == nil || sd2 == nil { + return false, errors.New("pdfcpu: EqualStreamDicts: stream dict not loaded") + } + + return bytes.Equal(sd1.Raw, sd2.Raw), nil +} + +func equalFontNames(v1, v2 types.Object, xRefTable *XRefTable) (bool, error) { + + v1, err := xRefTable.Dereference(v1) + if err != nil { + return false, err + } + bf1, ok := v1.(types.Name) + if !ok { + return false, errors.Errorf("equalFontNames: type cast problem") + } + + v2, err = xRefTable.Dereference(v2) + if err != nil { + return false, err + } + bf2, ok := v2.(types.Name) + if !ok { + return false, errors.Errorf("equalFontNames: type cast problem") + } + + // Ignore fontname prefix + i := strings.Index(string(bf1), "+") + if i > 0 { + bf1 = bf1[i+1:] + } + + i = strings.Index(string(bf2), "+") + if i > 0 { + bf2 = bf2[i+1:] + } + + //log.Debug.Printf("equalFontNames: bf1=%s fb2=%s\n", bf1, bf2) + + return bf1 == bf2, nil +} + +func equalDicts(d1, d2 types.Dict, xRefTable *XRefTable) (bool, error) { + + //log.Debug.Printf("equalDicts: %v\n%v\n", d1, d2) + + if d1.Len() != d2.Len() { + return false, nil + } + + t1, t2 := d1.Type(), d2.Type() + fontDicts := (t1 != nil && *t1 == "Font") && (t2 != nil && *t2 == "Font") + + for key, v1 := range d1 { + + v2, found := d2[key] + if !found { + //log.Debug.Printf("equalDict: return false, key=%s\n", key) + return false, nil + } + + // Special treatment for font dicts + if fontDicts && (key == "BaseFont" || key == "FontName" || key == "Name") { + + ok, err := equalFontNames(v1, v2, xRefTable) + if err != nil { + //log.Debug.Printf("equalDict: return2 false, key=%s v1=%v\nv2=%v\n", key, v1, v2) + return false, err + } + + if !ok { + //log.Debug.Printf("equalDict: return3 false, key=%s v1=%v\nv2=%v\n", key, v1, v2) + return false, nil + } + + continue + } + + ok, err := EqualObjects(v1, v2, xRefTable) + if err != nil { + //log.Debug.Printf("equalDict: return4 false, key=%s v1=%v\nv2=%v\n%v\n", key, v1, v2, err) + return false, err + } + + if !ok { + //log.Debug.Printf("equalDict: return5 false, key=%s v1=%v\nv2=%v\n", key, v1, v2) + return false, nil + } + + } + + //log.Debug.Println("equalDict: return true") + + return true, nil +} + +// EqualFontDicts returns true, if two font dicts are equal. +func EqualFontDicts(fd1, fd2 types.Dict, xRefTable *XRefTable) (bool, error) { + + //log.Debug.Printf("EqualFontDicts: %v\n%v\n", fd1, fd2) + + if fd1 == nil { + return fd2 == nil, nil + } + + if fd2 == nil { + return false, nil + } + + ok, err := equalDicts(fd1, fd2, xRefTable) + if err != nil { + return false, err + } + + return ok, nil +} diff --git a/pkg/pdfcpu/model/image.go b/pkg/pdfcpu/model/image.go new file mode 100644 index 0000000000000000000000000000000000000000..2dd30df25bcee635c0f9d9886e983073715f7f7d --- /dev/null +++ b/pkg/pdfcpu/model/image.go @@ -0,0 +1,628 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 model + +import ( + "bytes" + "image" + "image/color" + "image/draw" + "image/jpeg" + _ "image/png" + "io" + "math" + "os" + "path/filepath" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/filter" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" + _ "golang.org/x/image/webp" +) + +// Image is a Reader representing an image resource. +type Image struct { + io.Reader + Name string // Resource name + FileType string + PageNr int + ObjNr int + Width int // "Width" + Height int // "Height" + Bpc int // "BitsPerComponent" + Cs string // "ColorSpace" + Comp int // color component count + IsImgMask bool // "ImageMask" + HasImgMask bool // "Mask" + HasSMask bool // "SMask" + Thumb bool // "Thumbnail" + Interpol bool // "Interpolate" + Size int64 // "Length" + Filter string // filter pipeline + DecodeParms string +} + +// ImageFileName returns true for supported image file types. +func ImageFileName(fileName string) bool { + ext := strings.ToLower(filepath.Ext(fileName)) + return types.MemberOf(ext, []string{".png", ".webp", ".tif", ".tiff", ".jpg", ".jpeg"}) +} + +// ImageFileNames returns a slice of image file names contained in dir constrained by maxFileSize. +func ImageFileNames(dir string, maxFileSize types.ByteSize) ([]string, error) { + files, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + fn := []string{} + for i := 0; i < len(files); i++ { + fi := files[i] + fileInfo, err := fi.Info() + if err != nil { + continue + } + if types.ByteSize(fileInfo.Size()) > maxFileSize { + continue + } + if ImageFileName(fi.Name()) { + fn = append(fn, filepath.Join(dir, fi.Name())) + } + } + return fn, nil +} + +func createSMaskObject(xRefTable *XRefTable, buf []byte, w, h, bpc int) (*types.IndirectRef, error) { + sd := &types.StreamDict{ + Dict: types.Dict( + map[string]types.Object{ + "Type": types.Name("XObject"), + "Subtype": types.Name("Image"), + "BitsPerComponent": types.Integer(bpc), + "ColorSpace": types.Name(DeviceGrayCS), + "Width": types.Integer(w), + "Height": types.Integer(h), + }, + ), + Content: buf, + FilterPipeline: []types.PDFFilter{{Name: filter.Flate, DecodeParms: nil}}, + } + + sd.InsertName("Filter", filter.Flate) + + if err := sd.Encode(); err != nil { + return nil, err + } + + return xRefTable.IndRefForNewObject(*sd) +} + +func createFlateImageObject(xRefTable *XRefTable, buf, sm []byte, w, h, bpc int, cs string) (*types.StreamDict, error) { + var softMaskIndRef *types.IndirectRef + if sm != nil { + var err error + softMaskIndRef, err = createSMaskObject(xRefTable, sm, w, h, bpc) + if err != nil { + return nil, err + } + } + + // Create Flate stream dict. + sd, _ := xRefTable.NewStreamDictForBuf(buf) + sd.InsertName("Type", "XObject") + sd.InsertName("Subtype", "Image") + sd.InsertInt("Width", w) + sd.InsertInt("Height", h) + sd.InsertInt("BitsPerComponent", bpc) + sd.InsertName("ColorSpace", cs) + + if softMaskIndRef != nil { + sd.Insert("SMask", *softMaskIndRef) + } + + if w < 1000 || h < 1000 { + sd.Insert("Interpolate", types.Boolean(true)) + } + + if err := sd.Encode(); err != nil { + return nil, err + } + + return sd, nil +} + +// CreateDCTImageObject returns a DCT encoded stream dict. +func CreateDCTImageObject(xRefTable *XRefTable, buf []byte, w, h, bpc int, cs string) (*types.StreamDict, error) { + sd := &types.StreamDict{ + Dict: types.Dict( + map[string]types.Object{ + "Type": types.Name("XObject"), + "Subtype": types.Name("Image"), + "Width": types.Integer(w), + "Height": types.Integer(h), + "BitsPerComponent": types.Integer(bpc), + "ColorSpace": types.Name(cs), + }, + ), + Content: buf, + FilterPipeline: nil, + } + + if cs == DeviceCMYKCS { + sd.Insert("Decode", types.NewIntegerArray(1, 0, 1, 0, 1, 0, 1, 0)) + } + + if w < 1000 || h < 1000 { + sd.Insert("Interpolate", types.Boolean(true)) + } + + sd.InsertName("Filter", filter.DCT) + + // Calling Encode without FilterPipeline ensures an encoded stream in sd.Raw. + if err := sd.Encode(); err != nil { + return nil, err + } + + sd.Content = nil + + sd.FilterPipeline = []types.PDFFilter{{Name: filter.DCT, DecodeParms: nil}} + + return sd, nil +} + +func writeRGBAImageBuf(img image.Image) ([]byte, []byte) { + w := img.Bounds().Dx() + h := img.Bounds().Dy() + i := 0 + var sm []byte + buf := make([]byte, w*h*3) + var softMask bool + + for y := 0; y < h; y++ { + for x := 0; x < w; x++ { + c := img.At(x, y).(color.RGBA) + if !softMask { + if c.A != 0xFF { + softMask = true + sm = []byte{} + for j := 0; j < y*w+x; j++ { + sm = append(sm, 0xFF) + } + sm = append(sm, c.A) + } + } else { + sm = append(sm, c.A) + } + buf[i] = c.R + buf[i+1] = c.G + buf[i+2] = c.B + i += 3 + } + } + + return buf, sm +} + +func writeRGBA64ImageBuf(img image.Image) []byte { + w := img.Bounds().Dx() + h := img.Bounds().Dy() + i := 0 + buf := make([]byte, w*h*6) + + for y := 0; y < h; y++ { + for x := 0; x < w; x++ { + c := img.At(x, y).(color.RGBA64) + buf[i] = uint8(c.R >> 8) + buf[i+1] = uint8(c.R & 0x00FF) + buf[i+2] = uint8(c.G >> 8) + buf[i+3] = uint8(c.G & 0x00FF) + buf[i+4] = uint8(c.B >> 8) + buf[i+5] = uint8(c.B & 0x00FF) + i += 6 + } + } + + return buf +} + +// func writeYCbCrToRGBAImageBuf(img image.Image) []byte { +// w := img.Bounds().Dx() +// h := img.Bounds().Dy() +// i := 0 +// buf := make([]byte, w*h*3) + +// for y := 0; y < h; y++ { +// for x := 0; x < w; x++ { +// c := img.At(x, y).(color.YCbCr) +// r, g, b, _ := c.RGBA() +// buf[i] = uint8(r >> 8 & 0xFF) +// buf[i+1] = uint8(g >> 8 & 0xFF) +// buf[i+2] = uint8(b >> 8 & 0xFF) +// i += 3 +// } +// } +// return buf +// } + +func writeNRGBAImageBuf(xRefTable *XRefTable, img image.Image) ([]byte, []byte) { + w := img.Bounds().Dx() + h := img.Bounds().Dy() + i := 0 + buf := make([]byte, w*h*3) + var sm []byte + var softMask bool + + for y := 0; y < h; y++ { + for x := 0; x < w; x++ { + c := img.At(x, y).(color.NRGBA) + if !softMask { + if xRefTable != nil && c.A != 0xFF { + softMask = true + sm = []byte{} + for j := 0; j < y*w+x; j++ { + sm = append(sm, 0xFF) + } + sm = append(sm, c.A) + } + } else { + sm = append(sm, c.A) + } + + buf[i] = c.R + buf[i+1] = c.G + buf[i+2] = c.B + i += 3 + } + } + + return buf, sm +} + +func writeNRGBA64ImageBuf(xRefTable *XRefTable, img image.Image) ([]byte, []byte) { + w := img.Bounds().Dx() + h := img.Bounds().Dy() + i := 0 + buf := make([]byte, w*h*6) + var sm []byte + var softMask bool + + for y := 0; y < h; y++ { + for x := 0; x < w; x++ { + c := img.At(x, y).(color.NRGBA64) + if !softMask { + if xRefTable != nil && c.A != 0xFFFF { + softMask = true + sm = []byte{} + for j := 0; j < y*w+x; j++ { + sm = append(sm, 0xFF) + sm = append(sm, 0xFF) + } + sm = append(sm, uint8(c.A>>8)) + sm = append(sm, uint8(c.A&0x00FF)) + } + } else { + sm = append(sm, uint8(c.A>>8)) + sm = append(sm, uint8(c.A&0x00FF)) + } + + buf[i] = uint8(c.R >> 8) + buf[i+1] = uint8(c.R & 0x00FF) + buf[i+2] = uint8(c.G >> 8) + buf[i+3] = uint8(c.G & 0x00FF) + buf[i+4] = uint8(c.B >> 8) + buf[i+5] = uint8(c.B & 0x00FF) + i += 6 + } + } + + return buf, sm +} + +func writeGrayImageBuf(img image.Image) []byte { + w := img.Bounds().Dx() + h := img.Bounds().Dy() + i := 0 + buf := make([]byte, w*h) + + for y := 0; y < h; y++ { + for x := 0; x < w; x++ { + c := img.At(x, y).(color.Gray) + buf[i] = c.Y + i++ + } + } + + return buf +} + +func writeGray16ImageBuf(img image.Image) []byte { + w := img.Bounds().Dx() + h := img.Bounds().Dy() + i := 0 + buf := make([]byte, 2*w*h) + + for y := 0; y < h; y++ { + for x := 0; x < w; x++ { + c := img.At(x, y).(color.Gray16) + buf[i] = uint8(c.Y >> 8) + buf[i+1] = uint8(c.Y & 0x00FF) + i += 2 + } + } + + return buf +} + +func writeCMYKImageBuf(img image.Image) []byte { + w := img.Bounds().Dx() + h := img.Bounds().Dy() + i := 0 + buf := make([]byte, w*h*4) + + for y := 0; y < h; y++ { + for x := 0; x < w; x++ { + c := img.At(x, y).(color.CMYK) + buf[i] = c.C + buf[i+1] = c.M + buf[i+2] = c.Y + buf[i+3] = c.K + i += 4 + //fmt.Printf("x:%3d(%3d) y:%3d(%3d) c:#%02x m:#%02x y:#%02x k:#%02x\n", x1, x, y1, y, c.C, c.M, c.Y, c.K) + } + } + + return buf +} + +func convertToRGBA(img image.Image) *image.RGBA { + b := img.Bounds() + m := image.NewRGBA(image.Rect(0, 0, b.Dx(), b.Dy())) + draw.Draw(m, m.Bounds(), img, b.Min, draw.Src) + return m +} + +func convertToGray(img image.Image) *image.Gray { + b := img.Bounds() + m := image.NewGray(image.Rect(0, 0, b.Dx(), b.Dy())) + draw.Draw(m, m.Bounds(), img, b.Min, draw.Src) + return m +} + +func convertToSepia(img image.Image) *image.RGBA { + m := convertToRGBA(img) + w := img.Bounds().Dx() + h := img.Bounds().Dy() + for y := 0; y < h; y++ { + for x := 0; x < w; x++ { + c := m.At(x, y).(color.RGBA) + r := math.Round((float64(c.R) * .393) + (float64(c.G) * .769) + (float64(c.B) * .189)) + if r > 255 { + r = 255 + } + g := math.Round((float64(c.R) * .349) + (float64(c.G) * .686) + (float64(c.B) * .168)) + if g > 255 { + g = 255 + } + b := math.Round((float64(c.R) * .272) + (float64(c.G) * .534) + (float64(c.B) * .131)) + if b > 255 { + b = 255 + } + m.Set(x, y, color.RGBA{uint8(r), uint8(g), uint8(b), c.A}) + } + } + return m +} + +func createImageDict(xRefTable *XRefTable, buf, softMask []byte, w, h, bpc int, format, cs string) (*types.StreamDict, int, int, error) { + var ( + sd *types.StreamDict + err error + ) + switch format { + case "jpeg": + sd, err = CreateDCTImageObject(xRefTable, buf, w, h, bpc, cs) + default: + sd, err = createFlateImageObject(xRefTable, buf, softMask, w, h, bpc, cs) + } + return sd, w, h, err +} + +func encodeJPEG(img image.Image) ([]byte, string, error) { + var cs string + switch img.(type) { + case *image.Gray, *image.Gray16: + cs = DeviceGrayCS + case *image.YCbCr: + cs = DeviceRGBCS + case *image.CMYK: + cs = DeviceCMYKCS + default: + return nil, "", errors.Errorf("pdfcpu: unexpected color model for JPEG: %s", cs) + } + var buf bytes.Buffer + err := jpeg.Encode(&buf, img, nil) + return buf.Bytes(), cs, err +} + +func createImageBuf(xRefTable *XRefTable, img image.Image, format string) ([]byte, []byte, int, string, error) { + var buf []byte + var sm []byte // soft mask aka alpha mask + var bpc int + // TODO if dpi != 72 resample (applies to PNG,JPG,TIFF) + + if format == "jpeg" { + bb, cs, err := encodeJPEG(img) + return bb, sm, 8, cs, err + } + + var cs string + + switch img.(type) { + case *image.RGBA: + // A 32-bit alpha-premultiplied color, having 8 bits for each of red, green, blue and alpha. + // An alpha-premultiplied color component C has been scaled by alpha (A), so it has valid values 0 <= C <= A. + cs = DeviceRGBCS + bpc = 8 + buf, sm = writeRGBAImageBuf(img) + + case *image.RGBA64: + // A 64-bit alpha-premultiplied color, having 16 bits for each of red, green, blue and alpha. + // An alpha-premultiplied color component C has been scaled by alpha (A), so it has valid values 0 <= C <= A. + cs = DeviceRGBCS + bpc = 16 + buf = writeRGBA64ImageBuf(img) + + case *image.NRGBA: + // Non-alpha-premultiplied 32-bit color. + cs = DeviceRGBCS + bpc = 8 + buf, sm = writeNRGBAImageBuf(xRefTable, img) + + case *image.NRGBA64: + // Non-alpha-premultiplied 64-bit color. + cs = DeviceRGBCS + bpc = 16 + buf, sm = writeNRGBA64ImageBuf(xRefTable, img) + + case *image.Alpha: + return buf, sm, bpc, cs, errors.New("pdfcpu: unsupported image type: Alpha") + + case *image.Alpha16: + return buf, sm, bpc, cs, errors.New("pdfcpu: unsupported image type: Alpha16") + + case *image.Gray: + // 8-bit grayscale color. + cs = DeviceGrayCS + bpc = 8 + buf = writeGrayImageBuf(img) + + case *image.Gray16: + // 16-bit grayscale color. + cs = DeviceGrayCS + bpc = 16 + buf = writeGray16ImageBuf(img) + + case *image.CMYK: + // Opaque CMYK color, having 8 bits for each of cyan, magenta, yellow and black. + cs = DeviceCMYKCS + bpc = 8 + buf = writeCMYKImageBuf(img) + + case *image.YCbCr: + cs = DeviceRGBCS + bpc = 8 + buf, sm = writeRGBAImageBuf(convertToRGBA(img)) + + case *image.NYCbCrA: + return buf, sm, bpc, cs, errors.New("pdfcpu: unsupported image type: NYCbCrA") + + case *image.Paletted: + // In-memory image of uint8 indices into a given palette. + cs = DeviceRGBCS + bpc = 8 + buf, sm = writeRGBAImageBuf(convertToRGBA(img)) + + default: + return buf, sm, bpc, cs, errors.Errorf("pdfcpu: unsupported image type: %T", img) + } + + return buf, sm, bpc, cs, nil +} + +func colorSpaceForJPEGColorModel(cm color.Model) string { + switch cm { + case color.GrayModel: + return DeviceGrayCS + case color.YCbCrModel: + return DeviceRGBCS + case color.CMYKModel: + return DeviceCMYKCS + } + return "" +} + +func createDCTImageObjectForJPEG(xRefTable *XRefTable, c image.Config, bb bytes.Buffer) (*types.StreamDict, int, int, error) { + cs := colorSpaceForJPEGColorModel(c.ColorModel) + if cs == "" { + return nil, 0, 0, errors.New("pdfcpu: unexpected color model for JPEG") + } + + sd, err := CreateDCTImageObject(xRefTable, bb.Bytes(), c.Width, c.Height, 8, cs) + + return sd, c.Width, c.Height, err +} + +// CreateImageStreamDict returns a stream dict for image data represented by r and applies optional filters. +func CreateImageStreamDict(xRefTable *XRefTable, r io.Reader, gray, sepia bool) (*types.StreamDict, int, int, error) { + + var bb bytes.Buffer + tee := io.TeeReader(r, &bb) + + var sniff bytes.Buffer + if _, err := io.Copy(&sniff, tee); err != nil { + return nil, 0, 0, err + } + + c, format, err := image.DecodeConfig(&sniff) + if err != nil { + return nil, 0, 0, err + } + + if format == "jpeg" && !gray && !sepia { + return createDCTImageObjectForJPEG(xRefTable, c, bb) + } + + img, format, err := image.Decode(&bb) + if err != nil { + return nil, 0, 0, err + } + + if gray { + switch img.(type) { + case *image.Gray, *image.Gray16: + default: + img = convertToGray(img) + } + } + + if sepia { + switch img.(type) { + case *image.Gray, *image.Gray16: + default: + img = convertToSepia(img) + } + } + + imgBuf, softMask, bpc, cs, err := createImageBuf(xRefTable, img, format) + if err != nil { + return nil, 0, 0, err + } + + w, h := img.Bounds().Dx(), img.Bounds().Dy() + + return createImageDict(xRefTable, imgBuf, softMask, w, h, bpc, format, cs) +} + +// CreateImageResource creates a new XObject for given image data represented by r and applies optional filters. +func CreateImageResource(xRefTable *XRefTable, r io.Reader, gray, sepia bool) (*types.IndirectRef, int, int, error) { + sd, w, h, err := CreateImageStreamDict(xRefTable, r, gray, sepia) + if err != nil { + return nil, 0, 0, err + } + indRef, err := xRefTable.IndRefForNewObject(*sd) + return indRef, w, h, err +} diff --git a/pkg/pdfcpu/model/metadata.go b/pkg/pdfcpu/model/metadata.go new file mode 100644 index 0000000000000000000000000000000000000000..aae62cb0637bf1f540b84f12ce36639edb2064a6 --- /dev/null +++ b/pkg/pdfcpu/model/metadata.go @@ -0,0 +1,155 @@ +/* +Copyright 2024 The pdfcpu Authors. + +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 + + 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 model + +import ( + "encoding/xml" + "strings" + "time" +) + +type UserDate time.Time + +const userDateFormatNoTimeZone = "2006-01-02T15:04:05Z" +const userDateFormatNegTimeZone = "2006-01-02T15:04:05-07:00" +const userDateFormatPosTimeZone = "2006-01-02T15:04:05+07:00" + +func (ud *UserDate) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + dateString := "" + err := d.DecodeElement(&dateString, &start) + if err != nil { + return err + } + dat, err := time.Parse(userDateFormatNoTimeZone, dateString) + if err == nil { + *ud = UserDate(dat) + return nil + } + dat, err = time.Parse(userDateFormatPosTimeZone, dateString) + if err == nil { + *ud = UserDate(dat) + return nil + } + dat, err = time.Parse(userDateFormatNegTimeZone, dateString) + if err == nil { + *ud = UserDate(dat) + return nil + } + return err +} + +type Alt struct { + //XMLName xml.Name `xml:"http://www.w3.org/1999/02/22-rdf-syntax-ns# Alt"` + Entries []string `xml:"http://www.w3.org/1999/02/22-rdf-syntax-ns# li"` +} + +type Seq struct { + //XMLName xml.Name `xml:"http://www.w3.org/1999/02/22-rdf-syntax-ns# Seq"` + Entries []string `xml:"http://www.w3.org/1999/02/22-rdf-syntax-ns# li"` +} + +type Title struct { + //XMLName xml.Name `xml:"http://purl.org/dc/elements/1.1/ title"` + Alt Alt `xml:"http://www.w3.org/1999/02/22-rdf-syntax-ns# Alt"` +} + +type Desc struct { + //XMLName xml.Name `xml:"http://purl.org/dc/elements/1.1/ description"` + Alt Alt `xml:"http://www.w3.org/1999/02/22-rdf-syntax-ns# Alt"` +} + +type Creator struct { + //XMLName xml.Name `xml:"http://purl.org/dc/elements/1.1/ creator"` + Seq Seq `xml:"http://www.w3.org/1999/02/22-rdf-syntax-ns# Seq"` +} + +type Description struct { + //XMLName xml.Name `xml:"http://www.w3.org/1999/02/22-rdf-syntax-ns# Description"` + Title Title `xml:"http://purl.org/dc/elements/1.1/ title"` + Author Creator `xml:"http://purl.org/dc/elements/1.1/ creator"` + Subject Desc `xml:"http://purl.org/dc/elements/1.1/ description"` + Creator string `xml:"http://ns.adobe.com/xap/1.0/ CreatorTool"` + CreationDate UserDate `xml:"http://ns.adobe.com/xap/1.0/ CreateDate"` + ModDate UserDate `xml:"http://ns.adobe.com/xap/1.0/ ModifyDate"` + Producer string `xml:"http://ns.adobe.com/pdf/1.3/ Producer"` + Trapped bool `xml:"http://ns.adobe.com/pdf/1.3/ Trapped"` + Keywords string `xml:"http://ns.adobe.com/pdf/1.3/ Keywords"` +} + +type RDF struct { + XMLName xml.Name `xml:"http://www.w3.org/1999/02/22-rdf-syntax-ns# RDF"` + Description Description +} + +type XMPMeta struct { + XMLName xml.Name `xml:"adobe:ns:meta/ xmpmeta"` + RDF RDF +} + +func removeTag(s, kw string) string { + kwLen := len(kw) + i := strings.Index(s, kw) + if i < 0 { + return "" + } + + j := i + kwLen + + i = strings.LastIndex(s[:i], "<") + if i < 0 { + return "" + } + + block1 := s[:i] + + s = s[j:] + i = strings.Index(s, kw) + if i < 0 { + return "" + } + + j = i + kwLen + + block2 := s[j:] + + s1 := block1 + block2 + + return s1 +} + +func RemoveKeywords(metadata *[]byte) error { + + // Opt for simple byte removal instead of xml de/encoding. + + s := string(*metadata) + if len(s) == 0 { + return nil + } + + s = removeTag(s, "Keywords>") + if len(s) == 0 { + return nil + } + + // Possible Acrobat bug. + // Acrobat seems to use dc:subject for keywords but ***does not*** show the content in Subject. + s = removeTag(s, "subject>") + + *metadata = []byte(s) + + return nil +} diff --git a/pkg/pdfcpu/model/nameTree.go b/pkg/pdfcpu/model/nameTree.go new file mode 100644 index 0000000000000000000000000000000000000000..7429ecbfbc453293a754e35a40e90857cf3e29ab --- /dev/null +++ b/pkg/pdfcpu/model/nameTree.go @@ -0,0 +1,616 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 model + +import ( + "fmt" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +var errNameTreeDuplicateKey = errors.New("pdfcpu: name: duplicate key") + +const maxEntries = 3 + +// Node is an opinionated implementation of the PDF name tree. +// pdfcpu caches all name trees found in the PDF catalog with this data structure. +// The PDF spec does not impose any rules regarding a strategy for the creation of nodes. +// A binary tree was chosen where each leaf node has a limited number of entries (maxEntries). +// Once maxEntries has been reached a leaf node turns into an intermediary node with two kids, +// which are leaf nodes each of them holding half of the sorted entries of the original leaf node. +type Node struct { + Kids []*Node // Mirror of the name tree's Kids array, an array of indirect references. + Names []entry // Mirror of the name tree's Names array. + Kmin, Kmax string // Mirror of the name tree's Limit array[Kmin,Kmax]. + D types.Dict // The PDF dict representing this name tree node. +} + +// entry is a key value pair. +type entry struct { + k string + v types.Object +} + +func (n Node) leaf() bool { + return n.Kids == nil +} + +func keyLess(k, s string) bool { + return k < s +} + +func keyLessOrEqual(k, s string) bool { + return k == s || keyLess(k, s) +} + +func (n Node) withinLimits(k string) bool { + return keyLessOrEqual(n.Kmin, k) && keyLessOrEqual(k, n.Kmax) +} + +// Value returns the value for given key +func (n Node) Value(k string) (types.Object, bool) { + if !n.withinLimits(k) { + return nil, false + } + + if n.leaf() { + + // names are sorted by key. + for _, v := range n.Names { + + if v.k < k { + continue + } + + if v.k == k { + return v.v, true + } + + return nil, false + } + + return nil, false + } + + // kids are sorted by key ranges. + for _, v := range n.Kids { + if v.withinLimits(k) { + return v.Value(k) + } + } + + return nil, false +} + +// AppendToNames adds an entry to a leaf node (for internalizing name trees). +func (n *Node) AppendToNames(k string, v types.Object) { + //fmt.Printf("AddToLeaf: %s %v (%v)\n\n", k, v, &v) + + if n.Names == nil { + n.Names = make([]entry, 0, maxEntries) + } + + arr, ok := v.(types.Array) + if ok { + arr1 := make(types.Array, len(arr)) + for i, v := range arr { + if indRef, ok := v.(types.IndirectRef); ok { + arr1[i] = *types.NewIndirectRef(indRef.ObjectNumber.Value(), indRef.GenerationNumber.Value()) + } else { + arr1[i] = v + } + } + n.Names = append(n.Names, entry{k, arr1}) + } else { + n.Names = append(n.Names, entry{k, v}) + } +} + +type NameMap map[string][]types.Dict + +func (m NameMap) Add(k string, d types.Dict) { + dd, ok := m[k] + if !ok { + m[k] = []types.Dict{d} + return + } + m[k] = append(dd, d) +} + +func (n *Node) insertIntoLeaf(k string, v types.Object, m NameMap) error { + if log.DebugEnabled() { + log.Debug.Printf("Insert k:%s in the middle\n", k) + } + for i, e := range n.Names { + if keyLess(e.k, k) { + continue + } + if e.k == k { + return errNameTreeDuplicateKey + } + // Insert entry(k,v) at i + n.Names = append(n.Names, entry{}) + copy(n.Names[i+1:], n.Names[i:]) + n.Names[i] = entry{k, v} + return nil + } + if log.DebugEnabled() { + log.Debug.Printf("Insert k:%s at end\n", k) + } + n.Kmax = k + n.Names = append(n.Names, entry{k, v}) + return nil +} + +func updateNameRef(d types.Dict, keys []string, nameOld, nameNew string) error { + for _, k := range keys { + s, err := d.StringOrHexLiteralEntry(k) + if err != nil { + return err + } + if s != nil { + if *s != nameOld { + return errors.Errorf("invalid Name ref detected for: %s", nameOld) + } + d[k] = types.NewHexLiteral([]byte(nameNew)) + } + } + return nil +} + +func updateNameRefDicts(dd []types.Dict, nameRefDictKeys []string, nameOld, nameNew string) error { + // eg. + // "Dests": "D", "Dest" []string{"D", "Dest"} + // "EmbeddedFiles": F", "UF" []string{"F", "UF"} + + for _, d := range dd { + if err := updateNameRef(d, nameRefDictKeys, nameOld, nameNew); err != nil { + return err + } + } + + return nil +} + +func (n *Node) insertUniqueIntoLeaf(k string, v types.Object, m NameMap, nameRefDictKeys []string) (bool, error) { + var err error + kOrig := k + for first := true; first || err == errNameTreeDuplicateKey; first = false { + err = n.insertIntoLeaf(k, v, m) + if err == nil { + break + } + if err != errNameTreeDuplicateKey { + return false, err + } + if len(m) == 0 { + return true, nil + } + kNew := k + "\x01" + dd, ok := m[kOrig] + if !ok { + return true, nil + } + if err := updateNameRefDicts(dd, nameRefDictKeys, k, kNew); err != nil { + return false, err + } + k = kNew + } + + return false, nil +} + +// HandleLeaf processes a leaf node. +func (n *Node) HandleLeaf(xRefTable *XRefTable, k string, v types.Object, m NameMap, nameRefDictKeys []string) error { + // A leaf node contains up to maxEntries names. + // Any number of entries greater than maxEntries will be delegated to kid nodes. + + //fmt.Printf("HandleLeaf: %s %v\n\n", k, v) + + if len(n.Names) == 0 { + n.Names = append(n.Names, entry{k, v}) + n.Kmin, n.Kmax = k, k + if log.DebugEnabled() { + log.Debug.Printf("first key=%s\n", k) + } + return nil + } + + if log.DebugEnabled() { + log.Debug.Printf("kmin=%s kmax=%s\n", n.Kmin, n.Kmax) + } + + if keyLess(k, n.Kmin) { + // Prepend (k,v). + if log.DebugEnabled() { + log.Debug.Printf("Insert k:%s at beginning\n", k) + } + n.Kmin = k + n.Names = append(n.Names, entry{}) + copy(n.Names[1:], n.Names[0:]) + n.Names[0] = entry{k, v} + } else if keyLess(n.Kmax, k) { + // Append (k,v). + if log.DebugEnabled() { + log.Debug.Printf("Insert k:%s at end\n", k) + } + n.Kmax = k + n.Names = append(n.Names, entry{k, v}) + } else { + // Insert (k,v) while ensuring unique k. + ok, err := n.insertUniqueIntoLeaf(k, v, m, nameRefDictKeys) + if err != nil { + return err + } + if ok { + return nil + } + } + + // if len was already > maxEntries we know we are dealing with somebody elses name tree. + // In that case we do not know the branching strategy and therefore just add to Names and do not create kids. + // If len is within maxEntries we do not create kids either way. + if len(n.Names) != maxEntries+1 { + return nil + } + + // turn leaf into intermediate node with 2 kids/leafs (binary tree) + c := maxEntries + 1 + k1 := &Node{Names: make([]entry, c/2, maxEntries)} + copy(k1.Names, n.Names[:c/2]) + k1.Kmin = n.Names[0].k + k1.Kmax = n.Names[c/2-1].k + + k2 := &Node{Names: make([]entry, len(n.Names)-c/2, maxEntries)} + copy(k2.Names, n.Names[c/2:]) + k2.Kmin = n.Names[c/2].k + k2.Kmax = n.Names[c-1].k + + n.Kids = []*Node{k1, k2} + n.Names = nil + + return nil +} + +// Add adds an entry to a name tree. +func (n *Node) Add(xRefTable *XRefTable, k string, v types.Object, m NameMap, nameRefDictKeys []string) error { + //fmt.Printf("Add: %s %v\n", k, v) + + // The values associated with the keys may be objects of any type. + // Stream objects shall be specified by indirect object references. + // Dictionary, array, and string objects should be specified by indirect object references. + // Other PDF objects (null, number, boolean and name) should be specified as direct objects. + + if n.Names == nil { + n.Names = make([]entry, 0, maxEntries) + } + + if n.leaf() { + return n.HandleLeaf(xRefTable, k, v, m, nameRefDictKeys) + } + + if k == n.Kmin || k == n.Kmax { + return nil + } + + if keyLess(k, n.Kmin) { + n.Kmin = k + } else if keyLess(n.Kmax, k) { + n.Kmax = k + } + + // For intermediary nodes we delegate to the corresponding subtree. + for _, a := range n.Kids { + if keyLess(k, a.Kmin) || a.withinLimits(k) { + return a.Add(xRefTable, k, v, m, nameRefDictKeys) + } + } + + // Insert k into last (right most) subtree. + last := n.Kids[len(n.Kids)-1] + return last.Add(xRefTable, k, v, m, nameRefDictKeys) +} + +// AddTree adds a name tree to a name tree. +func (n *Node) AddTree(xRefTable *XRefTable, tree *Node, m NameMap, nameRefDictKeys []string) error { + if !tree.leaf() { + for _, v := range tree.Kids { + if err := n.AddTree(xRefTable, v, m, nameRefDictKeys); err != nil { + return err + } + } + return nil + } + + for _, e := range tree.Names { + if err := n.Add(xRefTable, e.k, e.v, m, nameRefDictKeys); err != nil { + return err + } + } + + return nil +} + +func (n *Node) removeFromNames(xRefTable *XRefTable, k string) (ok bool, err error) { + for i, v := range n.Names { + + if v.k < k { + continue + } + + if v.k == k { + + if xRefTable != nil { + // Remove object graph of value. + if log.DebugEnabled() { + log.Debug.Println("removeFromNames: deleting object graph of v") + } + if err := xRefTable.DeleteObjectGraph(v.v); err != nil { + return false, err + } + } + + n.Names = append(n.Names[:i], n.Names[i+1:]...) + return true, nil + } + + } + + return false, nil +} + +func (n *Node) removeSingleFromParent(xRefTable *XRefTable) error { + if xRefTable != nil { + // Remove object graph of value. + if log.DebugEnabled() { + log.Debug.Println("removeFromLeaf: deleting object graph of v") + } + if err := xRefTable.DeleteObjectGraph(n.Names[0].v); err != nil { + return err + } + } + n.Kmin, n.Kmax = "", "" + n.Names = nil + return nil +} + +func (n *Node) removeFromLeaf(xRefTable *XRefTable, k string) (empty, ok bool, err error) { + if keyLess(k, n.Kmin) || keyLess(n.Kmax, k) { + return false, false, nil + } + + // kmin < k < kmax + + // If sole entry gets deleted, remove this node from parent. + if len(n.Names) == 1 { + if err := n.removeSingleFromParent(xRefTable); err != nil { + return false, false, err + } + return true, true, nil + } + + if k == n.Kmin { + + if xRefTable != nil { + // Remove object graph of value. + if log.DebugEnabled() { + log.Debug.Println("removeFromLeaf: deleting object graph of v") + } + if err := xRefTable.DeleteObjectGraph(n.Names[0].v); err != nil { + return false, false, err + } + } + + n.Names = n.Names[1:] + n.Kmin = n.Names[0].k + return false, true, nil + } + + if k == n.Kmax { + + if xRefTable != nil { + // Remove object graph of value. + if log.DebugEnabled() { + log.Debug.Println("removeFromLeaf: deleting object graph of v") + } + if err := xRefTable.DeleteObjectGraph(n.Names[len(n.Names)-1].v); err != nil { + return false, false, err + } + } + + n.Names = n.Names[:len(n.Names)-1] + n.Kmax = n.Names[len(n.Names)-1].k + return false, true, nil + } + + if ok, err = n.removeFromNames(xRefTable, k); err != nil { + return false, false, err + } + + return false, ok, nil +} + +func (n *Node) removeKid(xRefTable *XRefTable, kid *Node, i int) (bool, error) { + if xRefTable != nil { + if err := xRefTable.DeleteObject(kid.D); err != nil { + return false, err + } + } + + if i == 0 { + // Remove first kid. + if log.DebugEnabled() { + log.Debug.Println("removeFromKids: remove first kid.") + } + n.Kids = n.Kids[1:] + } else if i == len(n.Kids)-1 { + if log.DebugEnabled() { + log.Debug.Println("removeFromKids: remove last kid.") + } + // Remove last kid. + n.Kids = n.Kids[:len(n.Kids)-1] + } else { + // Remove kid from the middle. + if log.DebugEnabled() { + log.Debug.Println("removeFromKids: remove kid form the middle.") + } + n.Kids = append(n.Kids[:i], n.Kids[i+1:]...) + } + + if len(n.Kids) == 1 { + + // If a single kid remains we can merge it with its parent. + // By doing this we get rid of a redundant intermediary node. + if log.DebugEnabled() { + log.Debug.Println("removeFromKids: only 1 kid") + } + + if xRefTable != nil { + if err := xRefTable.DeleteObject(n.D); err != nil { + return false, err + } + } + + *n = *n.Kids[0] + + if log.DebugEnabled() { + log.Debug.Printf("removeFromKids: new n = %s\n", n) + } + + return true, nil + } + + return false, nil +} + +func (n *Node) removeFromKids(xRefTable *XRefTable, k string) (ok bool, err error) { + // Locate the kid to recurse into, then remove k from that subtree. + for i, kid := range n.Kids { + + if !kid.withinLimits(k) { + continue + } + + empty, ok, err := kid.Remove(xRefTable, k) + if err != nil { + return false, err + } + if !ok { + return false, nil + } + + if empty { + + // This kid is now empty and needs to be removed. + + noKids, err := n.removeKid(xRefTable, kid, i) + if err != nil { + return false, err + } + if noKids { + return true, nil + } + + } + + // Update kMin, kMax for n. + n.Kmin = n.Kids[0].Kmin + n.Kmax = n.Kids[len(n.Kids)-1].Kmax + + return true, nil + } + + return false, nil +} + +// Remove removes an entry from a name tree. +// empty returns true if this node is an empty leaf node after removal. +// ok returns true if removal was successful. +func (n *Node) Remove(xRefTable *XRefTable, k string) (empty, ok bool, err error) { + if n.leaf() { + return n.removeFromLeaf(xRefTable, k) + } + + if ok, err = n.removeFromKids(xRefTable, k); err != nil { + return false, false, err + } + + return false, ok, nil + +} + +// Process traverses the nametree applying a handler to each entry (key-value pair). +func (n *Node) Process(xRefTable *XRefTable, handler func(*XRefTable, string, *types.Object) error) error { + if !n.leaf() { + for _, v := range n.Kids { + if err := v.Process(xRefTable, handler); err != nil { + return err + } + } + return nil + } + + for k, e := range n.Names { + if err := handler(xRefTable, e.k, &e.v); err != nil { + return err + } + n.Names[k] = e + } + + return nil +} + +// KeyList returns a sorted list of all keys. +func (n Node) KeyList() ([]string, error) { + list := []string{} + + keys := func(xRefTable *XRefTable, k string, v *types.Object) error { + list = append(list, fmt.Sprintf("%s %v", k, *v)) + return nil + } + + if err := n.Process(nil, keys); err != nil { + return nil, err + } + + return list, nil + +} + +func (n Node) String() string { + a := []string{} + + if n.leaf() { + a = append(a, "[") + for _, n := range n.Names { + a = append(a, fmt.Sprintf("(%s,%s)", n.k, n.v)) + } + a = append(a, fmt.Sprintf("{%s,%s}]", n.Kmin, n.Kmax)) + return strings.Join(a, "") + } + + a = append(a, fmt.Sprintf("{%s,%s}", n.Kmin, n.Kmax)) + + for _, v := range n.Kids { + a = append(a, v.String()) + } + + return strings.Join(a, ",") +} diff --git a/pkg/pdfcpu/model/nameTree_test.go b/pkg/pdfcpu/model/nameTree_test.go new file mode 100644 index 0000000000000000000000000000000000000000..82ee728794b302ff094e9fb853f07130db1bab32 --- /dev/null +++ b/pkg/pdfcpu/model/nameTree_test.go @@ -0,0 +1,202 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 model + +import ( + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +func checkAddResult(t *testing.T, r *Node, exp string, root bool) { + + l := r.String() + + if l != exp { + t.Fatalf("Add: %s != %s", l, exp) + } + + if root { + if !r.leaf() { + t.Fatal("root only node should be a leaf node") + } + return + } + + if r.leaf() { + t.Fatal("root node with kids should not be a leaf node") + } + +} + +func checkRemoveResult(t *testing.T, r *Node, k string, empty, ok bool, exp string, leaf bool) { + + if !ok { + t.Fatalf("could not Remove %s\n", k) + } + + if empty { + t.Fatalf("r should not be empty after removing %s\n", k) + } + + if leaf { + if !r.leaf() { + t.Fatalf("root node should be a leaf node after removing %s\n", k) + } + } else { + if r.leaf() { + t.Fatalf("root node with kids should not be a leaf node after removing %s\n", k) + } + } + + if empty { + t.Fatalf("r should not be empty after removing %s\n", k) + } + + l := r.String() + if l != exp { + t.Fatalf("Remove %s: %s != %s", k, l, exp) + } +} + +func buildNameTree(t *testing.T, r *Node) { + + r.Add(nil, "b", types.StringLiteral("bv"), nil, nil) + checkAddResult(t, r, "[(b,(bv)){b,b}]", true) + + _, ok, _ := r.Remove(nil, "x") + if ok { + t.Fatal("should not be able to Remove x") + } + + r.Add(nil, "f", types.StringLiteral("fv"), nil, nil) + checkAddResult(t, r, "[(b,(bv))(f,(fv)){b,f}]", true) + + _, ok, _ = r.Remove(nil, "c") + if ok { + t.Fatal("should not be able to Remove c") + } + + r.Add(nil, "d", types.StringLiteral("dv"), nil, nil) + checkAddResult(t, r, "[(b,(bv))(d,(dv))(f,(fv)){b,f}]", true) + + _, ok = r.Value("c") + if ok { + t.Fatal("should not find Value for c") + } + + r.Add(nil, "h", types.StringLiteral("hv"), nil, nil) + checkAddResult(t, r, "{b,h},[(b,(bv))(d,(dv)){b,d}],[(f,(fv))(h,(hv)){f,h}]", false) + + r.Add(nil, "a", types.StringLiteral("av"), nil, nil) + checkAddResult(t, r, "{a,h},[(a,(av))(b,(bv))(d,(dv)){a,d}],[(f,(fv))(h,(hv)){f,h}]", false) + + r.Add(nil, "i", types.StringLiteral("iv"), nil, nil) + checkAddResult(t, r, "{a,i},[(a,(av))(b,(bv))(d,(dv)){a,d}],[(f,(fv))(h,(hv))(i,(iv)){f,i}]", false) + + r.Add(nil, "c", types.StringLiteral("cv"), nil, nil) + checkAddResult(t, r, "{a,i},{a,d},[(a,(av))(b,(bv)){a,b}],[(c,(cv))(d,(dv)){c,d}],[(f,(fv))(h,(hv))(i,(iv)){f,i}]", false) +} + +func destroyNameTree(t *testing.T, r *Node) { + + _, ok, _ := r.Remove(nil, "g") + if ok { + t.Fatal("should not be able to Remove g") + } + + v, ok := r.Value("a") + if !ok { + t.Fatal("cannot find Value for a") + } + if v.String() != "(av)" { + t.Fatalf("Value for a should be: %s but is %s", "av", v) + } + + _, ok = r.Value("x") + if ok { + t.Fatal("should not find Value for x") + } + + _, ok, _ = r.Remove(nil, "x") + if ok { + t.Fatal("should not be able to Remove x") + } + + empty, ok, _ := r.Remove(nil, "b") + checkRemoveResult(t, r, "b", empty, ok, "{a,i},{a,d},[(a,(av)){a,a}],[(c,(cv))(d,(dv)){c,d}],[(f,(fv))(h,(hv))(i,(iv)){f,i}]", false) + + empty, ok, _ = r.Remove(nil, "a") + checkRemoveResult(t, r, "a", empty, ok, "{c,i},[(c,(cv))(d,(dv)){c,d}],[(f,(fv))(h,(hv))(i,(iv)){f,i}]", false) + + v, ok = r.Value("h") + if !ok { + t.Fatal("cannot find Value for h") + } + if v.String() != "(hv)" { + t.Fatalf("Value for h should be: %s but is %s", "hv", v) + } + + _, ok = r.Value("x") + if ok { + t.Fatal("should not find Value for x") + } + + empty, ok, _ = r.Remove(nil, "h") + checkRemoveResult(t, r, "h", empty, ok, "{c,i},[(c,(cv))(d,(dv)){c,d}],[(f,(fv))(i,(iv)){f,i}]", false) + + empty, ok, _ = r.Remove(nil, "i") + checkRemoveResult(t, r, "i", empty, ok, "{c,f},[(c,(cv))(d,(dv)){c,d}],[(f,(fv)){f,f}]", false) + + empty, ok, _ = r.Remove(nil, "f") + checkRemoveResult(t, r, "f", empty, ok, "[(c,(cv))(d,(dv)){c,d}]", true) + + _, ok = r.Value("x") + if ok { + t.Fatal("should not find Value for x") + } + + if err := r.Add(nil, "c", types.StringLiteral("cvv"), nil, nil); err != nil { + t.Fatalf("update c:should not trigger DuplicateKeyException") + } + + empty, ok, _ = r.Remove(nil, "c") + checkRemoveResult(t, r, "c", empty, ok, "[(d,(dv)){d,d}]", true) + + empty, ok, _ = r.Remove(nil, "d") + if !ok { + t.Fatal("could not Remove d") + } + if !r.leaf() { + t.Fatal("root node should be a leaf node after removing d") + } + if !empty { + t.Fatal("r should be empty after removing f") + } + l := r.String() + exp := "[{,}]" + if l != exp { + t.Fatalf("Remove d: %s != %s", l, exp) + } +} + +func TestNameTree(t *testing.T) { + + r := &Node{} + buildNameTree(t, r) + destroyNameTree(t, r) +} diff --git a/pkg/pdfcpu/model/nup.go b/pkg/pdfcpu/model/nup.go new file mode 100644 index 0000000000000000000000000000000000000000..330a004848370725c53433d6231cefb495c683bf --- /dev/null +++ b/pkg/pdfcpu/model/nup.go @@ -0,0 +1,374 @@ +/* +Copyright 2021 The pdfcpu Authors. + +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 + + 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 model + +import ( + "bytes" + "fmt" + "io" + "math" + + "github.com/pdfcpu/pdfcpu/pkg/filter" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/matrix" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +type orientation int + +func (o orientation) String() string { + switch o { + + case RightDown: + return "right down" + + case DownRight: + return "down right" + + case LeftDown: + return "left down" + + case DownLeft: + return "down left" + + } + + return "" +} + +// These are the defined anchors for relative positioning. +const ( + RightDown orientation = iota + DownRight + LeftDown + DownLeft +) + +type BorderStyling struct { + Color *color.SimpleColor + LineStyle *types.LineJoinStyle + Width float64 +} + +// NUp represents the command details for the command "NUp". +type NUp struct { + PageDim *types.Dim // Page dimensions in display unit. + PageSize string // Paper size eg. A4L, A4P, A4(=default=A4P), see paperSize.go + UserDim bool // true if one of dimensions or paperSize provided overriding the default. + Orient orientation // One of rd(=default),dr,ld,dl - grid orientation + Enforce bool // enforce best-fit orientation of individual content on grid. + Grid *types.Dim // Intra page grid dimensions eg (2,2) + PageGrid bool // Create a m x n grid of pages for PDF inputfiles only (think "extra page n-Up"). + ImgInputFile bool // Process image or PDF input files. + Margin float64 // Cropbox for n-Up content. + Border bool // Draw bounding box. + BorderOnCropbox *BorderStyling // Draw bounding box around crop box. + BookletGuides bool // Draw folding and cutting lines. + MultiFolio bool // Render booklet as sequence of folios. + FolioSize int // Booklet multifolio folio size: default: 8 + BookletType BookletType // Is this a booklet or booklet cover layout + BookletBinding BookletBinding // Does the booklet have short or long-edge binding + InpUnit types.DisplayUnit // input display unit. + BgColor *color.SimpleColor // background color +} + +// DefaultNUpConfig returns the default NUp configuration. +func DefaultNUpConfig() *NUp { + return &NUp{ + PageSize: "A4", + Orient: RightDown, + Margin: 3, + Border: true, + Enforce: true, + } +} + +func (nup NUp) String() string { + return fmt.Sprintf("N-Up conf: %s %s, orient=%s, grid=%s, pageGrid=%t, isImage=%t\n", + nup.PageSize, *nup.PageDim, nup.Orient, *nup.Grid, nup.PageGrid, nup.ImgInputFile) +} + +// N returns the nUp value. +func (nup NUp) N() int { + return int(nup.Grid.Height * nup.Grid.Width) +} + +func (nup NUp) IsTopFoldBinding() bool { + return (nup.PageDim.Portrait() && nup.BookletBinding == ShortEdge) || (nup.PageDim.Landscape() && nup.BookletBinding == LongEdge) +} + +func (nup NUp) IsBooklet() bool { + return nup.BookletType == Booklet || nup.BookletType == BookletAdvanced +} + +// RectsForGrid calculates dest rectangles for given grid. +func (nup NUp) RectsForGrid() []*types.Rectangle { + cols := int(nup.Grid.Width) + rows := int(nup.Grid.Height) + + maxX := float64(nup.PageDim.Width) + maxY := float64(nup.PageDim.Height) + + gw := maxX / float64(cols) + gh := maxY / float64(rows) + + var llx, lly float64 + rr := []*types.Rectangle{} + + switch nup.Orient { + + case RightDown: + for i := rows - 1; i >= 0; i-- { + for j := 0; j < cols; j++ { + llx = float64(j) * gw + lly = float64(i) * gh + rr = append(rr, types.NewRectangle(llx, lly, llx+gw, lly+gh)) + } + } + + case DownRight: + for i := 0; i < cols; i++ { + for j := rows - 1; j >= 0; j-- { + llx = float64(i) * gw + lly = float64(j) * gh + rr = append(rr, types.NewRectangle(llx, lly, llx+gw, lly+gh)) + } + } + + case LeftDown: + for i := rows - 1; i >= 0; i-- { + for j := cols - 1; j >= 0; j-- { + llx = float64(j) * gw + lly = float64(i) * gh + rr = append(rr, types.NewRectangle(llx, lly, llx+gw, lly+gh)) + } + } + + case DownLeft: + for i := cols - 1; i >= 0; i-- { + for j := rows - 1; j >= 0; j-- { + llx = float64(i) * gw + lly = float64(j) * gh + rr = append(rr, types.NewRectangle(llx, lly, llx+gw, lly+gh)) + } + } + } + + return rr +} + +func createNUpFormForPDF(xRefTable *XRefTable, resDict *types.IndirectRef, content []byte, cropBox *types.Rectangle) (*types.IndirectRef, error) { + sd := types.StreamDict{ + Dict: types.Dict( + map[string]types.Object{ + "Type": types.Name("XObject"), + "Subtype": types.Name("Form"), + "BBox": cropBox.Array(), + "Matrix": types.NewNumberArray(1, 0, 0, 1, -cropBox.LL.X, -cropBox.LL.Y), + "Resources": *resDict, + }, + ), + Content: content, + FilterPipeline: []types.PDFFilter{{Name: filter.Flate, DecodeParms: nil}}, + } + + sd.InsertName("Filter", filter.Flate) + + if err := sd.Encode(); err != nil { + return nil, err + } + + return xRefTable.IndRefForNewObject(sd) +} + +// NUpTilePDFBytesForPDF applies nup tiles to content bytes. +func NUpTilePDFBytes(wr io.Writer, rSrc, rDest *types.Rectangle, formResID string, nup *NUp, rotate bool) { + + // rScr is a rectangular region represented by form formResID in form space. + + // rDest is an arbitrary rectangular region in dest space. + // It is the location where we want the form content to get rendered on a "best fit" basis. + // Accounting for the aspect ratios of rSrc and rDest "best fit" tries to fit the largest version of rScr into rDest. + // This may result in a 90 degree rotation. + // + // rotate: + // indicates if we need to apply a post rotation of 180 degrees eg for booklets. + // + // enforceOrient: + // indicates if we need to enforce dest's orientation. + + // Draw bounding box. + if nup.Border { + fmt.Fprintf(wr, "[]0 d 0.1 w %.2f %.2f m %.2f %.2f l %.2f %.2f l %.2f %.2f l s ", + rDest.LL.X, rDest.LL.Y, rDest.UR.X, rDest.LL.Y, rDest.UR.X, rDest.UR.Y, rDest.LL.X, rDest.UR.Y, + ) + } + + // Apply margin to rDest which potentially makes it smaller. + rDestCr := rDest.CroppedCopy(nup.Margin) + + // Calculate transform matrix. + + // Best fit translation of a source rectangle into a destination rectangle. + // For nup we enforce the dest orientation, + // whereas in cases where the original orientation needs to be preserved eg. for booklets, we don't. + w, h, dx, dy, r := types.BestFitRectIntoRect(rSrc, rDestCr, nup.Enforce, false) + + if nup.BgColor != nil { + if nup.ImgInputFile { + // Fill background. + draw.FillRectNoBorder(wr, rDest, *nup.BgColor) + } else if nup.Margin > 0 { + // Fill margins. + m := nup.Margin + DrawMargins(wr, *nup.BgColor, rDest, 0, m, m, m, m) + } + } + + // Apply additional rotation. + if rotate { + r += 180 + } + + sx := w + sy := h + if !nup.ImgInputFile { + sx /= rSrc.Width() + sy /= rSrc.Height() + } + + sin := math.Sin(r * float64(matrix.DegToRad)) + cos := math.Cos(r * float64(matrix.DegToRad)) + + switch r { + case 90: + dx += h + case 180: + dx += w + dy += h + case 270: + dy += w + } + + dx += rDestCr.LL.X + dy += rDestCr.LL.Y + + m := matrix.CalcTransformMatrix(sx, sy, sin, cos, dx, dy) + + // Apply transform matrix and display form. + fmt.Fprintf(wr, "q %.5f %.5f %.5f %.5f %.5f %.5f cm /%s Do Q ", + m[0][0], m[0][1], m[1][0], m[1][1], m[2][0], m[2][1], formResID) +} + +func translationForPageRotation(pageRot int, w, h float64) (float64, float64) { + var dx, dy float64 + + switch pageRot { + case 90, -270: + dy = h + case -90, 270: + dx = w + case 180, -180: + dx, dy = w, h + } + + return dx, dy +} + +// ContentBytesForPageRotation returns content bytes compensating for rot. +func ContentBytesForPageRotation(rot int, w, h float64) []byte { + dx, dy := translationForPageRotation(rot, w, h) + // Note: PDF rotation is clockwise! + m := matrix.CalcRotateAndTranslateTransformMatrix(float64(-rot), dx, dy) + var b bytes.Buffer + fmt.Fprintf(&b, "%.5f %.5f %.5f %.5f %.5f %.5f cm ", m[0][0], m[0][1], m[1][0], m[1][1], m[2][0], m[2][1]) + return b.Bytes() +} + +// NUpTilePDFBytesForPDF applies nup tiles from PDF. +func (ctx *Context) NUpTilePDFBytesForPDF( + pageNr int, + formsResDict types.Dict, + buf *bytes.Buffer, + rDest *types.Rectangle, + nup *NUp, + rotate bool) error { + + consolidateRes := true + d, _, inhPAttrs, err := ctx.PageDict(pageNr, consolidateRes) + if err != nil { + return err + } + if d == nil { + return errors.Errorf("pdfcpu: unknown page number: %d\n", pageNr) + } + + // Retrieve content stream bytes. + bb, err := ctx.PageContent(d) + if err == ErrNoContent { + // TODO render if has annotations. + return nil + } + if err != nil { + return err + } + + // Create an object for this resDict in xRefTable. + ir, err := ctx.IndRefForNewObject(inhPAttrs.Resources) + if err != nil { + return err + } + + cropBox := inhPAttrs.MediaBox + if inhPAttrs.CropBox != nil { + cropBox = inhPAttrs.CropBox + } + + // Account for existing rotation. + if inhPAttrs.Rotate != 0 { + if types.IntMemberOf(inhPAttrs.Rotate, []int{+90, -90, +270, -270}) { + w := cropBox.Width() + cropBox.UR.X = cropBox.LL.X + cropBox.Height() + cropBox.UR.Y = cropBox.LL.Y + w + } + bb = append(ContentBytesForPageRotation(inhPAttrs.Rotate, cropBox.Width(), cropBox.Height()), bb...) + } + + formIndRef, err := createNUpFormForPDF(ctx.XRefTable, ir, bb, cropBox) + if err != nil { + return err + } + + formResID := fmt.Sprintf("Fm%d", pageNr) + formsResDict.Insert(formResID, *formIndRef) + + // Append to content stream buf of destination page. + NUpTilePDFBytes(buf, cropBox, rDest, formResID, nup, rotate) + + return nil +} + +// AppendPageTree appends a pagetree d1 to page tree d2. +func AppendPageTree(d1 *types.IndirectRef, countd1 int, d2 types.Dict) error { + a := d2.ArrayEntry("Kids") + a = append(a, *d1) + d2.Update("Kids", a) + return d2.IncrementBy("Count", countd1) +} diff --git a/pkg/pdfcpu/model/page.go b/pkg/pdfcpu/model/page.go new file mode 100644 index 0000000000000000000000000000000000000000..f11a4fa0b82c1fdd3efc65e7c8aa8d6f6898713f --- /dev/null +++ b/pkg/pdfcpu/model/page.go @@ -0,0 +1,106 @@ +/* + Copyright 2022 The pdfcpu Authors. + + 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 + + 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 model + +import ( + "bytes" + "strconv" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +type Resource struct { + ID string + IndRef *types.IndirectRef +} + +// FontResource represents an existing PDF font resource. +type FontResource struct { + Res Resource + Lang string + CIDSet *types.IndirectRef + FontFile *types.IndirectRef + ToUnicode *types.IndirectRef + W *types.IndirectRef +} + +// FontMap maps font names to font resources. +type FontMap map[string]FontResource + +// EnsureKey registers fontName with corresponding font resource id. +func (fm FontMap) EnsureKey(fontName string) string { + // TODO userfontname prefix + for k, v := range fm { + if k == fontName { + return v.Res.ID + } + } + key := "F" + strconv.Itoa(len(fm)) + fm[fontName] = FontResource{Res: Resource{ID: key}} + return key +} + +// ImageResource represents an existing PDF image resource. +type ImageResource struct { + Res Resource + Width int + Height int +} + +// ImageMap maps image filenames to image resources. +type ImageMap map[string]ImageResource + +type FieldAnnotation struct { + Dict types.Dict + IndRef *types.IndirectRef + Ind int + Field bool + Kids types.Array +} + +// Page represents rendered page content. +type Page struct { + MediaBox *types.Rectangle + CropBox *types.Rectangle + Fm FontMap + Im ImageMap + Annots []FieldAnnotation + AnnotTabs map[int]FieldAnnotation + LinkAnnots []LinkAnnotation + Buf *bytes.Buffer + Fields types.Array +} + +// NewPage creates a page for given mediaBox and cropBox. +func NewPage(mediaBox, cropBox *types.Rectangle) Page { + return Page{ + MediaBox: mediaBox, + CropBox: cropBox, + Fm: FontMap{}, + Im: ImageMap{}, + AnnotTabs: map[int]FieldAnnotation{}, + Buf: new(bytes.Buffer)} +} + +// NewPageWithBg creates a page for a mediaBox. +func NewPageWithBg(mediaBox *types.Rectangle, c color.SimpleColor) Page { + p := Page{MediaBox: mediaBox, Fm: FontMap{}, Im: ImageMap{}, Buf: new(bytes.Buffer)} + draw.FillRectNoBorder(p.Buf, mediaBox, c) + return p +} diff --git a/pkg/pdfcpu/model/parse.go b/pkg/pdfcpu/model/parse.go new file mode 100644 index 0000000000000000000000000000000000000000..f4cb1f03b1c95089a5aadd8045b9170d2d7b051b --- /dev/null +++ b/pkg/pdfcpu/model/parse.go @@ -0,0 +1,1201 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 model + +import ( + "context" + "strconv" + "strings" + "unicode" + + "github.com/pkg/errors" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +var ( + errArrayCorrupt = errors.New("pdfcpu: parse: corrupt array") + errArrayNotTerminated = errors.New("pdfcpu: parse: unterminated array") + errDictionaryCorrupt = errors.New("pdfcpu: parse: corrupt dictionary") + errDictionaryNotTerminated = errors.New("pdfcpu: parse: unterminated dictionary") + errHexLiteralCorrupt = errors.New("pdfcpu: parse: corrupt hex literal") + errHexLiteralNotTerminated = errors.New("pdfcpu: parse: hex literal not terminated") + errNameObjectCorrupt = errors.New("pdfcpu: parse: corrupt name object") + errNoArray = errors.New("pdfcpu: parse: no array") + errNoDictionary = errors.New("pdfcpu: parse: no dictionary") + errStringLiteralCorrupt = errors.New("pdfcpu: parse: corrupt string literal, possibly unbalanced parenthesis") + errBufNotAvailable = errors.New("pdfcpu: parse: no buffer available") + errXrefStreamMissingW = errors.New("pdfcpu: parse: xref stream dict missing entry W") + errXrefStreamCorruptW = errors.New("pdfcpu: parse: xref stream dict corrupt entry W: expecting array of 3 int") + errXrefStreamCorruptIndex = errors.New("pdfcpu: parse: xref stream dict corrupt entry Index") + errObjStreamMissingN = errors.New("pdfcpu: parse: obj stream dict missing entry W") + errObjStreamMissingFirst = errors.New("pdfcpu: parse: obj stream dict missing entry First") +) + +func positionToNextWhitespace(s string) (int, string) { + for i, c := range s { + if unicode.IsSpace(c) || c == 0x00 { + return i, s[i:] + } + } + return 0, s +} + +// PositionToNextWhitespaceOrChar trims a string to next whitespace or one of given chars. +// Returns the index of the position or -1 if no match. +func positionToNextWhitespaceOrChar(s, chars string) (int, string) { + if len(chars) == 0 { + return positionToNextWhitespace(s) + } + + for i, c := range s { + for _, m := range chars { + if c == m || unicode.IsSpace(c) || c == 0x00 { + return i, s[i:] + } + } + } + + return -1, s +} + +func positionToNextEOL(s string) (string, int) { + for i, c := range s { + for _, m := range "\x0A\x0D" { + if c == m { + return s[i:], i + } + } + } + return "", 0 +} + +// trimLeftSpace trims leading whitespace and trailing comment. +func trimLeftSpace(s string, relaxed bool) (string, bool) { + if log.ParseEnabled() { + log.Parse.Printf("TrimLeftSpace: begin %s\n", s) + } + + whitespace := func(c rune) bool { return unicode.IsSpace(c) || c == 0x00 } + + whitespaceNoEol := func(r rune) bool { + switch r { + case '\t', '\v', '\f', ' ', 0x85, 0xA0, 0x00: + return true + } + return false + } + + var eol bool + + for { + if relaxed { + s = strings.TrimLeftFunc(s, whitespaceNoEol) + if len(s) >= 1 && (s[0] == '\n' || s[0] == '\r') { + eol = true + } + } + s = strings.TrimLeftFunc(s, whitespace) + if log.ParseEnabled() { + log.Parse.Printf("1 outstr: <%s>\n", s) + } + if len(s) <= 1 || s[0] != '%' { + break + } + // trim PDF comment (= '%' up to eol) + s, _ = positionToNextEOL(s) + if log.ParseEnabled() { + log.Parse.Printf("2 outstr: <%s>\n", s) + } + } + + if log.ParseEnabled() { + log.Parse.Printf("TrimLeftSpace: end %s\n", s) + } + + return s, eol +} + +// HexString validates and formats a hex string to be of even length. +func hexString(s string) (*string, bool) { + if len(s) == 0 { + s1 := "" + return &s1, true + } + + var sb strings.Builder + i := 0 + + for _, c := range strings.ToUpper(s) { + if strings.ContainsRune(" \x09\x0A\x0C\x0D", c) { + if i%2 > 0 { + sb.WriteString("0") + i = 0 + } + continue + } + isHexChar := false + for _, hexch := range "ABCDEF1234567890" { + if c == hexch { + isHexChar = true + sb.WriteRune(c) + i++ + break + } + } + if !isHexChar { + return nil, false + } + } + + // If the final digit of a hexadecimal string is missing - + // that is, if there is an odd number of digits - the final digit shall be assumed to be 0. + if i%2 > 0 { + sb.WriteString("0") + } + + ss := sb.String() + return &ss, true +} + +// balancedParenthesesPrefix returns the index of the end position of the balanced parentheses prefix of s +// or -1 if unbalanced. s has to start with '(' +func balancedParenthesesPrefix(s string) int { + var j int + escaped := false + + for i := 0; i < len(s); i++ { + + c := s[i] + + if !escaped && c == '\\' { + escaped = true + continue + } + + if escaped { + escaped = false + continue + } + + if c == '(' { + j++ + } + + if c == ')' { + j-- + } + + if j == 0 { + return i + } + + } + + return -1 +} + +func forwardParseBuf(buf string, pos int) string { + if pos < len(buf) { + return buf[pos:] + } + return "" +} + +func delimiter(b byte) bool { + s := "<>[]()/" + for i := 0; i < len(s); i++ { + if b == s[i] { + return true + } + } + return false +} + +// ParseObjectAttributes parses object number and generation of the next object for given string buffer. +func ParseObjectAttributes(line *string) (objectNumber *int, generationNumber *int, err error) { + + if line == nil || len(*line) == 0 { + return nil, nil, errors.New("pdfcpu: ParseObjectAttributes: buf not available") + } + + if log.ParseEnabled() { + log.Parse.Printf("ParseObjectAttributes: buf=<%s>\n", *line) + } + + l := *line + var remainder string + + i := strings.Index(l, "obj") + if i < 0 { + return nil, nil, errors.New("pdfcpu: ParseObjectAttributes: can't find \"obj\"") + } + + remainder = l[i+len("obj"):] + l = l[:i] + + // object number + + l, _ = trimLeftSpace(l, false) + if len(l) == 0 { + return nil, nil, errors.New("pdfcpu: ParseObjectAttributes: can't find object number") + } + + i, _ = positionToNextWhitespaceOrChar(l, "%") + if i <= 0 { + return nil, nil, errors.New("pdfcpu: ParseObjectAttributes: can't find end of object number") + } + + objNr, err := strconv.Atoi(l[:i]) + if err != nil { + return nil, nil, err + } + + // generation number + + l = l[i:] + l, _ = trimLeftSpace(l, false) + if len(l) == 0 { + return nil, nil, errors.New("pdfcpu: ParseObjectAttributes: can't find generation number") + } + + i, _ = positionToNextWhitespaceOrChar(l, "%") + if i <= 0 { + return nil, nil, errors.New("pdfcpu: ParseObjectAttributes: can't find end of generation number") + } + + genNr, err := strconv.Atoi(l[:i]) + if err != nil { + return nil, nil, err + } + + objectNumber = &objNr + generationNumber = &genNr + + *line = remainder + + return objectNumber, generationNumber, nil +} + +func parseArray(c context.Context, line *string) (*types.Array, error) { + if log.ParseEnabled() { + log.Parse.Println("ParseObject: value = Array") + } + if line == nil || len(*line) == 0 { + return nil, errNoArray + } + + l := *line + + if log.ParseEnabled() { + log.Parse.Printf("ParseArray: %s\n", l) + } + + if !strings.HasPrefix(l, "[") { + return nil, errArrayCorrupt + } + + if len(l) == 1 { + return nil, errArrayNotTerminated + } + + // position behind '[' + l = forwardParseBuf(l, 1) + + // position to first non whitespace char after '[' + l, _ = trimLeftSpace(l, false) + + if len(l) == 0 { + // only whitespace after '[' + return nil, errArrayNotTerminated + } + + a := types.Array{} + + for !strings.HasPrefix(l, "]") { + + obj, err := ParseObjectContext(c, &l) + if err != nil { + return nil, err + } + if log.ParseEnabled() { + log.Parse.Printf("ParseArray: new array obj=%v\n", obj) + } + a = append(a, obj) + + // we are positioned on the char behind the last parsed array entry. + if len(l) == 0 { + return nil, errArrayNotTerminated + } + + // position to next non whitespace char. + l, _ = trimLeftSpace(l, false) + if len(l) == 0 { + return nil, errArrayNotTerminated + } + } + + // position behind ']' + l = forwardParseBuf(l, 1) + + *line = l + + if log.ParseEnabled() { + log.Parse.Printf("ParseArray: returning array (len=%d): %v\n", len(a), a) + } + + return &a, nil +} + +func parseStringLiteral(line *string) (types.Object, error) { + // Balanced pairs of parenthesis are allowed. + // Empty literals are allowed. + // \ needs special treatment. + // Allowed escape sequences: + // \n x0A + // \r x0D + // \t x09 + // \b x08 + // \f xFF + // \( x28 + // \) x29 + // \\ x5C + // \ddd octal code sequence, d=0..7 + + // Ignore '\' for undefined escape sequences. + + // Unescaped 0x0A,0x0D or combination gets parsed as 0x0A. + + // Join split lines by '\' eol. + + if line == nil || len(*line) == 0 { + return nil, errBufNotAvailable + } + + if log.ParseEnabled() { + log.Parse.Printf("ParseObject: value = String Literal: <%s>\n", *line) + } + + l := *line + + if log.ParseEnabled() { + log.Parse.Printf("parseStringLiteral: begin <%s>\n", l) + } + + if len(l) < 2 || !strings.HasPrefix(l, "(") { + return nil, errStringLiteralCorrupt + } + + // Calculate prefix with balanced parentheses, + // return index of enclosing ')'. + i := balancedParenthesesPrefix(l) + if i < 0 { + // No balanced parentheses. + return nil, errStringLiteralCorrupt + } + + // remove enclosing '(', ')' + balParStr := l[1:i] + + // Parse string literal, see 7.3.4.2 + //str := stringLiteral(balParStr) + + // position behind ')' + *line = forwardParseBuf(l[i:], 1) + + stringLiteral := types.StringLiteral(balParStr) + if log.ParseEnabled() { + log.Parse.Printf("parseStringLiteral: end <%s>\n", stringLiteral) + } + + return stringLiteral, nil +} + +func parseHexLiteral(line *string) (types.Object, error) { + if line == nil || len(*line) == 0 { + return nil, errBufNotAvailable + } + + l := *line + + if log.ParseEnabled() { + log.Parse.Printf("parseHexLiteral: %s\n", l) + } + + if len(l) < 2 || !strings.HasPrefix(l, "<") { + return nil, errHexLiteralCorrupt + } + + // position behind '<' + l = forwardParseBuf(l, 1) + + eov := strings.Index(l, ">") // end of hex literal. + if eov < 0 { + return nil, errHexLiteralNotTerminated + } + + hexStr, ok := hexString(strings.TrimSpace(l[:eov])) + if !ok { + return nil, errHexLiteralCorrupt + } + + // position behind '>' + *line = forwardParseBuf(l[eov:], 1) + + return types.HexLiteral(*hexStr), nil +} + +func decodeNameHexSequence(s string) (string, error) { + decoded, err := types.DecodeName(s) + if err != nil { + return "", errNameObjectCorrupt + } + + return decoded, nil +} + +func parseName(line *string) (*types.Name, error) { + // see 7.3.5 + if log.ParseEnabled() { + log.Parse.Println("ParseObject: value = Name Object") + } + if line == nil || len(*line) == 0 { + return nil, errBufNotAvailable + } + + l := *line + + if log.ParseEnabled() { + log.Parse.Printf("parseNameObject: %s\n", l) + } + if len(l) < 2 || !strings.HasPrefix(l, "/") { + return nil, errNameObjectCorrupt + } + + // position behind '/' + l = forwardParseBuf(l, 1) + + // cut off on whitespace or delimiter + eok, _ := positionToNextWhitespaceOrChar(l, "/<>()[]%") + if eok < 0 { + // Name terminated by eol. + *line = "" + } else { + *line = l[eok:] + l = l[:eok] + } + + // Decode optional #xx sequences + l, err := decodeNameHexSequence(l) + if err != nil { + return nil, err + } + + nameObj := types.Name(l) + return &nameObj, nil +} + +func insertKey(d types.Dict, key string, val types.Object) error { + if _, found := d[key]; !found { + d[key] = val + } else { + // for now we digest duplicate keys. + // TODO + // if !validationRelaxed { + // return errDictionaryDuplicateKey + // } + // if log.CLIEnabled() { + // log.CLI.Printf("ParseDict: digesting duplicate key\n") + // } + } + + if log.ParseEnabled() { + log.Parse.Printf("ParseDict: dict[%s]=%v\n", key, val) + } + + return nil +} + +func processDictKeys(c context.Context, line *string, relaxed bool) (types.Dict, error) { + l := *line + var eol bool + d := types.NewDict() + + for !strings.HasPrefix(l, ">>") { + + if err := c.Err(); err != nil { + return nil, err + } + + keyName, err := parseName(&l) + if err != nil { + return nil, err + } + + if log.ParseEnabled() { + log.Parse.Printf("ParseDict: key = %s\n", keyName) + } + + // Position to first non whitespace after key. + l, eol = trimLeftSpace(l, relaxed) + + if len(l) == 0 { + if log.ParseEnabled() { + log.Parse.Println("ParseDict: only whitespace after key") + } + // Only whitespace after key. + return nil, errDictionaryNotTerminated + } + + var val types.Object + + if eol { + // #252: For dicts with kv pairs terminated by eol we accept a missing value as an empty string. + val = types.StringLiteral("") + } else { + if val, err = ParseObject(&l); err != nil { + return nil, err + } + } + + // Specifying the null object as the value of a dictionary entry (7.3.7, "Dictionary Objects") + // shall be equivalent to omitting the entry entirely. + if val != nil { + if err := insertKey(d, string(*keyName), val); err != nil { + return nil, err + } + } + + // We are positioned on the char behind the last parsed dict value. + if len(l) == 0 { + return nil, errDictionaryNotTerminated + } + + // Position to next non whitespace char. + l, _ = trimLeftSpace(l, false) + if len(l) == 0 { + return nil, errDictionaryNotTerminated + } + + } + *line = l + return d, nil +} + +func parseDict(c context.Context, line *string, relaxed bool) (types.Dict, error) { + if line == nil || len(*line) == 0 { + return nil, errNoDictionary + } + + l := *line + + if log.ParseEnabled() { + log.Parse.Printf("ParseDict: %s\n", l) + } + + if len(l) < 4 || !strings.HasPrefix(l, "<<") { + return nil, errDictionaryCorrupt + } + + // position behind '<<' + l = forwardParseBuf(l, 2) + + // position to first non whitespace char after '<<' + l, _ = trimLeftSpace(l, false) + + if len(l) == 0 { + // only whitespace after '[' + return nil, errDictionaryNotTerminated + } + + d, err := processDictKeys(c, &l, relaxed) + if err != nil { + return nil, err + } + + // position behind '>>' + l = forwardParseBuf(l, 2) + + *line = l + + if log.ParseEnabled() { + log.Parse.Printf("ParseDict: returning dict at: %v\n", d) + } + + return d, nil +} + +func noBuf(l *string) bool { + return l == nil || len(*l) == 0 +} + +func startParseNumericOrIndRef(l string) (string, string, int) { + i1, _ := positionToNextWhitespaceOrChar(l, "/<([]>%") + var l1 string + if i1 > 0 { + l1 = l[i1:] + } else { + l1 = l[len(l):] + } + + str := l + if i1 > 0 { + str = l[:i1] + } + + /* + Integers are sometimes prefixed with any form of 0. + Following is a list of valid prefixes that can be safely ignored: + 0 + 0.000000000 + */ + if len(str) > 1 && str[0] == '0' { + if str[1] == '+' || str[1] == '-' { + str = str[1:] + } else if str[1] == '.' { + var i int + for i = 2; len(str) > i && str[i] == '0'; i++ { + } + if len(str) > i && (str[i] == '+' || str[i] == '-') { + str = str[i:] + } + } + } + return str, l1, i1 +} + +func isRangeError(err error) bool { + if err, ok := err.(*strconv.NumError); ok { + if err.Err == strconv.ErrRange { + return true + } + } + return false +} + +func parseIndRef(s, l, l1 string, line *string, i, i2 int, rangeErr bool) (types.Object, error) { + + g, err := strconv.Atoi(s) + if err != nil { + // 2nd int(generation number) not available. + // Can't be an indirect reference. + if log.ParseEnabled() { + log.Parse.Printf("parseIndRef: 3 objects, 2nd no int, value is no indirect ref but numeric int: %d\n", i) + } + *line = l1 + return types.Integer(i), nil + } + + l = l[i2:] + l, _ = trimLeftSpace(l, false) + + if len(l) == 0 { + if rangeErr { + return nil, err + } + // only whitespace + *line = l1 + return types.Integer(i), nil + } + + if l[0] == 'R' { + *line = forwardParseBuf(l, 1) + if rangeErr { + return nil, nil + } + // We have all 3 components to create an indirect reference. + return *types.NewIndirectRef(i, g), nil + } + + if rangeErr { + return nil, err + } + + // 'R' not available. + // Can't be an indirect reference. + if log.ParseEnabled() { + log.Parse.Printf("parseNumericOrIndRef: value is no indirect ref(no 'R') but numeric int: %d\n", i) + } + *line = l1 + + return types.Integer(i), nil +} + +func parseFloat(s string) (types.Object, error) { + f, err := strconv.ParseFloat(s, 64) + if err != nil { + return nil, err + } + + if log.ParseEnabled() { + log.Parse.Printf("parseFloat: value is: %f\n", f) + } + + return types.Float(f), nil +} + +func parseNumericOrIndRef(line *string) (types.Object, error) { + if noBuf(line) { + return nil, errBufNotAvailable + } + + l := *line + + // if this object is an integer we need to check for an indirect reference eg. 1 0 R + // otherwise it has to be a float + // we have to check first for integer + s, l1, i1 := startParseNumericOrIndRef(l) + + // Try int + var rangeErr bool + i, err := strconv.Atoi(s) + if err != nil { + rangeErr = isRangeError(err) + if !rangeErr { + // Try float + *line = l1 + return parseFloat(s) + } + + // #407 + i = 0 + } + + // We have an Int! + + // if not followed by whitespace return sole integer value. + if i1 <= 0 || delimiter(l[i1]) { + + if rangeErr { + return nil, err + } + + if log.ParseEnabled() { + log.Parse.Printf("parseNumericOrIndRef: value is numeric int: %d\n", i) + } + *line = l1 + return types.Integer(i), nil + } + + // Must be indirect reference. (123 0 R) + // Missing is the 2nd int and "R". + + l = l[i1:] + l, _ = trimLeftSpace(l, false) + if len(l) == 0 { + // only whitespace + if rangeErr { + return nil, err + } + *line = l1 + return types.Integer(i), nil + } + + i2, _ := positionToNextWhitespaceOrChar(l, "/<([]>") + + // if only 2 token, can't be indirect reference. + // if not followed by whitespace return sole integer value. + if i2 <= 0 || delimiter(l[i2]) { + if rangeErr { + return nil, err + } + if log.ParseEnabled() { + log.Parse.Printf("parseNumericOrIndRef: 2 objects => value is numeric int: %d\n", i) + } + *line = l1 + return types.Integer(i), nil + } + + s = l + if i2 > 0 { + s = l[:i2] + } + + return parseIndRef(s, l, l1, line, i, i2, rangeErr) +} + +func parseHexLiteralOrDict(c context.Context, l *string) (val types.Object, err error) { + if len(*l) < 2 { + return nil, errBufNotAvailable + } + + // if next char = '<' parseDict. + if (*l)[1] == '<' { + if log.ParseEnabled() { + log.Parse.Println("parseHexLiteralOrDict: value = Dictionary") + } + var ( + d types.Dict + err error + ) + if d, err = parseDict(c, l, false); err != nil { + if d, err = parseDict(c, l, true); err != nil { + return nil, err + } + } + val = d + } else { + // hex literals + if log.ParseEnabled() { + log.Parse.Println("parseHexLiteralOrDict: value = Hex Literal") + } + if val, err = parseHexLiteral(l); err != nil { + return nil, err + } + } + + return val, nil +} + +func parseBooleanOrNull(l string) (val types.Object, s string, ok bool) { + // null, absent object + if strings.HasPrefix(l, "null") { + if log.ParseEnabled() { + log.Parse.Println("parseBoolean: value = null") + } + return nil, "null", true + } + + // boolean true + if strings.HasPrefix(l, "true") { + if log.ParseEnabled() { + log.Parse.Println("parseBoolean: value = true") + } + return types.Boolean(true), "true", true + } + + // boolean false + if strings.HasPrefix(l, "false") { + if log.ParseEnabled() { + log.Parse.Println("parseBoolean: value = false") + } + return types.Boolean(false), "false", true + } + + return nil, "", false +} + +// ParseObject parses next Object from string buffer and returns the updated (left clipped) buffer. +func ParseObject(line *string) (types.Object, error) { + return ParseObjectContext(context.Background(), line) +} + +// ParseObjectContext parses next Object from string buffer and returns the updated (left clipped) buffer. +// If the passed context is cancelled, parsing will be interrupted. +func ParseObjectContext(c context.Context, line *string) (types.Object, error) { + if noBuf(line) { + return nil, errBufNotAvailable + } + + l := *line + + if log.ParseEnabled() { + log.Parse.Printf("ParseObject: buf= <%s>\n", l) + } + + // position to first non whitespace char + l, _ = trimLeftSpace(l, false) + if len(l) == 0 { + // only whitespace + return nil, errBufNotAvailable + } + + var value types.Object + var err error + + switch l[0] { + + case '[': // array + a, err := parseArray(c, &l) + if err != nil { + return nil, err + } + value = *a + + case '/': // name + nameObj, err := parseName(&l) + if err != nil { + return nil, err + } + value = *nameObj + + case '<': // hex literal or dict + value, err = parseHexLiteralOrDict(c, &l) + if err != nil { + return nil, err + } + + case '(': // string literal + if value, err = parseStringLiteral(&l); err != nil { + return nil, err + } + + default: + var valStr string + var ok bool + value, valStr, ok = parseBooleanOrNull(l) + if ok { + l = forwardParseBuf(l, len(valStr)) + break + } + // Must be numeric or indirect reference: + // int 0 r + // int + // float + if value, err = parseNumericOrIndRef(&l); err != nil { + return nil, err + } + + } + + if log.ParseEnabled() { + log.Parse.Printf("ParseObject returning %v\n", value) + } + + *line = l + + return value, nil +} + +func createXRefStreamDict(sd *types.StreamDict, objs []int) (*types.XRefStreamDict, error) { + // Read parameter W in order to decode the xref table. + // array of integers representing the size of the fields in a single cross-reference entry. + + var wIntArr [3]int + + a := sd.W() + if a == nil { + return nil, errXrefStreamMissingW + } + + // validate array with 3 positive integers + if len(a) != 3 { + return nil, errXrefStreamCorruptW + } + + f := func(ok bool, i int) bool { + return !ok || i < 0 + } + + i1, ok := a[0].(types.Integer) + if f(ok, i1.Value()) { + return nil, errXrefStreamCorruptW + } + wIntArr[0] = int(i1) + + i2, ok := a[1].(types.Integer) + if f(ok, i2.Value()) { + return nil, errXrefStreamCorruptW + } + wIntArr[1] = int(i2) + + i3, ok := a[2].(types.Integer) + if f(ok, i3.Value()) { + return nil, errXrefStreamCorruptW + } + wIntArr[2] = int(i3) + + return &types.XRefStreamDict{ + StreamDict: *sd, + Size: *sd.Size(), + Objects: objs, + W: wIntArr, + PreviousOffset: sd.Prev(), + }, nil +} + +// ParseXRefStreamDict creates a XRefStreamDict out of a StreamDict. +func ParseXRefStreamDict(sd *types.StreamDict) (*types.XRefStreamDict, error) { + if log.ParseEnabled() { + log.Parse.Println("ParseXRefStreamDict: begin") + } + if sd.Size() == nil { + return nil, errors.New("pdfcpu: ParseXRefStreamDict: \"Size\" not available") + } + + objs := []int{} + + // Read optional parameter Index + indArr := sd.Index() + if indArr != nil { + if log.ParseEnabled() { + log.Parse.Println("ParseXRefStreamDict: using index dict") + } + + if len(indArr)%2 != 0 { + return nil, errXrefStreamCorruptIndex + } + + for i := 0; i < len(indArr)/2; i++ { + + startObj, ok := indArr[i*2].(types.Integer) + if !ok { + return nil, errXrefStreamCorruptIndex + } + + count, ok := indArr[i*2+1].(types.Integer) + if !ok { + return nil, errXrefStreamCorruptIndex + } + + for j := 0; j < count.Value(); j++ { + objs = append(objs, startObj.Value()+j) + } + } + + } else { + if log.ParseEnabled() { + log.Parse.Println("ParseXRefStreamDict: no index dict") + } + for i := 0; i < *sd.Size(); i++ { + objs = append(objs, i) + + } + } + + xsd, err := createXRefStreamDict(sd, objs) + if err != nil { + return nil, err + } + + if log.ParseEnabled() { + log.Parse.Println("ParseXRefStreamDict: end") + } + + return xsd, nil +} + +// ObjectStreamDict creates a ObjectStreamDict out of a StreamDict. +func ObjectStreamDict(sd *types.StreamDict) (*types.ObjectStreamDict, error) { + if sd.First() == nil { + return nil, errObjStreamMissingFirst + } + + if sd.N() == nil { + return nil, errObjStreamMissingN + } + + osd := types.ObjectStreamDict{ + StreamDict: *sd, + ObjCount: *sd.N(), + FirstObjOffset: *sd.First(), + ObjArray: nil} + + return &osd, nil +} + +func detectMarker(line, marker string) int { + i := strings.Index(line, marker) + if i < 0 { + return i + } + + if i+len(marker) >= len(line) { + return -1 + } + + // Skip until keyword is followed by eol. + for c := line[i+len(marker)]; c != 0x0A && c != 0x0D; { + line = line[i+len(marker):] + i = strings.Index(line, marker) + if i < 0 { + return i + } + if i+len(marker) >= len(line) { + return -1 + } + } + + return i +} + +func detectMarkers(line string, off int, endInd, streamInd *int) { + //fmt.Printf("buflen=%d\n%s", len(line), hex.Dump([]byte(line))) + if *endInd <= 0 { + *endInd = detectMarker(line, "endobj") + if *endInd >= 0 { + //l := fmt.Sprintf("%x", *endInd) + //fmt.Printf("endobj: %s\n", l) + *endInd += off + + } + } + if *streamInd <= 0 { + *streamInd = detectMarker(line, "stream") + if *streamInd > 0 { + //l := fmt.Sprintf("%x", *streamInd) + //fmt.Printf("stream: %s\n", l) + *streamInd += off + } + } +} + +func positionAfterStringLiteral(line string) (string, int, error) { + i := balancedParenthesesPrefix(line) + if i < 0 { + return "", 0, errStringLiteralCorrupt + } + + line = forwardParseBuf(line[i:], 1) + + return line, i + 1, nil +} + +func DetectKeywords(line string) (endInd int, streamInd int, err error) { + off, i := 0, 0 + for { + + pos1 := strings.Index(line, "(") // TODO ignore "\("" + pos2 := strings.Index(line, "%") // TODO ignore "\%"" + + if pos1 < 0 && pos2 < 0 { + detectMarkers(line, off, &endInd, &streamInd) + return endInd, streamInd, nil + } + + if pos2 < 0 || (pos1 >= 0 && pos1 < pos2) { + // Skip string literal. + l := line[:pos1] + detectMarkers(l, off, &endInd, &streamInd) + if endInd > 0 || streamInd > 0 { + return endInd, streamInd, nil + } + line, i, err = positionAfterStringLiteral(line[pos1:]) + if err != nil { + if endInd < 0 && streamInd < 0 { + err = nil + } + return -1, -1, err + } + off += pos1 + i + continue + } + + // Skip comment. + l := line[:pos2] + detectMarkers(l, off, &endInd, &streamInd) + if endInd > 0 || streamInd > 0 { + return endInd, streamInd, nil + } + line, i = positionToNextEOL(line[pos2:]) + if line == "" { + return -1, -1, nil + } + off += pos2 + i + } +} diff --git a/pkg/pdfcpu/model/parseConfig.go b/pkg/pdfcpu/model/parseConfig.go new file mode 100644 index 0000000000000000000000000000000000000000..20d3fad9d300a4eb35b323af4166f7c59b2e7806 --- /dev/null +++ b/pkg/pdfcpu/model/parseConfig.go @@ -0,0 +1,145 @@ +//go:build !js +// +build !js + +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 model + +import ( + "bytes" + "io" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" + "gopkg.in/yaml.v2" +) + +type configuration struct { + CheckFileNameExt bool `yaml:"checkFileNameExt"` + Reader15 bool `yaml:"reader15"` + DecodeAllStreams bool `yaml:"decodeAllStreams"` + ValidationMode string `yaml:"validationMode"` + PostProcessValidate bool `yaml:"postProcessValidate"` + Eol string `yaml:"eol"` + WriteObjectStream bool `yaml:"writeObjectStream"` + WriteXRefStream bool `yaml:"writeXRefStream"` + EncryptUsingAES bool `yaml:"encryptUsingAES"` + EncryptKeyLength int `yaml:"encryptKeyLength"` + Permissions int `yaml:"permissions"` + Unit string `yaml:"unit"` + Units string `yaml:"units"` // Be flexible if version < v0.3.8 + TimestampFormat string `yaml:"timestampFormat"` + DateFormat string `yaml:"dateFormat"` + Optimize bool `yaml:"optimize"` + OptimizeResourceDicts bool `yaml:"optimizeResourceDicts"` + OptimizeDuplicateContentStreams bool `yaml:"optimizeDuplicateContentStreams"` + CreateBookmarks bool `yaml:"createBookmarks"` + NeedAppearances bool `yaml:"needAppearances"` +} + +func loadedConfig(c configuration, configPath string) *Configuration { + var conf Configuration + conf.Path = configPath + + conf.CheckFileNameExt = c.CheckFileNameExt + conf.Reader15 = c.Reader15 + conf.DecodeAllStreams = c.DecodeAllStreams + conf.WriteObjectStream = c.WriteObjectStream + conf.WriteXRefStream = c.WriteXRefStream + conf.EncryptUsingAES = c.EncryptUsingAES + conf.EncryptKeyLength = c.EncryptKeyLength + conf.Permissions = PermissionFlags(c.Permissions) + + switch c.ValidationMode { + case "ValidationStrict": + conf.ValidationMode = ValidationStrict + case "ValidationRelaxed": + conf.ValidationMode = ValidationRelaxed + } + + conf.PostProcessValidate = c.PostProcessValidate + + switch c.Eol { + case "EolLF": + conf.Eol = types.EolLF + case "EolCR": + conf.Eol = types.EolCR + case "EolCRLF": + conf.Eol = types.EolCRLF + } + + switch c.Unit { + case "points": + conf.Unit = types.POINTS + case "inches": + conf.Unit = types.INCHES + case "cm": + conf.Unit = types.CENTIMETRES + case "mm": + conf.Unit = types.MILLIMETRES + } + + conf.TimestampFormat = c.TimestampFormat + conf.DateFormat = c.DateFormat + conf.Optimize = c.Optimize + conf.OptimizeResourceDicts = c.OptimizeResourceDicts + conf.OptimizeDuplicateContentStreams = c.OptimizeDuplicateContentStreams + conf.CreateBookmarks = c.CreateBookmarks + conf.NeedAppearances = c.NeedAppearances + + return &conf +} + +func parseConfigFile(r io.Reader, configPath string) error { + var c configuration + + // Enforce default for old config files. + c.CheckFileNameExt = true + + var buf bytes.Buffer + if _, err := io.Copy(&buf, r); err != nil { + return err + } + + if err := yaml.Unmarshal(buf.Bytes(), &c); err != nil { + return err + } + + if !types.MemberOf(c.ValidationMode, []string{"ValidationStrict", "ValidationRelaxed"}) { + return errors.Errorf("invalid validationMode: %s", c.ValidationMode) + } + if !types.MemberOf(c.Eol, []string{"EolLF", "EolCR", "EolCRLF"}) { + return errors.Errorf("invalid eol: %s", c.Eol) + } + if c.Unit == "" { + // v0.3.8 modifies "units" to "unit". + if c.Units != "" { + c.Unit = c.Units + } + } + if !types.MemberOf(c.Unit, []string{"points", "inches", "cm", "mm"}) { + return errors.Errorf("invalid unit: %s", c.Unit) + } + + if !types.IntMemberOf(c.EncryptKeyLength, []int{40, 128, 256}) { + return errors.Errorf("encryptKeyLength possible values: 40, 128, 256, got: %s", c.Unit) + } + + loadedDefaultConfig = loadedConfig(c, configPath) + + return nil +} diff --git a/pkg/pdfcpu/model/parseConfig_js.go b/pkg/pdfcpu/model/parseConfig_js.go new file mode 100644 index 0000000000000000000000000000000000000000..02bf7e4cba16c8db8e942ca265afc39473bad487 --- /dev/null +++ b/pkg/pdfcpu/model/parseConfig_js.go @@ -0,0 +1,327 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 model + +import ( + "bufio" + "io" + "strconv" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// This gets rid of the gopkg.in/yaml.v2 dependency for wasm builds. + +func handleCheckFileNameExt(k, v string, c *Configuration) error { + v = strings.ToLower(v) + if v != "true" && v != "false" { + return errors.Errorf("config key %s is boolean", k) + } + c.CheckFileNameExt = v == "true" + return nil +} + +func handleConfReader15(k, v string, c *Configuration) error { + v = strings.ToLower(v) + if v != "true" && v != "false" { + return errors.Errorf("config key %s is boolean", k) + } + c.Reader15 = v == "true" + return nil +} + +func handleConfDecodeAllStreams(k, v string, c *Configuration) error { + v = strings.ToLower(v) + if v != "true" && v != "false" { + return errors.Errorf("config key %s is boolean", k) + } + c.DecodeAllStreams = v == "true" + return nil +} + +func handleConfPostProcessValidate(k, v string, c *Configuration) error { + v = strings.ToLower(v) + if v != "true" && v != "false" { + return errors.Errorf("config key %s is boolean", k) + } + c.PostProcessValidate = v == "true" + return nil +} + +func handleConfValidationMode(v string, c *Configuration) error { + v1 := strings.ToLower(v) + switch v1 { + case "validationstrict": + c.ValidationMode = ValidationStrict + case "validationrelaxed": + c.ValidationMode = ValidationRelaxed + default: + return errors.Errorf("invalid validationMode: %s", v) + } + return nil +} + +func handleConfEol(v string, c *Configuration) error { + v1 := strings.ToLower(v) + switch v1 { + case "eollf": + c.Eol = types.EolLF + case "eolcr": + c.Eol = types.EolCR + case "eolcrlf": + c.Eol = types.EolCRLF + default: + return errors.Errorf("invalid eol: %s", v) + } + return nil +} + +func handleConfWriteObjectStream(k, v string, c *Configuration) error { + v = strings.ToLower(v) + if v != "true" && v != "false" { + return errors.Errorf("config key %s is boolean", k) + } + c.WriteObjectStream = v == "true" + return nil +} + +func handleConfWriteXRefStream(k, v string, c *Configuration) error { + v = strings.ToLower(v) + if v != "true" && v != "false" { + return errors.Errorf("config key %s is boolean", k) + } + c.WriteXRefStream = v == "true" + return nil +} + +func handleConfEncryptUsingAES(k, v string, c *Configuration) error { + v = strings.ToLower(v) + if v != "true" && v != "false" { + return errors.Errorf("config key %s is boolean", k) + } + c.EncryptUsingAES = v == "true" + return nil +} + +func handleConfEncryptKeyLength(v string, c *Configuration) error { + i, err := strconv.Atoi(v) + if err != nil { + return errors.Errorf("encryptKeyLength is numeric, got: %s", v) + } + if !types.IntMemberOf(i, []int{40, 128, 256}) { + return errors.Errorf("encryptKeyLength possible values: 40, 128, 256, got: %s", v) + } + c.EncryptKeyLength = i + return nil +} + +func handleConfPermissions(v string, c *Configuration) error { + i, err := strconv.Atoi(v) + if err != nil { + return errors.Errorf("permissions is numeric, got: %s", v) + } + c.Permissions = PermissionFlags(i) + return nil +} + +func handleConfUnit(v string, c *Configuration) error { + v1 := v + switch v1 { + case "points": + c.Unit = types.POINTS + case "inches": + c.Unit = types.INCHES + case "cm": + c.Unit = types.CENTIMETRES + case "mm": + c.Unit = types.MILLIMETRES + default: + return errors.Errorf("invalid unit: %s", v) + } + return nil +} + +func handleTimestampFormat(v string, c *Configuration) error { + c.TimestampFormat = v + return nil +} + +func handleDateFormat(v string, c *Configuration) error { + c.DateFormat = v + return nil +} + +func handleOptimize(k, v string, c *Configuration) error { + v = strings.ToLower(v) + if v != "true" && v != "false" { + return errors.Errorf("config key %s is boolean", k) + } + c.Optimize = v == "true" + return nil +} + +func handleOptimizeResourceDicts(k, v string, c *Configuration) error { + v = strings.ToLower(v) + if v != "true" && v != "false" { + return errors.Errorf("config key %s is boolean", k) + } + c.OptimizeResourceDicts = v == "true" + return nil +} + +func handleOptimizeDuplicateContentStreams(k, v string, c *Configuration) error { + v = strings.ToLower(v) + if v != "true" && v != "false" { + return errors.Errorf("config key %s is boolean", k) + } + c.OptimizeDuplicateContentStreams = v == "true" + return nil +} + +func handleCreateBookmarks(k, v string, c *Configuration) error { + v = strings.ToLower(v) + if v != "true" && v != "false" { + return errors.Errorf("config key %s is boolean", k) + } + c.CreateBookmarks = v == "true" + return nil +} + +func handleNeedAppearances(k, v string, c *Configuration) error { + v = strings.ToLower(v) + if v != "true" && v != "false" { + return errors.Errorf("config key %s is boolean", k) + } + c.NeedAppearances = v == "true" + return nil +} + +func parseKeysPart1(k, v string, c *Configuration) (bool, error) { + switch k { + + case "checkFileNameExt": + return true, handleCheckFileNameExt(k, v, c) + + case "reader15": + return true, handleConfReader15(k, v, c) + + case "decodeAllStreams": + return true, handleConfDecodeAllStreams(k, v, c) + + case "validationMode": + return true, handleConfValidationMode(v, c) + + case "postProcessValidate": + return true, handleConfPostProcessValidate(k, v, c) + + case "eol": + return true, handleConfEol(v, c) + + case "writeObjectStream": + return true, handleConfWriteObjectStream(k, v, c) + + case "writeXRefStream": + return true, handleConfWriteXRefStream(k, v, c) + } + + return false, nil +} + +func parseKeysPart2(k, v string, c *Configuration) error { + switch k { + + case "encryptUsingAES": + return handleConfEncryptUsingAES(k, v, c) + + case "encryptKeyLength": + return handleConfEncryptKeyLength(v, c) + + case "permissions": + return handleConfPermissions(v, c) + + case "unit", "units": + return handleConfUnit(v, c) + + case "timestampFormat": + return handleTimestampFormat(v, c) + + case "dateFormat": + return handleDateFormat(v, c) + + case "optimize": + return handleOptimize(k, v, c) + + case "optimizeResourceDicts": + return handleOptimizeResourceDicts(k, v, c) + + case "optimizeDuplicateContentStreams": + return handleOptimizeDuplicateContentStreams(k, v, c) + + case "createBookmarks": + return handleCreateBookmarks(k, v, c) + + case "needAppearances": + return handleNeedAppearances(k, v, c) + } + + return nil +} + +func parseKeyValue(k, v string, c *Configuration) error { + ok, err := parseKeysPart1(k, v, c) + if err != nil { + return err + } + if ok { + return nil + } + return parseKeysPart2(k, v, c) +} + +func parseConfigFile(r io.Reader, configPath string) error { + //fmt.Println("parseConfigFile For JS") + var conf Configuration + conf.Path = configPath + + s := bufio.NewScanner(r) + for s.Scan() { + t := s.Text() + if len(t) == 0 || t[0] == '#' { + continue + } + ss := strings.Split(t, ": ") + if len(ss) != 2 { + return errors.Errorf("invalid entry: <%s>", t) + } + k := strings.TrimSpace(ss[0]) + v := strings.TrimSpace(ss[1]) + if len(k) == 0 || len(v) == 0 { + return errors.Errorf("invalid entry: <%s>", t) + } + if err := parseKeyValue(k, v, &conf); err != nil { + return err + } + } + if err := s.Err(); err != nil { + return err + } + + loadedDefaultConfig = &conf + return nil +} diff --git a/pkg/pdfcpu/model/parseContent.go b/pkg/pdfcpu/model/parseContent.go new file mode 100644 index 0000000000000000000000000000000000000000..c8addcf79bf937a81706bcb2b0c58c31f8df000c --- /dev/null +++ b/pkg/pdfcpu/model/parseContent.go @@ -0,0 +1,411 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 model + +import ( + "strings" + "unicode" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +var ( + errPageContentCorrupt = errors.New("pdfcpu: corrupt page content") + errTJExpressionCorrupt = errors.New("pdfcpu: corrupt TJ expression") + errBIExpressionCorrupt = errors.New("pdfcpu: corrupt BI expression") +) + +func whitespaceOrEOL(c rune) bool { + return unicode.IsSpace(c) || c == 0x0A || c == 0x0D || c == 0x00 +} + +func skipDict(l *string) error { + s := *l + if !strings.HasPrefix(s, "<<") { + return errDictionaryCorrupt + } + s = s[2:] + j := 0 + for { + i := strings.IndexAny(s, "<>") + if i < 0 { + return errDictionaryCorrupt + } + if s[i] == '<' { + j++ + s = s[i+1:] + continue + } + if s[i] == '>' { + if j > 0 { + j-- + s = s[i+1:] + continue + } + // >> ? + s = s[i:] + if !strings.HasPrefix(s, ">>") { + return errDictionaryCorrupt + } + *l = s[2:] + break + } + } + return nil +} + +func skipStringLiteral(l *string) error { + s := *l + i := 0 + for { + i = strings.IndexByte(s, byte(')')) + if i <= 0 || i > 0 && s[i-1] != '\\' { + break + } + k := 0 + for j := i - 1; j >= 0 && s[j] == '\\'; j-- { + k++ + } + if k%2 == 0 { + break + } + // Skip \) + s = s[i+1:] + } + if i < 0 { + return errStringLiteralCorrupt + } + s = s[i+1:] + *l = s + return nil +} + +func skipHexStringLiteral(l *string) error { + s := *l + i := strings.Index(s, ">") + if i < 0 { + return errHexLiteralCorrupt + } + s = s[i+1:] + *l = s + return nil +} + +func skipTJ(l *string) error { + // Each element shall be either a string or a number. + s := *l + for { + s = strings.TrimLeftFunc(s, whitespaceOrEOL) + if s[0] == ']' { + s = s[1:] + break + } + if s[0] == '(' { + if err := skipStringLiteral(&s); err != nil { + return err + } + } + if s[0] == '<' { + if err := skipHexStringLiteral(&s); err != nil { + return err + } + } + i, _ := positionToNextWhitespaceOrChar(s, "<(]") + if i < 0 { + return errTJExpressionCorrupt + } + s = s[i:] + } + *l = s + return nil +} + +func skipBI(l *string, prn PageResourceNames) error { + s := *l + for { + s = strings.TrimLeftFunc(s, whitespaceOrEOL) + if strings.HasPrefix(s, "EI") && whitespaceOrEOL(rune(s[2])) { + s = s[2:] + break + } + // TODO Check len(s) > 0 + if s[0] == '/' { + s = s[1:] + i, _ := positionToNextWhitespaceOrChar(s, "/") + if i < 0 { + return errBIExpressionCorrupt + } + token := s[:i] + if token == "CS" || token == "ColorSpace" { + s = s[i:] + s, _ = trimLeftSpace(s, false) + s = s[1:] + i, _ = positionToNextWhitespaceOrChar(s, "/") + if i < 0 { + return errBIExpressionCorrupt + } + name := s[:i] + if !types.MemberOf(name, []string{"DeviceGray", "DeviceRGB", "DeviceCMYK", "Indexed", "G", "RGB", "CMYK", "I"}) { + prn["ColorSpace"][name] = true + } + } + s = s[i:] + continue + } + i, _ := positionToNextWhitespaceOrChar(s, "/") + if i < 0 { + return errBIExpressionCorrupt + } + s = s[i:] + } + *l = s + return nil +} + +func positionToNextContentToken(line *string, prn PageResourceNames) (bool, error) { + l := *line + for { + l = strings.TrimLeftFunc(l, whitespaceOrEOL) + if len(l) == 0 { + // whitespace or eol only + return true, nil + } + if l[0] == '%' { + // Skip comment. + l, _ = positionToNextEOL(l) + continue + } + + if l[0] == '[' { + // Skip TJ expression: + // [()...()] TJ + // [<>...<>] TJ + if err := skipTJ(&l); err != nil { + return true, err + } + continue + } + if l[0] == '(' { + // Skip text strings as in TJ, Tj, ', " expressions + if err := skipStringLiteral(&l); err != nil { + return true, err + } + continue + } + if l[0] == '<' { + // Skip hex strings as in TJ, Tj, ', " expressions + if err := skipHexStringLiteral(&l); err != nil { + return true, err + } + continue + } + if strings.HasPrefix(l, "BI") && (l[2] == '/' || whitespaceOrEOL(rune(l[2]))) { + // Handle inline image + l = l[2:] + if err := skipBI(&l, prn); err != nil { + return true, err + } + continue + } + *line = l + return false, nil + } +} + +func nextContentToken(line *string, prn PageResourceNames) (string, error) { + // A token is either a name or some chunk terminated by white space or one of /, (, [ + if noBuf(line) { + return "", nil + } + l := *line + t := "" + + //log.Parse.Printf("nextContentToken: start buf= <%s>\n", *line) + + // Skip Tj, TJ and inline images. + done, err := positionToNextContentToken(&l, prn) + if err != nil { + return t, err + } + if done { + return "", nil + } + + if l[0] == '/' { + // Cut off at / [ ( < or white space. + l1 := l[1:] + i, _ := positionToNextWhitespaceOrChar(l1, "/[(<") + if i <= 0 { + *line = "" + return t, errPageContentCorrupt + } + t = l1[:i] + l1 = l1[i:] + l1 = strings.TrimLeftFunc(l1, whitespaceOrEOL) + if !strings.HasPrefix(l1, "<<") { + t = "/" + t + *line = l1 + return t, nil + } + if err := skipDict(&l1); err != nil { + return t, err + } + *line = l1 + return t, nil + } + + i, _ := positionToNextWhitespaceOrChar(l, "/[(<") + if i <= 0 { + *line = "" + return l, nil + } + t = l[:i] + l = l[i:] + if strings.HasPrefix(l, "<<") { + if err := skipDict(&l); err != nil { + return t, err + } + } + *line = l + return t, nil +} + +func resourceNameAtPos1(s, name string, prn PageResourceNames) bool { + switch s { + case "cs", "CS": + if !types.MemberOf(name, []string{"DeviceGray", "DeviceRGB", "DeviceCMYK", "Pattern"}) { + prn["ColorSpace"][name] = true + if log.ParseEnabled() { + log.Parse.Printf("ColorSpace[%s]\n", name) + } + } + return true + + case "gs": + prn["ExtGState"][name] = true + if log.ParseEnabled() { + log.Parse.Printf("ExtGState[%s]\n", name) + } + return true + + case "Do": + prn["XObject"][name] = true + if log.ParseEnabled() { + log.Parse.Printf("XObject[%s]\n", name) + } + return true + + case "sh": + prn["Shading"][name] = true + if log.ParseEnabled() { + log.Parse.Printf("Shading[%s]\n", name) + } + return true + + case "scn", "SCN": + prn["Pattern"][name] = true + if log.ParseEnabled() { + log.Parse.Printf("Pattern[%s]\n", name) + } + return true + + case "ri", "BMC", "MP": + return true + + } + + return false +} + +func resourceNameAtPos2(s, name string, prn PageResourceNames) bool { + switch s { + case "Tf": + prn["Font"][name] = true + if log.ParseEnabled() { + log.Parse.Printf("Font[%s]\n", name) + } + return true + case "BDC", "DP": + prn["Properties"][name] = true + if log.ParseEnabled() { + log.Parse.Printf("Properties[%s]\n", name) + } + return true + } + return false +} + +func parseContent(s string) (PageResourceNames, error) { + var ( + name string + n bool + ) + prn := NewPageResourceNames() + + //fmt.Printf("parseContent:\n%s\n", hex.Dump([]byte(s))) + + for pos := 0; ; { + t, err := nextContentToken(&s, prn) + if log.ParseEnabled() { + log.Parse.Printf("t = <%s>\n", t) + } + if err != nil { + return nil, err + } + if t == "" { + return prn, nil + } + + if t[0] == '/' { + name = t[1:] + if n { + pos++ + } else { + n = true + pos = 0 + } + if log.ParseEnabled() { + log.Parse.Printf("name=%s\n", name) + } + continue + } + + if !n { + if log.ParseEnabled() { + log.Parse.Printf("skip:%s\n", t) + } + continue + } + + pos++ + if pos == 1 { + if resourceNameAtPos1(t, name, prn) { + n = false + } + continue + } + if pos == 2 { + if resourceNameAtPos2(t, name, prn) { + n = false + } + continue + } + return nil, errPageContentCorrupt + } +} diff --git a/pkg/pdfcpu/model/parseContent_test.go b/pkg/pdfcpu/model/parseContent_test.go new file mode 100644 index 0000000000000000000000000000000000000000..2bd38f4c62b6cd8683eed26b9b4a279351a32a1e --- /dev/null +++ b/pkg/pdfcpu/model/parseContent_test.go @@ -0,0 +1,52 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 model + +import ( + "reflect" + "testing" +) + +func TestParseContent(t *testing.T) { + s := `/CS0 cs/DeviceGray CS/Span<>>, Span<>>, Span<>>, + Span<>>, Span<>>, Span<>> BDC + /a1 BMC/a2 MP /a3 /MC0 BDC/P0 scn/RelativeColorimetric ri/P1 SCN/GS0 gs[(Q[i,j]/2.)16.6(The/]maxi\)-)]TJ/CS1 CS/a4<>> BDC /a5 <>> + BDC (0.5*\(1/8\)*64 or +/4.\))Tj/T1_0 1 Tf <00150015> Tj /Im5 Do/a5 << /A >> BDC/a6/MC1 DP /a7<<>>DP + BI /IM true/W 1/CS/CS2/H 1/BPC 1 ID EI Q /Pattern cs/Span<>> BDC/SH1 sh` + + want := NewPageResourceNames() + want["ColorSpace"]["CS0"] = true + want["ColorSpace"]["CS1"] = true + want["ColorSpace"]["CS2"] = true + want["ExtGState"]["GS0"] = true + want["Font"]["T1_0"] = true + want["Pattern"]["P0"] = true + want["Pattern"]["P1"] = true + want["Properties"]["MC0"] = true + want["Properties"]["MC1"] = true + want["Shading"]["SH1"] = true + want["XObject"]["Im5"] = true + + got, err := parseContent(s) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(want, got) { + t.Fatalf("want:\n%s\ngot:\n%s\n", want, got) + } +} diff --git a/pkg/pdfcpu/model/parse_array_test.go b/pkg/pdfcpu/model/parse_array_test.go new file mode 100644 index 0000000000000000000000000000000000000000..8a41e1b2ef8abd541de8c58d52ca586bf370d045 --- /dev/null +++ b/pkg/pdfcpu/model/parse_array_test.go @@ -0,0 +1,245 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 model + +import "testing" + +func doTestParseArrayOK(parseString string, t *testing.T) { + _, err := ParseObject(&parseString) + if err != nil { + t.Errorf("parseArray failed: <%v> <%s>\n", err, parseString) + return + } +} + +func doTestParseArrayFail(parseString string, t *testing.T) { + s := parseString + _, err := ParseObject(&parseString) + if err == nil { + t.Errorf("parseArray should have returned an error for %s\n", s) + } +} + +func TestParseArray(t *testing.T) { + + // 10000 x null + s := "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + "null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null null " + + s1 := "15538 0 R 15538 0 R 15538 0 R 15538 0 R 15538 0 R 15538 0 R 15539 0 R 15539 0 R 15539 0 R 15539 0 R 15540 0 R 15540 0 R 15540 0 R 15541 0 R 15541 0 R 15541 0 R 15541 0 R 15541 0 R 15541 0 R null 15541 0 R 15541 0 R 15542 0 R 15542 0 R 17468 0 R 17469 0 R 15543 0 R 17466 0 R 17467 0 R 15545 0 R 15545 0 R 15545 0 R 15545 0 R 15545 0 R 15545 0 R 15546 0 R 15546 0 R 15546 0 R 15546 0 R 15546 0 R 15546 0 R 15547 0 R 15548 0 R" + + //30000 null objs + doTestParseArrayOK("["+s+s+s+s1+"]", t) + + doTestParseArrayOK("[]", t) + doTestParseArrayOK("[ ]", t) + doTestParseArrayOK("[ ]abc", t) + + // Negative testing + doTestParseArrayFail("[", t) + doTestParseArrayFail("[ ", t) + + // Hex literals + doTestParseArrayFail("[<", t) + doTestParseArrayFail("[<>", t) + doTestParseArrayFail("[< >", t) + doTestParseArrayFail("[ <", t) + doTestParseArrayFail("[ <>", t) + doTestParseArrayFail("[ < >", t) + doTestParseArrayFail("[", t) + doTestParseArrayFail("[", t) + doTestParseArrayFail("[< ABG>", t) + doTestParseArrayFail("[", t) + doTestParseArrayFail("[", t) + doTestParseArrayFail("[ ", t) + doTestParseArrayFail("[<0ab> ", t) + doTestParseArrayFail("[a]", t) + doTestParseArrayFail("[<0ab> a]", t) + doTestParseArrayFail("[<0ab> a]", t) + + doTestParseArrayOK("[]", t) + doTestParseArrayOK("[< AB >]", t) + doTestParseArrayOK("[]", t) + doTestParseArrayOK("[]", t) + doTestParseArrayOK("[]", t) + doTestParseArrayOK("[<0ab> ]", t) + doTestParseArrayOK("[<0ab>]", t) + doTestParseArrayOK("[<0abc>]", t) + doTestParseArrayOK("[<0abc> <345bca> ]", t) + doTestParseArrayOK("[<01 05 02 02 03 00 00 00 00 00 00 00>]", t) + doTestParseArrayOK("[< 0 0 2 6 6 6 5 6 5 2 2 4>]", t) + + // String literals + doTestParseArrayOK("[(abc) (def) <20ff>]..", t) + doTestParseArrayOK("[(abc)()]..", t) + doTestParseArrayOK("[<743EEC2AFD93A438D87F5ED3D51166A8>]", t) + + // Mixed string and hex literals + doTestParseArrayOK("[(abc(i)) (<>)]", t) + doTestParseArrayOK("[(abc)<20ff>]..", t) + + // Arrays + doTestParseArrayOK("[/N[/A/B/C]]", t) + + // Dictionaries + doTestParseArrayOK("[<>269 0 R]", t) + doTestParseArrayOK("[/Name 123<>>]", t) + + doTestParseArrayOK("[/DictName<>>]", t) + doTestParseArrayOK("[/DictName</C[(Go!)]>>]", t) + doTestParseArrayOK("[/Name<<>>]", t) + doTestParseArrayOK("[/Name<>]", t) + doTestParseArrayOK("[/Name<>>]", t) + doTestParseArrayOK("[/Name<>]", t) + doTestParseArrayOK("[/CalRGB<>]", t) + + // Name objects + doTestParseArrayOK("[/]", t) + doTestParseArrayOK("[/ ]", t) + doTestParseArrayOK("[/N]", t) + doTestParseArrayOK("[/Name]", t) + doTestParseArrayOK("[/First /Last]", t) + doTestParseArrayOK("[ /First/Last]", t) + doTestParseArrayOK("[ /First/Last ]", t) + doTestParseArrayOK("[/PDF/ImageC/Text]", t) + doTestParseArrayOK("[/PDF /Text /ImageB /ImageC /ImageI]", t) + doTestParseArrayOK("[<004>]/Name[(abc)]]", t) + + // Numerics + doTestParseArrayOK("[0.000000-16763662]", t) // = -16763662 + doTestParseArrayOK("[1.09 2.00056]", t) + doTestParseArrayOK("[1.09 null true false [/Name1 2.00056]]", t) + doTestParseArrayOK("[[2.22 2.22 2.22][0.95043 1 1.09]]", t) + doTestParseArrayOK("[1]", t) + doTestParseArrayOK("[1.01]", t) + doTestParseArrayOK("[1 null]", t) + doTestParseArrayOK("[1 true[2 0]]", t) + doTestParseArrayOK("[1.09 2.00056]]", t) + doTestParseArrayOK("[0 0 841.89 595.28]", t) + doTestParseArrayOK("[ 487 190]", t) + doTestParseArrayOK("[0.95043 1 1.09]", t) + + // Indirect object references + doTestParseArrayOK("[1 true[2 0 R 14 0 R]]", t) + doTestParseArrayOK("[22 0 1 R 14 23 0 R 24 0 R 25 0 R 27 0 R 28 0 R 29 0 R 31 0 R]", t) + + // Dictionaries + doTestParseArrayOK("[/Name<<>>]", t) + doTestParseArrayOK("[/Name<< /Sub /Value>>]", t) + doTestParseArrayOK("[/Name<>]", t) + doTestParseArrayOK("[/Name<>>]", t) + doTestParseArrayOK("[/Name<>]", t) + doTestParseArrayOK("[/CalRGB<>]", t) +} diff --git a/pkg/pdfcpu/model/parse_box_test.go b/pkg/pdfcpu/model/parse_box_test.go new file mode 100644 index 0000000000000000000000000000000000000000..615f47e4309acb6f546ce5aca910055138025c9e --- /dev/null +++ b/pkg/pdfcpu/model/parse_box_test.go @@ -0,0 +1,171 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 model + +import ( + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +func doTestParseBoxListOK(s string, t *testing.T) { + t.Helper() + _, err := ParseBoxList(s) + if err != nil { + t.Errorf("parseBoxList failed: <%v> <%s>\n", err, s) + return + } +} + +func doTestParseBoxListFail(s string, t *testing.T) { + t.Helper() + _, err := ParseBoxList(s) + if err == nil { + t.Errorf("parseBoxList should have failed: <%s>\n", s) + return + } +} + +func TestParseBoxList(t *testing.T) { + doTestParseBoxListOK("", t) + doTestParseBoxListOK("m ", t) + doTestParseBoxListOK("media, crop", t) + doTestParseBoxListOK("m, c, t, b, a", t) + doTestParseBoxListOK("c,t,b,a,m", t) + doTestParseBoxListOK("media,crop,bleed,trim,art", t) + + doTestParseBoxListFail("crap", t) + doTestParseBoxListFail("c t b a ", t) + doTestParseBoxListFail("media;crop;bleed;trim;art", t) + +} + +func doTestParseBoxOK(s string, t *testing.T) { + t.Helper() + _, err := ParseBox(s, types.POINTS) + if err != nil { + t.Errorf("parseBox failed: <%v> <%s>\n", err, s) + return + } +} + +func doTestParseBoxFail(s string, t *testing.T) { + t.Helper() + _, err := ParseBox(s, types.POINTS) + if err == nil { + t.Errorf("parseBox should have failed: <%s>\n", s) + return + } +} + +func TestParseBox(t *testing.T) { + + // Box by rectangle. + doTestParseBoxOK("[0 0 200 400]", t) + doTestParseBoxOK("[200 400 0 0]", t) + doTestParseBoxOK("[-50 -50 200 400]", t) + doTestParseBoxOK("[2.5 2.5 200 400]", t) + doTestParseBoxFail("[2.5 200 400]", t) + doTestParseBoxFail("[2.5 200 400 500 600]", t) + doTestParseBoxFail("[-50 -50 200 x]", t) + + // Box by 1 margin value. + doTestParseBoxOK("10.5%", t) + doTestParseBoxOK("-10.5%", t) + doTestParseBoxOK("10", t) + doTestParseBoxOK("-10", t) + doTestParseBoxOK("10 abs", t) + doTestParseBoxOK(".5", t) + doTestParseBoxOK(".5 abs", t) + doTestParseBoxOK(".4 rel", t) + doTestParseBoxFail("50%", t) + doTestParseBoxFail("0.6 rel", t) + + // Box by 2 margin values. + doTestParseBoxOK("10% -40%", t) + doTestParseBoxOK("10 5", t) + doTestParseBoxOK("10 5 abs", t) + doTestParseBoxOK(".1 .5", t) + doTestParseBoxOK(".1 .5 abs", t) + doTestParseBoxOK(".1 .4 rel", t) + doTestParseBoxFail("10% 40", t) + doTestParseBoxFail(".5 .5 rel", t) + + // Box by 3 margin values. + doTestParseBoxOK("10% 15.5% 10%", t) + doTestParseBoxOK("10 5 15", t) + doTestParseBoxOK("10 5 15 abs", t) + doTestParseBoxOK(".1 .155 .1", t) + doTestParseBoxOK(".1 .155 .1 abs", t) + doTestParseBoxOK(".1 .155 .1 rel", t) + doTestParseBoxOK(".1 .155 .6 rel", t) + doTestParseBoxFail("10% 15.5 10%", t) + doTestParseBoxFail(".1 .155 r .1 .1", t) + doTestParseBoxFail(".1 .155 rel .1", t) + + // Box by 4 margin values. + doTestParseBoxOK("40% 40% 10% 10%", t) + doTestParseBoxOK("0.4 0.4 20 20", t) + doTestParseBoxOK("0.4 0.4 .1 .1", t) + doTestParseBoxOK("0.4 0.4 .1 .1 abs", t) + doTestParseBoxOK("0.4 0.4 .1 .1 rel", t) + doTestParseBoxOK("10% 20% 60% 70%", t) + doTestParseBoxOK("-40% 40% 10% 10%", t) + doTestParseBoxFail("40% 40% 70% 0%", t) + doTestParseBoxFail("40% 40% 100 100", t) + + // Box by arbitrary relative position within parent box. + doTestParseBoxOK("dim:30 30", t) + doTestParseBoxOK("dim:30 30 abs", t) + doTestParseBoxOK("dim:.3 .3 rel", t) + doTestParseBoxOK("dim:30% 30%", t) + doTestParseBoxOK("pos:tl, dim:30 30", t) + doTestParseBoxOK("pos:bl, off: 5 5, dim:30 30", t) + doTestParseBoxOK("pos:bl, off: -5 -5, dim:.3 .3 rel", t) + doTestParseBoxFail("pos:tl", t) + doTestParseBoxFail("off:.23 .5", t) +} + +func doTestParsePageBoundariesOK(s string, t *testing.T) { + t.Helper() + _, err := ParsePageBoundaries(s, types.POINTS) + if err != nil { + t.Errorf("parsePageBoundaries failed: <%v> <%s>\n", err, s) + return + } +} + +func doTestParsePageBoundariesFail(s string, t *testing.T) { + t.Helper() + _, err := ParsePageBoundaries(s, types.POINTS) + if err == nil { + t.Errorf("parsePageBoundaries should have failed: <%s>\n", s) + return + } +} + +func TestParsePageBoundaries(t *testing.T) { + doTestParsePageBoundariesOK("trim:10", t) + doTestParsePageBoundariesOK("media:[0 0 200 200], crop:10 20, trim:crop, art:bleed, bleed:art", t) + doTestParsePageBoundariesOK("media:[0 0 200 200], art:bleed, bleed:art", t) + doTestParsePageBoundariesOK("media:[0 0 200 200], art:bleed, trim:art", t) + doTestParsePageBoundariesOK("media:[0 0 200 200], art:bleed, trim:bleed", t) + doTestParsePageBoundariesOK("media:[0 0 200 200], trim:[30 30 170 170], art:bleed", t) + doTestParsePageBoundariesOK("media:[0 0 200 200]", t) + doTestParsePageBoundariesOK("media:10", t) + doTestParsePageBoundariesFail("media:trim", t) +} diff --git a/pkg/pdfcpu/model/parse_common_test.go b/pkg/pdfcpu/model/parse_common_test.go new file mode 100644 index 0000000000000000000000000000000000000000..ebd7634a45177d518e5a6c6d09fcc62a04e9dfc8 --- /dev/null +++ b/pkg/pdfcpu/model/parse_common_test.go @@ -0,0 +1,114 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 model + +import ( + "fmt" + "testing" +) + +func doTestParseObjectOK(parseString string, t *testing.T) { + str := parseString + o, err := ParseObject(&parseString) + if err != nil { + t.Errorf("parseObject failed: <%v>\n", err) + return + } + + //var nextParseString string + //if &parseString == nil { + // nextParseString = "end of parseString.\n" + //} else { + var nextParseString = fmt.Sprintf("next parseString: <%s>\n\n", parseString) + //} + + t.Logf("\nparseString: <%s>\nparsed Object: %v\n%s", str, o, nextParseString) +} + +func doTestParseObjectFail(parseString string, t *testing.T) { + s := parseString + _, err := ParseObject(&parseString) + if err == nil { + t.Errorf("parseObject should have returned an error for %s\n", s) + } else { + t.Logf("parseString: <%s> parsed Object, expected error: <%v>\n", parseString, err) + } +} + +func TestParseObject(t *testing.T) { + + doTestParseObjectOK("null ", t) + doTestParseObjectOK("true ", t) + doTestParseObjectOK("[true%comment\x0Anull]", t) + doTestParseObjectOK("[[%comment\x0dnull][%empty\x0A\x0Dtrue]false%comment\x0A]", t) + doTestParseObjectOK("<<>>", t) + doTestParseObjectOK("<>", t) + doTestParseObjectOK("<>", t) + doTestParseObjectOK("<>", t) + doTestParseObjectOK("[<><>]", t) + doTestParseObjectOK("/Name ", t) + doTestParseObjectFail("/Na#me", t) + doTestParseObjectFail("/Na#2me", t) + doTestParseObjectOK("/Na#20me", t) + doTestParseObjectOK("[null]abc", t) + + doTestParseObjectFail("/", t) + doTestParseObjectOK("/(", t) + doTestParseObjectOK("//", t) + doTestParseObjectOK("/abc/", t) + doTestParseObjectOK("/abc", t) + doTestParseObjectOK("/abc/def", t) + + doTestParseObjectOK("%comment\x0D%\x0a", t) + doTestParseObjectOK("[<0ab>%comment\x0a]", t) + doTestParseObjectOK("</Key2>>", t) + doTestParseObjectOK("<< /Key1 /Key2 >>", t) + doTestParseObjectOK("<>>", t) + doTestParseObjectOK("<>>", t) + doTestParseObjectOK("<>>", t) + doTestParseObjectOK("<>>", t) + doTestParseObjectOK("<>", t) + + doTestParseObjectOK("()", t) + doTestParseObjectOK("(gopher\\\x28go)", t) + doTestParseObjectOK("(gop\x0aher\\(go)", t) + doTestParseObjectOK("(go\\pher\\)go)", t) + + doTestParseObjectOK("[%comment\x0d(gopher\\ago)%comment\x0a]", t) + doTestParseObjectOK("()", t) + doTestParseObjectOK("<>", t) + + doTestParseObjectOK("[(abc)true/n1<20f>]..", t) + doTestParseObjectOK("[(abc)()]..", t) + doTestParseObjectOK("[<743EEC2AFD93A438D87F5ED3D51166A8>]", t) + + doTestParseObjectOK("1", t) + doTestParseObjectOK("1/", t) + + doTestParseObjectOK("3.43", t) + doTestParseObjectOK("3.43<", t) + + doTestParseObjectOK("1.2", t) + doTestParseObjectOK("[<0ab>]", t) + + doTestParseObjectOK("1 0 R%comment\x0a", t) + doTestParseObjectOK("[1 0 R /n 2 0 R]", t) + doTestParseObjectOK("<>", t) + doTestParseObjectOK("(!\\(S:\\356[\\272H\\355>>R{sb\\007)", t) + + doTestParseObjectOK("18446744071963345064 0 R", t) +} diff --git a/pkg/pdfcpu/model/parse_dict_test.go b/pkg/pdfcpu/model/parse_dict_test.go new file mode 100644 index 0000000000000000000000000000000000000000..b6004f76ff9028c58486c55911c567911c28d1f9 --- /dev/null +++ b/pkg/pdfcpu/model/parse_dict_test.go @@ -0,0 +1,174 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 model + +import ( + "fmt" + "strings" + "testing" +) + +func doTestParseDictOK(parseString string, t *testing.T) { + _, err := ParseObject(&parseString) + if err != nil { + t.Errorf("parseDict failed: <%v>\n", err) + return + } +} + +func doTestParseDictFail(parseString string, t *testing.T) { + s := parseString + o, err := ParseObject(&parseString) + if err == nil { + t.Errorf("parseDict should have returned an error for %s\n%v\n", s, o) + } +} + +func doTestParseDictGeneral(t *testing.T) { + doTestParseDictOK("<>", t) + doTestParseDictOK("<< /Key1 /Key2 >>", t) + doTestParseDictFail("<<", t) + doTestParseDictFail("<<>", t) + doTestParseDictOK("<<>>", t) + doTestParseDictOK("<< >>", t) + doTestParseDictOK("<>", t) + doTestParseDictOK("<>/XObject<>/ProcSet[/PDF/Text/ImageB/ImageC/ImageI]>>/MediaBox[ 0 0 595.32 841.92]/Contents 4 0 R/Group<>/Tabs/S/StructParents 0>>", t) +} + +func doTestParseDictNameObjects(t *testing.T) { + // Name Objects + doTestParseDictOK("<>", t) + doTestParseDictOK("<>", t) + doTestParseDictOK("<>", t) // empty name + doTestParseDictOK("<>", t) + doTestParseDictOK("<>", t) + doTestParseDictOK("<< /Key /Value>>", t) + doTestParseDictOK("<< /Key/Value >>", t) + doTestParseDictOK("<< /Key /Value >>", t) + doTestParseDictOK("<>", t) +} + +func doTestParseDictStringLiteral(t *testing.T) { + // String literals + doTestParseDictOK("<>..", t) + doTestParseDictOK("<>inner2)def) >>..", t) +} + +func doTestParseDictHexLiteral(t *testing.T) { + // Hex literals + doTestParseDictFail("<>", t) + doTestParseDictFail("<>", t) + doTestParseDictFail("<", t) + doTestParseDictFail("<", t) + doTestParseDictFail("<>>", t) + doTestParseDictFail("<>>", t) + doTestParseDictFail("<", t) + doTestParseDictOK("</Key2>>", t) + doTestParseDictOK("<< /Key1 /Key2 >>", t) + doTestParseDictOK("<>>", t) + doTestParseDictOK("<>>", t) + doTestParseDictOK("<>>", t) + doTestParseDictOK("<>>", t) + doTestParseDictOK("<< /Panose <01 05 02 02 03 00 00 00 00 00 00 00> >>", t) + doTestParseDictOK("<< /Panose < 0 0 2 6 6 6 5 6 5 2 2 4> >>", t) + doTestParseDictOK("<>>", t) +} + +func doTestParseDictDict(t *testing.T) { + // Dictionaries + doTestParseDictOK("<>>>", t) + doTestParseDictOK("<>>>", t) + doTestParseDictOK("<>>>", t) + doTestParseDictOK("<>>>", t) + doTestParseDictOK("<>>>", t) + doTestParseDictOK("<>>>", t) + doTestParseDictOK("<>]>>", t) + doTestParseDictOK("<> /B3]>>", t) + doTestParseDictOK("<>]>>", t) + doTestParseDictOK("<>>]>>", t) +} + +func doTestParseDictArray(t *testing.T) { + // Arrays + doTestParseDictOK("<>", t) + doTestParseDictOK("<12.24 (gopher)]>>", t) + doTestParseDictOK("<12.24 (gopher)] /Key2[(abc)2.34[2 0 R]]>>", t) + doTestParseDictOK("<>", t) + doTestParseDictOK("<>269 0 R]/P 258 0 R/S/Link/Pg 19 0 R>>", t) +} + +func doTestParseDictBool(t *testing.T) { + // null, true, false + doTestParseDictOK("<>", t) + doTestParseDictOK("<>", t) + doTestParseDictOK("<>", t) + doTestParseDictFail("<>", t) +} + +func doTestParseDictNumerics(t *testing.T) { + // Numerics + doTestParseDictOK("<>", t) + doTestParseDictOK("<>", t) + doTestParseDictFail("<>", t) +} + +func doTestParseDictIndirectRefs(t *testing.T) { + // Indirect object references + doTestParseDictOK("<>", t) + doTestParseDictOK("<>", t) +} + +func doTestParseDictWithComments(t *testing.T) { + + doTestParseDictOK(`<%comment after hex string end +/Keywords(PDF,Compacted,Syntax,ISO 32000-2:2020)/CreationDate(D:20200317)/Author(Peter Wyatt)/Creator<48616e642d65646974>/Producer<48616e642d65646974>>>/ID[<18D6B641245C03FABE67D93AD879D6EC><6264992C92074533A46A019C7CF9BFB6>]/Size 7>>`, t) + + doTestParseDictOK(`<>/ProcSet[null]/ExtGState<>/Font<>>>>>>>`, t) + +} + +func doTestLargeDicts(t *testing.T) { + var sb strings.Builder + sb.WriteString("<<") + sb.WriteString("/Key#28#29 (Value)") + for i := 0; i < 50000; i++ { + sb.WriteString(fmt.Sprintf("/Key%d (Value)", i)) + } + sb.WriteString(">>") + + doTestParseDictOK(sb.String(), t) +} + +func TestParseDict(t *testing.T) { + doTestParseDictGeneral(t) + doTestParseDictNameObjects(t) + doTestParseDictStringLiteral(t) + doTestParseDictHexLiteral(t) + doTestParseDictDict(t) + doTestParseDictArray(t) + doTestParseDictBool(t) + doTestParseDictNumerics(t) + doTestParseDictIndirectRefs(t) + doTestParseDictWithComments(t) + doTestLargeDicts(t) +} diff --git a/pkg/pdfcpu/model/parse_test.go b/pkg/pdfcpu/model/parse_test.go new file mode 100644 index 0000000000000000000000000000000000000000..6ec3afd499c671cc5790d89417661b6fc2b16699 --- /dev/null +++ b/pkg/pdfcpu/model/parse_test.go @@ -0,0 +1,92 @@ +/* +Copyright 2024 The pdfcpu Authors. + +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 + + 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 model + +import ( + "testing" +) + +func TestDecodeNameHexInvalid(t *testing.T) { + testcases := []string{ + "#", + "#A", + "#a", + "#G0", + "#00", + "Fo\x00", + } + for _, tc := range testcases { + if decoded, err := decodeNameHexSequence(tc); err == nil { + t.Errorf("expected error decoding %s, got %s", tc, decoded) + } + } +} + +func TestDecodeNameHexValid(t *testing.T) { + testcases := []struct { + Input string + Expected string + }{ + {"", ""}, + {"Foo", "Foo"}, + {"A#23", "A#"}, + // Examples from "7.3.5 Name Objects" + {"Name1", "Name1"}, + {"ASomewhatLongerName", "ASomewhatLongerName"}, + {"A;Name_With-Various***Characters?", "A;Name_With-Various***Characters?"}, + {"1.2", "1.2"}, + {"$$", "$$"}, + {"@pattern", "@pattern"}, + {".notdef", ".notdef"}, + {"Lime#20Green", "Lime Green"}, + {"paired#28#29parentheses", "paired()parentheses"}, + {"The_Key_of_F#23_Minor", "The_Key_of_F#_Minor"}, + {"A#42", "AB"}, + } + for _, tc := range testcases { + decoded, err := decodeNameHexSequence(tc.Input) + if err != nil { + t.Errorf("decoding %s failed: %s", tc.Input, err) + } else if decoded != tc.Expected { + t.Errorf("expected %s when decoding %s, got %s", tc.Expected, tc.Input, decoded) + } + } +} + +func TestDetectKeywords(t *testing.T) { + msg := "detectKeywords" + + s := "1 0 obj\n<<\n /Lang (en-endobject-stream-UK%) % comment \n>>\nendobj\n\n2 0 obj\n" + // 0....... ..1 .........2.........3.........4.........5..... ... .6 + endInd, _, err := DetectKeywords(s) + if err != nil { + t.Errorf("%s failed: %v", msg, err) + } + if endInd != 59 { + t.Errorf("%s failed: want %d, got %d", msg, 59, endInd) + } + + s = "1 0 obj\n<<\n /Lang (en-endobject-stream-UK%) % endobject" + endInd, _, err = DetectKeywords(s) + if err != nil { + t.Errorf("%s failed: %v", msg, err) + } + if endInd > 0 { + t.Errorf("%s failed: want %d, got %d", msg, 0, endInd) + } + +} diff --git a/pkg/pdfcpu/model/repair.go b/pkg/pdfcpu/model/repair.go new file mode 100644 index 0000000000000000000000000000000000000000..44e4918f67fea4a7855a9596416d998f830b555b --- /dev/null +++ b/pkg/pdfcpu/model/repair.go @@ -0,0 +1,56 @@ +/* +Copyright 2024 The pdfcpu Authors. + +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 + + 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 model + +import ( + "fmt" + + "github.com/pdfcpu/pdfcpu/pkg/log" +) + +func ReportSpecViolation(xRefTable *XRefTable, err error) { + // TODO Apply across code base. + pre := fmt.Sprintf("digesting spec violation around obj#(%d)", xRefTable.CurObj) + if log.DebugEnabled() { + log.Debug.Printf("%s: %v\n", pre, err) + } + if log.ReadEnabled() { + log.Read.Printf("%s: %v\n", pre, err) + } + if log.ValidateEnabled() { + log.Validate.Printf("%s: %v\n", pre, err) + } + if log.CLIEnabled() { + log.CLI.Printf("%s: %v\n", pre, err) + } +} + +func ShowRepaired(msg string) { + msg = "repaired: " + msg + if log.DebugEnabled() { + log.Debug.Println("pdfcpu " + msg) + } + if log.ReadEnabled() { + log.Read.Println("pdfcpu " + msg) + } + if log.ValidateEnabled() { + log.Validate.Println("pdfcpu " + msg) + } + if log.CLIEnabled() { + log.CLI.Println(msg) + } +} diff --git a/pkg/pdfcpu/model/resize.go b/pkg/pdfcpu/model/resize.go new file mode 100644 index 0000000000000000000000000000000000000000..6fdfeef760424326b38416de78e6e1e1104e9004 --- /dev/null +++ b/pkg/pdfcpu/model/resize.go @@ -0,0 +1,192 @@ +/* +Copyright 2021 The pdfcpu Authors. + +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 + + 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 model + +import ( + "strconv" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +type Resize struct { + Scale float64 // scale factor x > 0, x > 1 enlarges, x < 1 shrinks down + Unit types.DisplayUnit // display unit + PageDim *types.Dim // page dimensions in display unit + PageSize string // paper size eg. A2,A3,A4,Legal,Ledger,... + EnforceOrient bool // enforce orientation of PageDim + UserDim bool // true if dimensions set by dim rather than formsize + Border bool // true to render original crop box + BgColor *color.SimpleColor // background color +} + +func (r Resize) EnforceOrientation() bool { + return r.EnforceOrient || strings.HasSuffix(r.PageSize, "P") || strings.HasSuffix(r.PageSize, "L") +} + +func parsePageDimRes(v string, u types.DisplayUnit) (*types.Dim, string, error) { + + ss := strings.Split(v, " ") + if len(ss) != 2 { + return nil, v, errors.Errorf("pdfcpu: illegal dimension string: need 2 values one may be 0, %s\n", v) + } + + w, err := strconv.ParseFloat(ss[0], 64) + if err != nil || w < 0 { + return nil, v, errors.Errorf("pdfcpu: dimension width must be >= 0: %s\n", ss[0]) + } + + h, err := strconv.ParseFloat(ss[1], 64) + if err != nil || h < 0 { + return nil, v, errors.Errorf("pdfcpu: dimension height must >= 0: %s\n", ss[1]) + } + + d := types.Dim{Width: types.ToUserSpace(w, u), Height: types.ToUserSpace(h, u)} + + return &d, "", nil +} + +func parseDimensionsRes(s string, res *Resize) (err error) { + res.PageDim, _, err = parsePageDimRes(s, res.Unit) + res.UserDim = true + return err +} + +func parseEnforceOrientation(s string, res *Resize) error { + switch strings.ToLower(s) { + case "on", "true", "t": + res.EnforceOrient = true + case "off", "false", "f": + res.EnforceOrient = false + default: + return errors.New("pdfcpu: enforce orientation, please provide one of: on/off true/false") + } + + return nil +} + +func parsePageFormatRes(s string, res *Resize) error { + + // Optional: appended last letter L indicates landscape mode. + // Optional: appended last letter P indicates portrait mode. + // eg. A4L means A4 in landscape mode whereas A4 defaults to A4P + // The default mode is defined implicitly via PaperSize dimensions. + + var landscape, portrait bool + + v := s + if strings.HasSuffix(v, "L") { + v = v[:len(v)-1] + landscape = true + } else if strings.HasSuffix(v, "P") { + v = v[:len(v)-1] + portrait = true + } + + d, ok := types.PaperSize[v] + if !ok { + return errors.Errorf("pdfcpu: page format %s is unsupported.\n", v) + } + + if (d.Portrait() && landscape) || (d.Landscape() && portrait) { + d.Width, d.Height = d.Height, d.Width + res.EnforceOrient = true + } + + res.PageDim = d + res.PageSize = v + + return nil +} + +func parseScaleFactorSimple(s string) (float64, error) { + + sc, err := strconv.ParseFloat(s, 64) + if err != nil { + return 0, errors.Errorf("pdfcpu: scale factor must be a float value: %s\n", s) + } + + if sc <= 0 || sc == 1 { + return 0, errors.Errorf("pdfcpu: invalid scale factor %.2f: 0.0 < i < 1.0 or i > 1.0\n", sc) + } + + return sc, nil +} + +func parseScaleFactorRes(s string, res *Resize) (err error) { + res.Scale, err = parseScaleFactorSimple(s) + return err +} + +func parseBackgroundColorRes(s string, res *Resize) error { + c, err := color.ParseColor(s) + if err != nil { + return err + } + res.BgColor = &c + return nil +} + +func parseBorderRes(s string, res *Resize) error { + switch strings.ToLower(s) { + case "on", "true", "t": + res.Border = true + case "off", "false", "f": + res.Border = false + default: + return errors.New("pdfcpu: resize border, please provide one of: on/off true/false t/f") + } + + return nil +} + +type resizeParameterMap map[string]func(string, *Resize) error + +var ResizeParamMap = resizeParameterMap{ + "dimensions": parseDimensionsRes, + "enforce": parseEnforceOrientation, + "formsize": parsePageFormatRes, + "papersize": parsePageFormatRes, + "scalefactor": parseScaleFactorRes, + "bgcolor": parseBackgroundColorRes, + "border": parseBorderRes, +} + +// Handle applies parameter completion and on success parse parameter values into resize. +func (m resizeParameterMap) Handle(paramPrefix, paramValueStr string, res *Resize) error { + + var param string + + // Completion support + for k := range m { + if !strings.HasPrefix(k, strings.ToLower(paramPrefix)) { + continue + } + if len(param) > 0 { + return errors.Errorf("pdfcpu: ambiguous parameter prefix \"%s\"", paramPrefix) + } + param = k + } + + if param == "" { + return errors.Errorf("pdfcpu: unknown parameter prefix \"%s\"", paramPrefix) + } + + return m[param](paramValueStr, res) +} diff --git a/pkg/pdfcpu/model/resource.go b/pkg/pdfcpu/model/resource.go new file mode 100644 index 0000000000000000000000000000000000000000..e04679f923772ec4d08420b602c7556b68d5d7b0 --- /dev/null +++ b/pkg/pdfcpu/model/resource.go @@ -0,0 +1,170 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 model + +import ( + "fmt" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +// FontObject represents a font used in a PDF file. +type FontObject struct { + ResourceNames []string + Prefix string + FontName string + FontDict types.Dict + Data []byte + Extension string +} + +// AddResourceName adds a resourceName referring to this font. +func (fo *FontObject) AddResourceName(resourceName string) { + for _, resName := range fo.ResourceNames { + if resName == resourceName { + return + } + } + fo.ResourceNames = append(fo.ResourceNames, resourceName) +} + +// ResourceNamesString returns a string representation of all the resource names of this font. +func (fo FontObject) ResourceNamesString() string { + var resNames []string + resNames = append(resNames, fo.ResourceNames...) + return strings.Join(resNames, ",") +} + +// Data returns the raw data belonging to this image object. +// func (fo FontObject) Data() []byte { +// return nil +// } + +// SubType returns the SubType of this font. +func (fo FontObject) SubType() string { + var subType string + if fo.FontDict.Subtype() != nil { + subType = *fo.FontDict.Subtype() + } + return subType +} + +// Encoding returns the Encoding of this font. +func (fo FontObject) Encoding() string { + encoding := "Built-in" + pdfObject, found := fo.FontDict.Find("Encoding") + if found { + switch enc := pdfObject.(type) { + case types.Name: + encoding = enc.Value() + default: + encoding = "Custom" + } + } + return encoding +} + +// Embedded returns true if the font is embedded into this PDF file. +func (fo FontObject) Embedded() (embedded bool) { + + _, embedded = fo.FontDict.Find("FontDescriptor") + + if !embedded { + _, embedded = fo.FontDict.Find("DescendantFonts") + } + + return +} + +func (fo FontObject) String() string { + return fmt.Sprintf("%-10s %-30s %-10s %-20s %-8v %s\n", + fo.Prefix, fo.FontName, + fo.SubType(), fo.Encoding(), + fo.Embedded(), fo.ResourceNamesString()) +} + +// ImageObject represents an image used in a PDF file. +type ImageObject struct { + ResourceNames []string + ImageDict *types.StreamDict +} + +// AddResourceName adds a resourceName to this imageObject's ResourceNames dict. +func (io *ImageObject) AddResourceName(resourceName string) { + for _, resName := range io.ResourceNames { + if resName == resourceName { + return + } + } + io.ResourceNames = append(io.ResourceNames, resourceName) +} + +// ResourceNamesString returns a string representation of the ResourceNames for this image. +func (io ImageObject) ResourceNamesString() string { + var resNames []string + resNames = append(resNames, io.ResourceNames...) + return strings.Join(resNames, ",") +} + +var resourceTypes = types.NewStringSet([]string{"ColorSpace", "ExtGState", "Font", "Pattern", "Properties", "Shading", "XObject"}) + +// PageResourceNames represents the required resource names for a specific page as extracted from its content streams. +type PageResourceNames map[string]types.StringSet + +// NewPageResourceNames returns initialized pageResourceNames. +func NewPageResourceNames() PageResourceNames { + m := make(map[string]types.StringSet, len(resourceTypes)) + for k := range resourceTypes { + m[k] = types.StringSet{} + } + return m +} + +// Resources returns a set of all required resource names for subdict s. +func (prn PageResourceNames) Resources(s string) types.StringSet { + return prn[s] +} + +// HasResources returns true for any resource names present in resource subDict s. +func (prn PageResourceNames) HasResources(s string) bool { + return len(prn.Resources(s)) > 0 +} + +// HasContent returns true in any resource names present. +func (prn PageResourceNames) HasContent() bool { + for k := range resourceTypes { + if prn.HasResources(k) { + return true + } + } + return false +} + +func (prn PageResourceNames) String() string { + sep := ", " + var ss []string + s := []string{"PageResourceNames:\n"} + for k := range resourceTypes { + ss = nil + for k := range prn.Resources(k) { + ss = append(ss, k) + } + s = append(s, k+": "+strings.Join(ss, sep)+"\n") + } + return strings.Join(s, "") +} diff --git a/pkg/pdfcpu/model/resources/Roboto-Regular.ttf b/pkg/pdfcpu/model/resources/Roboto-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..2b6392ffe8712b9c5450733320cd220d6c0f4bce Binary files /dev/null and b/pkg/pdfcpu/model/resources/Roboto-Regular.ttf differ diff --git a/pkg/pdfcpu/model/resources/config.yml b/pkg/pdfcpu/model/resources/config.yml new file mode 100644 index 0000000000000000000000000000000000000000..6eec0c6f95d63cb26252aa355011689318e68237 --- /dev/null +++ b/pkg/pdfcpu/model/resources/config.yml @@ -0,0 +1,68 @@ +############################# +# Default configuration # +############################# + +# toggle for inFilename extension check (.pdf) +checkFileNameExt: true + +reader15: true + +decodeAllStreams: false + +# validationMode: +# ValidationStrict, +# ValidationRelaxed, +validationMode: ValidationRelaxed + +# validate cross reference table right before writing. +postProcessValidate: true + +# eol for writing: +# EolLF +# EolCR +# EolCRLF +eol: EolLF + +writeObjectStream: true +writeXRefStream: true +encryptUsingAES: true + +# encryptKeyLength: max 256 +encryptKeyLength: 256 + +# permissions for encrypted files: +# 0xF0C3 (PermissionsNone) +# 0xF8C7 (PermissionsPrint) +# 0xFFFF (PermissionsAll) +# See more at model.PermissionFlags and PDF spec table 22 +permissions: 0xF0C3 + +# displayUnit: +# points +# inches +# cm +# mm +unit: points + +# timestamp format: yyyy-mm-dd hh:mm +# Switch month and year by using: 2006-02-01 15:04 +# See more at https://pkg.go.dev/time@go1.17.1#pkg-constants +timestampFormat: 2006-01-02 15:04 + +# date format: yyyy-mm-dd +dateFormat: 2006-01-02 + +# toggle optimization +optimize: true + +# optimize page resources via content stream analysis. +optimizeResourceDicts: true + +# optimize duplicate content streams across pages. +optimizeDuplicateContentStreams: false + +# merge creates bookmarks. +createBookmarks: true + +# Viewer is expected to supply appearance streams for form fields. +needAppearances: false diff --git a/pkg/pdfcpu/model/stat.go b/pkg/pdfcpu/model/stat.go new file mode 100644 index 0000000000000000000000000000000000000000..ba788b4c04a4278966ac1ac696a48b98fce62738 --- /dev/null +++ b/pkg/pdfcpu/model/stat.go @@ -0,0 +1,142 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 model + +import ( + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +// The PDF root object fields. +const ( + RootVersion = iota + RootExtensions + RootPageLabels + RootNames + RootDests + RootViewerPrefs + RootPageLayout + RootPageMode + RootOutlines + RootThreads + RootOpenAction + RootAA + RootURI + RootAcroForm + RootMetadata + RootStructTreeRoot + RootMarkInfo + RootLang + RootSpiderInfo + RootOutputIntents + RootPieceInfo + RootOCProperties + RootPerms + RootLegal + RootRequirements + RootCollection + RootNeedsRendering +) + +// The PDF page object fields. +const ( + PageLastModified = iota + PageResources + PageMediaBox + PageCropBox + PageBleedBox + PageTrimBox + PageArtBox + PageBoxColorInfo + PageContents + PageRotate + PageGroup + PageThumb + PageB + PageDur + PageTrans + PageAnnots + PageAA + PageMetadata + PagePieceInfo + PageStructParents + PageID + PagePZ + PageSeparationInfo + PageTabs + PageTemplateInstantiated + PagePresSteps + PageUserUnit + PageVP +) + +// PDFStats is a container for stats. +type PDFStats struct { + // Used root attributes + rootAttrs types.IntSet + // Used page attributes + pageAttrs types.IntSet +} + +// NewPDFStats returns a new PDFStats object. +func NewPDFStats() PDFStats { + return PDFStats{rootAttrs: types.IntSet{}, pageAttrs: types.IntSet{}} +} + +// AddRootAttr adds the occurrence of a field with given name to the rootAttrs set. +func (stats PDFStats) AddRootAttr(name int) { + stats.rootAttrs[name] = true +} + +// UsesRootAttr returns true if a field with given name is contained in the rootAttrs set. +func (stats PDFStats) UsesRootAttr(name int) bool { + return stats.rootAttrs[name] +} + +// AddPageAttr adds the occurrence of a field with given name to the pageAttrs set. +func (stats PDFStats) AddPageAttr(name int) { + stats.pageAttrs[name] = true +} + +// UsesPageAttr returns true if a field with given name is contained in the pageAttrs set. +func (stats PDFStats) UsesPageAttr(name int) bool { + return stats.pageAttrs[name] +} + +// ValidationTimingStats prints processing time stats for validation. +func ValidationTimingStats(dur1, dur2, dur float64) { + if !log.StatsEnabled() { + return + } + log.Stats.Println("Timing:") + log.Stats.Printf("read : %6.3fs %4.1f%%\n", dur1, dur1/dur*100) + log.Stats.Printf("validate : %6.3fs %4.1f%%\n", dur2, dur2/dur*100) + log.Stats.Printf("total processing time: %6.3fs\n\n", dur) +} + +// TimingStats prints processing time stats for an operation. +func TimingStats(op string, durRead, durVal, durOpt, durWrite, durTotal float64) { + if !log.StatsEnabled() { + return + } + log.Stats.Println("Timing:") + log.Stats.Printf("read : %6.3fs %4.1f%%\n", durRead, durRead/durTotal*100) + log.Stats.Printf("validate : %6.3fs %4.1f%%\n", durVal, durVal/durTotal*100) + log.Stats.Printf("optimize : %6.3fs %4.1f%%\n", durOpt, durOpt/durTotal*100) + log.Stats.Printf("%-21s: %6.3fs %4.1f%%\n", op, durWrite, durWrite/durTotal*100) + log.Stats.Printf("total processing time: %6.3fs\n\n", durTotal) +} diff --git a/pkg/pdfcpu/model/text.go b/pkg/pdfcpu/model/text.go new file mode 100644 index 0000000000000000000000000000000000000000..51d8291fb9f28a3d6d9ca5544cf491420c20427f --- /dev/null +++ b/pkg/pdfcpu/model/text.go @@ -0,0 +1,778 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 model + +import ( + "encoding/binary" + "fmt" + "io" + "math" + "strings" + "unicode/utf8" + + "github.com/pdfcpu/pdfcpu/pkg/font" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/matrix" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +// TextDescriptor contains all attributes needed for rendering a text column in PDF user space. +type TextDescriptor struct { + Text string // A multi line string using \n for line breaks. + FontName string // Name of the core or user font to be used. + RTL bool // Right to left user font. + Embed bool // Embed font. + FontKey string // Resource id registered for FontName. + FontSize int // Fontsize in points. + X, Y float64 // Position of first char's baseline. + Dx, Dy float64 // Horizontal and vertical offsets for X,Y. + MTop, MBot float64 // Top and bottom margins applied to text bounding box. + MLeft, MRight float64 // Left and right margins applied to text bounding box. + MinHeight float64 // The minimum height of this text's bounding box. + Rotation float64 // 0..360 degree rotation angle. + ScaleAbs bool // Scaling type, true=absolute, false=relative to container dimensions. + Scale float64 // font scaling factor > 0 (and <= 1 for relative scaling). + HAlign types.HAlignment // Horizontal text alignment. + VAlign types.VAlignment // Vertical text alignment. + RMode draw.RenderMode // Text render mode + StrokeCol color.SimpleColor // Stroke color to be used for rendering text corresponding to RMode. + FillCol color.SimpleColor // Fill color to be used for rendering text corresponding to RMode. + ShowTextBB bool // Render bounding box including BackgroundCol, border and margins. + ShowBackground bool // Render background of bounding box using BackgroundCol. + BackgroundCol color.SimpleColor // Bounding box fill color. + ShowBorder bool // Render border using BorderCol, BorderWidth and BorderStyle. + BorderWidth float64 // Border width, visibility depends on ShowBorder. + BorderStyle types.LineJoinStyle // Border style, also visible if ShowBorder is false as long as ShowBackground is true. + BorderCol color.SimpleColor // Border color. + ParIndent bool // Indent first line of paragraphs or space between paragraphs. + ShowLineBB bool // Render line bounding boxes in black (for HAlign != AlignJustify only) + ShowMargins bool // Render margins in light gray. + ShowPosition bool // Highlight position. + HairCross bool // Draw haircross at X,Y +} + +func deltaAlignMiddle(fontName string, fontSize, lines int, mTop, mBot float64) float64 { + return -font.Ascent(fontName, fontSize) + (float64(lines)*font.LineHeight(fontName, fontSize)+mTop+mBot)/2 - mTop +} + +func deltaAlignTop(fontName string, fontSize int, mTop float64) float64 { + return -font.Ascent(fontName, fontSize) - mTop +} + +func deltaAlignBottom(fontName string, fontSize, lines int, mBot float64) float64 { + return -font.Ascent(fontName, fontSize) + float64(lines)*font.LineHeight(fontName, fontSize) + mBot +} + +var unicodeToCP1252 = map[rune]byte{ + 0x20AC: 128, // € Euro Sign Note: Width in metrics file is not correct! + 0x201A: 130, // ‚ Single Low-9 Quotation Mark + 0x0192: 131, // ƒ Latin Small Letter F with Hook + 0x201E: 132, // „ Double Low-9 Quotation Mark + 0x2026: 133, // … Horizontal Ellipsis + 0x2020: 134, // † Dagger + 0x2021: 135, // ‡ Double Dagger + 0x02C6: 136, // ˆ Modifier Letter Circumflex Accent + 0x2030: 137, // ‰ Per Mille Sign + 0x0160: 138, // Š Latin Capital Letter S with Caron + 0x2039: 139, // ‹ Single Left-Pointing Angle Quotation Mark + 0x0152: 140, // Œ Latin Capital Ligature Oe + 0x017D: 142, // Ž Latin Capital Letter Z with Caron + 0x2018: 145, // ‘ Left Single Quotation Mark + 0x2019: 146, // ’ Right Single Quotation Mark + 0x201C: 147, // “ Left Double Quotation Mark + 0x201D: 148, // ” Right Double Quotation Mark + 0x2022: 149, // • Bullet + 0x2013: 150, // – En Dash + 0x2014: 151, // — Em Dash + 0x02DC: 152, // ˜ Small Tilde + 0x2122: 153, // ™ Trade Mark Sign Emoji + 0x0161: 154, // š Latin Small Letter S with Caron + 0x203A: 155, // › Single Right-Pointing Angle Quotation Mark + 0x0153: 156, // œ Latin Small Ligature Oe + 0x017E: 158, // ž Latin Small Letter Z with Caron + 0x0178: 159, // Ÿ Latin Capital Letter Y with Diaeresis +} + +func DecodeUTF8ToByte(s string) string { + var sb strings.Builder + for _, r := range s { + // Unicode => char code + if r <= 0xFF { + sb.WriteByte(byte(r)) + continue + } + if b, ok := unicodeToCP1252[r]; ok { + sb.WriteByte(b) + continue + } + sb.WriteByte(byte(0x20)) + } + return sb.String() +} + +func calcBoundingBoxForRectAndPoint(r *types.Rectangle, p types.Point) *types.Rectangle { + llx, lly, urx, ury := r.LL.X, r.LL.Y, r.UR.X, r.UR.Y + if p.X < r.LL.X { + llx = p.X + } else if p.X > r.UR.X { + urx = p.X + } + if p.Y < r.LL.Y { + lly = p.Y + } else if p.Y > r.UR.Y { + ury = p.Y + } + return types.NewRectangle(llx, lly, urx, ury) +} + +func CalcBoundingBoxForRects(r1, r2 *types.Rectangle) *types.Rectangle { + if r1 == nil && r2 == nil { + return types.NewRectangle(0, 0, 0, 0) + } + if r1 == nil { + return r2.Clone() + } + if r2 == nil { + return r1.Clone() + } + bbox := calcBoundingBoxForRectAndPoint(r1, r2.LL) + return calcBoundingBoxForRectAndPoint(bbox, r2.UR) +} + +func calcBoundingBoxForLines(lines []string, x, y float64, fontName string, fontSize int) (*types.Rectangle, string) { + var ( + box *types.Rectangle + maxLine string + maxWidth float64 + ) + // TODO Return error if lines == nil or empty. + for _, s := range lines { + bbox := CalcBoundingBox(s, x, y, fontName, fontSize) + if bbox.Width() > maxWidth { + maxWidth = bbox.Width() + maxLine = s + } + box = CalcBoundingBoxForRects(box, bbox) + y -= bbox.Height() + } + return box, maxLine +} + +func PrepBytes(xRefTable *XRefTable, s, fontName string, embed, rtl bool) string { + if font.IsUserFont(fontName) { + if rtl { + s = types.Reverse(s) + } + bb := []byte{} + if !embed { + for _, r := range s { + b := make([]byte, 2) + binary.BigEndian.PutUint16(b, uint16(r)) + bb = append(bb, b...) + } + } else { + usedGIDs, ok := xRefTable.UsedGIDs[fontName] + if !ok { + xRefTable.UsedGIDs[fontName] = map[uint16]bool{} + usedGIDs = xRefTable.UsedGIDs[fontName] + } + + font.UserFontMetricsLock.RLock() + ttf := font.UserFontMetrics[fontName] + font.UserFontMetricsLock.RUnlock() + + for _, r := range s { + gid, ok := ttf.Chars[uint32(r)] + if ok { + b := make([]byte, 2) + binary.BigEndian.PutUint16(b, gid) + bb = append(bb, b...) + usedGIDs[gid] = true + } // else "invalid char" + } + } + s = string(bb) + } + s1, _ := types.Escape(s) + return *s1 +} + +func writeStringToBuf(xRefTable *XRefTable, w io.Writer, s string, x, y float64, td TextDescriptor) { + s = PrepBytes(xRefTable, s, td.FontName, td.Embed, td.RTL) + fmt.Fprintf(w, "BT 0 Tw %.2f %.2f %.2f RG %.2f %.2f %.2f rg %.2f %.2f Td %d Tr (%s) Tj ET ", + td.StrokeCol.R, td.StrokeCol.G, td.StrokeCol.B, td.FillCol.R, td.FillCol.G, td.FillCol.B, x, y, td.RMode, s) +} + +func setFont(w io.Writer, fontID string, fontSize float32) { + fmt.Fprintf(w, "BT /%s %.2f Tf ET ", fontID, fontSize) +} + +func CalcBoundingBox(s string, x, y float64, fontName string, fontSize int) *types.Rectangle { + w := font.TextWidth(s, fontName, fontSize) + h := font.LineHeight(fontName, fontSize) + y -= math.Ceil(font.Descent(fontName, fontSize)) + return types.NewRectangle(x, y, x+w, y+h) +} + +func horAdjustBoundingBoxForLines(r, box *types.Rectangle, dx, dy float64, x, y *float64) { + if r.UR.X-box.LL.X < box.Width() { + dx -= box.Width() - (r.UR.X - box.LL.X) + *x += dx + box.Translate(dx, 0) + } else if box.LL.X < r.LL.X { + dx += r.LL.X - box.LL.X + *x += dx + box.Translate(dx, 0) + } + if r.UR.Y-box.LL.Y < box.Height() { + dy -= box.Height() - (r.UR.Y - box.LL.Y) + *y += dy + box.Translate(0, dy) + } else if box.LL.Y < r.LL.Y { + dy += r.LL.Y - box.LL.Y + *y += dy + box.Translate(0, dy) + } +} + +func prepJustifiedLine(xRefTable *XRefTable, lines *[]string, strbuf []string, strWidth, w float64, fontSize int, fontName string, embed, rtl bool) { + blank := PrepBytes(xRefTable, " ", fontName, embed, false) + var sb strings.Builder + sb.WriteString("[") + wc := len(strbuf) + dx := font.GlyphSpaceUnits(float64((w-strWidth))/float64(wc-1), fontSize) + for i := 0; i < wc; i++ { + j := i + if rtl { + j = wc - 1 - i + } + s := PrepBytes(xRefTable, strbuf[j], fontName, embed, rtl) + sb.WriteString(fmt.Sprintf(" (%s)", s)) + if i < wc-1 { + sb.WriteString(fmt.Sprintf(" %d (%s)", -int(dx), blank)) + } + } + sb.WriteString(" ] TJ") + *lines = append(*lines, sb.String()) +} + +func newPrepJustifiedString( + xRefTable *XRefTable, + fontName string, + fontSize int) func(lines *[]string, s string, w float64, fontName string, fontSize *int, lastline, parIndent, cjk, rtl bool) int { + + // Not yet rendered content. + strbuf := []string{} + + // Width of strbuf's content in user space implied by fontSize. + var strWidth float64 + + // Indent first line of paragraphs. + var indent bool = true + + // Indentation string for first line of paragraphs. + identPrefix := " " + + blankWidth := font.TextWidth(" ", fontName, fontSize) + + return func(lines *[]string, s string, w float64, fontName string, fontSize *int, lastline, parIndent, embed, rtl bool) int { + + if len(s) == 0 { + if len(strbuf) > 0 { + s1 := PrepBytes(xRefTable, strings.Join(strbuf, " "), fontName, embed, rtl) + if rtl { + dx := font.GlyphSpaceUnits(w-strWidth, *fontSize) + s = fmt.Sprintf("[ %d (%s) ] TJ ", -int(dx), s1) + } else { + s = fmt.Sprintf("(%s) Tj", s1) + } + *lines = append(*lines, s) + strbuf = []string{} + strWidth = 0 + } + if lastline { + return 0 + } + indent = true + if parIndent { + return 0 + } + return 1 + } + + linefeeds := 0 + ss := strings.Split(s, " ") + if parIndent && len(strbuf) == 0 && indent { + ss[0] = identPrefix + ss[0] + } + + for _, s1 := range ss { + s1Width := font.TextWidth(s1, fontName, *fontSize) + bw := 0. + if len(strbuf) > 0 { + bw = blankWidth + } + if w-strWidth-(s1Width+bw) > 0 { + strWidth += s1Width + bw + strbuf = append(strbuf, s1) + continue + } + // Ensure s1 fits into w. + fs := font.Size(s1, fontName, w) + if fs < *fontSize { + *fontSize = fs + } + if len(strbuf) == 0 { + prepJustifiedLine(xRefTable, lines, []string{s1}, s1Width, w, *fontSize, fontName, embed, rtl) + } else { + // Note: Previous lines have whitespace based on bigger font size. + prepJustifiedLine(xRefTable, lines, strbuf, strWidth, w, *fontSize, fontName, embed, rtl) + strbuf = []string{s1} + strWidth = s1Width + } + linefeeds++ + indent = false + } + return 0 + } +} + +// Prerender justified text in order to calculate bounding box height. +func preRenderJustifiedText( + xRefTable *XRefTable, + lines *[]string, + r *types.Rectangle, + x, y, width float64, + td TextDescriptor, + mLeft, mRight, borderWidth float64, + fontSize *int) float64 { + + var ww float64 + if !td.ScaleAbs { + ww = r.Width() * td.Scale + } else { + if width > 0 { + ww = width * td.Scale + } else { + box, _ := calcBoundingBoxForLines(*lines, x, y, td.FontName, *fontSize) + ww = box.Width() * td.Scale + } + } + ww -= mLeft + mRight + 2*borderWidth + prepJustifiedString := newPrepJustifiedString(xRefTable, td.FontName, *fontSize) + l := []string{} + for i, s := range *lines { + linefeeds := prepJustifiedString(&l, s, ww, td.FontName, fontSize, false, td.ParIndent, td.Embed, td.RTL) + for j := 0; j < linefeeds; j++ { + l = append(l, "") + } + isLastLine := i == len(*lines)-1 + if isLastLine { + prepJustifiedString(&l, "", ww, td.FontName, fontSize, true, td.ParIndent, td.Embed, td.RTL) + } + } + *lines = l + return ww +} + +func scaleFontSize(r *types.Rectangle, lines []string, scaleAbs bool, + scale, width, x, y, mLeft, mRight, borderWidth float64, + fontName string, fontSize *int) { + if scaleAbs { + *fontSize = int(float64(*fontSize) * scale) + } else { + www := width + if width == 0 { + box, _ := calcBoundingBoxForLines(lines, x, y, fontName, *fontSize) + www = box.Width() + mLeft + mRight + 2*borderWidth + } + *fontSize = int(r.Width() * scale * float64(*fontSize) / www) + } +} + +func horizontalWrapUp(box *types.Rectangle, maxLine string, hAlign types.HAlignment, + x *float64, width, ww, mLeft, mRight, borderWidth float64, + fontName string, fontSize *int) { + switch hAlign { + case types.AlignLeft: + box.Translate(mLeft+borderWidth, 0) + *x += mLeft + borderWidth + case types.AlignJustify: + box.Translate(mLeft+borderWidth, 0) + *x += mLeft + borderWidth + case types.AlignRight: + box.Translate(-box.Width()-mRight-borderWidth, 0) + *x -= mRight + borderWidth + case types.AlignCenter: + box.Translate(-box.Width()/2, 0) + } + + if hAlign == types.AlignJustify { + box.UR.X = box.LL.X + ww + mRight + borderWidth + box.LL.X -= mLeft + borderWidth + } else if width > 0 { + netWidth := width - 2*borderWidth - mLeft - mRight + if box.Width() > netWidth { + *fontSize = font.Size(maxLine, fontName, netWidth) + } + switch hAlign { + case types.AlignLeft: + box.UR.X = box.LL.X + width - mLeft - borderWidth + box.LL.X -= mLeft + borderWidth + case types.AlignRight: + box.LL.X = box.UR.X - width + box.Translate(mRight+borderWidth, 0) + case types.AlignCenter: + box.LL.X = box.UR.X - width + box.Translate(box.Width()/2-(box.UR.X-*x), 0) + } + } else { + box.LL.X -= mLeft + borderWidth + box.UR.X += mRight + borderWidth + } +} + +func createBoundingBoxForColumn(xRefTable *XRefTable, r *types.Rectangle, x, y *float64, + width float64, + td TextDescriptor, + dx, dy float64, + mTop, mBot, mLeft, mRight float64, + borderWidth float64, + fontSize *int, lines *[]string) *types.Rectangle { + + var ww float64 + if td.HAlign == types.AlignJustify { + ww = preRenderJustifiedText(xRefTable, lines, r, *x, *y, width, td, mLeft, mRight, borderWidth, fontSize) + } + + if td.HAlign != types.AlignJustify { + scaleFontSize(r, *lines, td.ScaleAbs, td.Scale, width, *x, *y, mLeft, mRight, borderWidth, td.FontName, fontSize) + } + + // Apply vertical alignment. + var dy1 float64 + switch td.VAlign { + case types.AlignTop: + dy1 = deltaAlignTop(td.FontName, *fontSize, mTop+borderWidth) + case types.AlignMiddle: + dy1 = deltaAlignMiddle(td.FontName, *fontSize, len(*lines), mTop, mBot) + case types.AlignBottom: + dy1 = deltaAlignBottom(td.FontName, *fontSize, len(*lines), mBot) + } + *y += math.Ceil(dy1) + + box, maxLine := calcBoundingBoxForLines(*lines, *x, *y, td.FontName, *fontSize) + // maxLine for hAlign != AlignJustify only! + horizontalWrapUp(box, maxLine, td.HAlign, x, width, ww, mLeft, mRight, borderWidth, td.FontName, fontSize) + + box.LL.Y -= mBot + borderWidth + box.UR.Y += mTop + borderWidth + + if td.MinHeight > 0 && box.Height() < td.MinHeight { + box.LL.Y = box.UR.Y - td.MinHeight + } + + horAdjustBoundingBoxForLines(r, box, dx, dy, x, y) + + return box +} + +func flushJustifiedStringToBuf(w io.Writer, s string, x, y float64, strokeCol, fillCol color.SimpleColor, rm draw.RenderMode) { + fmt.Fprintf(w, "BT 0 Tw %.2f %.2f %.2f RG %.2f %.2f %.2f rg %.2f %.2f Td %d Tr %s ET ", + strokeCol.R, strokeCol.G, strokeCol.B, fillCol.R, fillCol.G, fillCol.B, x, y, rm, s) +} + +func scaleXForRegion(x float64, mediaBox, region *types.Rectangle) float64 { + return x / mediaBox.Width() * region.Width() +} + +func scaleYForRegion(y float64, mediaBox, region *types.Rectangle) float64 { + return y / mediaBox.Width() * region.Width() +} + +func DrawMargins(w io.Writer, c color.SimpleColor, colBB *types.Rectangle, borderWidth, mLeft, mRight, mTop, mBot float64) { + if mLeft <= 0 && mRight <= 0 && mTop <= 0 && mBot <= 0 { + return + } + + var r *types.Rectangle + + if mBot > 0 { + r = types.RectForWidthAndHeight(colBB.LL.X+borderWidth, colBB.LL.Y+borderWidth, colBB.Width()-2*borderWidth, mBot) + draw.FillRectNoBorder(w, r, c) + } + + if mTop > 0 { + r = types.RectForWidthAndHeight(colBB.LL.X+borderWidth, colBB.UR.Y-borderWidth-mTop, colBB.Width()-2*borderWidth, mTop) + draw.FillRectNoBorder(w, r, c) + } + + if mLeft > 0 { + r = types.RectForWidthAndHeight(colBB.LL.X+borderWidth, colBB.LL.Y+borderWidth+mBot, mLeft, colBB.Height()-2*borderWidth-mTop-mBot) + draw.FillRectNoBorder(w, r, c) + } + + if mRight > 0 { + r = types.RectForWidthAndHeight(colBB.UR.X-borderWidth-mRight, colBB.LL.Y+borderWidth+mBot, mRight, colBB.Height()-2*borderWidth-mTop-mBot) + draw.FillRectNoBorder(w, r, c) + } + +} + +func renderBackgroundAndBorder(w io.Writer, td TextDescriptor, borderWidth float64, colBB *types.Rectangle) { + r := types.RectForWidthAndHeight(colBB.LL.X+borderWidth/2, colBB.LL.Y+borderWidth/2, colBB.Width()-borderWidth, colBB.Height()-borderWidth) + if td.ShowBackground { + c := td.BackgroundCol + if td.ShowBorder { + c = td.BorderCol + } + draw.FillRect(w, r, borderWidth, &c, td.BackgroundCol, &td.BorderStyle) + } else if td.ShowBorder { + draw.DrawRect(w, r, borderWidth, &td.BorderCol, &td.BorderStyle) + } +} + +func renderText(xRefTable *XRefTable, w io.Writer, lines []string, td TextDescriptor, x, y float64, fontSize int) { + lh := font.LineHeight(td.FontName, fontSize) + for _, s := range lines { + if td.HAlign != types.AlignJustify { + lineBB := CalcBoundingBox(s, x, y, td.FontName, fontSize) + // Apply horizontal alignment. + var dx float64 + switch td.HAlign { + case types.AlignCenter: + dx = lineBB.Width() / 2 + case types.AlignRight: + dx = lineBB.Width() + } + lineBB.Translate(-dx, 0) + if td.ShowLineBB { + // Draw line bounding box. + draw.SetStrokeColor(w, color.Black) + draw.DrawRectSimple(w, lineBB) + } + writeStringToBuf(xRefTable, w, s, x-dx, y, td) + y -= lh + continue + } + + if len(s) > 0 { + flushJustifiedStringToBuf(w, s, x, y, td.StrokeCol, td.FillCol, td.RMode) + } + y -= lh + } +} + +// This is a patched version of strings.FieldsFunc that also returns empty fields. +func fieldsFunc(s string, f func(rune) bool) []string { + // A span is used to record a slice of s of the form s[start:end]. + // The start index is inclusive and the end index is exclusive. + type span struct { + start int + end int + } + spans := make([]span, 0, 32) + + // Find the field start and end indices. + wasField := false + fromIndex := 0 + for i, rune := range s { + if f(rune) { + if wasField { + spans = append(spans, span{start: fromIndex, end: i}) + wasField = false + } else { + spans = append(spans, span{}) + } + } else { + if !wasField { + fromIndex = i + wasField = true + } + } + } + + // Last field might end at EOF. + if wasField { + spans = append(spans, span{fromIndex, len(s)}) + } + + // Create strings from recorded field indices. + a := make([]string, len(spans)) + for i, span := range spans { + a[i] = s[span.start:span.end] + } + + return a +} + +func SplitMultilineStr(s string) []string { + s = strings.ReplaceAll(s, "\\n", "\n") + var lines []string + return append(lines, fieldsFunc(s, func(c rune) bool { return c == 0x0a })...) +} + +// WriteColumn writes a text column using s at position x/y using a certain font, fontsize and a desired horizontal and vertical alignment. +// Enforce a desired column width by supplying a width > 0 (especially useful for justified text). +// It returns the bounding box of this column. +func WriteColumn(xRefTable *XRefTable, w io.Writer, mediaBox, region *types.Rectangle, td TextDescriptor, width float64) *types.Rectangle { + x, y, dx, dy := td.X, td.Y, td.Dx, td.Dy + mTop, mBot, mLeft, mRight := td.MTop, td.MBot, td.MLeft, td.MRight + s, fontSize, borderWidth := td.Text, td.FontSize, td.BorderWidth + + r := mediaBox + if region != nil { + r = region + dx = scaleXForRegion(dx, mediaBox, r) + dy = scaleYForRegion(dy, mediaBox, r) + width = scaleXForRegion(width, mediaBox, r) + fontSize = int(scaleYForRegion(float64(fontSize), mediaBox, r)) + mTop = scaleYForRegion(mTop, mediaBox, r) + mBot = scaleYForRegion(mBot, mediaBox, r) + mLeft = scaleXForRegion(mLeft, mediaBox, r) + mRight = scaleXForRegion(mRight, mediaBox, r) + borderWidth = scaleXForRegion(borderWidth, mediaBox, r) + } + + if x >= 0 { + x = r.LL.X + x + } + if y >= 0 { + y = r.LL.Y + y + } + + // Position text horizontally centered for x < 0. + if x < 0 { + x = r.LL.X + r.Width()/2 + } + + // Position text vertically centered for y < 0. + if y < 0 { + y = r.LL.Y + r.Height()/2 + } + + // Apply offset. + x += dx + y += dy + + // Cache haircross coordinates. + x0, y0 := x, y + + if font.IsCoreFont(td.FontName) && utf8.ValidString(s) { + s = DecodeUTF8ToByte(s) + } + + lines := SplitMultilineStr(s) + + if !td.ScaleAbs { + if td.Scale > 1 { + td.Scale = 1 + } + } + + // Create bounding box and prerender content stream bytes for justified text. + colBB := createBoundingBoxForColumn(xRefTable, + r, &x, &y, width, td, dx, dy, mTop, mBot, mLeft, mRight, borderWidth, &fontSize, &lines) + + fmt.Fprint(w, "q ") + + setFont(w, td.FontKey, float32(fontSize)) + m := matrix.CalcRotateTransformMatrix(td.Rotation, colBB) + fmt.Fprintf(w, "%.5f %.5f %.5f %.5f %.5f %.5f cm ", m[0][0], m[0][1], m[1][0], m[1][1], m[2][0], m[2][1]) + + x -= colBB.LL.X + y -= colBB.LL.Y + colBB.Translate(-colBB.LL.X, -colBB.LL.Y) + + // Render background and border. + if td.ShowTextBB { + renderBackgroundAndBorder(w, td, borderWidth, colBB) + } + + // Render margins + if td.ShowMargins { + DrawMargins(w, color.LightGray, colBB, borderWidth, mLeft, mRight, mTop, mBot) + } + + // Render text. + renderText(xRefTable, w, lines, td, x, y, fontSize) + + fmt.Fprintf(w, "Q ") + + if td.HairCross { + draw.DrawHairCross(w, x0, y0, r) + } + + if td.ShowPosition { + draw.DrawCircle(w, x0, y0, 5, color.Black, &color.Red) + } + + return colBB +} + +// WriteMultiLine writes s at position x/y using a certain font, fontsize and a desired horizontal and vertical alignment. +// It returns the bounding box of this text column. +func WriteMultiLine(xRefTable *XRefTable, w io.Writer, mediaBox, region *types.Rectangle, td TextDescriptor) *types.Rectangle { + return WriteColumn(xRefTable, w, mediaBox, region, td, 0) +} + +// AnchorPosAndAlign calculates position and alignment for an anchored rectangle r. +func AnchorPosAndAlign(a types.Anchor, r *types.Rectangle) (x, y float64, hAlign types.HAlignment, vAlign types.VAlignment) { + switch a { + case types.TopLeft: + x, y, hAlign, vAlign = 0, r.Height(), types.AlignLeft, types.AlignTop + case types.TopCenter: + x, y, hAlign, vAlign = -1, r.Height(), types.AlignCenter, types.AlignTop + case types.TopRight: + x, y, hAlign, vAlign = r.Width(), r.Height(), types.AlignRight, types.AlignTop + case types.Left: + x, y, hAlign, vAlign = 0, -1, types.AlignLeft, types.AlignMiddle + case types.Center: + x, y, hAlign, vAlign = -1, -1, types.AlignCenter, types.AlignMiddle + case types.Right: + x, y, hAlign, vAlign = r.Width(), -1, types.AlignRight, types.AlignMiddle + case types.BottomLeft: + x, y, hAlign, vAlign = 0, 0, types.AlignLeft, types.AlignMiddle + case types.BottomCenter: + x, y, hAlign, vAlign = -1, 0, types.AlignCenter, types.AlignMiddle + case types.BottomRight: + x, y, hAlign, vAlign = r.Width(), 0, types.AlignRight, types.AlignMiddle + } + return +} + +// WriteMultiLineAnchored writes multiple lines with anchored position and returns its bounding box. +func WriteMultiLineAnchored(xRefTable *XRefTable, w io.Writer, mediaBox, region *types.Rectangle, td TextDescriptor, a types.Anchor) *types.Rectangle { + r := mediaBox + if region != nil { + r = region + } + td.X, td.Y, td.HAlign, td.VAlign = AnchorPosAndAlign(a, r) + return WriteMultiLine(xRefTable, w, mediaBox, region, td) +} + +// WriteColumnAnchored writes a justified text column with anchored position and returns its bounding box. +func WriteColumnAnchored(xRefTable *XRefTable, w io.Writer, mediaBox, region *types.Rectangle, td TextDescriptor, a types.Anchor, width float64) *types.Rectangle { + r := mediaBox + if region != nil { + r = region + } + td.HAlign = types.AlignJustify + td.X, td.Y, _, td.VAlign = AnchorPosAndAlign(a, r) + return WriteColumn(xRefTable, w, mediaBox, region, td, width) +} diff --git a/pkg/pdfcpu/model/version.go b/pkg/pdfcpu/model/version.go new file mode 100644 index 0000000000000000000000000000000000000000..d13afbff04f8d0a5051c082cc09118422f2501ec --- /dev/null +++ b/pkg/pdfcpu/model/version.go @@ -0,0 +1,76 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 model + +import ( + "fmt" + + "github.com/pkg/errors" +) + +// VersionStr is the current pdfcpu version. +var VersionStr = "v0.8.1 dev" + +// Version is a type for the internal representation of PDF versions. +type Version int + +const ( + V10 Version = iota + V11 + V12 + V13 + V14 + V15 + V16 + V17 + V20 +) + +// PDFVersion returns the PDFVersion for a version string. +func PDFVersion(versionStr string) (Version, error) { + + switch versionStr { + case "1.0": + return V10, nil + case "1.1": + return V11, nil + case "1.2": + return V12, nil + case "1.3": + return V13, nil + case "1.4": + return V14, nil + case "1.5": + return V15, nil + case "1.6": + return V16, nil + case "1.7": + return V17, nil + case "2.0": + return V20, nil + } + + return -1, errors.New(versionStr) +} + +// String returns a string representation for a given PDFVersion. +func (v Version) String() string { + if v == V20 { + return "2.0" + } + return "1." + fmt.Sprintf("%d", v) +} diff --git a/pkg/pdfcpu/model/watermark.go b/pkg/pdfcpu/model/watermark.go new file mode 100644 index 0000000000000000000000000000000000000000..af21780a3031a19bd5f33dae8a114eab58e79daf --- /dev/null +++ b/pkg/pdfcpu/model/watermark.go @@ -0,0 +1,461 @@ +/* +Copyright 2022 The pdfcpu Authors. + +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 + + 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 model + +import ( + "fmt" + "io" + "math" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/matrix" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +const ( + DegToRad = math.Pi / 180 + RadToDeg = 180 / math.Pi +) + +// Rotation along one of 2 diagonals +const ( + NoDiagonal = iota + DiagonalLLToUR + DiagonalULToLR +) + +// Watermark mode +const ( + WMText = iota + WMImage + WMPDF +) + +type formCache map[types.Rectangle]*types.IndirectRef + +type PdfResources struct { + Content []byte + ResDict *types.IndirectRef + Bb *types.Rectangle // visible region in user space +} + +// Watermark represents the basic structure and command details for the commands "Stamp" and "Watermark". +type Watermark struct { + OnTop bool // if true STAMP else WATERMARK. + Mode int // WMText, WMImage or WMPDF + FileName string // image or PDF file name + Image io.Reader // image reader + PDF io.ReadSeeker // PDF read seeker + TextString string // raw display text. + TextLines []string // display multiple lines of text. + URL string // overlay link annotation for stamps. + InpUnit types.DisplayUnit // input display unit. + Pos types.Anchor // position anchor, one of tl,tc,tr,l,c,r,bl,bc,br. + Dx, Dy float64 // anchor offset. + HAlign *types.HAlignment // horizonal alignment for text watermarks. + FontName string // supported are Adobe base fonts only. (as of now: Helvetica, Times-Roman, Courier) + FontSize int // font scaling factor. + ScaledFontSize int // font scaling factor for a specific page + ScriptName string // ISO 15924: Hans, Hant, Hira, Kana, Jpan, Hang, Kore: if set, font will not be embedded. + RTL bool // if true, render text from right to left + Color color.SimpleColor // text fill color(=non stroking color) for backwards compatibility. + FillColor color.SimpleColor // text fill color(=non stroking color). + StrokeColor color.SimpleColor // text stroking color + BgColor *color.SimpleColor // text bounding box background color + MLeft, MRight float64 // left and right bounding box margin + MTop, MBot float64 // top and bottom bounding box margin + BorderWidth float64 // Border width, visible if BgColor is set. + BorderStyle types.LineJoinStyle // Border style (bounding box corner style), visible if BgColor is set. + BorderColor *color.SimpleColor // border color + Rotation float64 // rotation to apply in degrees. -180 <= x <= 180 + Diagonal int // paint along the diagonal. + UserRotOrDiagonal bool // true if one of rotation or diagonal provided overriding the default. + Opacity float64 // opacity of the watermark. 0 <= x <= 1 + RenderMode draw.RenderMode // fill=0, stroke=1 fill&stroke=2 + Scale float64 // relative scale factor: 0 <= x <= 1, absolute scale factor: 0 <= x + ScaleEff float64 // effective scale factor + ScaleAbs bool // true for absolute scaling. + Update bool // true for updating instead of adding a page watermark. + Ocg, ExtGState, Font, Img *types.IndirectRef // resources + Width, Height int // image or page dimensions + + // PDF stamp + bbPDF *types.Rectangle // bounding box + PdfRes map[int]PdfResources // content & corresponding resources + PdfPageNrSrc int // page number of the source PDF file serving as stamp provider, 0 for multi stamping + PdfMultiStartPageNrSrc int // start page number of the source PDF file serving as stamp provider. + PdfMultiStartPageNrDest int // start page number of the destination PDF file. + + // page specific + Bb *types.Rectangle // bounding box of the form representing this watermark. + BbTrans types.QuadLiteral // Transformed bounding box. + Vp *types.Rectangle // view port, page dimensions. + PageRot int // page rotation in effect. + Form *types.IndirectRef // form dependent on given page dimensions. + + // house keeping + Objs types.IntSet // objects for which wm has been applied already. + FCache formCache // form cache. +} + +// DefaultWatermarkConfig returns the default configuration. +func DefaultWatermarkConfig() *Watermark { + return &Watermark{ + PdfPageNrSrc: 0, + PdfMultiStartPageNrSrc: 1, + PdfMultiStartPageNrDest: 1, + FontName: "Helvetica", + FontSize: 24, + RTL: false, + Pos: types.Center, + Scale: 0.5, + ScaleAbs: false, + Color: color.Gray, + StrokeColor: color.Gray, + FillColor: color.Gray, + Diagonal: DiagonalLLToUR, + Opacity: 1.0, + RenderMode: draw.RMFill, + PdfRes: map[int]PdfResources{}, + Objs: types.IntSet{}, + FCache: formCache{}, + TextLines: []string{}, + } +} + +// Recycle resets all caches. +func (wm *Watermark) Recycle() { + wm.Objs = types.IntSet{} + wm.FCache = formCache{} +} + +// IsText returns true if the watermark content is text. +func (wm Watermark) IsText() bool { + return wm.Mode == WMText +} + +// IsPDF returns true if the watermark content is PDF. +func (wm Watermark) IsPDF() bool { + return wm.Mode == WMPDF +} + +// IsImage returns true if the watermark content is an image. +func (wm Watermark) IsImage() bool { + return wm.Mode == WMImage +} + +// Typ returns the nature of wm. +func (wm Watermark) Typ() string { + if wm.IsImage() { + return "image" + } + if wm.IsPDF() { + return "pdf" + } + return "text" +} + +func (wm Watermark) String() string { + var s string + if !wm.OnTop { + s = "not " + } + + t := wm.TextString + if len(t) == 0 { + t = wm.FileName + } + + sc := "relative" + if wm.ScaleAbs { + sc = "absolute" + } + + bbox := "" + if wm.Bb != nil { + bbox = (*wm.Bb).String() + } + + vp := "" + if wm.Vp != nil { + vp = (*wm.Vp).String() + } + + return fmt.Sprintf("Watermark: <%s> is %son top, typ:%s\n"+ + "%s %d points\n"+ + "PDFpage#: %d\n"+ + "scaling: %.1f %s\n"+ + "color: %s\n"+ + "rotation: %.1f\n"+ + "diagonal: %d\n"+ + "opacity: %.1f\n"+ + "renderMode: %d\n"+ + "bbox:%s\n"+ + "vp:%s\n"+ + "pageRotation: %d\n", + t, s, wm.Typ(), + wm.FontName, wm.FontSize, + wm.PdfPageNrSrc, + wm.Scale, sc, + wm.Color, + wm.Rotation, + wm.Diagonal, + wm.Opacity, + wm.RenderMode, + bbox, + vp, + wm.PageRot, + ) +} + +// OnTopString returns "watermark" or "stamp" whichever applies. +func (wm Watermark) OnTopString() string { + s := "watermark" + if wm.OnTop { + s = "stamp" + } + return s +} + +// MultiStamp returns true if wm is a multi stamp. +func (wm Watermark) MultiStamp() bool { + return wm.PdfPageNrSrc == 0 +} + +// CalcBoundingBox returns the bounding box for wm and pageNr. +func (wm *Watermark) CalcBoundingBox(pageNr int) { + bb := types.RectForDim(float64(wm.Width), float64(wm.Height)) + + if wm.IsPDF() { + wm.bbPDF = wm.PdfRes[wm.PdfPageNrSrc].Bb + if wm.MultiStamp() { + i := wm.PdfResIndex(pageNr) + wm.bbPDF = wm.PdfRes[i].Bb + } + wm.Width = int(wm.bbPDF.Width()) + wm.Height = int(wm.bbPDF.Height()) + bb = wm.bbPDF.CroppedCopy(0) + } + + ar := bb.AspectRatio() + + if wm.ScaleAbs { + w1 := wm.Scale * bb.Width() + bb.UR.X = bb.LL.X + w1 + bb.UR.Y = bb.LL.Y + w1/ar + wm.Bb = bb + wm.ScaleEff = wm.Scale + return + } + + if ar >= 1 { + // Landscape + w1 := wm.Scale * wm.Vp.Width() + bb.UR.X = bb.LL.X + w1 + bb.UR.Y = bb.LL.Y + w1/ar + wm.ScaleEff = w1 / float64(wm.Width) + } else { + // Portrait + h1 := wm.Scale * wm.Vp.Height() + bb.UR.Y = bb.LL.Y + h1 + bb.UR.X = bb.LL.X + h1*ar + wm.ScaleEff = h1 / float64(wm.Height) + } + + wm.Bb = bb +} + +// LowerLeftCorner returns the lower left corner for a bounding box anchored onto vp. +func LowerLeftCorner(vp *types.Rectangle, bbw, bbh float64, a types.Anchor) types.Point { + + var p types.Point + vpw := vp.Width() + vph := vp.Height() + + switch a { + + case types.TopLeft: + p.X = vp.LL.X + p.Y = vp.UR.Y - bbh + + case types.TopCenter: + p.X = vp.LL.X + (vpw/2 - bbw/2) + p.Y = vp.UR.Y - bbh + + case types.TopRight: + p.X = vp.UR.X - bbw + p.Y = vp.UR.Y - bbh + + case types.Left: + p.X = vp.LL.X + p.Y = vp.LL.Y + (vph/2 - bbh/2) + + case types.Center: + p.X = vp.LL.X + (vpw/2 - bbw/2) + p.Y = vp.LL.Y + (vph/2 - bbh/2) + + case types.Right: + p.X = vp.UR.X - bbw + p.Y = vp.LL.Y + (vph/2 - bbh/2) + + case types.BottomLeft: + p.X = vp.LL.X + p.Y = vp.LL.Y + + case types.BottomCenter: + p.X = vp.LL.X + (vpw/2 - bbw/2) + p.Y = vp.LL.Y + + case types.BottomRight: + p.X = vp.UR.X - bbw + p.Y = vp.LL.Y + } + + return p +} + +func (wm *Watermark) alignWithPageBoundariesForNegRot() (float64, float64) { + w, h := wm.Bb.Width(), wm.Bb.Height() + var dx, dy float64 + + switch wm.Pos { + + case types.TopLeft: + dx, dy = 0, h + + case types.TopCenter: + dx, dy = (w-h)/2, h + + case types.TopRight: + dx, dy = w-h, h + + case types.Left: + dx, dy = 0, (w+h)/2 + + case types.Right: + dx, dy = w-h, (w+h)/2 + + case types.BottomLeft: + dx, dy = 0, w + + case types.BottomCenter: + dx, dy = (w-h)/2, w + + case types.BottomRight: + dx, dy = w-h, w + } + + return dx, dy +} + +func (wm *Watermark) alignWithPageBoundariesForPosRot() (float64, float64) { + w, h := wm.Bb.Width(), wm.Bb.Height() + var dx, dy float64 + + switch wm.Pos { + + case types.TopLeft: + dx, dy = h, h-w + + case types.TopCenter: + dx, dy = (w+h)/2, h-w + + case types.TopRight: + dx, dy = w, h-w + + case types.Left: + dx, dy = h, (h-w)/2 + + case types.Right: + dx, dy = w, (h-w)/2 + + case types.BottomLeft: + dx, dy = h, 0 + + case types.BottomCenter: + dx, dy = (w+h)/2, 0 + + case types.BottomRight: + dx, dy = w, 0 + + } + + return dx, dy +} + +func (wm *Watermark) alignWithPageBoundaries() (float64, float64) { + if wm.Rotation == 90 { + return wm.alignWithPageBoundariesForPosRot() + } + // wm.Rotation == -90 + return wm.alignWithPageBoundariesForNegRot() +} + +// CalcTransformMatrix return the transform matrix for a watermark. +func (wm *Watermark) CalcTransformMatrix() matrix.Matrix { + var sin, cos float64 + r := wm.Rotation + + if wm.Diagonal != NoDiagonal { + + // Calculate the angle of the diagonal with respect of the aspect ratio of the bounding box. + r = math.Atan(wm.Vp.Height()/wm.Vp.Width()) * float64(RadToDeg) + + if wm.Bb.AspectRatio() < 1 { + r -= 90 + } + + if wm.Diagonal == DiagonalULToLR { + r = -r + } + + } + + sin = math.Sin(float64(r) * float64(DegToRad)) + cos = math.Cos(float64(r) * float64(DegToRad)) + + var dx, dy float64 + if !wm.IsImage() && !wm.IsPDF() { + dy = wm.Bb.LL.Y + } + + ll := LowerLeftCorner(wm.Vp, wm.Bb.Width(), wm.Bb.Height(), wm.Pos) + + if wm.Pos != types.Center && (r == 90 || r == -90) { + dx, dy = wm.alignWithPageBoundaries() + dx = ll.X + dx + wm.Dx + dy = ll.Y + dy + wm.Dy + } else { + dx = ll.X + wm.Bb.Width()/2 + wm.Dx + sin*(wm.Bb.Height()/2+dy) - cos*wm.Bb.Width()/2 + dy = ll.Y + wm.Bb.Height()/2 + wm.Dy - cos*(wm.Bb.Height()/2+dy) - sin*wm.Bb.Width()/2 + } + + return matrix.CalcTransformMatrix(1, 1, sin, cos, dx, dy) +} + +func (wm *Watermark) PdfResIndex(pageNr int) int { + if !wm.MultiStamp() { + return wm.PdfPageNrSrc + } + maxStampPageNr := wm.PdfMultiStartPageNrDest + len(wm.PdfRes) - 1 + i := pageNr + if pageNr > maxStampPageNr { + i = maxStampPageNr + } + return i +} diff --git a/pkg/pdfcpu/model/xreftable.go b/pkg/pdfcpu/model/xreftable.go new file mode 100644 index 0000000000000000000000000000000000000000..50127bd9ac79309c577a47d2b1f2b71639edc6cf --- /dev/null +++ b/pkg/pdfcpu/model/xreftable.go @@ -0,0 +1,2813 @@ +/* + Copyright 2021 The pdfcpu Authors. + + 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 + + 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 model + +import ( + "bufio" + "bytes" + "encoding/hex" + "fmt" + "io" + "os" + "path" + "sort" + "strings" + "time" + + "github.com/pdfcpu/pdfcpu/pkg/filter" + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/scan" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +var ErrNoContent = errors.New("pdfcpu: page without content") + +var zero int64 = 0 + +// XRefTableEntry represents an entry in the PDF cross reference table. +// +// This may wrap a free object, a compressed object or any in use PDF object: +// +// Dict, StreamDict, ObjectStreamDict, PDFXRefStreamDict, +// Array, Integer, Float, Name, StringLiteral, HexLiteral, Boolean +type XRefTableEntry struct { + Free bool + Offset *int64 + Generation *int + RefCount int + Object types.Object + Compressed bool + ObjectStream *int + ObjectStreamInd *int + Valid bool +} + +// NewXRefTableEntryGen0 returns a cross reference table entry for an object with generation 0. +func NewXRefTableEntryGen0(obj types.Object) *XRefTableEntry { + zero := 0 + return &XRefTableEntry{Generation: &zero, Object: obj} +} + +// NewFreeHeadXRefTableEntry returns the xref table entry for object 0 +// which is per definition the head of the free list (list of free objects). +func NewFreeHeadXRefTableEntry() *XRefTableEntry { + freeHeadGeneration := types.FreeHeadGeneration + + return &XRefTableEntry{ + Free: true, + Generation: &freeHeadGeneration, + Offset: &zero, + } +} + +// Enc wraps around all defined encryption attributes. +type Enc struct { + O, U []byte + OE, UE []byte + Perms []byte + L, P, R, V int + Emd bool // encrypt meta data + ID []byte +} + +// AnnotMap represents annotations by object number of the corresponding annotation dict. +type AnnotMap map[int]AnnotationRenderer + +type Annot struct { + IndRefs *[]types.IndirectRef + Map AnnotMap +} + +// PgAnnots represents a map of page annotations by type. +type PgAnnots map[AnnotationType]Annot + +// XRefTable represents a PDF cross reference table plus stats for a PDF file. +type XRefTable struct { + Table map[int]*XRefTableEntry + Size *int // Object count from PDF trailer dict. + PageCount int // Number of pages. + Root *types.IndirectRef // Pointer to catalog (reference to root object). + RootDict types.Dict // Catalog + Names map[string]*Node // Cache for name trees as found in catalog. + NameRefs map[string]NameMap // Name refs for merging only + Encrypt *types.IndirectRef // Encrypt dict. + E *Enc + EncKey []byte // Encrypt key. + AES4Strings bool + AES4Streams bool + AES4EmbeddedStreams bool + + // PDF Version + HeaderVersion *Version // The PDF version the source is claiming to us as per its header. + RootVersion *Version // Optional PDF version taking precedence over the header version. + + // Document information section + ID types.Array // from trailer + Info *types.IndirectRef // Infodict (reference to info dict object) + Title string + Subject string + Author string + Creator string + Producer string + CreationDate string + ModDate string + Keywords string + KeywordList types.StringSet + Properties map[string]string + CatalogXMPMeta *XMPMeta + + PageLayout *PageLayout + PageMode *PageMode + ViewerPref *ViewerPreferences + + // Linearization section (not yet supported) + OffsetPrimaryHintTable *int64 + OffsetOverflowHintTable *int64 + LinearizationObjs types.IntSet + + // Page annotation cache + PageAnnots map[int]PgAnnots + + // Thumbnail images + PageThumbs map[int]types.IndirectRef + + // Offspec section + AdditionalStreams *types.Array // array of IndirectRef - trailer :e.g., Oasis "Open Doc" + + // Statistics + Stats PDFStats + + Tagged bool // File is using tags. This is important for ??? + + // Validation + CurPage int // current page during validation + CurObj int // current object during validation, the last dereferenced object + Conf *Configuration // current command being executed + ValidationMode int // see Configuration + ValidateLinks bool // check for broken links in LinkAnnotations/URIDicts. + Valid bool // true means successful validated against ISO 32000. + URIs map[int]map[string]string // URIs for link checking + + Optimized bool + Watermarked bool + Form types.Dict + Outlines types.Dict + SignatureExist bool + AppendOnly bool + + // Fonts + UsedGIDs map[string]map[uint16]bool + FillFonts map[string]types.IndirectRef +} + +// NewXRefTable creates a new XRefTable. +func newXRefTable(conf *Configuration) (xRefTable *XRefTable) { + return &XRefTable{ + Table: map[int]*XRefTableEntry{}, + Names: map[string]*Node{}, + NameRefs: map[string]NameMap{}, + KeywordList: types.StringSet{}, + Properties: map[string]string{}, + LinearizationObjs: types.IntSet{}, + PageAnnots: map[int]PgAnnots{}, + PageThumbs: map[int]types.IndirectRef{}, + Stats: NewPDFStats(), + ValidationMode: conf.ValidationMode, + ValidateLinks: conf.ValidateLinks, + URIs: map[int]map[string]string{}, + UsedGIDs: map[string]map[uint16]bool{}, + FillFonts: map[string]types.IndirectRef{}, + Conf: conf, + } +} + +// Version returns the PDF version of the PDF writer that created this file. +// Before V1.4 this is the header version. +// Since V1.4 the catalog may contain a Version entry which takes precedence over the header version. +func (xRefTable *XRefTable) Version() Version { + if xRefTable.RootVersion != nil { + return *xRefTable.RootVersion + } + + return *xRefTable.HeaderVersion +} + +// VersionString return a string representation for this PDF files PDF version. +func (xRefTable *XRefTable) VersionString() string { + return xRefTable.Version().String() +} + +// ParseRootVersion returns a string representation for an optional Version entry in the root object. +func (xRefTable *XRefTable) ParseRootVersion() (v *string, err error) { + // Look in the catalog/root for a name entry "Version". + // This entry overrides the header version. + + rootDict, err := xRefTable.Catalog() + if err != nil { + return nil, err + } + + return rootDict.NameEntry("Version"), nil +} + +// ValidateVersion validates against the xRefTable's version. +func (xRefTable *XRefTable) ValidateVersion(element string, sinceVersion Version) error { + if xRefTable.Version() < sinceVersion { + return errors.Errorf("%s: unsupported in version %s\n", element, xRefTable.VersionString()) + } + + return nil +} + +func (xRefTable *XRefTable) currentCommand() CommandMode { + return xRefTable.Conf.Cmd +} + +func (xRefTable *XRefTable) IsMerging() bool { + cmd := xRefTable.currentCommand() + return cmd == MERGECREATE || cmd == MERGEAPPEND +} + +// EnsureVersionForWriting sets the version to the highest supported PDF Version 1.7. +// This is necessary to allow validation after adding features not supported +// by the original version of a document as during watermarking. +func (xRefTable *XRefTable) EnsureVersionForWriting() { + v := V17 + xRefTable.RootVersion = &v +} + +// IsLinearizationObject returns true if object #i is a a linearization object. +func (xRefTable *XRefTable) IsLinearizationObject(i int) bool { + return xRefTable.LinearizationObjs[i] +} + +// LinearizationObjsString returns a formatted string and the number of objs. +func (xRefTable *XRefTable) LinearizationObjsString() (int, string) { + var objs []int + for k := range xRefTable.LinearizationObjs { + if xRefTable.LinearizationObjs[k] { + objs = append(objs, k) + } + } + sort.Ints(objs) + + var linObj []string + for _, i := range objs { + linObj = append(linObj, fmt.Sprintf("%d", i)) + } + + return len(linObj), strings.Join(linObj, ",") +} + +// Exists returns true if xRefTable contains an entry for objNumber. +func (xRefTable *XRefTable) Exists(objNr int) bool { + _, found := xRefTable.Table[objNr] + return found +} + +// Find returns the XRefTable entry for given object number. +func (xRefTable *XRefTable) Find(objNr int) (*XRefTableEntry, bool) { + e, found := xRefTable.Table[objNr] + if !found { + return nil, false + } + return e, true +} + +// FindObject returns the object of the XRefTableEntry for a specific object number. +func (xRefTable *XRefTable) FindObject(objNr int) (types.Object, error) { + entry, ok := xRefTable.Find(objNr) + if !ok { + return nil, errors.Errorf("FindObject: obj#%d not registered in xRefTable", objNr) + } + return entry.Object, nil +} + +// Free returns the cross ref table entry for given number of a free object. +func (xRefTable *XRefTable) Free(objNr int) (*XRefTableEntry, error) { + entry, found := xRefTable.Find(objNr) + if !found { + return nil, nil + } + if !entry.Free { + return nil, errors.Errorf("Free: object #%d found, but not free.", objNr) + } + return entry, nil +} + +// NextForFree returns the number of the object the free object with objNumber links to. +// This is the successor of this free object in the free list. +func (xRefTable *XRefTable) NextForFree(objNr int) (int, error) { + entry, err := xRefTable.Free(objNr) + if err != nil { + return 0, err + } + + return int(*entry.Offset), nil +} + +// FindTableEntryLight returns the XRefTable entry for given object number. +func (xRefTable *XRefTable) FindTableEntryLight(objNr int) (*XRefTableEntry, bool) { + return xRefTable.Find(objNr) +} + +// FindTableEntry returns the XRefTable entry for given object and generation numbers. +func (xRefTable *XRefTable) FindTableEntry(objNr int, genNr int) (*XRefTableEntry, bool) { + if log.TraceEnabled() { + log.Trace.Printf("FindTableEntry: obj#:%d gen:%d \n", objNr, genNr) + } + return xRefTable.Find(objNr) +} + +// FindTableEntryForIndRef returns the XRefTable entry for given indirect reference. +func (xRefTable *XRefTable) FindTableEntryForIndRef(indRef *types.IndirectRef) (*XRefTableEntry, bool) { + if indRef == nil { + return nil, false + } + return xRefTable.FindTableEntry(indRef.ObjectNumber.Value(), indRef.GenerationNumber.Value()) +} + +// IncrementRefCount increments the number of references for the object pointed to by indRef. +func (xRefTable *XRefTable) IncrementRefCount(indRef *types.IndirectRef) { + if entry, ok := xRefTable.FindTableEntryForIndRef(indRef); ok { + entry.RefCount++ + } +} + +// InsertNew adds given xRefTableEntry at next new objNumber into the cross reference table. +// Only to be called once an xRefTable has been generated completely and all trailer dicts have been processed. +// xRefTable.Size is the size entry of the first trailer dict processed. +// Called on creation of new object streams. +// Called by InsertAndUseRecycled. +func (xRefTable *XRefTable) InsertNew(xRefTableEntry XRefTableEntry) (objNr int) { + objNr = *xRefTable.Size + xRefTable.Table[objNr] = &xRefTableEntry + *xRefTable.Size++ + return +} + +// InsertAndUseRecycled adds given xRefTableEntry into the cross reference table utilizing the freelist. +func (xRefTable *XRefTable) InsertAndUseRecycled(xRefTableEntry XRefTableEntry) (objNr int, err error) { + // see 7.5.4 Cross-Reference Table + + // Hacky: + // Although we increment the obj generation when recycling objects, + // we always use generation 0 when reusing recycled objects. + // This is because pdfcpu does not reuse objects + // in an incremental fashion like laid out in the PDF spec. + + if log.WriteEnabled() { + log.Write.Println("InsertAndUseRecycled: begin") + } + + // Get Next free object from freelist. + freeListHeadEntry, err := xRefTable.Free(0) + if err != nil { + return 0, err + } + + // If none available, add new object & return. + if *freeListHeadEntry.Offset == 0 { + xRefTableEntry.RefCount = 1 + objNr = xRefTable.InsertNew(xRefTableEntry) + if log.WriteEnabled() { + log.Write.Printf("InsertAndUseRecycled: end, new objNr=%d\n", objNr) + } + return objNr, nil + } + + // Recycle free object, update free list & return. + objNr = int(*freeListHeadEntry.Offset) + entry, found := xRefTable.FindTableEntryLight(objNr) + if !found { + return 0, errors.Errorf("InsertAndRecycle: no entry for obj #%d\n", objNr) + } + + // The new free list head entry becomes the old head entry's successor. + freeListHeadEntry.Offset = entry.Offset + + // The old head entry becomes garbage. + entry.Free = false + entry.Offset = nil + + // Create a new entry for the recycled object. + // TODO use entrys generation. + xRefTableEntry.RefCount = 1 + xRefTable.Table[objNr] = &xRefTableEntry + + if log.WriteEnabled() { + log.Write.Printf("InsertAndUseRecycled: end, recycled objNr=%d\n", objNr) + } + + return objNr, nil +} + +// InsertObject inserts an object into the xRefTable. +func (xRefTable *XRefTable) InsertObject(obj types.Object) (objNr int, err error) { + xRefTableEntry := NewXRefTableEntryGen0(obj) + xRefTableEntry.RefCount = 1 + return xRefTable.InsertNew(*xRefTableEntry), nil +} + +// IndRefForNewObject inserts an object into the xRefTable and returns an indirect reference to it. +func (xRefTable *XRefTable) IndRefForNewObject(obj types.Object) (*types.IndirectRef, error) { + xRefTableEntry := NewXRefTableEntryGen0(obj) + objNr, err := xRefTable.InsertAndUseRecycled(*xRefTableEntry) + if err != nil { + return nil, err + } + + return types.NewIndirectRef(objNr, *xRefTableEntry.Generation), nil +} + +// NewStreamDictForBuf creates a streamDict for buf. +func (xRefTable *XRefTable) NewStreamDictForBuf(buf []byte) (*types.StreamDict, error) { + sd := types.StreamDict{ + Dict: types.NewDict(), + Content: buf, + FilterPipeline: []types.PDFFilter{{Name: filter.Flate, DecodeParms: nil}}, + } + sd.InsertName("Filter", filter.Flate) + return &sd, nil +} + +// NewStreamDictForFile creates a streamDict for filename. +func (xRefTable *XRefTable) NewStreamDictForFile(filename string) (*types.StreamDict, error) { + buf, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + + return xRefTable.NewStreamDictForBuf(buf) +} + +// NewEmbeddedStreamDict creates and returns an embeddedStreamDict containing the bytes represented by r. +func (xRefTable *XRefTable) NewEmbeddedStreamDict(r io.Reader, modDate time.Time) (*types.IndirectRef, error) { + var buf bytes.Buffer + if _, err := io.Copy(&buf, r); err != nil { + return nil, err + } + + bb := buf.Bytes() + + sd, err := xRefTable.NewStreamDictForBuf(bb) + if err != nil { + return nil, err + } + + sd.InsertName("Type", "EmbeddedFile") + d := types.NewDict() + d.InsertInt("Size", len(bb)) + d.Insert("ModDate", types.StringLiteral(types.DateString(modDate))) + sd.Insert("Params", d) + if err = sd.Encode(); err != nil { + return nil, err + } + + return xRefTable.IndRefForNewObject(*sd) +} + +func (xRefTable *XRefTable) locateObjForIndRef(ir types.IndirectRef) (types.Object, error) { + objNr := int(ir.ObjectNumber) + + entry, found := xRefTable.FindTableEntryLight(objNr) + if !found { + return nil, errors.Errorf("pdfcpu: locateObjForIndRef: no xref entry found for obj #%d\n", objNr) + } + + // Check for multiple indRefs. + if entry.RefCount > 1 { + entry.RefCount-- + // By returning nil we signal this object is still in use and can't be deleted. + return nil, nil + } + + // Since this is the only indRef we can move on and delete the entire object graph. + return xRefTable.Dereference(ir) +} + +// FreeObject marks an objects xref table entry as free and inserts it into the free list right after the head. +func (xRefTable *XRefTable) FreeObject(objNr int) error { + // see 7.5.4 Cross-Reference Table + + if log.DebugEnabled() { + log.Debug.Printf("FreeObject: begin %d\n", objNr) + } + + freeListHeadEntry, err := xRefTable.Free(0) + if err != nil { + return err + } + + entry, found := xRefTable.FindTableEntryLight(objNr) + if !found { + return errors.Errorf("FreeObject: no entry for obj #%d\n", objNr) + } + + if entry.Free { + if log.DebugEnabled() { + log.Debug.Printf("FreeObject: end %d already free\n", objNr) + } + return nil + } + + *entry.Generation++ + entry.Free = true + entry.Compressed = false + entry.Offset = freeListHeadEntry.Offset + entry.Object = nil + entry.RefCount = 0 + + next := int64(objNr) + freeListHeadEntry.Offset = &next + + if log.DebugEnabled() { + log.Debug.Printf("FreeObject: end %d\n", objNr) + } + + return nil +} + +// DeleteObject makes a deep remove of o. +func (xRefTable *XRefTable) DeleteObject(o types.Object) error { + var err error + + ir, ok := o.(types.IndirectRef) + if ok { + o, err = xRefTable.locateObjForIndRef(ir) + if err != nil || o == nil { + return err + } + if err = xRefTable.FreeObject(ir.ObjectNumber.Value()); err != nil { + return err + } + } + + switch o := o.(type) { + + case types.Dict: + for _, v := range o { + err := xRefTable.DeleteObject(v) + if err != nil { + return err + } + } + + case types.StreamDict: + for _, v := range o.Dict { + err := xRefTable.DeleteObject(v) + if err != nil { + return err + } + } + + case types.Array: + for _, v := range o { + err := xRefTable.DeleteObject(v) + if err != nil { + return err + } + } + + } + + return nil +} + +// DeleteObjectGraph deletes all objects reachable by indRef. +func (xRefTable *XRefTable) DeleteObjectGraph(o types.Object) error { + if log.DebugEnabled() { + log.Debug.Println("DeleteObjectGraph: begin") + } + + indRef, ok := o.(types.IndirectRef) + if !ok { + return nil + } + + // Delete ObjectGraph for object indRef.ObjectNumber.Value() via recursion. + if err := xRefTable.DeleteObject(indRef); err != nil { + return err + } + + if log.DebugEnabled() { + log.Debug.Println("DeleteObjectGraph: end") + } + + return nil +} + +// NewEmbeddedFileStreamDict returns an embeddedFileStreamDict containing the file "filename". +func (xRefTable *XRefTable) NewEmbeddedFileStreamDict(filename string) (*types.IndirectRef, error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + + fi, err := f.Stat() + if err != nil { + return nil, err + } + + return xRefTable.NewEmbeddedStreamDict(f, fi.ModTime()) +} + +// NewSoundStreamDict returns a new sound stream dict. +func (xRefTable *XRefTable) NewSoundStreamDict(filename string, samplingRate int, fileSpecDict types.Dict) (*types.IndirectRef, error) { + sd, err := xRefTable.NewStreamDictForFile(filename) + if err != nil { + return nil, err + } + sd.InsertName("Type", "Sound") + sd.InsertInt("R", samplingRate) + sd.InsertInt("C", 2) + sd.InsertInt("B", 8) + sd.InsertName("E", "Signed") + if fileSpecDict != nil { + sd.Insert("F", fileSpecDict) + } else { + sd.Insert("F", types.StringLiteral(path.Base(filename))) + } + + if err = sd.Encode(); err != nil { + return nil, err + } + + return xRefTable.IndRefForNewObject(*sd) +} + +// NewFileSpecDict creates and returns a new fileSpec dictionary. +func (xRefTable *XRefTable) NewFileSpecDict(f, uf, desc string, indRefStreamDict types.IndirectRef) (types.Dict, error) { + d := types.NewDict() + d.InsertName("Type", "Filespec") + + s, err := types.EscapeUTF16String(f) + if err != nil { + return nil, err + } + d.InsertString("F", *s) + + if s, err = types.EscapeUTF16String(uf); err != nil { + return nil, err + } + d.InsertString("UF", *s) + + efDict := types.NewDict() + efDict.Insert("F", indRefStreamDict) + efDict.Insert("UF", indRefStreamDict) + d.Insert("EF", efDict) + + if desc != "" { + if s, err = types.EscapeUTF16String(desc); err != nil { + return nil, err + } + d.InsertString("Desc", *s) + } + + // CI, optional, collection item dict, since V1.7 + // a corresponding collection schema dict in a collection. + ciDict := types.NewDict() + //add contextual meta info here. + d.Insert("CI", ciDict) + + return d, nil +} + +func (xRefTable *XRefTable) freeObjects() types.IntSet { + m := types.IntSet{} + + for k, v := range xRefTable.Table { + if v != nil && v.Free && k > 0 { + m[k] = true + } + } + + return m +} + +func anyKey(m types.IntSet) int { + for k := range m { + return k + } + return -1 +} + +func (xRefTable *XRefTable) handleDanglingFree(m types.IntSet, head *XRefTableEntry) error { + for i := range m { + + entry, found := xRefTable.FindTableEntryLight(i) + if !found { + return errors.Errorf("pdfcpu: ensureValidFreeList: no xref entry found for obj #%d\n", i) + } + + if !entry.Free { + return errors.Errorf("pdfcpu: ensureValidFreeList: xref entry is not free for obj #%d\n", i) + } + + if *entry.Generation == types.FreeHeadGeneration { + entry.Offset = &zero + continue + } + + entry.Offset = head.Offset + next := int64(i) + head.Offset = &next + } + return nil +} + +func (xRefTable *XRefTable) validateFreeList(f int, m types.IntSet, e *XRefTableEntry) (*XRefTableEntry, int, error) { + var lastValid *XRefTableEntry + var nextFree int + + for f != 0 { + if log.TraceEnabled() { + log.Trace.Printf("EnsureValidFreeList: validating obj #%d %v\n", f, m) + } + // verify if obj f is one of the free objects recorded. + if !m[f] { + if len(m) > 0 && lastValid == nil { + lastValid = e + f = anyKey(m) + nextFree = f + continue + } + // Repair last entry. + *e.Offset = 0 + break + } + + delete(m, f) + + var err error + if e, err = xRefTable.Free(f); err != nil { + return nil, 0, err + } + if e == nil { + return nil, 0, errors.Errorf("pdfcpu: ensureValidFreeList: no xref entry found for obj #%d\n", f) + } + + f = int(*e.Offset) + } + + return lastValid, nextFree, nil +} + +// EnsureValidFreeList ensures the integrity of the free list associated with the recorded free objects. +// See 7.5.4 Cross-Reference Table +func (xRefTable *XRefTable) EnsureValidFreeList() error { + if log.TraceEnabled() { + log.Trace.Println("EnsureValidFreeList: begin") + } + + m := xRefTable.freeObjects() + + // Verify free object 0 as free list head. + head, _ := xRefTable.Find(0) + if head == nil { + g0 := types.FreeHeadGeneration + head = &XRefTableEntry{Free: true, Offset: &zero, Generation: &g0} + xRefTable.Table[0] = head + } + + // verify generation of 56535 + if *head.Generation != types.FreeHeadGeneration { + // Fix generation for obj 0. + *head.Generation = types.FreeHeadGeneration + } + + if len(m) == 0 { + + // no free object other than 0. + + // repair if necessary + if *head.Offset != 0 { + *head.Offset = 0 + } + + if log.TraceEnabled() { + log.Trace.Println("EnsureValidFreeList: empty free list.") + } + return nil + } + + e := head + f := int(*e.Offset) + + lastValid, nextFree, err := xRefTable.validateFreeList(f, m, e) + if err != nil { + return err + } + + if lastValid != nil { + *lastValid.Offset = int64(nextFree) + } + + if len(m) == 0 { + if log.TraceEnabled() { + log.Trace.Println("EnsureValidFreeList: end, regular linked list") + } + return nil + } + + // insert remaining free objects into verified linked list + // unless they are forever deleted with generation 65535. + // In that case they have to point to obj 0. + err = xRefTable.handleDanglingFree(m, head) + + if log.TraceEnabled() { + log.Trace.Println("EnsureValidFreeList: end") + } + + return err +} + +func (xRefTable *XRefTable) DeleteDictEntry(d types.Dict, key string) error { + o, found := d.Find(key) + if !found { + return nil + } + if err := xRefTable.DeleteObject(o); err != nil { + return err + } + d.Delete(key) + return nil +} + +// UndeleteObject ensures an object is not recorded in the free list. +// e.g. sometimes caused by indirect references to free objects in the original PDF file. +func (xRefTable *XRefTable) UndeleteObject(objectNumber int) error { + if log.DebugEnabled() { + log.Debug.Printf("UndeleteObject: begin %d\n", objectNumber) + } + + f, err := xRefTable.Free(0) + if err != nil { + return err + } + + // until we have found the last free object which should point to obj 0. + for *f.Offset != 0 { + objNr := int(*f.Offset) + + entry, err := xRefTable.Free(objNr) + if err != nil { + return err + } + + if objNr == objectNumber { + if log.DebugEnabled() { + log.Debug.Printf("UndeleteObject end: undeleting obj#%d\n", objectNumber) + } + *f.Offset = *entry.Offset + entry.Offset = nil + if *entry.Generation > 0 { + *entry.Generation-- + } + entry.Free = false + return nil + } + + f = entry + } + + if log.DebugEnabled() { + log.Debug.Printf("UndeleteObject: end: obj#%d not in free list.\n", objectNumber) + } + + return nil +} + +// IsValidObj returns true if the object with objNr and genNr is valid. +func (xRefTable *XRefTable) IsValidObj(objNr, genNr int) (bool, error) { + entry, found := xRefTable.FindTableEntry(objNr, genNr) + if !found { + return false, errors.Errorf("pdfcpu: IsValid: no entry for obj#%d\n", objNr) + } + if entry.Free { + return false, errors.Errorf("pdfcpu: IsValid: unexpected free entry for obj#%d\n", objNr) + } + return entry.Valid, nil +} + +// IsValid returns true if the object referenced by ir is valid. +func (xRefTable *XRefTable) IsValid(ir types.IndirectRef) (bool, error) { + return xRefTable.IsValidObj(ir.ObjectNumber.Value(), ir.GenerationNumber.Value()) +} + +// SetValid marks the xreftable entry of the object referenced by ir as valid. +func (xRefTable *XRefTable) SetValid(ir types.IndirectRef) error { + entry, found := xRefTable.FindTableEntry(ir.ObjectNumber.Value(), ir.GenerationNumber.Value()) + if !found { + return errors.Errorf("pdfcpu: SetValid: no entry for obj#%d\n", ir.ObjectNumber.Value()) + } + if entry.Free { + return errors.Errorf("pdfcpu: SetValid: unexpected free entry for obj#%d\n", ir.ObjectNumber.Value()) + } + entry.Valid = true + return nil +} + +// DereferenceStreamDict resolves a stream dictionary object. +func (xRefTable *XRefTable) DereferenceStreamDict(o types.Object) (*types.StreamDict, bool, error) { + // TODO Check if we still need the bool return value + indRef, ok := o.(types.IndirectRef) + if !ok { + sd, ok := o.(types.StreamDict) + if !ok { + return nil, false, errors.Errorf("pdfcpu: DereferenceStreamDict: wrong type <%v> %T", o, o) + } + return &sd, false, nil + } + + // 7.3.10 + // An indirect reference to an undefined object shall not be considered an error by a conforming reader; + // it shall be treated as a reference to the null object. + entry, found := xRefTable.FindTableEntry(indRef.ObjectNumber.Value(), indRef.GenerationNumber.Value()) + if !found || entry.Object == nil || entry.Free { + return nil, false, nil + } + ev := entry.Valid + if !entry.Valid { + entry.Valid = true + } + sd, ok := entry.Object.(types.StreamDict) + if !ok { + return nil, false, errors.Errorf("pdfcpu: DereferenceStreamDict: wrong type <%v> %T", o, entry.Object) + } + + return &sd, ev, nil +} + +// DereferenceXObjectDict resolves an XObject. +func (xRefTable *XRefTable) DereferenceXObjectDict(indRef types.IndirectRef) (*types.StreamDict, error) { + sd, _, err := xRefTable.DereferenceStreamDict(indRef) + if err != nil { + return nil, err + } + if sd == nil { + return nil, nil + } + + subType := sd.Dict.Subtype() + if subType == nil { + return nil, errors.Errorf("pdfcpu: DereferenceXObjectDict: missing stream dict Subtype %s\n", indRef) + } + + if *subType != "Image" && *subType != "Form" { + return nil, errors.Errorf("pdfcpu: DereferenceXObjectDict: unexpected stream dict Subtype %s\n", *subType) + } + + return sd, nil +} + +// Catalog returns a pointer to the root object / catalog. +func (xRefTable *XRefTable) Catalog() (types.Dict, error) { + if xRefTable.RootDict != nil { + return xRefTable.RootDict, nil + } + + if xRefTable.Root == nil { + return nil, errors.New("pdfcpu: Catalog: missing root dict") + } + + o, err := xRefTable.indRefToObject(xRefTable.Root, true) + if err != nil || o == nil { + return nil, err + } + + d, ok := o.(types.Dict) + if !ok { + return nil, errors.New("pdfcpu: catalog: corrupt root catalog") + } + + xRefTable.RootDict = d + + return xRefTable.RootDict, nil +} + +// EncryptDict returns a pointer to the root object / catalog. +func (xRefTable *XRefTable) EncryptDict() (types.Dict, error) { + o, err := xRefTable.indRefToObject(xRefTable.Encrypt, true) + if err != nil || o == nil { + return nil, err + } + + d, ok := o.(types.Dict) + if !ok { + return nil, errors.New("pdfcpu: encryptDict: corrupt encrypt dict") + } + + return d, nil +} + +// CatalogHasPieceInfo returns true if the root has an entry for \"PieceInfo\". +func (xRefTable *XRefTable) CatalogHasPieceInfo() (bool, error) { + rootDict, err := xRefTable.Catalog() + if err != nil { + return false, err + } + obj, hasPieceInfo := rootDict.Find("PieceInfo") + return hasPieceInfo && obj != nil, nil +} + +// Pages returns the Pages reference contained in the catalog. +func (xRefTable *XRefTable) Pages() (*types.IndirectRef, error) { + rootDict, err := xRefTable.Catalog() + if err != nil { + return nil, err + } + return rootDict.IndirectRefEntry("Pages"), nil +} + +// MissingObjects returns the number of objects that were not written +// plus the corresponding comma separated string representation. +func (xRefTable *XRefTable) MissingObjects() (int, *string) { + var missing []string + + for i := 0; i < *xRefTable.Size; i++ { + if !xRefTable.Exists(i) { + missing = append(missing, fmt.Sprintf("%d", i)) + } + } + + var s *string + + if len(missing) > 0 { + joined := strings.Join(missing, ",") + s = &joined + } + + return len(missing), s +} + +func (xRefTable *XRefTable) sortedKeys() []int { + var keys []int + for k := range xRefTable.Table { + keys = append(keys, k) + } + sort.Ints(keys) + return keys +} + +func objStr(entry *XRefTableEntry, objNr int) string { + typeStr := fmt.Sprintf("%T", entry.Object) + + d, ok := entry.Object.(types.Dict) + if ok { + if d.Type() != nil { + typeStr += fmt.Sprintf(" type=%s", *d.Type()) + } + if d.Subtype() != nil { + typeStr += fmt.Sprintf(" subType=%s", *d.Subtype()) + } + } + + if entry.ObjectStream != nil { + // was compressed, offset is nil. + return fmt.Sprintf("%5d: was compressed %d[%d] generation=%d %s \n%s\n", objNr, *entry.ObjectStream, *entry.ObjectStreamInd, *entry.Generation, typeStr, entry.Object) + } + + // regular in use object with offset. + if entry.Offset != nil { + return fmt.Sprintf("%5d: offset=%8d generation=%d %s \n%s\n", objNr, *entry.Offset, *entry.Generation, typeStr, entry.Object) + } + + return fmt.Sprintf("%5d: offset=nil generation=%d %s \n%s\n", objNr, *entry.Generation, typeStr, entry.Object) +} + +func (xRefTable *XRefTable) DumpObject(objNr, mode int) { + // mode + // 0 .. silent / obj only + // 1 .. ascii + // 2 .. hex + entry := xRefTable.Table[objNr] + if entry == nil || entry.Free || entry.Compressed || entry.Object == nil { + fmt.Println(":(") + return + } + + str := objStr(entry, objNr) + + if mode > 0 { + sd, ok := entry.Object.(types.StreamDict) + if ok { + + err := sd.Decode() + if err == filter.ErrUnsupportedFilter { + str += "stream filter unsupported!" + fmt.Println(str) + return + } + if err != nil { + str += "decoding problem encountered!" + fmt.Println(str) + return + } + + s := "decoded stream content (length = %d)\n%s\n" + s1 := "" + switch mode { + case 1: + sc := bufio.NewScanner(bytes.NewReader(sd.Content)) + sc.Split(scan.Lines) + for sc.Scan() { + s1 += sc.Text() + "\n" + } + str += fmt.Sprintf(s, len(sd.Content), s1) + case 2: + str += fmt.Sprintf(s, len(sd.Content), hex.Dump(sd.Content)) + } + } + + osd, ok := entry.Object.(types.ObjectStreamDict) + if ok { + str += fmt.Sprintf("object stream count:%d size of objectarray:%d\n", osd.ObjCount, len(osd.ObjArray)) + } + } + + fmt.Println(str) +} + +func (xRefTable *XRefTable) list(logStr []string) []string { + // Print list of XRefTable entries to logString. + for _, k := range xRefTable.sortedKeys() { + + entry := xRefTable.Table[k] + + var str string + + if entry.Free { + str = fmt.Sprintf("%5d: f next=%8d generation=%d\n", k, *entry.Offset, *entry.Generation) + } else if entry.Compressed { + str = fmt.Sprintf("%5d: c => obj:%d[%d] generation=%d \n%s\n", k, *entry.ObjectStream, *entry.ObjectStreamInd, *entry.Generation, entry.Object) + } else { + if entry.Object != nil { + + typeStr := fmt.Sprintf("%T", entry.Object) + + d, ok := entry.Object.(types.Dict) + + if ok { + if d.Type() != nil { + typeStr += fmt.Sprintf(" type=%s", *d.Type()) + } + if d.Subtype() != nil { + typeStr += fmt.Sprintf(" subType=%s", *d.Subtype()) + } + } + + if entry.ObjectStream != nil { + // was compressed, offset is nil. + str = fmt.Sprintf("%5d: was compressed %d[%d] generation=%d %s \n%s\n", + k, *entry.ObjectStream, *entry.ObjectStreamInd, *entry.Generation, typeStr, entry.Object) + } else { + // regular in use object with offset. + if entry.Offset != nil { + str = fmt.Sprintf("%5d: offset=%8d generation=%d %s \n%s\n", + k, *entry.Offset, *entry.Generation, typeStr, entry.Object) + } else { + str = fmt.Sprintf("%5d: offset=nil generation=%d %s \n%s\n", + k, *entry.Generation, typeStr, entry.Object) + } + + } + + sd, ok := entry.Object.(types.StreamDict) + if ok && log.TraceEnabled() { + s := "decoded stream content (length = %d)\n%s\n" + if sd.IsPageContent { + str += fmt.Sprintf(s, len(sd.Content), sd.Content) + } else { + str += fmt.Sprintf(s, len(sd.Content), hex.Dump(sd.Content)) + } + } + + osd, ok := entry.Object.(types.ObjectStreamDict) + if ok { + str += fmt.Sprintf("object stream count:%d size of objectarray:%d\n", osd.ObjCount, len(osd.ObjArray)) + } + + } else { + if entry.Offset == nil { + str = fmt.Sprintf("%5d: offset= none generation=%d nil\n", k, *entry.Generation) + } else { + str = fmt.Sprintf("%5d: offset=%8d generation=%d nil\n", k, *entry.Offset, *entry.Generation) + } + } + } + + logStr = append(logStr, str) + } + + return logStr +} + +// Dump the free list to logStr. +// At this point the free list is assumed to be a linked list with its last node linked to the beginning. +func (xRefTable *XRefTable) freeList(logStr []string) ([]string, error) { + if log.TraceEnabled() { + log.Trace.Printf("freeList begin") + } + + head, err := xRefTable.Free(0) + if err != nil { + return nil, err + } + + if *head.Offset == 0 { + return append(logStr, "\nEmpty free list.\n"), nil + } + + f := int(*head.Offset) + + logStr = append(logStr, "\nfree list:\n obj next generation\n") + logStr = append(logStr, fmt.Sprintf("%5d %5d %5d\n", 0, f, types.FreeHeadGeneration)) + + for f != 0 { + if log.TraceEnabled() { + log.Trace.Printf("freeList validating free object %d\n", f) + } + + entry, err := xRefTable.Free(f) + if err != nil { + return nil, err + } + + next := int(*entry.Offset) + generation := *entry.Generation + s := fmt.Sprintf("%5d %5d %5d\n", f, next, generation) + logStr = append(logStr, s) + if log.TraceEnabled() { + log.Trace.Printf("freeList: %s", s) + } + + f = next + } + + if log.TraceEnabled() { + log.Trace.Printf("freeList end") + } + + return logStr, nil +} + +func (xRefTable *XRefTable) bindNameTreeNode(name string, n *Node, root bool) error { + var dict types.Dict + + if n.D == nil { + dict = types.NewDict() + n.D = dict + } else { + if root { + namesDict, err := xRefTable.NamesDict() + if err != nil { + return err + } + if namesDict == nil { + return errors.New("pdfcpu: root entry \"Names\" corrupt") + } + namesDict.Update(name, n.D) + } + if log.DebugEnabled() { + log.Debug.Printf("bind dict = %v\n", n.D) + } + dict = n.D + } + + if !root { + dict.Update("Limits", types.NewHexLiteralArray(n.Kmin, n.Kmax)) + } else { + dict.Delete("Limits") + } + + if n.leaf() { + a := types.Array{} + for _, e := range n.Names { + a = append(a, types.NewHexLiteral([]byte(e.k))) + a = append(a, e.v) + } + dict.Update("Names", a) + if log.DebugEnabled() { + log.Debug.Printf("bound nametree node(leaf): %s/n", dict) + } + return nil + } + + kids := types.Array{} + for _, k := range n.Kids { + if err := xRefTable.bindNameTreeNode(name, k, false); err != nil { + return err + } + indRef, err := xRefTable.IndRefForNewObject(k.D) + if err != nil { + return err + } + kids = append(kids, *indRef) + } + + dict.Update("Kids", kids) + dict.Delete("Names") + + if log.DebugEnabled() { + log.Debug.Printf("bound nametree node(intermediary): %s/n", dict) + } + + return nil +} + +// BindNameTrees syncs up the internal name tree cache with the xreftable. +func (xRefTable *XRefTable) BindNameTrees() error { + if log.WriteEnabled() { + log.Write.Println("BindNameTrees..") + } + + // Iterate over internal name tree rep. + for k, v := range xRefTable.Names { + if log.WriteEnabled() { + log.Write.Printf("bindNameTree: %s\n", k) + } + if err := xRefTable.bindNameTreeNode(k, v, true); err != nil { + return err + } + } + + return nil +} + +// LocateNameTree locates/ensures a specific name tree. +func (xRefTable *XRefTable) LocateNameTree(nameTreeName string, ensure bool) error { + if xRefTable.Names[nameTreeName] != nil { + return nil + } + + d, err := xRefTable.Catalog() + if err != nil { + return err + } + + o, found := d.Find("Names") + if !found { + if !ensure { + return nil + } + dict := types.NewDict() + + indRef, err := xRefTable.IndRefForNewObject(dict) + if err != nil { + return err + } + d.Insert("Names", *indRef) + + d = dict + } else { + d, err = xRefTable.DereferenceDict(o) + if err != nil { + return err + } + } + + o, found = d.Find(nameTreeName) + if !found { + if !ensure { + return nil + } + dict := types.NewDict() + dict.Insert("Names", types.Array{}) + + indRef, err := xRefTable.IndRefForNewObject(dict) + if err != nil { + return err + } + + d.Insert(nameTreeName, *indRef) + + xRefTable.Names[nameTreeName] = &Node{D: dict} + + return nil + } + + d1, err := xRefTable.DereferenceDict(o) + if err != nil { + return err + } + + xRefTable.Names[nameTreeName] = &Node{D: d1} + + return nil +} + +// NamesDict returns the dict that contains all name trees. +func (xRefTable *XRefTable) NamesDict() (types.Dict, error) { + d, err := xRefTable.Catalog() + if err != nil { + return nil, err + } + + o, found := d.Find("Names") + if !found { + dict := types.NewDict() + indRef, err := xRefTable.IndRefForNewObject(dict) + if err != nil { + return nil, err + } + d["Names"] = *indRef + return dict, nil + } + + return xRefTable.DereferenceDict(o) +} + +// RemoveNameTree removes a specific name tree. +// Also removes a resulting empty names dict. +func (xRefTable *XRefTable) RemoveNameTree(nameTreeName string) error { + namesDict, err := xRefTable.NamesDict() + if err != nil { + return err + } + + if namesDict == nil { + return errors.New("pdfcpu: removeNameTree: root entry \"Names\" corrupt") + } + + // We have an existing name dict. + + // Delete the name tree. + if err = xRefTable.DeleteDictEntry(namesDict, nameTreeName); err != nil { + return err + } + if namesDict.Len() > 0 { + return nil + } + + // Remove empty names dict. + rootDict, err := xRefTable.Catalog() + if err != nil { + return err + } + if err = xRefTable.DeleteDictEntry(rootDict, "Names"); err != nil { + return err + } + + if log.DebugEnabled() { + log.Debug.Printf("Deleted Names from root: %s\n", rootDict) + } + + return nil +} + +// RemoveCollection removes an existing Collection entry from the catalog. +func (xRefTable *XRefTable) RemoveCollection() error { + rootDict, err := xRefTable.Catalog() + if err != nil { + return err + } + return xRefTable.DeleteDictEntry(rootDict, "Collection") +} + +// EnsureCollection makes sure there is a Collection entry in the catalog. +// Needed for portfolio / portable collections eg. for file attachments. +func (xRefTable *XRefTable) EnsureCollection() error { + rootDict, err := xRefTable.Catalog() + if err != nil { + return err + } + + if _, found := rootDict.Find("Collection"); found { + return nil + } + + dict := types.NewDict() + dict.Insert("Type", types.Name("Collection")) + dict.Insert("View", types.Name("D")) + + schemaDict := types.NewDict() + schemaDict.Insert("Type", types.Name("CollectionSchema")) + + fileNameCFDict := types.NewDict() + fileNameCFDict.Insert("Type", types.Name("CollectionField")) + fileNameCFDict.Insert("Subtype", types.Name("F")) + fileNameCFDict.Insert("N", types.StringLiteral("Filename")) + fileNameCFDict.Insert("O", types.Integer(1)) + schemaDict.Insert("FileName", fileNameCFDict) + + descCFDict := types.NewDict() + descCFDict.Insert("Type", types.Name("CollectionField")) + descCFDict.Insert("Subtype", types.Name("Desc")) + descCFDict.Insert("N", types.StringLiteral("Description")) + descCFDict.Insert("O", types.Integer(2)) + schemaDict.Insert("Description", descCFDict) + + sizeCFDict := types.NewDict() + sizeCFDict.Insert("Type", types.Name("CollectionField")) + sizeCFDict.Insert("Subtype", types.Name("Size")) + sizeCFDict.Insert("N", types.StringLiteral("Size")) + sizeCFDict.Insert("O", types.Integer(3)) + schemaDict.Insert("Size", sizeCFDict) + + modDateCFDict := types.NewDict() + modDateCFDict.Insert("Type", types.Name("CollectionField")) + modDateCFDict.Insert("Subtype", types.Name("ModDate")) + modDateCFDict.Insert("N", types.StringLiteral("Last Modification")) + modDateCFDict.Insert("O", types.Integer(4)) + schemaDict.Insert("ModDate", modDateCFDict) + + //TODO use xRefTable.InsertAndUseRecycled(xRefTableEntry) + + indRef, err := xRefTable.IndRefForNewObject(schemaDict) + if err != nil { + return err + } + dict.Insert("Schema", *indRef) + + sortDict := types.NewDict() + sortDict.Insert("S", types.Name("ModDate")) + sortDict.Insert("A", types.Boolean(false)) + dict.Insert("Sort", sortDict) + + indRef, err = xRefTable.IndRefForNewObject(dict) + if err != nil { + return err + } + rootDict.Insert("Collection", *indRef) + + return nil +} + +// RemoveEmbeddedFilesNameTree removes both the embedded files name tree and the Collection dict. +func (xRefTable *XRefTable) RemoveEmbeddedFilesNameTree() error { + delete(xRefTable.Names, "EmbeddedFiles") + + if err := xRefTable.RemoveNameTree("EmbeddedFiles"); err != nil { + return err + } + + return xRefTable.RemoveCollection() +} + +// IDFirstElement returns the first element of ID. +func (xRefTable *XRefTable) IDFirstElement() (id []byte, err error) { + hl, ok := xRefTable.ID[0].(types.HexLiteral) + if ok { + return hl.Bytes() + } + + sl, ok := xRefTable.ID[0].(types.StringLiteral) + if !ok { + return nil, errors.New("pdfcpu: ID must contain hex literals or string literals") + } + + bb, err := types.Unescape(sl.Value()) + if err != nil { + return nil, err + } + + return bb, nil +} + +// InheritedPageAttrs represents all inherited page attributes. +type InheritedPageAttrs struct { + Resources types.Dict + MediaBox *types.Rectangle + CropBox *types.Rectangle + Rotate int +} + +func rect(xRefTable *XRefTable, a types.Array) (*types.Rectangle, error) { + llx, err := xRefTable.DereferenceNumber(a[0]) + if err != nil { + return nil, err + } + + lly, err := xRefTable.DereferenceNumber(a[1]) + if err != nil { + return nil, err + } + + urx, err := xRefTable.DereferenceNumber(a[2]) + if err != nil { + return nil, err + } + + ury, err := xRefTable.DereferenceNumber(a[3]) + if err != nil { + return nil, err + } + + return types.NewRectangle(llx, lly, urx, ury), nil +} + +func weaveResourceSubDict(d1, d2 types.Dict) { + for k, v := range d1 { + if v != nil { + v = v.Clone() + } + d2[k] = v + } +} + +func (xRefTable *XRefTable) consolidateResources(obj types.Object, pAttrs *InheritedPageAttrs) error { + d, err := xRefTable.DereferenceDict(obj) + if err != nil { + return err + } + if len(d) == 0 { + return nil + } + + if pAttrs.Resources == nil { + // Create a resource dict that eventually will contain any inherited resources + // walking down from page root to leaf node representing the page in question. + pAttrs.Resources = d.Clone().(types.Dict) + for k, v := range pAttrs.Resources { + o, err := xRefTable.Dereference(v) + if err != nil { + return err + } + pAttrs.Resources[k] = o.Clone() + } + if log.WriteEnabled() { + log.Write.Printf("pA:\n%s\n", pAttrs.Resources) + } + return nil + } + + // Accumulate any resources defined in this page node into the inherited resources. + for k, v := range d { + if k == "ProcSet" || v == nil { + continue + } + d1, err := xRefTable.DereferenceDict(v) + if err != nil { + return err + } + if d1 == nil { + continue + } + // We have identified a subdict that needs to go into the inherited res dict. + if pAttrs.Resources[k] == nil { + pAttrs.Resources[k] = d1.Clone() + continue + } + d2, ok := pAttrs.Resources[k].(types.Dict) + if !ok { + return errors.Errorf("pdfcpu: checkInheritedPageAttrs: expected Dict d2: %T", pAttrs.Resources[k]) + } + // Weave sub dict d1 into inherited sub dict d2. + // Any existing resource names will be overridden. + weaveResourceSubDict(d1, d2) + } + + return nil +} + +func (xRefTable *XRefTable) checkInheritedPageAttrs(pageDict types.Dict, pAttrs *InheritedPageAttrs, consolidateRes bool) error { + // Return mediaBox, cropBox and rotate as inherited. + // if consolidateRes is true + // then consolidate all inherited resources as required by content stream + // else return pageDict resources. + var ( + obj types.Object + found bool + ) + + if obj, found = pageDict.Find("MediaBox"); found { + a, err := xRefTable.DereferenceArray(obj) + if err != nil { + return err + } + if pAttrs.MediaBox, err = rect(xRefTable, a); err != nil { + return err + } + } + + if obj, found = pageDict.Find("CropBox"); found { + a, err := xRefTable.DereferenceArray(obj) + if err != nil { + return err + } + if pAttrs.CropBox, err = rect(xRefTable, a); err != nil { + return err + } + } + + if obj, found = pageDict.Find("Rotate"); found { + i, err := xRefTable.DereferenceInteger(obj) + if err != nil { + return err + } + pAttrs.Rotate = i.Value() + } + + if obj, found = pageDict.Find("Resources"); !found { + return nil + } + + if !consolidateRes { + // Return resourceDict as is. + d, err := xRefTable.DereferenceDict(obj) + if err != nil { + return err + } + pAttrs.Resources = d + return nil + } + + // Accumulate inherited resources. + return xRefTable.consolidateResources(obj, pAttrs) +} + +// PageContent returns the content in PDF syntax for page dict d. +func (xRefTable *XRefTable) PageContent(d types.Dict) ([]byte, error) { + o, _ := d.Find("Contents") + + o, err := xRefTable.Dereference(o) + if err != nil || o == nil { + return nil, err + } + + bb := []byte{} + + switch o := o.(type) { + + case types.StreamDict: + // no further processing. + err := o.Decode() + if err == filter.ErrUnsupportedFilter { + return nil, errors.New("pdfcpu: unsupported filter: unable to decode content") + } + if err != nil { + return nil, err + } + + bb = append(bb, o.Content...) + + case types.Array: + // process array of content stream dicts. + for _, o := range o { + if o == nil { + continue + } + o, _, err := xRefTable.DereferenceStreamDict(o) + if err != nil { + return nil, err + } + if o == nil { + continue + } + err = o.Decode() + if err == filter.ErrUnsupportedFilter { + return nil, errors.New("pdfcpu: unsupported filter: unable to decode content") + } + if err != nil { + return nil, err + } + bb = append(bb, o.Content...) + } + + default: + return nil, errors.Errorf("pdfcpu: page content must be stream dict or array") + } + + if len(bb) == 0 { + return nil, ErrNoContent + } + + return bb, nil +} + +func consolidateResourceSubDict(d types.Dict, key string, prn PageResourceNames, pageNr int) error { + o := d[key] + if o == nil { + if prn.HasResources(key) { + return errors.Errorf("pdfcpu: page %d: missing required resource subdict: %s\n%s", pageNr, key, prn) + } + return nil + } + if !prn.HasResources(key) { + d.Delete(key) + return nil + } + d1 := o.(types.Dict) + set := types.StringSet{} + res := prn.Resources(key) + // Iterate over inherited resource sub dict and remove any entries not required. + for k := range d1 { + ki := types.Name(k).Value() + if !res[ki] { + d1.Delete(k) + continue + } + set[ki] = true + } + // Check for missing resource sub dict entries. + for k := range res { + if !set[k] { + return errors.Errorf("pdfcpu: page %d: missing required %s: %s", pageNr, key, k) + } + } + d[key] = d1 + return nil +} + +func consolidateResourceDict(d types.Dict, prn PageResourceNames, pageNr int) error { + for k := range resourceTypes { + if err := consolidateResourceSubDict(d, k, prn, pageNr); err != nil { + return err + } + } + return nil +} + +func (xRefTable *XRefTable) consolidateResourcesWithContent(pageDict, resDict types.Dict, page int, consolidateRes bool) error { + if !consolidateRes { + return nil + } + + bb, err := xRefTable.PageContent(pageDict) + if err != nil { + if err == ErrNoContent { + return nil + } + return err + } + + // Calculate resources required by the content stream of this page. + prn, err := parseContent(string(bb)) + if err != nil { + return err + } + + // Compare required resouces (prn) with available resources (pAttrs.resources). + // Remove any resource that's not required. + // Return an error for any required resource missing. + // TODO Calculate and accumulate resources required by content streams of any present form or type 3 fonts. + return consolidateResourceDict(resDict, prn, page) +} + +func (xRefTable *XRefTable) processPageTreeForPageDict(root *types.IndirectRef, pAttrs *InheritedPageAttrs, p *int, page int, consolidateRes bool) (types.Dict, *types.IndirectRef, error) { + // Walk this page tree all the way down to the leaf node representing page. + + //fmt.Printf("entering processPageTreeForPageDict: p=%d obj#%d\n", *p, root.ObjectNumber.Value()) + + d, err := xRefTable.DereferenceDict(*root) + if err != nil { + return nil, nil, err + } + + pageCount := d.IntEntry("Count") + if pageCount != nil { + if *p+*pageCount < page { + // Skip sub pagetree. + *p += *pageCount + return nil, nil, nil + } + } + + // Return the current state of all page attributes that may be inherited. + if err = xRefTable.checkInheritedPageAttrs(d, pAttrs, consolidateRes); err != nil { + return nil, nil, err + } + + kids := d.ArrayEntry("Kids") + if kids == nil { + return d, root, xRefTable.consolidateResourcesWithContent(d, pAttrs.Resources, page, consolidateRes) + } + + for _, o := range kids { + + if o == nil { + continue + } + + // Dereference next page node dict. + indRef, ok := o.(types.IndirectRef) + if !ok { + return nil, nil, errors.Errorf("pdfcpu: processPageTreeForPageDict: corrupt page node dict") + } + + pageNodeDict, err := xRefTable.DereferenceDict(indRef) + if err != nil { + return nil, nil, err + } + + switch *pageNodeDict.Type() { + + case "Pages": + // Recurse over sub pagetree. + pageDict, pageDictIndRef, err := xRefTable.processPageTreeForPageDict(&indRef, pAttrs, p, page, consolidateRes) + if err != nil { + return nil, nil, err + } + if pageDict != nil { + return pageDict, pageDictIndRef, nil + } + + case "Page": + *p++ + if *p == page { + return xRefTable.processPageTreeForPageDict(&indRef, pAttrs, p, page, consolidateRes) + } + + } + + } + + return nil, nil, nil +} + +// PageDict returns a specific page dict along with the resources, mediaBox and CropBox in effect. +// consolidateRes ensures optimized resources in InheritedPageAttrs. +func (xRefTable *XRefTable) PageDict(pageNr int, consolidateRes bool) (types.Dict, *types.IndirectRef, *InheritedPageAttrs, error) { + var ( + inhPAttrs InheritedPageAttrs + pageCount int + ) + + if pageNr <= 0 || pageNr > xRefTable.PageCount { + return nil, nil, nil, errors.New("pdfcpu: page not found") + } + + // Get an indirect reference to the page tree root dict. + pageRootDictIndRef, err := xRefTable.Pages() + if err != nil { + return nil, nil, nil, err + } + + // Calculate and return only resources that are really needed by + // any content stream of this page and any possible forms or type 3 fonts referenced. + pageDict, pageDictindRef, err := xRefTable.processPageTreeForPageDict(pageRootDictIndRef, &inhPAttrs, &pageCount, pageNr, consolidateRes) + if err != nil { + return nil, nil, nil, err + } + + return pageDict, pageDictindRef, &inhPAttrs, nil +} + +// PageDictIndRef returns the pageDict IndRef for a logical page number. +func (xRefTable *XRefTable) PageDictIndRef(page int) (*types.IndirectRef, error) { + var ( + inhPAttrs InheritedPageAttrs + pageCount int + ) + + // Get an indirect reference to the page tree root dict. + pageRootDictIndRef, err := xRefTable.Pages() + if err != nil { + return nil, err + } + + // Calculate and return only resources that are really needed by + // any content stream of this page and any possible forms or type 3 fonts referenced. + consolidateRes := false + _, ir, err := xRefTable.processPageTreeForPageDict(pageRootDictIndRef, &inhPAttrs, &pageCount, page, consolidateRes) + + return ir, err +} + +// Calculate logical page number for page dict object number. +func (xRefTable *XRefTable) processPageTreeForPageNumber(root *types.IndirectRef, pageCount *int, pageObjNr int) (int, error) { + //fmt.Printf("entering processPageTreeForPageNumber: p=%d obj#%d\n", *p, root.ObjectNumber.Value()) + + d, err := xRefTable.DereferenceDict(*root) + if err != nil { + return 0, err + } + + // Iterate over page tree. + for _, o := range d.ArrayEntry("Kids") { + + if o == nil { + continue + } + + // Dereference next page node dict. + indRef, ok := o.(types.IndirectRef) + if !ok { + return 0, errors.Errorf("pdfcpu: processPageTreeForPageNumber: corrupt page node dict") + } + + objNr := indRef.ObjectNumber.Value() + + pageNodeDict, err := xRefTable.DereferenceDict(indRef) + if err != nil { + return 0, err + } + + switch *pageNodeDict.Type() { + + case "Pages": + // Recurse over sub pagetree. + pageNr, err := xRefTable.processPageTreeForPageNumber(&indRef, pageCount, pageObjNr) + if err != nil { + return 0, err + } + if pageNr > 0 { + return pageNr, nil + } + + case "Page": + *pageCount++ + if objNr == pageObjNr { + return *pageCount, nil + } + } + + } + + return 0, nil +} + +// PageNumber returns the logical page number for a page dict object number. +func (xRefTable *XRefTable) PageNumber(pageObjNr int) (int, error) { + // Get an indirect reference to the page tree root dict. + pageRootDict, _ := xRefTable.Pages() + pageCount := 0 + return xRefTable.processPageTreeForPageNumber(pageRootDict, &pageCount, pageObjNr) +} + +// EnsurePageCount evaluates the page count for xRefTable if necessary. +func (xRefTable *XRefTable) EnsurePageCount() error { + if xRefTable.PageCount > 0 { + return nil + } + + pageRoot, err := xRefTable.Pages() + if err != nil { + return err + } + + d, err := xRefTable.DereferenceDict(*pageRoot) + if err != nil { + return err + } + + pageCount := d.IntEntry("Count") + if pageCount == nil { + return errors.New("pdfcpu: pageDict: missing \"Count\"") + } + + xRefTable.PageCount = *pageCount + + return nil +} + +func (xRefTable *XRefTable) resolvePageBoundary(d types.Dict, boxName string) (*types.Rectangle, error) { + obj, found := d.Find(boxName) + if !found { + return nil, nil + } + a, err := xRefTable.DereferenceArray(obj) + if err != nil { + return nil, err + } + return rect(xRefTable, a) +} + +func (xRefTable *XRefTable) collectPageBoundariesForPage(d types.Dict, pb []PageBoundaries, inhMediaBox, inhCropBox *types.Rectangle, rot, p int) error { + if inhMediaBox != nil { + pb[p].Media = &Box{Rect: inhMediaBox, Inherited: true} + } + r, err := xRefTable.resolvePageBoundary(d, "MediaBox") + if err != nil { + return err + } + if r != nil { + pb[p].Media = &Box{Rect: r, Inherited: false} + } + if pb[p].Media == nil { + return errors.New("pdfcpu: collectMediaBoxesForPageTree: mediaBox is nil") + } + + if inhCropBox != nil { + pb[p].Crop = &Box{Rect: inhCropBox, Inherited: true} + } + r, err = xRefTable.resolvePageBoundary(d, "CropBox") + if err != nil { + return err + } + if r != nil { + pb[p].Crop = &Box{Rect: r, Inherited: false} + } + + r, err = xRefTable.resolvePageBoundary(d, "TrimBox") + if err != nil { + return err + } + if r != nil { + pb[p].Trim = &Box{Rect: r} + } + + r, err = xRefTable.resolvePageBoundary(d, "BleedBox") + if err != nil { + return err + } + if r != nil { + pb[p].Bleed = &Box{Rect: r} + } + + r, err = xRefTable.resolvePageBoundary(d, "ArtBox") + if err != nil { + return err + } + if r != nil { + pb[p].Art = &Box{Rect: r} + } + + pb[p].Rot = rot + + return nil +} + +func (xRefTable *XRefTable) collectMediaBoxAndCropBox(d types.Dict, inhMediaBox, inhCropBox **types.Rectangle) error { + obj, found := d.Find("MediaBox") + if found { + a, err := xRefTable.DereferenceArray(obj) + if err != nil { + return err + } + if *inhMediaBox, err = rect(xRefTable, a); err != nil { + return err + } + *inhCropBox = nil + } + + obj, found = d.Find("CropBox") + if found { + a, err := xRefTable.DereferenceArray(obj) + if err != nil { + return err + } + if *inhCropBox, err = rect(xRefTable, a); err != nil { + return err + } + } + return nil +} + +func (xRefTable *XRefTable) collectPageBoundariesForPageTreeKids( + kids types.Array, + inhMediaBox, inhCropBox **types.Rectangle, + pb []PageBoundaries, + r int, + p *int, + selectedPages types.IntSet) error { + + // Iterate over page tree. + for _, o := range kids { + + if o == nil { + continue + } + + // Dereference next page node dict. + indRef, ok := o.(types.IndirectRef) + if !ok { + return errors.Errorf("pdfcpu: collectPageBoundariesForPageTreeKids: corrupt page node dict") + } + + pageNodeDict, err := xRefTable.DereferenceDict(indRef) + if err != nil { + return err + } + + switch *pageNodeDict.Type() { + + case "Pages": + if err = xRefTable.collectPageBoundariesForPageTree(&indRef, inhMediaBox, inhCropBox, pb, r, p, selectedPages); err != nil { + return err + } + + case "Page": + collect := len(selectedPages) == 0 + if !collect { + _, collect = selectedPages[(*p)+1] + } + if collect { + if err = xRefTable.collectPageBoundariesForPageTree(&indRef, inhMediaBox, inhCropBox, pb, r, p, selectedPages); err != nil { + return err + } + } + *p++ + } + + } + + return nil +} + +func (xRefTable *XRefTable) collectPageBoundariesForPageTree( + root *types.IndirectRef, + inhMediaBox, inhCropBox **types.Rectangle, + pb []PageBoundaries, + r int, + p *int, + selectedPages types.IntSet) error { + + d, err := xRefTable.DereferenceDict(*root) + if err != nil { + return err + } + + if obj, found := d.Find("Rotate"); found { + i, err := xRefTable.DereferenceInteger(obj) + if err != nil { + return err + } + r = i.Value() + } + + if err := xRefTable.collectMediaBoxAndCropBox(d, inhMediaBox, inhCropBox); err != nil { + return err + } + + o, _ := d.Find("Kids") + o, _ = xRefTable.Dereference(o) + if o == nil { + return xRefTable.collectPageBoundariesForPage(d, pb, *inhMediaBox, *inhCropBox, r, *p) + } + + kids, ok := o.(types.Array) + if !ok { + return errors.New("pdfcpu: validatePagesDict: corrupt \"Kids\" entry") + } + + return xRefTable.collectPageBoundariesForPageTreeKids(kids, inhMediaBox, inhCropBox, pb, r, p, selectedPages) +} + +// PageBoundaries returns a sorted slice with page boundaries +// for all pages sorted ascending by page number. +func (xRefTable *XRefTable) PageBoundaries(selectedPages types.IntSet) ([]PageBoundaries, error) { + // if err := xRefTable.EnsurePageCount(); err != nil { + // return nil, err + // } + + // Get an indirect reference to the page tree root dict. + root, err := xRefTable.Pages() + if err != nil { + return nil, err + } + + i := 0 + mb := &types.Rectangle{} + cb := &types.Rectangle{} + pbs := make([]PageBoundaries, xRefTable.PageCount) + if err := xRefTable.collectPageBoundariesForPageTree(root, &mb, &cb, pbs, 0, &i, selectedPages); err != nil { + return nil, err + } + return pbs, nil +} + +// PageDims returns a sorted slice with effective media box dimensions +// for all pages sorted ascending by page number. +func (xRefTable *XRefTable) PageDims() ([]types.Dim, error) { + pbs, err := xRefTable.PageBoundaries(nil) + if err != nil { + return nil, err + } + + dims := make([]types.Dim, len(pbs)) + for i, pb := range pbs { + d := pb.MediaBox().Dimensions() + if pb.Rot%180 != 0 { + d.Width, d.Height = d.Height, d.Width + } + dims[i] = d + } + + return dims, nil +} + +func (xRefTable *XRefTable) EmptyPage(parentIndRef *types.IndirectRef, mediaBox *types.Rectangle) (*types.IndirectRef, error) { + sd, _ := xRefTable.NewStreamDictForBuf(nil) + + if err := sd.Encode(); err != nil { + return nil, err + } + + contentsIndRef, err := xRefTable.IndRefForNewObject(*sd) + if err != nil { + return nil, err + } + + pageDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("Page"), + "Parent": *parentIndRef, + "Resources": types.NewDict(), + "MediaBox": mediaBox.Array(), + "Contents": *contentsIndRef, + }, + ) + + return xRefTable.IndRefForNewObject(pageDict) +} + +func (xRefTable *XRefTable) pageMediaBox(d types.Dict) (*types.Rectangle, error) { + o, found := d.Find("MediaBox") + if !found { + return nil, errors.Errorf("pdfcpu: pageMediaBox: missing mediaBox") + } + + a, err := xRefTable.DereferenceArray(o) + if err != nil { + return nil, err + } + + return rect(xRefTable, a) +} + +func (xRefTable *XRefTable) emptyPage(parent *types.IndirectRef, d types.Dict, dim *types.Dim, pAttrs *InheritedPageAttrs) (*types.IndirectRef, error) { + if dim != nil { + return xRefTable.EmptyPage(parent, types.RectForDim(dim.Width, dim.Height)) + } + + mediaBox, err := pAttrs.MediaBox, error(nil) + if mediaBox == nil { + mediaBox, err = xRefTable.pageMediaBox(d) + if err != nil { + return nil, err + } + } + + // TODO cache empty page + return xRefTable.EmptyPage(parent, mediaBox) +} + +func (xRefTable *XRefTable) insertBlankPages( + parent *types.IndirectRef, + pAttrs *InheritedPageAttrs, + p *int, selectedPages types.IntSet, + dim *types.Dim, + before bool) (int, error) { + + d, err := xRefTable.DereferenceDict(*parent) + if err != nil { + return 0, err + } + + consolidateRes := false + if err = xRefTable.checkInheritedPageAttrs(d, pAttrs, consolidateRes); err != nil { + return 0, err + } + + kids := d.ArrayEntry("Kids") + if kids == nil { + return 0, nil + } + + i := 0 + a := types.Array{} + + for _, o := range kids { + + if o == nil { + continue + } + + // Dereference next page node dict. + ir, ok := o.(types.IndirectRef) + if !ok { + return 0, errors.Errorf("pdfcpu: insertBlankPages: corrupt page node dict") + } + + pageNodeDict, err := xRefTable.DereferenceDict(ir) + if err != nil { + return 0, err + } + + switch *pageNodeDict.Type() { + + case "Pages": + // Recurse over sub pagetree. + j, err := xRefTable.insertBlankPages(&ir, pAttrs, p, selectedPages, dim, before) + if err != nil { + return 0, err + } + a = append(a, ir) + i += j + + case "Page": + *p++ + if !before { + a = append(a, ir) + i++ + } + if selectedPages[*p] { + // Insert empty page. + indRef, err := xRefTable.emptyPage(parent, pageNodeDict, dim, pAttrs) + if err != nil { + return 0, err + } + a = append(a, *indRef) + i++ + xRefTable.SetValid(*indRef) + } + if before { + a = append(a, ir) + i++ + } + } + + } + + d.Update("Kids", a) + d.Update("Count", types.Integer(i)) + + return i, nil +} + +// InsertBlankPages inserts a blank page before or after each selected page. +func (xRefTable *XRefTable) InsertBlankPages(pages types.IntSet, dim *types.Dim, before bool) error { + root, err := xRefTable.Pages() + if err != nil { + return err + } + + var inhPAttrs InheritedPageAttrs + p := 0 + + _, err = xRefTable.insertBlankPages(root, &inhPAttrs, &p, pages, dim, before) + + return err +} + +// Zip in ctx's pages: for each page weave in the corresponding ctx page as long as there is one. +func (xRefTable *XRefTable) InsertPages(parent *types.IndirectRef, p *int, ctx *Context) (int, error) { + d, err := xRefTable.DereferenceDict(*parent) + if err != nil { + return 0, err + } + + kids := d.ArrayEntry("Kids") + if kids == nil { + return 0, nil + } + + i := 0 + a := types.Array{} + + for _, o := range kids { + + if o == nil { + continue + } + + // Dereference next page node dict. + ir, ok := o.(types.IndirectRef) + if !ok { + return 0, errors.Errorf("pdfcpu: InsertPagesIntoPageTree: corrupt page node dict") + } + + pageNodeDict, err := xRefTable.DereferenceDict(ir) + if err != nil { + return 0, err + } + + switch *pageNodeDict.Type() { + + case "Pages": + // Recurse over sub pagetree. + j, err := xRefTable.InsertPages(&ir, p, ctx) + if err != nil { + return 0, err + } + a = append(a, ir) + i += j + + case "Page": + *p++ + a = append(a, ir) + i++ + if *p <= ctx.PageCount { + // append indRef for ctx page i after this page + d1, indRef1, inhPAttrs, err := ctx.PageDict(*p, false) + if err != nil { + return 0, err + } + d1["Parent"] = *parent + if _, found := d1["Rotate"]; !found { + d1["Rotate"] = types.Integer(inhPAttrs.Rotate) + } + if _, found := d1["MediaBox"]; !found { + d1["MediaBox"] = inhPAttrs.MediaBox.Array() + } + a = append(a, *indRef1) + i++ + } + + } + + } + + d.Update("Kids", a) + d.Update("Count", types.Integer(i)) + + return i, nil +} + +func (xRefTable *XRefTable) AppendPages(rootPageIndRef *types.IndirectRef, fromPageNr int, ctx *Context) (int, error) { + // Create an intermediary page node containing kids array with indRefs For all ctx Pages fromPageNr - end + + rootPageDict, err := xRefTable.DereferenceDict(*rootPageIndRef) + if err != nil { + return 0, err + } + + // Ensure page root with pages. + d := types.NewDict() + d.InsertName("Type", "Pages") + + indRef, err := xRefTable.IndRefForNewObject(d) + if err != nil { + return 0, err + } + + rootPageDict["Parent"] = *indRef + + kids := types.Array{*rootPageIndRef} + + count := ctx.PageCount - fromPageNr + 1 + + d1 := types.Dict( + map[string]types.Object{ + "Type": types.Name("Pages"), + "Parent": *indRef, + "Count": types.Integer(count), + }, + ) + + indRef1, err := xRefTable.IndRefForNewObject(d1) + if err != nil { + return 0, err + } + + kids1 := types.Array{} + + for i := fromPageNr; i <= ctx.PageCount; i++ { + d, indRef2, inhPAttrs, err := ctx.PageDict(i, false) + if err != nil { + return 0, err + } + d["Parent"] = *indRef1 + if _, found := d["Rotate"]; !found { + d["Rotate"] = types.Integer(inhPAttrs.Rotate) + } + if _, found := d["MediaBox"]; !found { + d["MediaBox"] = inhPAttrs.MediaBox.Array() + } + kids1 = append(kids1, *indRef2) + } + d1["Kids"] = kids1 + + d["Kids"] = append(kids, *indRef1) + + pageCount := *rootPageDict.IntEntry("Count") + count + d["Count"] = types.Integer(pageCount) + + rootDict, err := xRefTable.Catalog() + if err != nil { + return 0, err + } + + rootDict["Pages"] = *indRef + + return pageCount, nil +} + +// StreamDictIndRef creates a new stream dict for bb. +func (xRefTable *XRefTable) StreamDictIndRef(bb []byte) (*types.IndirectRef, error) { + sd, _ := xRefTable.NewStreamDictForBuf(bb) + if err := sd.Encode(); err != nil { + return nil, err + } + return xRefTable.IndRefForNewObject(*sd) +} + +func (xRefTable *XRefTable) insertContent(pageDict types.Dict, bb []byte) error { + sd, _ := xRefTable.NewStreamDictForBuf(bb) + if err := sd.Encode(); err != nil { + return err + } + + indRef, err := xRefTable.IndRefForNewObject(*sd) + if err != nil { + return err + } + + pageDict.Insert("Contents", *indRef) + + return nil +} + +func appendToContentStream(sd *types.StreamDict, bb []byte) error { + err := sd.Decode() + if err == filter.ErrUnsupportedFilter { + if log.InfoEnabled() { + log.Info.Println("unsupported filter: unable to patch content with watermark.") + } + return nil + } + if err != nil { + return err + } + + sd.Content = append(sd.Content, ' ') + sd.Content = append(sd.Content, bb...) + return sd.Encode() +} + +// AppendContent appends bb to pageDict's content stream. +func (xRefTable *XRefTable) AppendContent(pageDict types.Dict, bb []byte) error { + obj, found := pageDict.Find("Contents") + if !found { + return xRefTable.insertContent(pageDict, bb) + } + + var entry *XRefTableEntry + var objNr int + + indRef, ok := obj.(types.IndirectRef) + if ok { + objNr = indRef.ObjectNumber.Value() + genNr := indRef.GenerationNumber.Value() + entry, _ = xRefTable.FindTableEntry(objNr, genNr) + obj = entry.Object + } + + switch o := obj.(type) { + + case types.StreamDict: + + if err := appendToContentStream(&o, bb); err != nil { + return err + } + entry.Object = o + + case types.Array: + + // Get stream dict for last array element. + o1 := o[len(o)-1] + indRef, _ = o1.(types.IndirectRef) + objNr = indRef.ObjectNumber.Value() + genNr := indRef.GenerationNumber.Value() + entry, _ = xRefTable.FindTableEntry(objNr, genNr) + sd, _ := (entry.Object).(types.StreamDict) + + if err := appendToContentStream(&sd, bb); err != nil { + return err + } + entry.Object = o + + default: + return errors.Errorf("pdfcpu: corrupt page \"Content\"") + + } + + return nil +} + +func (xRefTable *XRefTable) HasUsedGIDs(fontName string) bool { + usedGIDs, ok := xRefTable.UsedGIDs[fontName] + return ok && len(usedGIDs) > 0 +} + +func (xRefTable *XRefTable) NameRef(nameType string) NameMap { + nm, ok := xRefTable.NameRefs[nameType] + if !ok { + nm = NameMap{} + xRefTable.NameRefs[nameType] = nm + return nm + } + return nm +} + +func (xRefTable *XRefTable) RemoveSignature() { + if xRefTable.SignatureExist || xRefTable.AppendOnly { + // TODO enable incremental writing + if log.CLIEnabled() { + log.CLI.Println("removing signature...") + } + // root -> Perms -> UR3 -> = Sig dict + d1 := xRefTable.RootDict + delete(d1, "Perms") + d2 := xRefTable.Form + delete(d2, "SigFlags") + delete(d2, "XFA") + if xRefTable.Version() == V20 { + // deprecated in PDF 2.0 + delete(d2, "NeedAppearances") + } + d1["AcroForm"] = d2 + delete(d1, "Extensions") + } +} + +func (xRefTable *XRefTable) BindPrinterPreferences(vp *ViewerPreferences, d types.Dict) { + if vp.PrintArea != nil { + d.InsertName("PrintArea", vp.PrintArea.String()) + } + if vp.PrintClip != nil { + d.InsertName("PrintClip", vp.PrintClip.String()) + } + if vp.PrintScaling != nil { + d.InsertName("PrintScaling", vp.PrintScaling.String()) + } + if vp.Duplex != nil { + d.InsertName("Duplex", vp.Duplex.String()) + } + if vp.PickTrayByPDFSize != nil { + d.InsertBool("PickTrayByPDFSize", *vp.PickTrayByPDFSize) + } + if len(vp.PrintPageRange) > 0 { + d.Insert("PrintPageRange", vp.PrintPageRange) + } + if vp.NumCopies != nil { + d.Insert("NumCopies", *vp.NumCopies) + } + if len(vp.Enforce) > 0 { + d.Insert("Enforce", vp.Enforce) + } +} + +func (xRefTable *XRefTable) BindViewerPreferences() { + vp := xRefTable.ViewerPref + d := types.NewDict() + + if vp.HideToolbar != nil { + d.InsertBool("HideToolbar", *vp.HideToolbar) + } + if vp.HideMenubar != nil { + d.InsertBool("HideMenubar", *vp.HideMenubar) + } + if vp.HideWindowUI != nil { + d.InsertBool("HideWindowUI", *vp.HideWindowUI) + } + if vp.FitWindow != nil { + d.InsertBool("FitWindow", *vp.FitWindow) + } + if vp.CenterWindow != nil { + d.InsertBool("CenterWindow", *vp.CenterWindow) + } + if vp.DisplayDocTitle != nil { + d.InsertBool("DisplayDocTitle", *vp.DisplayDocTitle) + } + if vp.NonFullScreenPageMode != nil { + pm := PageMode(*vp.NonFullScreenPageMode) + d.InsertName("NonFullScreenPageMode", pm.String()) + } + if vp.Direction != nil { + d.InsertName("Direction", vp.Direction.String()) + } + if vp.ViewArea != nil { + d.InsertName("ViewArea", vp.ViewArea.String()) + } + if vp.ViewClip != nil { + d.InsertName("ViewClip", vp.ViewClip.String()) + } + + xRefTable.BindPrinterPreferences(vp, d) + + xRefTable.RootDict["ViewerPreferences"] = d +} + +// RectForArray returns a new rectangle for given Array. +func (xRefTable *XRefTable) RectForArray(a types.Array) (*types.Rectangle, error) { + llx, err := xRefTable.DereferenceNumber(a[0]) + if err != nil { + return nil, err + } + + lly, err := xRefTable.DereferenceNumber(a[1]) + if err != nil { + return nil, err + } + + urx, err := xRefTable.DereferenceNumber(a[2]) + if err != nil { + return nil, err + } + + ury, err := xRefTable.DereferenceNumber(a[3]) + if err != nil { + return nil, err + } + + return types.NewRectangle(llx, lly, urx, ury), nil +} diff --git a/pkg/pdfcpu/model/zoom.go b/pkg/pdfcpu/model/zoom.go new file mode 100644 index 0000000000000000000000000000000000000000..d2b72b2c52970bb82ac8604b79194132d14b238d --- /dev/null +++ b/pkg/pdfcpu/model/zoom.go @@ -0,0 +1,147 @@ +/* +Copyright 2024 The pdfcpu Authors. + +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 + + 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 model + +import ( + "strconv" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +type Zoom struct { + Factor float64 // zoom factor x > 0, x > 1 zooms in, x < 1 zooms out + HMargin float64 // horizontal margin implying some (usually negative) scale factor + VMargin float64 // vertical margin implying some (usually negative) scale factor + Unit types.DisplayUnit // display unit + Border bool // border around page content when zooming out + BgColor *color.SimpleColor // background color when zooming out +} + +func (z *Zoom) EnsureFactorAndMargins(w, h float64) error { + if z.Factor > 0 { + z.HMargin = (w - (w * z.Factor)) / 2 + z.VMargin = (h - (h * z.Factor)) / 2 + return nil + } + if z.HMargin > 0 { + z.Factor = (w - 2*z.HMargin) / w + z.VMargin = (h - (h * z.Factor)) / 2 + } + z.Factor = (h - 2*z.VMargin) / h + z.HMargin = (w - (w * z.Factor)) / 2 + + return nil +} + +func parseHMargin(s string, zoom *Zoom) error { + m, err := strconv.ParseFloat(s, 64) + if err != nil || m == 0 { + return errors.Errorf("pdfcpu: \"hmargin\" must be a numeric value and must not be 0, got %s\n", s) + } + + if zoom.VMargin != 0 { + return errors.New("pdfcpu: only one of \"hmargin\" and \"vmargin\" allowed") + } + + zoom.HMargin = types.ToUserSpace(m, zoom.Unit) + return nil +} + +func parseVMargin(s string, zoom *Zoom) error { + m, err := strconv.ParseFloat(s, 64) + if err != nil || m == 0 { + return errors.Errorf("pdfcpu: \"vmargin\" must be a numeric value and must not be 0, got %s\n", s) + } + + if zoom.HMargin != 0 { + return errors.New("pdfcpu: only one of \"hmargin\" and \"vmargin\" allowed") + } + + zoom.VMargin = types.ToUserSpace(m, zoom.Unit) + return nil +} + +func parseZoomFactor(s string, zoom *Zoom) (err error) { + zf, err := strconv.ParseFloat(s, 64) + if err != nil { + return errors.Errorf("pdfcpu: zoom factor must be a float value: %s\n", s) + } + + if zf <= 0 || zf == 1 { + return errors.Errorf("pdfcpu: invalid zoom factor %.2f: 0.0 < i < 1.0 or i > 1.0\n", zf) + } + + zoom.Factor = zf + return err +} + +func parseBackgroundColorZoom(s string, zoom *Zoom) error { + c, err := color.ParseColor(s) + if err != nil { + return err + } + zoom.BgColor = &c + return nil +} + +func parseBorderZoom(s string, zoom *Zoom) error { + switch strings.ToLower(s) { + case "on", "true", "t": + zoom.Border = true + case "off", "false", "f": + zoom.Border = false + default: + return errors.New("pdfcpu: zoom border, please provide one of: on/off true/false t/f") + } + + return nil +} + +type zoomParameterMap map[string]func(string, *Zoom) error + +var ZoomParamMap = zoomParameterMap{ + "factor": parseZoomFactor, + "hmargin": parseHMargin, + "vmargin": parseVMargin, + "bgcolor": parseBackgroundColorZoom, + "border": parseBorderZoom, +} + +// Handle applies parameter completion and on success parse parameter values into zoom. +func (m zoomParameterMap) Handle(paramPrefix, paramValueStr string, zoom *Zoom) error { + var param string + + // Completion support + for k := range m { + if !strings.HasPrefix(k, strings.ToLower(paramPrefix)) { + continue + } + if len(param) > 0 { + return errors.Errorf("pdfcpu: ambiguous parameter prefix \"%s\"", paramPrefix) + } + param = k + } + + if param == "" { + return errors.Errorf("pdfcpu: unknown parameter prefix \"%s\"", paramPrefix) + } + + return m[param](paramValueStr, zoom) +} diff --git a/pkg/pdfcpu/nup.go b/pkg/pdfcpu/nup.go new file mode 100644 index 0000000000000000000000000000000000000000..affe2c747612910783e7da3e36e320252d484dd9 --- /dev/null +++ b/pkg/pdfcpu/nup.go @@ -0,0 +1,833 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 pdfcpu + +import ( + "bytes" + "fmt" + "io" + "os" + "sort" + "strconv" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/filter" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw" + pdffont "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/font" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +var ( + errInvalidGridDims = errors.New("pdfcpu grid: dimensions must be: m > 0, n > 0") + errInvalidNUpConfig = errors.New("pdfcpu: invalid configuration string") +) + +var ( + NUpValues = []int{2, 3, 4, 6, 8, 9, 12, 16} + nUpDims = map[int]types.Dim{ + 2: {Width: 2, Height: 1}, + 3: {Width: 3, Height: 1}, + 4: {Width: 2, Height: 2}, + 6: {Width: 3, Height: 2}, + 8: {Width: 4, Height: 2}, + 9: {Width: 3, Height: 3}, + 12: {Width: 4, Height: 3}, + 16: {Width: 4, Height: 4}, + } +) + +type nUpParamMap map[string]func(string, *model.NUp) error + +var nupParamMap = nUpParamMap{ + "dimensions": parseDimensionsNUp, + "formsize": parsePageFormatNUp, + "papersize": parsePageFormatNUp, + "orientation": parseOrientation, + "border": parseElementBorder, + "cropboxborder": parseElementBorderOnCropbox, + "margin": parseElementMargin, + "backgroundcolor": parseSheetBackgroundColor, + "bgcolor": parseSheetBackgroundColor, + "guides": parseBookletGuides, + "multifolio": parseBookletMultifolio, + "foliosize": parseBookletFolioSize, + "btype": parseBookletType, + "binding": parseBookletBinding, + "enforce": parseEnforce, +} + +// Handle applies parameter completion and if successful +// parses the parameter values into import. +func (m nUpParamMap) Handle(paramPrefix, paramValueStr string, nup *model.NUp) error { + var param string + + // Completion support + for k := range m { + if !strings.HasPrefix(k, strings.ToLower(paramPrefix)) { + continue + } + if len(param) > 0 { + return errors.Errorf("pdfcpu: ambiguous parameter prefix \"%s\"", paramPrefix) + } + param = k + } + + if param == "" { + return errors.Errorf("pdfcpu: ambiguous parameter prefix \"%s\"", paramPrefix) + } + + return m[param](paramValueStr, nup) +} + +func parsePageFormatNUp(s string, nup *model.NUp) (err error) { + if nup.UserDim { + return errors.New("pdfcpu: only one of formsize(papersize) or dimensions allowed") + } + nup.PageDim, nup.PageSize, err = types.ParsePageFormat(s) + nup.UserDim = true + return err +} + +func parseDimensionsNUp(s string, nup *model.NUp) (err error) { + if nup.UserDim { + return errors.New("pdfcpu: only one of formsize(papersize) or dimensions allowed") + } + nup.PageDim, nup.PageSize, err = ParsePageDim(s, nup.InpUnit) + nup.UserDim = true + + return err +} + +func parseOrientation(s string, nup *model.NUp) error { + switch s { + case "rd": + nup.Orient = model.RightDown + case "dr": + nup.Orient = model.DownRight + case "ld": + nup.Orient = model.LeftDown + case "dl": + nup.Orient = model.DownLeft + default: + return errors.Errorf("pdfcpu: unknown nUp orientation: %s", s) + } + + return nil +} + +func parseEnforce(s string, nup *model.NUp) error { + switch strings.ToLower(s) { + case "on", "true", "t": + nup.Enforce = true + case "off", "false", "f": + nup.Enforce = false + default: + return errors.New("pdfcpu: enforce best-fit orientation of content, please provide one of: on/off true/false") + } + + return nil +} + +func parseElementBorder(s string, nup *model.NUp) error { + switch strings.ToLower(s) { + case "on", "true", "t": + nup.Border = true + case "off", "false", "f": + nup.Border = false + default: + return errors.New("pdfcpu: nUp border, please provide one of: on/off true/false t/f") + } + + return nil +} + +func parseElementBorderOnCropbox(s string, nup *model.NUp) error { + // w + // w r g b + // w #c + // w round + // w round r g b + // w round #c + + var err error + + b := strings.Split(s, " ") + if len(b) == 0 || len(b) > 5 { + return errors.Errorf("pdfcpu: borders: need 1,2,3,4 or 5 int values, %s\n", s) + } + + switch b[0] { + case "off", "false", "f": + return nil + case "on", "true", "t": + nup.BorderOnCropbox = &model.BorderStyling{Width: 1} + return nil + } + + nup.BorderOnCropbox = &model.BorderStyling{} + width, err := strconv.ParseFloat(b[0], 64) + if err != nil { + return err + } + if width == 0 { + return errors.New("pdfcpu: borders: need width > 0") + } + nup.BorderOnCropbox.Width = width + + if len(b) == 1 { + return nil + } + if strings.HasPrefix("round", b[1]) { + style := types.LJRound + nup.BorderOnCropbox.LineStyle = &style + if len(b) == 2 { + return nil + } + c, err := color.ParseColor(strings.Join(b[2:], " ")) + nup.BorderOnCropbox.Color = &c + return err + } + + c, err := color.ParseColor(strings.Join(b[1:], " ")) + nup.BorderOnCropbox.Color = &c + return err +} + +func parseBookletGuides(s string, nup *model.NUp) error { + switch strings.ToLower(s) { + case "on", "true", "t": + nup.BookletGuides = true + case "off", "false", "f": + nup.BookletGuides = false + default: + return errors.New("pdfcpu: booklet guides, please provide one of: on/off true/false t/f") + } + + return nil +} + +func parseBookletMultifolio(s string, nup *model.NUp) error { + switch strings.ToLower(s) { + case "on", "true", "t": + nup.MultiFolio = true + case "off", "false", "f": + nup.MultiFolio = false + default: + return errors.New("pdfcpu: booklet guides, please provide one of: on/off true/false t/f") + } + + return nil +} + +func parseBookletFolioSize(s string, nup *model.NUp) error { + i, err := strconv.Atoi(s) + if err != nil { + return errors.Errorf("pdfcpu: illegal folio size: must be an numeric value, %s\n", s) + } + + nup.FolioSize = i + return nil +} + +func parseBookletType(s string, nup *model.NUp) error { + switch strings.ToLower(s) { + case "booklet": + nup.BookletType = model.Booklet + case "bookletadvanced": + nup.BookletType = model.BookletAdvanced + case "perfectbound": + nup.BookletType = model.BookletPerfectBound + default: + return errors.New("pdfcpu: booklet type, please provide one of: booklet perfectbound") + } + return nil +} + +func parseBookletBinding(s string, nup *model.NUp) error { + switch strings.ToLower(s) { + case "short": + nup.BookletBinding = model.ShortEdge + case "long": + nup.BookletBinding = model.LongEdge + default: + return errors.New("pdfcpu: booklet binding, please provide one of: short long") + } + return nil +} + +func parseElementMargin(s string, nup *model.NUp) error { + f, err := strconv.ParseFloat(s, 64) + if err != nil { + return err + } + + if f < 0 { + return errors.New("pdfcpu: nUp margin, Please provide a positive value") + } + + nup.Margin = types.ToUserSpace(f, nup.InpUnit) + + return nil +} + +func parseSheetBackgroundColor(s string, nup *model.NUp) error { + c, err := color.ParseColor(s) + if err != nil { + return err + } + nup.BgColor = &c + return nil +} + +// ParseNUpDetails parses a NUp command string into an internal structure. +func ParseNUpDetails(s string, nup *model.NUp) error { + if s == "" { + return errInvalidNUpConfig + } + + ss := strings.Split(s, ",") + + for _, s := range ss { + + ss1 := strings.Split(s, ":") + if len(ss1) != 2 { + return errInvalidNUpConfig + } + + paramPrefix := strings.TrimSpace(ss1[0]) + paramValueStr := strings.TrimSpace(ss1[1]) + + if err := nupParamMap.Handle(paramPrefix, paramValueStr, nup); err != nil { + return err + } + } + + return nil +} + +// PDFNUpConfig returns an NUp configuration for Nup-ing PDF files. +func PDFNUpConfig(val int, desc string, conf *model.Configuration) (*model.NUp, error) { + nup := model.DefaultNUpConfig() + if conf == nil { + conf = model.NewDefaultConfiguration() + } + nup.InpUnit = conf.Unit + if desc != "" { + if err := ParseNUpDetails(desc, nup); err != nil { + return nil, err + } + } + if !types.IntMemberOf(val, NUpValues) { + ss := make([]string, len(NUpValues)) + for i, v := range NUpValues { + ss[i] = strconv.Itoa(v) + } + return nil, errors.Errorf("pdfcpu: n must be one of %s", strings.Join(ss, ", ")) + } + return nup, ParseNUpValue(val, nup) +} + +// ImageNUpConfig returns an NUp configuration for Nup-ing image files. +func ImageNUpConfig(val int, desc string, conf *model.Configuration) (*model.NUp, error) { + nup, err := PDFNUpConfig(val, desc, conf) + if err != nil { + return nil, err + } + nup.ImgInputFile = true + return nup, nil +} + +// PDFGridConfig returns a grid configuration for Nup-ing PDF files. +func PDFGridConfig(rows, cols int, desc string, conf *model.Configuration) (*model.NUp, error) { + nup := model.DefaultNUpConfig() + if conf == nil { + conf = model.NewDefaultConfiguration() + } + nup.InpUnit = conf.Unit + nup.PageGrid = true + if desc != "" { + if err := ParseNUpDetails(desc, nup); err != nil { + return nil, err + } + } + return nup, ParseNUpGridDefinition(rows, cols, nup) +} + +// ImageGridConfig returns a grid configuration for Nup-ing image files. +func ImageGridConfig(rows, cols int, desc string, conf *model.Configuration) (*model.NUp, error) { + nup, err := PDFGridConfig(rows, cols, desc, conf) + if err != nil { + return nil, err + } + nup.ImgInputFile = true + return nup, nil +} + +// ParseNUpValue parses the NUp value into an internal structure. +func ParseNUpValue(n int, nUp *model.NUp) error { + // The n-Up layout depends on the orientation of the chosen output paper size. + // This optional paper size may also be specified by dimensions in user unit. + // The default paper size is A4 or A4P (A4 in portrait mode) respectively. + var portrait bool + if nUp.PageDim == nil { + portrait = types.PaperSize[nUp.PageSize].Portrait() + } else { + portrait = types.RectForDim(nUp.PageDim.Width, nUp.PageDim.Height).Portrait() + } + + d := nUpDims[n] + if portrait { + d.Width, d.Height = d.Height, d.Width + } + + nUp.Grid = &d + + return nil +} + +// ParseNUpGridDefinition parses NUp grid dimensions into an internal structure. +func ParseNUpGridDefinition(rows, cols int, nUp *model.NUp) error { + m := cols + if m <= 0 { + return errInvalidGridDims + } + + n := rows + if n <= 0 { + return errInvalidGridDims + } + + nUp.Grid = &types.Dim{Width: float64(m), Height: float64(n)} + + return nil +} + +func nUpImagePDFBytes(w io.Writer, imgWidth, imgHeight int, nup *model.NUp, formResID string) { + for _, r := range nup.RectsForGrid() { + // Append to content stream. + model.NUpTilePDFBytes(w, types.RectForDim(float64(imgWidth), float64(imgHeight)), r, formResID, nup, false) + } +} + +func createNUpFormForImage(xRefTable *model.XRefTable, imgIndRef *types.IndirectRef, w, h, i int) (*types.IndirectRef, error) { + imgResID := fmt.Sprintf("Im%d", i) + bb := types.RectForDim(float64(w), float64(h)) + + var b bytes.Buffer + fmt.Fprintf(&b, "/%s Do ", imgResID) + + d := types.Dict( + map[string]types.Object{ + "ProcSet": types.NewNameArray("PDF", "Text", "ImageB", "ImageC", "ImageI"), + "XObject": types.Dict(map[string]types.Object{imgResID: *imgIndRef}), + }, + ) + + ir, err := xRefTable.IndRefForNewObject(d) + if err != nil { + return nil, err + } + + sd := types.StreamDict{ + Dict: types.Dict( + map[string]types.Object{ + "Type": types.Name("XObject"), + "Subtype": types.Name("Form"), + "BBox": bb.Array(), + "Matrix": types.NewIntegerArray(1, 0, 0, 1, 0, 0), + "Resources": *ir, + }, + ), + Content: b.Bytes(), + FilterPipeline: []types.PDFFilter{{Name: filter.Flate, DecodeParms: nil}}, + } + + sd.InsertName("Filter", filter.Flate) + + if err = sd.Encode(); err != nil { + return nil, err + } + + return xRefTable.IndRefForNewObject(sd) +} + +// NewNUpPageForImage creates a new page dict in xRefTable for given image filename and n-up conf. +func NewNUpPageForImage(xRefTable *model.XRefTable, fileName string, parentIndRef *types.IndirectRef, nup *model.NUp) (*types.IndirectRef, error) { + f, err := os.Open(fileName) + if err != nil { + return nil, err + } + defer f.Close() + + // create image dict. + imgIndRef, w, h, err := model.CreateImageResource(xRefTable, f, false, false) + if err != nil { + return nil, err + } + + resID := 0 + + formIndRef, err := createNUpFormForImage(xRefTable, imgIndRef, w, h, resID) + if err != nil { + return nil, err + } + + formResID := fmt.Sprintf("Fm%d", resID) + + resourceDict := types.Dict( + map[string]types.Object{ + "XObject": types.Dict(map[string]types.Object{formResID: *formIndRef}), + }, + ) + + resIndRef, err := xRefTable.IndRefForNewObject(resourceDict) + if err != nil { + return nil, err + } + + var buf bytes.Buffer + nUpImagePDFBytes(&buf, w, h, nup, formResID) + sd, _ := xRefTable.NewStreamDictForBuf(buf.Bytes()) + if err = sd.Encode(); err != nil { + return nil, err + } + + contentsIndRef, err := xRefTable.IndRefForNewObject(*sd) + if err != nil { + return nil, err + } + + dim := nup.PageDim + mediaBox := types.RectForDim(dim.Width, dim.Height) + + pageDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("Page"), + "Parent": *parentIndRef, + "MediaBox": mediaBox.Array(), + "Resources": *resIndRef, + "Contents": *contentsIndRef, + }, + ) + + return xRefTable.IndRefForNewObject(pageDict) +} + +// NUpFromOneImage creates one page with instances of one image. +func NUpFromOneImage(ctx *model.Context, fileName string, nup *model.NUp, pagesDict types.Dict, pagesIndRef *types.IndirectRef) error { + indRef, err := NewNUpPageForImage(ctx.XRefTable, fileName, pagesIndRef, nup) + if err != nil { + return err + } + + if err := ctx.SetValid(*indRef); err != nil { + return err + } + + if err = model.AppendPageTree(indRef, 1, pagesDict); err != nil { + return err + } + + ctx.PageCount++ + + return nil +} + +func wrapUpPage(ctx *model.Context, nup *model.NUp, d types.Dict, buf bytes.Buffer, pagesDict types.Dict, pagesIndRef *types.IndirectRef) error { + xRefTable := ctx.XRefTable + + var fm model.FontMap + if nup.BookletGuides { + // For booklets only. + fm = model.DrawBookletGuides(nup, &buf) + } + + resourceDict := types.Dict( + map[string]types.Object{ + "XObject": d, + }, + ) + + fontRes, err := pdffont.FontResources(xRefTable, fm) + if err != nil { + return err + } + + if len(fontRes) > 0 { + resourceDict["Font"] = fontRes + } + + resIndRef, err := xRefTable.IndRefForNewObject(resourceDict) + if err != nil { + return err + } + + sd, _ := xRefTable.NewStreamDictForBuf(buf.Bytes()) + if err = sd.Encode(); err != nil { + return err + } + + contentsIndRef, err := xRefTable.IndRefForNewObject(*sd) + if err != nil { + return err + } + + dim := nup.PageDim + mediaBox := types.RectForDim(dim.Width, dim.Height) + + pageDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("Page"), + "Parent": *pagesIndRef, + "MediaBox": mediaBox.Array(), + "Resources": *resIndRef, + "Contents": *contentsIndRef, + }, + ) + + indRef, err := xRefTable.IndRefForNewObject(pageDict) + if err != nil { + return err + } + + if err := ctx.SetValid(*indRef); err != nil { + return err + } + + if err = model.AppendPageTree(indRef, 1, pagesDict); err != nil { + return err + } + + ctx.PageCount++ + + return nil +} + +func nupPageNumber(i int, sortedPageNumbers []int) int { + var pageNumber int + if i < len(sortedPageNumbers) { + pageNumber = sortedPageNumbers[i] + } + return pageNumber +} + +func sortSelectedPages(pages types.IntSet) []int { + var pageNumbers []int + for k, v := range pages { + if v { + pageNumbers = append(pageNumbers, k) + } + } + sort.Ints(pageNumbers) + return pageNumbers +} + +func nupPages( + ctx *model.Context, + selectedPages types.IntSet, + nup *model.NUp, + pagesDict types.Dict, + pagesIndRef *types.IndirectRef) error { + + var buf bytes.Buffer + formsResDict := types.NewDict() + rr := nup.RectsForGrid() + + sortedPageNumbers := sortSelectedPages(selectedPages) + pageCount := len(sortedPageNumbers) + // pageCount must be a multiple of n. + // If not, we will insert blank pages at the end. + if pageCount%nup.N() != 0 { + pageCount += nup.N() - pageCount%nup.N() + } + + for i := 0; i < pageCount; i++ { + + if i > 0 && i%len(rr) == 0 { + // Wrap complete page. + if err := wrapUpPage(ctx, nup, formsResDict, buf, pagesDict, pagesIndRef); err != nil { + return err + } + buf.Reset() + formsResDict = types.NewDict() + } + + rDest := rr[i%len(rr)] + + pageNr := nupPageNumber(i, sortedPageNumbers) + if pageNr == 0 { + // This is an empty page at the end. + if nup.BgColor != nil { + draw.FillRectNoBorder(&buf, rDest, *nup.BgColor) + } + continue + } + + if err := ctx.NUpTilePDFBytesForPDF(pageNr, formsResDict, &buf, rDest, nup, false); err != nil { + return err + } + } + + // Wrap incomplete nUp page. + return wrapUpPage(ctx, nup, formsResDict, buf, pagesDict, pagesIndRef) +} + +// NUpFromMultipleImages creates pages in NUp-style rendering each image once. +func NUpFromMultipleImages(ctx *model.Context, fileNames []string, nup *model.NUp, pagesDict types.Dict, pagesIndRef *types.IndirectRef) error { + if nup.PageGrid { + nup.PageDim.Width *= nup.Grid.Width + nup.PageDim.Height *= nup.Grid.Height + } + + xRefTable := ctx.XRefTable + formsResDict := types.NewDict() + var buf bytes.Buffer + rr := nup.RectsForGrid() + + // fileCount must be a multiple of n. + // If not, we will insert blank pages at the end. + fileCount := len(fileNames) + if fileCount%nup.N() != 0 { + fileCount += nup.N() - fileCount%nup.N() + } + + for i := 0; i < fileCount; i++ { + + if i > 0 && i%len(rr) == 0 { + // Wrap complete nUp page. + if err := wrapUpPage(ctx, nup, formsResDict, buf, pagesDict, pagesIndRef); err != nil { + return err + } + buf.Reset() + formsResDict = types.NewDict() + } + + rDest := rr[i%len(rr)] + + var fileName string + if i < len(fileNames) { + fileName = fileNames[i] + } + + if fileName == "" { + // This is an empty page at the end. + if nup.BgColor != nil { + draw.FillRectNoBorder(&buf, rDest, *nup.BgColor) + } + continue + } + + f, err := os.Open(fileName) + if err != nil { + return err + } + + imgIndRef, w, h, err := model.CreateImageResource(xRefTable, f, false, false) + if err != nil { + return err + } + + if err := f.Close(); err != nil { + return err + } + + formIndRef, err := createNUpFormForImage(xRefTable, imgIndRef, w, h, i) + if err != nil { + return err + } + + formResID := fmt.Sprintf("Fm%d", i) + formsResDict.Insert(formResID, *formIndRef) + + // Append to content stream of page i. + model.NUpTilePDFBytes(&buf, types.RectForDim(float64(w), float64(h)), rr[i%len(rr)], formResID, nup, false) + } + + // Wrap incomplete nUp page. + return wrapUpPage(ctx, nup, formsResDict, buf, pagesDict, pagesIndRef) +} + +// NUpFromPDF creates an n-up version of the PDF represented by xRefTable. +func NUpFromPDF(ctx *model.Context, selectedPages types.IntSet, nup *model.NUp) error { + var mb *types.Rectangle + if nup.PageDim == nil { + // No page dimensions specified, use cropBox of page 1 as mediaBox(=cropBox). + consolidateRes := false + d, _, inhPAttrs, err := ctx.PageDict(1, consolidateRes) + if err != nil { + return err + } + if d == nil { + return errors.Errorf("unknown page number: %d\n", 1) + } + + cropBox := inhPAttrs.MediaBox + if inhPAttrs.CropBox != nil { + cropBox = inhPAttrs.CropBox + } + + // Account for existing rotation. + if inhPAttrs.Rotate != 0 { + if types.IntMemberOf(inhPAttrs.Rotate, []int{+90, -90, +270, -270}) { + w := cropBox.Width() + cropBox.UR.X = cropBox.LL.X + cropBox.Height() + cropBox.UR.Y = cropBox.LL.Y + w + } + } + + mb = cropBox + } else { + mb = types.RectForDim(nup.PageDim.Width, nup.PageDim.Height) + } + + if nup.PageGrid { + mb.UR.X = mb.LL.X + float64(nup.Grid.Width)*mb.Width() + mb.UR.Y = mb.LL.Y + float64(nup.Grid.Height)*mb.Height() + } + + pagesDict := types.Dict( + map[string]types.Object{ + "Type": types.Name("Pages"), + "Count": types.Integer(0), + "MediaBox": mb.Array(), + }, + ) + + pagesIndRef, err := ctx.IndRefForNewObject(pagesDict) + if err != nil { + return err + } + + nup.PageDim = &types.Dim{Width: mb.Width(), Height: mb.Height()} + + if err = nupPages(ctx, selectedPages, nup, pagesDict, pagesIndRef); err != nil { + return err + } + + // Replace original pagesDict. + rootDict, err := ctx.Catalog() + if err != nil { + return err + } + + rootDict.Update("Pages", *pagesIndRef) + + return nil +} diff --git a/pkg/pdfcpu/optimize.go b/pkg/pdfcpu/optimize.go new file mode 100644 index 0000000000000000000000000000000000000000..08121a881943d9aa5a309f82aed442302ef5b881 --- /dev/null +++ b/pkg/pdfcpu/optimize.go @@ -0,0 +1,1545 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 pdfcpu + +import ( + "bytes" + "sort" + + "github.com/pdfcpu/pdfcpu/pkg/log" + pdffont "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/font" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +func optimizeContentStreamUsage(ctx *model.Context, sd *types.StreamDict, objNr int) (*types.IndirectRef, error) { + f := ctx.Optimize.ContentStreamCache + if len(f) == 0 { + f[objNr] = sd + return nil, nil + } + + if f[objNr] != nil { + return nil, nil + } + + cachedObjNrs := []int{} + for objNr, sd1 := range f { + if *sd1.StreamLength == *sd.StreamLength { + cachedObjNrs = append(cachedObjNrs, objNr) + } + } + if len(cachedObjNrs) == 0 { + f[objNr] = sd + return nil, nil + } + + for _, objNr := range cachedObjNrs { + sd1 := f[objNr] + if bytes.Equal(sd.Raw, sd1.Raw) { + ir := types.NewIndirectRef(objNr, 0) + ctx.IncrementRefCount(ir) + return ir, nil + } + } + + f[objNr] = sd + return nil, nil +} + +func removeEmptyContentStreams(ctx *model.Context, pageDict types.Dict, obj types.Object, pageObjNumber int) error { + var contentArr types.Array + + if ir, ok := obj.(types.IndirectRef); ok { + + objNr := ir.ObjectNumber.Value() + entry, found := ctx.FindTableEntry(objNr, ir.GenerationNumber.Value()) + if !found { + return errors.Errorf("removeEmptyContentStreams: obj#:%d illegal indRef for Contents\n", pageObjNumber) + } + + contentStreamDict, ok := entry.Object.(types.StreamDict) + if ok { + if err := contentStreamDict.Decode(); err != nil { + return err + } + if len(contentStreamDict.Content) == 0 { + pageDict.Delete("Contents") + } + return nil + } + + contentArr, ok = entry.Object.(types.Array) + if !ok { + return errors.Errorf("removeEmptyContentStreams: obj#:%d page content entry neither stream dict nor array.\n", pageObjNumber) + } + + } else if contentArr, ok = obj.(types.Array); !ok { + return errors.Errorf("removeEmptyContentStreams: obj#:%d corrupt page content array\n", pageObjNumber) + } + + var newContentArr types.Array + + for _, c := range contentArr { + + ir, ok := c.(types.IndirectRef) + if !ok { + return errors.Errorf("removeEmptyContentStreams: obj#:%d corrupt page content array entry\n", pageObjNumber) + } + + objNr := ir.ObjectNumber.Value() + entry, found := ctx.FindTableEntry(objNr, ir.GenerationNumber.Value()) + if !found { + return errors.Errorf("removeEmptyContentStreams: obj#:%d illegal indRef for Contents\n", pageObjNumber) + } + + contentStreamDict, ok := entry.Object.(types.StreamDict) + if !ok { + return errors.Errorf("identifyPageContent: obj#:%d page content entry is no stream dict\n", pageObjNumber) + } + + if err := contentStreamDict.Decode(); err != nil { + return err + } + if len(contentStreamDict.Content) > 0 { + newContentArr = append(newContentArr, c) + } + } + + pageDict["Contents"] = newContentArr + + return nil +} + +func optimizePageContent(ctx *model.Context, pageDict types.Dict, pageObjNumber int) error { + o, found := pageDict.Find("Contents") + if !found { + return nil + } + + if err := removeEmptyContentStreams(ctx, pageDict, o, pageObjNumber); err != nil { + return err + } + + o, found = pageDict.Find("Contents") + if !found { + return nil + } + + if !ctx.OptimizeDuplicateContentStreams { + return nil + } + + if log.OptimizeEnabled() { + log.Optimize.Println("identifyPageContent begin") + } + + var contentArr types.Array + + if ir, ok := o.(types.IndirectRef); ok { + + objNr := ir.ObjectNumber.Value() + entry, found := ctx.FindTableEntry(objNr, ir.GenerationNumber.Value()) + if !found { + return errors.Errorf("identifyPageContent: obj#:%d illegal indRef for Contents\n", pageObjNumber) + } + + contentStreamDict, ok := entry.Object.(types.StreamDict) + if ok { + ir, err := optimizeContentStreamUsage(ctx, &contentStreamDict, objNr) + if err != nil { + return err + } + if ir != nil { + pageDict["Contents"] = *ir + } + contentStreamDict.IsPageContent = true + entry.Object = contentStreamDict + if log.OptimizeEnabled() { + log.Optimize.Printf("identifyPageContent end: ok obj#%d\n", objNr) + } + return nil + } + + contentArr, ok = entry.Object.(types.Array) + if !ok { + return errors.Errorf("identifyPageContent: obj#:%d page content entry neither stream dict nor array.\n", pageObjNumber) + } + + } else if contentArr, ok = o.(types.Array); !ok { + return errors.Errorf("identifyPageContent: obj#:%d corrupt page content array\n", pageObjNumber) + } + + // TODO Activate content array opimization as soon as we have a proper test file. + + _ = contentArr + + // for i, c := range contentArr { + + // ir, ok := c.(IndirectRef) + // if !ok { + // return errors.Errorf("identifyPageContent: obj#:%d corrupt page content array entry\n", pageObjNumber) + // } + + // objNr := ir.ObjectNumber.Value() + // entry, found := ctx.FindTableEntry(objNr, ir.GenerationNumber.Value()) + // if !found { + // return errors.Errorf("identifyPageContent: obj#:%d illegal indRef for Contents\n", pageObjNumber) + // } + + // contentStreamDict, ok := entry.Object.(StreamDict) + // if !ok { + // return errors.Errorf("identifyPageContent: obj#:%d page content entry is no stream dict\n", pageObjNumber) + // } + + // ir1, err := optimizeContentStreamUsage(ctx, &contentStreamDict, objNr) + // if err != nil { + // return err + // } + // if ir1 != nil { + // contentArr[i] = *ir1 + // } + + // contentStreamDict.IsPageContent = true + // entry.Object = contentStreamDict + // log.Optimize.Printf("identifyPageContent: ok obj#%d\n", ir.GenerationNumber.Value()) + // } + + if log.OptimizeEnabled() { + log.Optimize.Println("identifyPageContent end") + } + + return nil +} + +// resourcesDictForPageDict returns the resource dict for a page dict if there is any. +func resourcesDictForPageDict(xRefTable *model.XRefTable, pageDict types.Dict, pageObjNumber int) (types.Dict, error) { + o, found := pageDict.Find("Resources") + if !found { + if log.OptimizeEnabled() { + log.Optimize.Printf("resourcesDictForPageDict end: No resources dict for page object %d, may be inherited\n", pageObjNumber) + } + return nil, nil + } + + return xRefTable.DereferenceDict(o) +} + +// handleDuplicateFontObject returns nil or the object number of the registered font if it matches this font. +func handleDuplicateFontObject(ctx *model.Context, fontDict types.Dict, fName, rName string, objNr, pageNumber int) (*int, error) { + // Get a slice of all font object numbers for font name. + fontObjNrs, found := ctx.Optimize.Fonts[fName] + if !found { + // There is no registered font with fName. + return nil, nil + } + + // Get the set of font object numbers for pageNumber. + pageFonts := ctx.Optimize.PageFonts[pageNumber] + + // Iterate over all registered font object numbers for font name. + // Check if this font dict matches the font dict of each font object number. + for _, fontObjNr := range fontObjNrs { + + // Get the font object from the lookup table. + fontObject, ok := ctx.Optimize.FontObjects[fontObjNr] + if !ok { + continue + } + + if log.OptimizeEnabled() { + log.Optimize.Printf("handleDuplicateFontObject: comparing with fontDict Obj %d\n", fontObjNr) + } + + // Check if the input fontDict matches the fontDict of this fontObject. + ok, err := model.EqualFontDicts(fontObject.FontDict, fontDict, ctx.XRefTable) + if err != nil { + return nil, err + } + + if !ok { + // No match! + continue + } + + // We have detected a redundant font dict! + if log.OptimizeEnabled() { + log.Optimize.Printf("handleDuplicateFontObject: redundant fontObj#:%d basefont %s already registered with obj#:%d !\n", objNr, fName, fontObjNr) + } + + // Register new page font with pageNumber. + // The font for font object number is used instead of objNr. + pageFonts[fontObjNr] = true + + // Add the resource name of this duplicate font to the list of registered resource names. + fontObject.AddResourceName(rName) + + // Register fontDict as duplicate. + ctx.Optimize.DuplicateFonts[objNr] = fontDict + + // Return the fontObjectNumber that will be used instead of objNr. + return &fontObjNr, nil + } + + return nil, nil +} + +func pageImages(ctx *model.Context, pageNumber int) types.IntSet { + pageImages := ctx.Optimize.PageImages[pageNumber] + if pageImages == nil { + pageImages = types.IntSet{} + ctx.Optimize.PageImages[pageNumber] = pageImages + } + + return pageImages +} + +func pageFonts(ctx *model.Context, pageNumber int) types.IntSet { + pageFonts := ctx.Optimize.PageFonts[pageNumber] + if pageFonts == nil { + pageFonts = types.IntSet{} + ctx.Optimize.PageFonts[pageNumber] = pageFonts + } + + return pageFonts +} + +func registerFontDictObjNr(ctx *model.Context, fName string, objNr int) { + if log.OptimizeEnabled() { + log.Optimize.Printf("optimizeFontResourcesDict: adding new font %s obj#%d\n", fName, objNr) + } + + fontObjNrs, found := ctx.Optimize.Fonts[fName] + if found { + if log.OptimizeEnabled() { + log.Optimize.Printf("optimizeFontResourcesDict: appending %d to %s\n", objNr, fName) + } + ctx.Optimize.Fonts[fName] = append(fontObjNrs, objNr) + } else { + ctx.Optimize.Fonts[fName] = []int{objNr} + } +} + +// Get rid of redundant fonts for given fontResources dictionary. +func optimizeFontResourcesDict(ctx *model.Context, rDict types.Dict, pageNumber, pageObjNumber int) error { + if log.OptimizeEnabled() { + log.Optimize.Printf("optimizeFontResourcesDict begin: page=%d pageObjNumber=%d %s\nPageFonts=%v\n", pageNumber, pageObjNumber, rDict, ctx.Optimize.PageFonts) + } + + pageFonts := pageFonts(ctx, pageNumber) + + // Iterate over font resource dict. + for rName, v := range rDict { + + indRef, ok := v.(types.IndirectRef) + if !ok { + continue + } + + objNr := int(indRef.ObjectNumber) + + if log.OptimizeEnabled() { + log.Optimize.Printf("optimizeFontResourcesDict: processing font: %s, objj#=%d\n", rName, objNr) + } + + if _, found := ctx.Optimize.FontObjects[objNr]; found { + // This font has already been registered. + //log.Optimize.Printf("optimizeFontResourcesDict: Fontobject %d already registered\n", objectNumber) + pageFonts[objNr] = true + continue + } + + // We are dealing with a new font. + fontDict, err := ctx.DereferenceFontDict(indRef) + if err != nil { + return err + } + if fontDict == nil { + continue + } + + if log.OptimizeEnabled() { + log.Optimize.Printf("optimizeFontResourcesDict: fontDict: %s\n", fontDict) + } + + // Get the unique font name. + prefix, fName, err := pdffont.Name(ctx.XRefTable, fontDict, objNr) + if err != nil { + return err + } + + if log.OptimizeEnabled() { + log.Optimize.Printf("optimizeFontResourcesDict: baseFont: prefix=%s name=%s\n", prefix, fName) + } + + // Check if fontDict is a duplicate and if so return the object number of the original. + originalObjNr, err := handleDuplicateFontObject(ctx, fontDict, fName, rName, objNr, pageNumber) + if err != nil { + return err + } + + if originalObjNr != nil { + // We have identified a redundant fontDict! + // Update font resource dict so that rName points to the original. + ir := types.NewIndirectRef(*originalObjNr, 0) + rDict[rName] = *ir + ctx.IncrementRefCount(ir) + continue + } + + registerFontDictObjNr(ctx, fName, objNr) + + ctx.Optimize.FontObjects[objNr] = + &model.FontObject{ + ResourceNames: []string{rName}, + Prefix: prefix, + FontName: fName, + FontDict: fontDict, + } + + pageFonts[objNr] = true + } + + if log.OptimizeEnabled() { + log.Optimize.Println("optimizeFontResourcesDict end:") + } + + return nil +} + +// handleDuplicateImageObject returns nil or the object number of the registered image if it matches this image. +func handleDuplicateImageObject(ctx *model.Context, imageDict *types.StreamDict, resourceName string, objNr, pageNumber int) (*int, error) { + // Get the set of image object numbers for pageNumber. + pageImages := ctx.Optimize.PageImages[pageNumber] + + // Process image dict, check if this is a duplicate. + for imageObjNr, imageObject := range ctx.Optimize.ImageObjects { + + if log.OptimizeEnabled() { + log.Optimize.Printf("handleDuplicateImageObject: comparing with imagedict Obj %d\n", imageObjNr) + } + + // Check if the input imageDict matches the imageDict of this imageObject. + ok, err := model.EqualStreamDicts(imageObject.ImageDict, imageDict, ctx.XRefTable) + if err != nil { + return nil, err + } + + if !ok { + // No match! + continue + } + + // We have detected a redundant image dict. + if log.OptimizeEnabled() { + log.Optimize.Printf("handleDuplicateImageObject: redundant imageObj#:%d already registered with obj#:%d !\n", objNr, imageObjNr) + } + + // Register new page image for pageNumber. + // The image for image object number is used instead of objNr. + pageImages[imageObjNr] = true + + // Add the resource name of this duplicate image to the list of registered resource names. + imageObject.AddResourceName(resourceName) + + // Register imageDict as duplicate. + ctx.Optimize.DuplicateImages[objNr] = imageDict + + // Return the imageObjectNumber that will be used instead of objNr. + return &imageObjNr, nil + } + + return nil, nil +} + +func optimizeXObjectImage(ctx *model.Context, osd *types.StreamDict, rName string, objNr, pageNumber int, pageImages types.IntSet) (*types.IndirectRef, error) { + + // Already registered image object that appears in different resources dicts. + if _, found := ctx.Optimize.ImageObjects[objNr]; found { + // This image has already been registered. + //log.Optimize.Printf("optimizeXObjectResourcesDict: Imageobject %d already registered\n", objNr) + pageImages[objNr] = true + return nil, nil + } + + // Check if image is a duplicate and if so return the object number of the original. + originalObjNr, err := handleDuplicateImageObject(ctx, osd, rName, objNr, pageNumber) + if err != nil { + return nil, err + } + + if originalObjNr != nil { + // We have identified a redundant image! + // Update xobject resource dict so that rName points to the original. + ir := types.NewIndirectRef(*originalObjNr, 0) + ctx.IncrementRefCount(ir) + return ir, nil + } + + // Register new image dict. + if log.OptimizeEnabled() { + log.Optimize.Printf("optimizeXObjectResourcesDict: adding new image obj#%d\n", objNr) + } + + ctx.Optimize.ImageObjects[objNr] = + &model.ImageObject{ + ResourceNames: []string{rName}, + ImageDict: osd, + } + + pageImages[objNr] = true + return nil, nil +} + +func optimizeXObjectForm(ctx *model.Context, sd *types.StreamDict, objNr int) (*types.IndirectRef, error) { + + f := ctx.Optimize.FormStreamCache + if len(f) == 0 { + f[objNr] = sd + return nil, nil + } + + if f[objNr] != nil { + return nil, nil + } + + cachedObjNrs := []int{} + for objNr, sd1 := range f { + if *sd1.StreamLength == *sd.StreamLength { + cachedObjNrs = append(cachedObjNrs, objNr) + } + } + if len(cachedObjNrs) == 0 { + f[objNr] = sd + return nil, nil + } + + for _, objNr1 := range cachedObjNrs { + sd1 := f[objNr1] + ok, err := model.EqualStreamDicts(sd, sd1, ctx.XRefTable) + if err != nil { + return nil, err + } + if ok { + ir := types.NewIndirectRef(objNr1, 0) + ctx.IncrementRefCount(ir) + return ir, nil + } + } + + f[objNr] = sd + return nil, nil +} + +func optimizeFormResources(ctx *model.Context, o types.Object, pageNumber, pageObjNumber int, visitedRes []types.Object) error { + d, err := ctx.DereferenceDict(o) + if err != nil { + return err + } + if d != nil { + // Optimize image and font resources. + if err = optimizeResources(ctx, d, pageNumber, pageObjNumber, visitedRes); err != nil { + return err + } + } + return nil +} + +func visited(o types.Object, visited []types.Object) bool { + for _, obj := range visited { + if obj == o { + return true + } + } + return false +} + +func optimizeForm(ctx *model.Context, osd *types.StreamDict, rName string, rDict types.Dict, objNr, pageNumber, pageObjNumber int, vis []types.Object) error { + + ir, err := optimizeXObjectForm(ctx, osd, objNr) + if err != nil { + return err + } + + if ir != nil { + rDict[rName] = *ir + return nil + } + + o, found := osd.Find("Resources") + if !found { + return nil + } + + indRef, ok := o.(types.IndirectRef) + if ok { + if visited(indRef, vis) { + return nil + } + vis = append(vis, indRef) + } + + return optimizeFormResources(ctx, o, pageNumber, pageObjNumber, vis) +} + +func optimizeXObjectResourcesDict(ctx *model.Context, rDict types.Dict, pageNumber, pageObjNumber int, vis []types.Object) error { + if log.OptimizeEnabled() { + log.Optimize.Printf("optimizeXObjectResourcesDict page#%dbegin: %s\n", pageObjNumber, rDict) + } + + pageImages := pageImages(ctx, pageNumber) + + for rName, v := range rDict { + + indRef, ok := v.(types.IndirectRef) + if !ok { + continue + } + + if visited(indRef, vis) { + continue + } + vis = append(vis, indRef) + + objNr := int(indRef.ObjectNumber) + + if log.OptimizeEnabled() { + log.Optimize.Printf("optimizeXObjectResourcesDict: processing XObject: %s, obj#=%d\n", rName, objNr) + } + + sd, err := ctx.DereferenceXObjectDict(indRef) + if err != nil { + return err + } + if sd == nil { + continue + } + + if err := ctx.DeleteDictEntry(sd.Dict, "PieceInfo"); err != nil { + return err + } + + if *sd.Dict.Subtype() == "Image" { + ir, err := optimizeXObjectImage(ctx, sd, rName, objNr, pageNumber, pageImages) + if err != nil { + return err + } + if ir != nil { + rDict[rName] = *ir + } + continue + } + + if *sd.Subtype() == "Form" { + if err := optimizeForm(ctx, sd, rName, rDict, objNr, pageNumber, pageObjNumber, vis); err != nil { + return err + } + } + + } + + if log.OptimizeEnabled() { + log.Optimize.Println("optimizeXObjectResourcesDict end") + } + + return nil +} + +// Optimize given resource dictionary by removing redundant fonts and images. +func optimizeResources(ctx *model.Context, resourcesDict types.Dict, pageNumber, pageObjNumber int, visitedRes []types.Object) error { + if log.OptimizeEnabled() { + log.Optimize.Printf("optimizeResources begin: pageNumber=%d pageObjNumber=%d\n", pageNumber, pageObjNumber) + } + + if resourcesDict == nil { + if log.OptimizeEnabled() { + log.Optimize.Printf("optimizeResources end: No resources dict available") + } + return nil + } + + // Process Font resource dict, get rid of redundant fonts. + o, found := resourcesDict.Find("Font") + if found { + + d, err := ctx.DereferenceDict(o) + if err != nil { + return err + } + + if d == nil { + return errors.Errorf("pdfcpu: optimizeResources: font resource dict is null for page %d pageObj %d\n", pageNumber, pageObjNumber) + } + + if err = optimizeFontResourcesDict(ctx, d, pageNumber, pageObjNumber); err != nil { + return err + } + + } + + // Note: An optional ExtGState resource dict may contain binary content in the following entries: "SMask", "HT". + + // Process XObject resource dict, get rid of redundant images. + o, found = resourcesDict.Find("XObject") + if found { + + d, err := ctx.DereferenceDict(o) + if err != nil { + return err + } + + if d == nil { + return errors.Errorf("pdfcpu: optimizeResources: xobject resource dict is null for page %d pageObj %d\n", pageNumber, pageObjNumber) + } + + if err = optimizeXObjectResourcesDict(ctx, d, pageNumber, pageObjNumber, visitedRes); err != nil { + return err + } + + } + + if log.OptimizeEnabled() { + log.Optimize.Println("optimizeResources end") + } + + return nil +} + +// Process the resources dictionary for given page number and optimize by removing redundant resources. +func parseResourcesDict(ctx *model.Context, pageDict types.Dict, pageNumber, pageObjNumber int) error { + if ctx.Optimize.Cache[pageObjNumber] { + return nil + } + ctx.Optimize.Cache[pageObjNumber] = true + + // The logical pageNumber is pageNumber+1. + if log.OptimizeEnabled() { + log.Optimize.Printf("parseResourcesDict begin page: %d, object:%d\n", pageNumber+1, pageObjNumber) + } + + // Get resources dict for this page. + d, err := resourcesDictForPageDict(ctx.XRefTable, pageDict, pageObjNumber) + if err != nil { + return err + } + + // dict may be nil for inherited resource dicts. + if d != nil { + + // Optimize image and font resources. + if err = optimizeResources(ctx, d, pageNumber, pageObjNumber, []types.Object{}); err != nil { + return err + } + + } + + if log.OptimizeEnabled() { + log.Optimize.Printf("parseResourcesDict end page: %d, object:%d\n", pageNumber+1, pageObjNumber) + } + + return nil +} + +// Iterate over all pages and optimize content & resources. +func parsePagesDict(ctx *model.Context, pagesDict types.Dict, pageNumber int) (int, error) { + // TODO Integrate resource consolidation based on content stream requirements. + + count, found := pagesDict.Find("Count") + if !found { + return pageNumber, errors.New("pdfcpu: parsePagesDict: missing Count") + } + + if log.OptimizeEnabled() { + log.Optimize.Printf("parsePagesDict begin (next page=%d has %s pages): %s\n", pageNumber+1, count.(types.Integer), pagesDict) + } + + ctx.Optimize.Cache = map[int]bool{} + + // Iterate over page tree. + o, found := pagesDict.Find("Kids") + if !found { + return pageNumber, errors.New("pdfcpu: corrupt \"Kids\" entry") + } + + kids, err := ctx.DereferenceArray(o) + if err != nil || kids == nil { + return pageNumber, errors.New("pdfcpu: corrupt \"Kids\" entry") + } + + for _, v := range kids { + + // Dereference next page node dict. + ir, _ := v.(types.IndirectRef) + + if log.OptimizeEnabled() { + log.Optimize.Printf("parsePagesDict PageNode: %s\n", ir) + } + + d, err := ctx.DereferencePageNodeDict(ir) + if err != nil { + return 0, errors.Wrap(err, "parsePagesDict: can't locate Pagedict or Pagesdict") + } + + dictType := d.Type() + + // Note: Resource dicts may be inherited. + + if *dictType == "Pages" { + + // Recurse over pagetree and optimize resources. + pageNumber, err = parsePagesDict(ctx, d, pageNumber) + if err != nil { + return 0, err + } + + continue + } + + // Process page dict. + + if err = optimizePageContent(ctx, d, int(ir.ObjectNumber)); err != nil { + return 0, err + } + + if err := ctx.DeleteDictEntry(d, "PieceInfo"); err != nil { + return 0, err + } + + // Parse and optimize resource dict for one page. + if err = parseResourcesDict(ctx, d, pageNumber, int(ir.ObjectNumber)); err != nil { + return 0, err + } + + pageNumber++ + } + + if log.OptimizeEnabled() { + log.Optimize.Printf("parsePagesDict end: %s\n", pagesDict) + } + + return pageNumber, nil +} + +func traverse(xRefTable *model.XRefTable, value types.Object, duplObjs types.IntSet) error { + if indRef, ok := value.(types.IndirectRef); ok { + duplObjs[int(indRef.ObjectNumber)] = true + o, err := xRefTable.Dereference(indRef) + if err != nil { + return err + } + traverseObjectGraphAndMarkDuplicates(xRefTable, o, duplObjs) + } + if d, ok := value.(types.Dict); ok { + traverseObjectGraphAndMarkDuplicates(xRefTable, d, duplObjs) + } + if sd, ok := value.(types.StreamDict); ok { + traverseObjectGraphAndMarkDuplicates(xRefTable, sd, duplObjs) + } + if a, ok := value.(types.Array); ok { + traverseObjectGraphAndMarkDuplicates(xRefTable, a, duplObjs) + } + + return nil +} + +// Traverse the object graph for a Object and mark all objects as potential duplicates. +func traverseObjectGraphAndMarkDuplicates(xRefTable *model.XRefTable, obj types.Object, duplObjs types.IntSet) error { + if log.OptimizeEnabled() { + log.Optimize.Printf("traverseObjectGraphAndMarkDuplicates begin type=%T\n", obj) + } + + switch x := obj.(type) { + + case types.Dict: + if log.OptimizeEnabled() { + log.Optimize.Println("traverseObjectGraphAndMarkDuplicates: dict") + } + for _, value := range x { + if err := traverse(xRefTable, value, duplObjs); err != nil { + return err + } + } + + case types.StreamDict: + if log.OptimizeEnabled() { + log.Optimize.Println("traverseObjectGraphAndMarkDuplicates: streamDict") + } + for _, value := range x.Dict { + if err := traverse(xRefTable, value, duplObjs); err != nil { + return err + } + } + + case types.Array: + if log.OptimizeEnabled() { + log.Optimize.Println("traverseObjectGraphAndMarkDuplicates: arr") + } + for _, value := range x { + if err := traverse(xRefTable, value, duplObjs); err != nil { + return err + } + } + } + + if log.OptimizeEnabled() { + log.Optimize.Println("traverseObjectGraphAndMarkDuplicates end") + } + + return nil +} + +// Identify and mark all potential duplicate objects. +func calcRedundantObjects(ctx *model.Context) error { + if log.OptimizeEnabled() { + log.Optimize.Println("calcRedundantObjects begin") + } + + for i, fontDict := range ctx.Optimize.DuplicateFonts { + ctx.Optimize.DuplicateFontObjs[i] = true + // Identify and mark all involved potential duplicate objects for a redundant font. + if err := traverseObjectGraphAndMarkDuplicates(ctx.XRefTable, fontDict, ctx.Optimize.DuplicateFontObjs); err != nil { + return err + } + } + + for i, sd := range ctx.Optimize.DuplicateImages { + ctx.Optimize.DuplicateImageObjs[i] = true + // Identify and mark all involved potential duplicate objects for a redundant image. + if err := traverseObjectGraphAndMarkDuplicates(ctx.XRefTable, *sd, ctx.Optimize.DuplicateImageObjs); err != nil { + return err + } + } + + if log.OptimizeEnabled() { + log.Optimize.Println("calcRedundantObjects end") + } + + return nil +} + +// Iterate over all pages and optimize resources. +// Get rid of duplicate embedded fonts and images. +func optimizeFontAndImages(ctx *model.Context) error { + if log.OptimizeEnabled() { + log.Optimize.Println("optimizeFontAndImages begin") + } + + // Get a reference to the PDF indirect reference of the page tree root dict. + indRefPages, err := ctx.Pages() + if err != nil { + return err + } + + // Dereference and get a reference to the page tree root dict. + pageTreeRootDict, err := ctx.XRefTable.DereferenceDict(*indRefPages) + if err != nil { + return err + } + + // Detect the number of pages of this PDF file. + pageCount := pageTreeRootDict.IntEntry("Count") + if pageCount == nil { + return errors.New("pdfcpu: optimizeFontAndImagess: missing \"Count\" in page root dict") + } + + // If PageCount already set by validation doublecheck. + if ctx.PageCount > 0 && ctx.PageCount != *pageCount { + return errors.New("pdfcpu: optimizeFontAndImagess: unexpected page root dict pageCount discrepancy") + } + + // If we optimize w/o prior validation, set PageCount. + if ctx.PageCount == 0 { + ctx.PageCount = *pageCount + } + + // Prepare optimization environment. + ctx.Optimize.PageFonts = make([]types.IntSet, ctx.PageCount) + ctx.Optimize.PageImages = make([]types.IntSet, ctx.PageCount) + + // Iterate over page dicts and optimize resources. + _, err = parsePagesDict(ctx, pageTreeRootDict, 0) + if err != nil { + return err + } + + ctx.Optimize.ContentStreamCache = map[int]*types.StreamDict{} + ctx.Optimize.FormStreamCache = map[int]*types.StreamDict{} + + // Identify all duplicate objects. + if err = calcRedundantObjects(ctx); err != nil { + return err + } + + if log.OptimizeEnabled() { + log.Optimize.Println("optimizeFontAndImages end") + } + + return nil +} + +// Return stream length for font file object. +func streamLengthFontFile(xRefTable *model.XRefTable, indirectRef *types.IndirectRef) (*int64, error) { + if log.OptimizeEnabled() { + log.Optimize.Println("streamLengthFontFile begin") + } + + objectNumber := indirectRef.ObjectNumber + + sd, _, err := xRefTable.DereferenceStreamDict(*indirectRef) + if err != nil { + return nil, err + } + + if sd == nil || (*sd).StreamLength == nil { + return nil, errors.Errorf("pdfcpu: streamLengthFontFile: fontFile Streamlength is nil for object %d\n", objectNumber) + } + + if log.OptimizeEnabled() { + log.Optimize.Println("streamLengthFontFile end") + } + + return (*sd).StreamLength, nil +} + +// Calculate amount of memory used by embedded fonts for stats. +func calcEmbeddedFontsMemoryUsage(ctx *model.Context) error { + if log.OptimizeEnabled() { + log.Optimize.Printf("calcEmbeddedFontsMemoryUsage begin: %d fontObjects\n", len(ctx.Optimize.FontObjects)) + } + + fontFileIndRefs := map[types.IndirectRef]bool{} + + var objectNumbers []int + + // Sorting unnecessary. + for k := range ctx.Optimize.FontObjects { + objectNumbers = append(objectNumbers, k) + } + sort.Ints(objectNumbers) + + // Iterate over all embedded font objects and record font file references. + for _, objectNumber := range objectNumbers { + + fontObject := ctx.Optimize.FontObjects[objectNumber] + + // Only embedded fonts have binary data. + if !fontObject.Embedded() { + continue + } + + if err := processFontFilesForFontDict(ctx.XRefTable, fontObject.FontDict, objectNumber, fontFileIndRefs); err != nil { + return err + } + } + + // Iterate over font file references and calculate total font size. + for ir := range fontFileIndRefs { + streamLength, err := streamLengthFontFile(ctx.XRefTable, &ir) + if err != nil { + return err + } + ctx.Read.BinaryFontSize += *streamLength + } + + if log.OptimizeEnabled() { + log.Optimize.Println("calcEmbeddedFontsMemoryUsage end") + } + + return nil +} + +// fontDescriptorFontFileIndirectObjectRef returns the indirect object for the font file for given font descriptor. +func fontDescriptorFontFileIndirectObjectRef(fontDescriptorDict types.Dict) *types.IndirectRef { + if log.OptimizeEnabled() { + log.Optimize.Println("fontDescriptorFontFileIndirectObjectRef begin") + } + + ir := fontDescriptorDict.IndirectRefEntry("FontFile") + + if ir == nil { + ir = fontDescriptorDict.IndirectRefEntry("FontFile2") + } + + if ir == nil { + ir = fontDescriptorDict.IndirectRefEntry("FontFile3") + } + + if log.OptimizeEnabled() { + log.Optimize.Println("FontDescriptorFontFileIndirectObjectRef end") + } + + return ir +} + +func trivialFontDescriptor(xRefTable *model.XRefTable, fontDict types.Dict, objNr int) (types.Dict, error) { + o, ok := fontDict.Find("FontDescriptor") + if !ok { + return nil, nil + } + + // fontDescriptor directly available. + + d, err := xRefTable.DereferenceDict(o) + if err != nil { + return nil, err + } + + if d == nil { + return nil, errors.Errorf("pdfcpu: trivialFontDescriptor: FontDescriptor is null for font object %d\n", objNr) + } + + if d.Type() != nil && *d.Type() != "FontDescriptor" { + return nil, errors.Errorf("pdfcpu: trivialFontDescriptor: FontDescriptor dict incorrect dict type for font object %d\n", objNr) + } + + return d, nil +} + +// FontDescriptor gets the font descriptor for this font. +func fontDescriptor(xRefTable *model.XRefTable, fontDict types.Dict, objNr int) (types.Dict, error) { + if log.OptimizeEnabled() { + log.Optimize.Println("fontDescriptor begin") + } + + d, err := trivialFontDescriptor(xRefTable, fontDict, objNr) + if err != nil { + return nil, err + } + if d != nil { + return d, nil + } + + // Try to access a fontDescriptor in a Descendent font for Type0 fonts. + + o, ok := fontDict.Find("DescendantFonts") + if !ok { + //logErrorOptimize.Printf("FontDescriptor: Neither FontDescriptor nor DescendantFonts for font object %d\n", objectNumber) + return nil, nil + } + + // A descendant font is contained in an array of size 1. + + a, err := xRefTable.DereferenceArray(o) + if err != nil || a == nil { + return nil, errors.Errorf("pdfcpu: fontDescriptor: DescendantFonts: IndirectRef or Array wth length 1 expected for font object %d\n", objNr) + } + if len(a) > 1 { + return nil, errors.Errorf("pdfcpu: fontDescriptor: DescendantFonts Array length > 1 %v\n", a) + } + + // dict is the fontDict of the descendant font. + d, err = xRefTable.DereferenceDict(a[0]) + if err != nil { + return nil, errors.Errorf("pdfcpu: fontDescriptor: No descendant font dict for %v\n", a) + } + if d == nil { + return nil, errors.Errorf("pdfcpu: fontDescriptor: descendant font dict is null for %v\n", a) + } + + if *d.Type() != "Font" { + return nil, errors.Errorf("pdfcpu: fontDescriptor: font dict with incorrect dict type for %v\n", d) + } + + o, ok = d.Find("FontDescriptor") + if !ok { + log.Optimize.Printf("fontDescriptor: descendant font not embedded %s\n", d) + return nil, nil + } + + d, err = xRefTable.DereferenceDict(o) + if err != nil { + return nil, errors.Errorf("pdfcpu: fontDescriptor: No FontDescriptor dict for font object %d\n", objNr) + } + + if log.OptimizeEnabled() { + log.Optimize.Println("fontDescriptor end") + } + + return d, nil +} + +// Record font file objects referenced by this fonts font descriptor for stats and size calculation. +func processFontFilesForFontDict(xRefTable *model.XRefTable, fontDict types.Dict, objectNumber int, indRefsMap map[types.IndirectRef]bool) error { + if log.OptimizeEnabled() { + log.Optimize.Println("processFontFilesForFontDict begin") + } + + // Note: + // "ToUnicode" is also an entry containing binary content that could be inspected for duplicate content. + + d, err := fontDescriptor(xRefTable, fontDict, objectNumber) + if err != nil { + return err + } + + if d != nil { + if ir := fontDescriptorFontFileIndirectObjectRef(d); ir != nil { + indRefsMap[*ir] = true + } + } + + if log.OptimizeEnabled() { + log.Optimize.Println("processFontFilesForFontDict end") + } + + return nil +} + +// Calculate amount of memory used by duplicate embedded fonts for stats. +func calcRedundantEmbeddedFontsMemoryUsage(ctx *model.Context) error { + if log.OptimizeEnabled() { + log.Optimize.Println("calcRedundantEmbeddedFontsMemoryUsage begin") + } + + fontFileIndRefs := map[types.IndirectRef]bool{} + + // Iterate over all duplicate fonts and record font file references. + for objectNumber, fontDict := range ctx.Optimize.DuplicateFonts { + + // Duplicate Fonts have to be embedded, so no check here. + if err := processFontFilesForFontDict(ctx.XRefTable, fontDict, objectNumber, fontFileIndRefs); err != nil { + return err + } + + } + + // Iterate over font file references and calculate total font size. + for ir := range fontFileIndRefs { + + streamLength, err := streamLengthFontFile(ctx.XRefTable, &ir) + if err != nil { + return err + } + + ctx.Read.BinaryFontDuplSize += *streamLength + } + + if log.OptimizeEnabled() { + log.Optimize.Println("calcRedundantEmbeddedFontsMemoryUsage end") + } + + return nil +} + +// Calculate amount of memory used by embedded fonts and duplicate embedded fonts for stats. +func calcFontBinarySizes(ctx *model.Context) error { + if log.OptimizeEnabled() { + log.Optimize.Println("calcFontBinarySizes begin") + } + + if err := calcEmbeddedFontsMemoryUsage(ctx); err != nil { + return err + } + + if err := calcRedundantEmbeddedFontsMemoryUsage(ctx); err != nil { + return err + } + + if log.OptimizeEnabled() { + log.Optimize.Println("calcFontBinarySizes end") + } + + return nil +} + +// Calculate amount of memory used by images and duplicate images for stats. +func calcImageBinarySizes(ctx *model.Context) { + if log.OptimizeEnabled() { + log.Optimize.Println("calcImageBinarySizes begin") + } + + // Calc memory usage for images. + for _, imageObject := range ctx.Optimize.ImageObjects { + ctx.Read.BinaryImageSize += *imageObject.ImageDict.StreamLength + } + + // Calc memory usage for duplicate images. + for _, imageDict := range ctx.Optimize.DuplicateImages { + ctx.Read.BinaryImageDuplSize += *imageDict.StreamLength + } + + if log.OptimizeEnabled() { + log.Optimize.Println("calcImageBinarySizes end") + } +} + +// Calculate memory usage of binary data for stats. +func calcBinarySizes(ctx *model.Context) error { + if log.OptimizeEnabled() { + log.Optimize.Println("calcBinarySizes begin") + } + + // Calculate font memory usage for stats. + if err := calcFontBinarySizes(ctx); err != nil { + return err + } + + // Calculate image memory usage for stats. + calcImageBinarySizes(ctx) + + // Note: Content streams also represent binary content. + + if log.OptimizeEnabled() { + log.Optimize.Println("calcBinarySizes end") + } + + return nil +} + +func fixDeepDict(ctx *model.Context, d types.Dict) error { + for k, v := range d { + ir, err := fixDeepObject(ctx, v) + if err != nil { + return err + } + if ir != nil { + d[k] = *ir + } + } + + return nil +} + +func fixDeepArray(ctx *model.Context, a types.Array) error { + for i, v := range a { + ir, err := fixDeepObject(ctx, v) + if err != nil { + return err + } + if ir != nil { + a[i] = *ir + } + } + + return nil +} + +func fixDirectObject(ctx *model.Context, o types.Object) error { + switch o := o.(type) { + case types.Dict: + for k, v := range o { + ir, err := fixDeepObject(ctx, v) + if err != nil { + return err + } + if ir != nil { + o[k] = *ir + } + } + case types.Array: + for i, v := range o { + ir, err := fixDeepObject(ctx, v) + if err != nil { + return err + } + if ir != nil { + o[i] = *ir + } + } + } + + return nil +} + +func fixIndirectObject(ctx *model.Context, ir *types.IndirectRef) error { + objNr := int(ir.ObjectNumber) + + if ctx.Optimize.Cache[objNr] { + return nil + } + ctx.Optimize.Cache[objNr] = true + + entry, found := ctx.Find(objNr) + if !found { + return nil + } + + if entry.Free { + // This is a reference to a free object that needs to be fixed. + + //fmt.Printf("fixNullObject: #%d g%d\n", objNr, genNr) + + if ctx.Optimize.NullObjNr == nil { + nr, err := ctx.InsertObject(nil) + if err != nil { + return err + } + ctx.Optimize.NullObjNr = &nr + } + + ir.ObjectNumber = types.Integer(*ctx.Optimize.NullObjNr) + + return nil + } + + var err error + + switch o := entry.Object.(type) { + + case types.Dict: + err = fixDeepDict(ctx, o) + + case types.StreamDict: + err = fixDeepDict(ctx, o.Dict) + + case types.Array: + err = fixDeepArray(ctx, o) + + } + + return err +} + +func fixDeepObject(ctx *model.Context, o types.Object) (*types.IndirectRef, error) { + ir, ok := o.(types.IndirectRef) + if !ok { + return nil, fixDirectObject(ctx, o) + } + + err := fixIndirectObject(ctx, &ir) + return &ir, err +} + +func fixReferencesToFreeObjects(ctx *model.Context) error { + return fixDirectObject(ctx, ctx.RootDict) +} + +func CacheFormFonts(ctx *model.Context) error { + + d, err := primitives.FormFontResDict(ctx.XRefTable) + if err != nil { + return err + } + + // Iterate over font resource dict. + for rName, v := range d { + + indRef, ok := v.(types.IndirectRef) + if !ok { + continue + } + + if log.OptimizeEnabled() { + log.Optimize.Printf("optimizeFontResourcesDict: processing font: %s, %s\n", rName, indRef) + } + + objNr := int(indRef.ObjectNumber) + + if log.OptimizeEnabled() { + log.Optimize.Printf("optimizeFontResourcesDict: objectNumber = %d\n", objNr) + } + + fontDict, err := ctx.DereferenceFontDict(indRef) + if err != nil { + return err + } + if fontDict == nil { + continue + } + + if log.OptimizeEnabled() { + log.Optimize.Printf("optimizeFontResourcesDict: fontDict: %s\n", fontDict) + } + + // Get the unique font name. + prefix, fName, err := pdffont.Name(ctx.XRefTable, fontDict, objNr) + if err != nil { + return err + } + + if log.OptimizeEnabled() { + log.Optimize.Printf("optimizeFontResourcesDict: baseFont: prefix=%s name=%s\n", prefix, fName) + } + + registerFontDictObjNr(ctx, fName, objNr) + + ctx.Optimize.FormFontObjects[objNr] = + &model.FontObject{ + ResourceNames: []string{rName}, + Prefix: prefix, + FontName: fName, + FontDict: fontDict, + } + } + + return nil +} + +func optimizeResourceDicts(ctx *model.Context) error { + for i := 1; i <= ctx.PageCount; i++ { + d, _, inhPAttrs, err := ctx.PageDict(i, true) + if err != nil { + return err + } + if d == nil { + continue + } + if len(inhPAttrs.Resources) > 0 { + d["Resources"] = inhPAttrs.Resources + } + } + // TODO Remove resource dicts from inner nodes. + return nil +} + +// OptimizeXRefTable optimizes an xRefTable by locating and getting rid of redundant embedded fonts and images. +func OptimizeXRefTable(ctx *model.Context) error { + if log.InfoEnabled() { + log.Info.Println("optimizing fonts & images") + } + if log.OptimizeEnabled() { + log.Optimize.Println("optimizeXRefTable begin") + } + + // Sometimes free objects are used although they are part of the free object list. + // Replace references to free xref table entries with a reference to a NULL object. + if err := fixReferencesToFreeObjects(ctx); err != nil { + return err + } + + if ctx.Cmd == model.OPTIMIZE && ctx.Conf.OptimizeResourceDicts { + // Extra step with potential for performance hit when processing large files. + if err := optimizeResourceDicts(ctx); err != nil { + return err + } + } + + // Get rid of duplicate embedded fonts and images. + if err := optimizeFontAndImages(ctx); err != nil { + return err + } + + // Get rid of PieceInfo dict from root. + if err := ctx.DeleteDictEntry(ctx.RootDict, "PieceInfo"); err != nil { + return err + } + + // Calculate memory usage of binary content for stats. + if err := calcBinarySizes(ctx); err != nil { + return err + } + + ctx.Optimized = true + + if log.OptimizeEnabled() { + log.Optimize.Println("optimizeXRefTable end") + } + + return nil +} diff --git a/pkg/pdfcpu/page.go b/pkg/pdfcpu/page.go new file mode 100644 index 0000000000000000000000000000000000000000..09c33810481c661cc38a2565ada4f04e43bb2277 --- /dev/null +++ b/pkg/pdfcpu/page.go @@ -0,0 +1,263 @@ +/* +Copyright 2022 The pdfcpu Authors. + +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 + + 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 pdfcpu + +import ( + "fmt" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +type pagesParamMap map[string]func(string, *PageConfiguration) error + +// Handle applies parameter completion and if successful +// parses the parameter values into pages. +func (m pagesParamMap) Handle(paramPrefix, paramValueStr string, pageConf *PageConfiguration) error { + + var param string + + // Completion support + for k := range m { + if !strings.HasPrefix(k, strings.ToLower(paramPrefix)) { + continue + } + if len(param) > 0 { + return errors.Errorf("pdfcpu: ambiguous parameter prefix \"%s\"", paramPrefix) + } + param = k + } + + if param == "" { + return errors.Errorf("pdfcpu: unknown parameter prefix \"%s\"", paramPrefix) + } + + return m[param](paramValueStr, pageConf) +} + +var pParamMap = pagesParamMap{ + "dimensions": parseDimensions, + "formsize": parsePageFormat, + "papersize": parsePageFormat, +} + +// PageConfiguration represents the page config for the "pages insert" command. +type PageConfiguration struct { + PageDim *types.Dim // page dimensions in display unit. + PageSize string // one of A0,A1,A2,A3,A4(=default),A5,A6,A7,A8,Letter,Legal,Ledger,Tabloid,Executive,ANSIC,ANSID,ANSIE. + UserDim bool // true if one of dimensions or paperSize provided overriding the default. + InpUnit types.DisplayUnit // input display unit. +} + +// DefaultPageConfiguration returns the default configuration. +func DefaultPageConfiguration() *PageConfiguration { + return &PageConfiguration{ + PageDim: types.PaperSize["A4"], + PageSize: "A4", + InpUnit: types.POINTS, + } +} + +func (p PageConfiguration) String() string { + return fmt.Sprintf("Page config: %s %s\n", p.PageSize, p.PageDim) +} + +func parsePageFormat(s string, p *PageConfiguration) (err error) { + if p.UserDim { + return errors.New("pdfcpu: only one of formsize(papersize) or dimensions allowed") + } + p.PageDim, p.PageSize, err = types.ParsePageFormat(s) + p.UserDim = true + return err +} + +func parseDimensions(s string, p *PageConfiguration) (err error) { + if p.UserDim { + return errors.New("pdfcpu: only one of formsize(papersize) or dimensions allowed") + } + p.PageDim, p.PageSize, err = ParsePageDim(s, p.InpUnit) + p.UserDim = true + return err +} + +// ParsePageConfiguration parses a page configuration string into an internal structure. +func ParsePageConfiguration(s string, u types.DisplayUnit) (*PageConfiguration, error) { + + if s == "" { + return nil, nil + } + + pageConf := DefaultPageConfiguration() + pageConf.InpUnit = u + + ss := strings.Split(s, ",") + + for _, s := range ss { + + ss1 := strings.Split(s, ":") + if len(ss1) != 2 { + return nil, errors.New("pdfcpu: Invalid page configuration string. Please consult pdfcpu help pages") + } + + paramPrefix := strings.TrimSpace(ss1[0]) + paramValueStr := strings.TrimSpace(ss1[1]) + + if err := pParamMap.Handle(paramPrefix, paramValueStr, pageConf); err != nil { + return nil, err + } + } + + return pageConf, nil +} + +func addPages( + ctxSrc, ctxDest *model.Context, + pageNrs []int, + usePgCache bool, + pagesIndRef types.IndirectRef, + pagesDict types.Dict, + fieldsSrc, fieldsDest *types.Array, + migrated map[int]int) error { + + // Used by collect, extractPages, split + + pageCache := map[int]*types.IndirectRef{} + + for _, i := range pageNrs { + + if usePgCache { + if indRef, ok := pageCache[i]; ok { + if err := model.AppendPageTree(indRef, 1, pagesDict); err != nil { + return err + } + continue + } + } + + d, pageIndRef, inhPAttrs, err := ctxSrc.PageDict(i, true) + if err != nil { + return err + } + if d == nil { + return errors.Errorf("pdfcpu: unknown page number: %d\n", i) + } + + obj, err := migrateIndRef(pageIndRef, ctxSrc, ctxDest, migrated) + if err != nil { + return err + } + + d = obj.(types.Dict) + d["Resources"] = inhPAttrs.Resources.Clone() + d["Parent"] = pagesIndRef + d["MediaBox"] = inhPAttrs.MediaBox.Array() + if inhPAttrs.Rotate%360 > 0 { + d["Rotate"] = types.Integer(inhPAttrs.Rotate) + } + + if err := migratePageDict(d, *pageIndRef, ctxSrc, ctxDest, migrated); err != nil { + return err + } + + if d["Annots"] != nil && len(*fieldsSrc) > 0 { + if err := migrateFields(d, fieldsSrc, fieldsDest, ctxSrc, ctxDest, migrated); err != nil { + return err + } + } + + if err := model.AppendPageTree(pageIndRef, 1, pagesDict); err != nil { + return err + } + + if usePgCache { + pageCache[i] = pageIndRef + } + } + + return nil +} + +func migrateNamedDests(ctxSrc *model.Context, n *model.Node, migrated map[int]int) error { + patchValues := func(xRefTable *model.XRefTable, k string, v *types.Object) error { + arr, err := xRefTable.DereferenceArray(*v) + if err == nil { + arr[0] = patchObject(arr[0], migrated) + *v = arr + return nil + } + d, err := xRefTable.DereferenceDict(*v) + if err != nil { + return err + } + arr = d.ArrayEntry("D") + arr[0] = patchObject(arr[0], migrated) + *v = d + return nil + } + + return n.Process(ctxSrc.XRefTable, patchValues) +} + +// AddPages adds pages and corresponding resources from ctxSrc to ctxDest. +func AddPages(ctxSrc, ctxDest *model.Context, pageNrs []int, usePgCache bool) error { + + pagesIndRef, err := ctxDest.Pages() + if err != nil { + return err + } + + pagesDict, err := ctxDest.DereferenceDict(*pagesIndRef) + if err != nil { + return err + } + + fieldsSrc, fieldsDest := types.Array{}, types.Array{} + + if ctxSrc.Form != nil { + o, _ := ctxSrc.Form.Find("Fields") + fieldsSrc, err = ctxSrc.DereferenceArray(o) + if err != nil { + return err + } + } + + migrated := map[int]int{} + + if err := addPages(ctxSrc, ctxDest, pageNrs, usePgCache, *pagesIndRef, pagesDict, &fieldsSrc, &fieldsDest, migrated); err != nil { + return err + } + + if ctxSrc.Form != nil && len(fieldsDest) > 0 { + d := ctxSrc.Form.Clone().(types.Dict) + if err := migrateFormDict(d, fieldsDest, ctxSrc, ctxDest, migrated); err != nil { + return err + } + ctxDest.RootDict["AcroForm"] = d + } + + if n, ok := ctxSrc.Names["Dests"]; ok { + // Carry over used named destinations. + if err := migrateNamedDests(ctxSrc, n, migrated); err != nil { + return err + } + ctxDest.Names = map[string]*model.Node{"Dests": n} + } + + return nil +} diff --git a/pkg/pdfcpu/primitives/band.go b/pkg/pdfcpu/primitives/band.go new file mode 100644 index 0000000000000000000000000000000000000000..5f6ea736c54a0eb0f1e4f8cf9d01b9fbaa310b38 --- /dev/null +++ b/pkg/pdfcpu/primitives/band.go @@ -0,0 +1,228 @@ +/* + Copyright 2021 The pdfcpu Authors. + + 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 + + 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 primitives + +import ( + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/format" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// HorizontalBand is a horizontal region used for header and footer. +type HorizontalBand struct { + pdf *PDF + Left string + Center string + Right string + position types.Anchor // topcenter, center, bottomcenter + Height float64 + Dx, Dy int + BackgroundColor string `json:"bgCol"` + bgCol *color.SimpleColor + Font *FormFont + From int + Thru int + Border bool + RTL bool +} + +func (hb *HorizontalBand) validate() error { + + pdf := hb.pdf + + if hb.BackgroundColor != "" { + sc, err := pdf.parseColor(hb.BackgroundColor) + if err != nil { + return err + } + hb.bgCol = sc + } + + if hb.Font != nil { + hb.Font.pdf = pdf + if err := hb.Font.validate(); err != nil { + return err + } + } + + if hb.Height <= 0 { + return errors.Errorf("pdfcpu: missing header/footer height") + } + + return nil +} + +func (hb *HorizontalBand) renderAnchoredImageBox( + imageName string, + r *types.Rectangle, + a types.Anchor, + p *model.Page, + pageNr int, + images model.ImageMap) error { + + ib := hb.pdf.ImageBoxPool[imageName] + if ib == nil { + return errors.Errorf("pdfcpu: HorizontalBand - unable to resolve $%s", imageName) + } + + if ib.Margin != nil && ib.Margin.Name != "" { + return errors.Errorf("pdfcpu: HorizontalBand - unsupported named margin %s", ib.Margin.Name) + } + + if ib.Border != nil && ib.Border.Name != "" { + return errors.Errorf("pdfcpu: HorizontalBand - unsupported named border %s", ib.Border.Name) + } + + if ib.Padding != nil && ib.Padding.Name != "" { + return errors.Errorf("pdfcpu: HorizontalBand - unsupported named padding %s", ib.Padding.Name) + } + + // push state + anchor, anchored, dest := ib.anchor, ib.anchored, ib.dest + + ib.anchor, ib.anchored, ib.dest = a, true, r + + if err := ib.render(p, pageNr, images); err != nil { + return err + } + + // pop state + ib.anchor, ib.anchored, ib.dest = anchor, anchored, dest + + return nil +} + +func (hb *HorizontalBand) renderAnchoredTextBox( + s string, + r *types.Rectangle, + a types.Anchor, + p *model.Page, + pageNr int, + fonts model.FontMap) error { + + pdf := hb.pdf + font := hb.Font + bgCol := hb.bgCol + + fontName := font.Name + fontLang := font.Lang + fontSize := font.Size + col := font.col + t, _ := format.Text(s, pdf.TimestampFormat, pageNr, pdf.pageCount()) + + id, err := pdf.idForFontName(fontName, fontLang, p.Fm, fonts, pageNr) + if err != nil { + return err + } + + td := model.TextDescriptor{ + Text: t, + FontName: fontName, + Embed: true, + FontKey: id, + FontSize: fontSize, + Scale: 1., + ScaleAbs: true, + RTL: hb.RTL, // for user fonts only! + } + + if col != nil { + td.StrokeCol, td.FillCol = *col, *col + } + + if bgCol != nil { + td.ShowBackground, td.ShowTextBB, td.BackgroundCol = true, true, *bgCol + } + + model.WriteMultiLineAnchored(hb.pdf.XRefTable, p.Buf, r, nil, td, a) + + return nil +} + +func (hb *HorizontalBand) renderComponent( + content string, + a types.Anchor, + r *types.Rectangle, + p *model.Page, + pageNr int, + fonts model.FontMap, + images model.ImageMap) error { + + if content[0] == '$' { + return hb.renderAnchoredImageBox(content[1:], r, a, p, pageNr, images) + } + + return hb.renderAnchoredTextBox(content, r, a, p, pageNr, fonts) +} + +func (hb *HorizontalBand) render(p *model.Page, pageNr int, fonts model.FontMap, images model.ImageMap, top bool) error { + + if pageNr < hb.From || (hb.Thru > 0 && pageNr > hb.Thru) { + return nil + } + + left := types.BottomLeft + center := types.BottomCenter + right := types.BottomRight + if top { + left = types.Left + center = types.Center + right = types.Right + } + + if hb.Font.Name[0] == '$' { + if err := hb.pdf.calcFont(hb.Font); err != nil { + return err + } + } + + llx := p.CropBox.LL.X + float64(hb.Dx) + lly := p.CropBox.LL.Y + float64(hb.Dy) + if top { + lly = p.CropBox.UR.Y - float64(hb.Dy) - hb.Height + } + w := p.CropBox.Width() - float64(2*hb.Dx) + h := hb.Height + r := types.RectForWidthAndHeight(llx, lly, w, h) + + if hb.Left != "" { + if err := hb.renderComponent(hb.Left, left, r, p, pageNr, fonts, images); err != nil { + return err + } + } + + if hb.Center != "" { + if err := hb.renderComponent(hb.Center, center, r, p, pageNr, fonts, images); err != nil { + return err + } + } + + if hb.Right != "" { + if err := hb.renderComponent(hb.Right, right, r, p, pageNr, fonts, images); err != nil { + return err + } + } + + if hb.Border { + draw.DrawRect(p.Buf, r, 0, &color.Black, nil) + } + + return nil +} diff --git a/pkg/pdfcpu/primitives/bar.go b/pkg/pdfcpu/primitives/bar.go new file mode 100644 index 0000000000000000000000000000000000000000..7acb6a53f17e17915011d3c1266affc4e870d319 --- /dev/null +++ b/pkg/pdfcpu/primitives/bar.go @@ -0,0 +1,97 @@ +/* + Copyright 2021 The pdfcpu Authors. + + 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 + + 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 primitives + +import ( + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// Bar represents a horizontal or vertical bar used by content. +type Bar struct { + pdf *PDF + content *Content + X, Y float64 // either or determines orientation. + Width int + Color string `json:"col"` + col *color.SimpleColor + Style string + style types.LineJoinStyle + Hide bool +} + +func (b *Bar) validate() error { + + if b.X != 0 && b.Y != 0 || b.X < 0 || b.Y < 0 { + return errors.Errorf("pdfcpu: bar: supply positive values for either x (vertical bar) or y (horizontal)") + } + + if b.Color != "" { + sc, err := b.pdf.parseColor(b.Color) + if err != nil { + return err + } + b.col = sc + } + + b.style = types.LJMiter + if b.Style != "" { + switch b.Style { + case "miter": + b.style = types.LJMiter + case "round": + b.style = types.LJRound + case "bevel": + b.style = types.LJBevel + default: + return errors.Errorf("pdfcpu: invalid bar style: %s (should be \"miter\", \"round\" or \"bevel\")", b.Style) + } + } + + return nil +} + +func (b *Bar) render(p *model.Page) error { + + if b.col == nil { + return nil + } + + cBox := b.content.Box() + + var px, py, qx, qy float64 + + if b.X > 0 { + // Vertical bar + px, py = b.X, 0 + qx, qy = px, cBox.Height() + } else { + // Horizontal bar + px, py = 0, b.Y + qx, qy = cBox.Width(), py + } + + px, py = types.NormalizeCoord(px, py, cBox, b.pdf.origin, true) + qx, qy = types.NormalizeCoord(qx, qy, cBox, b.pdf.origin, true) + + draw.DrawLine(p.Buf, px, py, qx, qy, float64(b.Width), b.col, &b.style) + + return nil +} diff --git a/pkg/pdfcpu/primitives/border.go b/pkg/pdfcpu/primitives/border.go new file mode 100644 index 0000000000000000000000000000000000000000..1b05878b55e2e7af71e928e9cc6b847ade767bce --- /dev/null +++ b/pkg/pdfcpu/primitives/border.go @@ -0,0 +1,87 @@ +/* + Copyright 2021 The pdfcpu Authors. + + 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 + + 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 primitives + +import ( + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +type Border struct { + pdf *PDF + Name string + Width int + Color string `json:"col"` + col *color.SimpleColor + Style string + style types.LineJoinStyle +} + +func (b *Border) validate() error { + + if b.Name == "$" { + return errors.New("pdfcpu: invalid border reference $") + } + + if b.Color != "" { + sc, err := b.pdf.parseColor(b.Color) + if err != nil { + return err + } + b.col = sc + } + + b.style = types.LJMiter + if b.Style != "" { + switch b.Style { + case "miter": + b.style = types.LJMiter + case "round": + b.style = types.LJRound + case "bevel": + b.style = types.LJBevel + default: + return errors.Errorf("pdfcpu: invalid border style: %s (should be \"miter\", \"round\" or \"bevel\")", b.Style) + } + } + + return nil +} + +func (b *Border) mergeIn(b0 *Border) { + if b.Width == 0 { + b.Width = b0.Width + } + if b.col == nil { + b.col = b0.col + } + if b.style == types.LJMiter { + b.style = b0.style + } +} + +// func (b *Border) SetCol(c color.SimpleColor) { +// b.col = &c +// } + +func (b Border) calc() (boWidth float64, boCol *color.SimpleColor) { + if b.col == nil { + return 0, &color.Black + } + return float64(b.Width), b.col +} diff --git a/pkg/pdfcpu/primitives/buttons.go b/pkg/pdfcpu/primitives/buttons.go new file mode 100644 index 0000000000000000000000000000000000000000..606b55664049329d1fde4409a38ff5dedb3729a9 --- /dev/null +++ b/pkg/pdfcpu/primitives/buttons.go @@ -0,0 +1,171 @@ +/* + Copyright 2021 The pdfcpu Authors. + + 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 + + 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 primitives + +import ( + "bytes" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +type Buttons struct { + pdf *PDF + Values []string + Label *TextFieldLabel + Gap int // horizontal space between radio button and its value + widths []float64 + maxWidth float64 + boundingBox *types.Rectangle +} + +func (b *Buttons) Rtl() bool { + if b.Label == nil { + return false + } + return b.Label.RTL +} + +func (b *Buttons) validate(defValue, value string) error { + + if len(b.Values) < 2 { + return errors.New("pdfcpu: radiobuttongroups.buttons missing values") + } + + if defValue != "" { + if !types.MemberOf(defValue, b.Values) { + return errors.Errorf("pdfcpu: radiobuttongroups invalid default: %s", defValue) + } + } + + if value != "" { + if !types.MemberOf(value, b.Values) { + return errors.Errorf("pdfcpu: radiobuttongroups invalid value: %s", value) + } + } + + if b.Label == nil { + return errors.New("pdfcpu: radiobuttongroups.buttons: missing label") + } + + b.Label.pdf = b.pdf + if err := b.Label.validate(); err != nil { + return err + } + + pos := b.Label.relPos + if pos == types.RelPosTop || pos == types.RelPosBottom { + return errors.New("pdfcpu: radiobuttongroups.buttons.label: pos must be left or right") + } + + b.Label.HorAlign = types.AlignLeft + if pos == types.RelPosLeft { + // A radio button label on the left side of a radio button is right aligned. + b.Label.HorAlign = types.AlignRight + } + + if b.Gap <= 0 { + b.Gap = 3 + } + + return nil +} + +func (b *Buttons) calcLeftAlignedHorLabelWidths(td model.TextDescriptor) { + var maxw float64 + for i := 0; i < len(b.Values); i++ { + td.Text = b.Values[i] + bb := model.WriteMultiLine(b.pdf.XRefTable, new(bytes.Buffer), types.RectForFormat("A4"), nil, td) + // Leave last label width as is. + if i == len(b.Values)-1 { + b.maxWidth = maxw + for i := range b.widths { + b.widths[i] = maxw + } + if bb.Width() > maxw { + b.widths[i] = bb.Width() + } + return + } + if bb.Width() > maxw { + maxw = bb.Width() + } + } +} + +func (b *Buttons) calcRightAlignedHorLabelWidths(td model.TextDescriptor) { + var maxw float64 + for i := 0; i < len(b.Values); i++ { + td.Text = b.Values[i] + bb := model.WriteMultiLine(b.pdf.XRefTable, new(bytes.Buffer), types.RectForFormat("A4"), nil, td) + // Leave first label width as is. + if i == 0 { + b.widths[0] = bb.Width() + continue + } + if bb.Width() > maxw { + maxw = bb.Width() + } + } + b.maxWidth = maxw + if b.widths[0] < maxw { + b.widths[0] = maxw + } + for i := 1; i < len(b.Values); i++ { + b.widths[i] = maxw + } +} + +func (b *Buttons) calcHorLabelWidths(td model.TextDescriptor) { + if b.Label.HorAlign == types.AlignLeft { + b.calcLeftAlignedHorLabelWidths(td) + return + } + b.calcRightAlignedHorLabelWidths(td) +} + +func (b *Buttons) calcVerLabelWidths(td model.TextDescriptor) { + var maxw float64 + for _, v := range b.Values { + td.Text = v + bb := model.WriteMultiLine(b.pdf.XRefTable, new(bytes.Buffer), types.RectForFormat("A4"), nil, td) + if bb.Width() > maxw { + maxw = bb.Width() + } + } + for i := range b.widths { + b.widths[i] = maxw + } + b.maxWidth = maxw +} + +func (b *Buttons) calcLabelWidths(hor bool) { + b.widths = make([]float64, len(b.Values)) + td := model.TextDescriptor{ + FontName: b.Label.Font.Name, + FontSize: b.Label.Font.Size, + RTL: b.Label.RTL, + Scale: 1., + ScaleAbs: true, + } + if hor { + b.calcHorLabelWidths(td) + return + } + b.calcVerLabelWidths(td) +} diff --git a/pkg/pdfcpu/primitives/checkBox.go b/pkg/pdfcpu/primitives/checkBox.go new file mode 100644 index 0000000000000000000000000000000000000000..2c1d6f4c9fc42febb8acd29d1ce941035ec29767 --- /dev/null +++ b/pkg/pdfcpu/primitives/checkBox.go @@ -0,0 +1,777 @@ +/* + Copyright 2021 The pdfcpu Authors. + + 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 + + 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 primitives + +import ( + "bytes" + "fmt" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + pdffont "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/font" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// CheckBox represents a form checkbox including a positioned label. +type CheckBox struct { + pdf *PDF + content *Content + Label *TextFieldLabel + ID string + Tip string + Value bool // checked state + Default bool + Position [2]float64 `json:"pos"` // x,y + x, y float64 + Width float64 + Dx, Dy float64 + boundingBox *types.Rectangle + Margin *Margin // applied to content box + BackgroundColor string `json:"bgCol"` + bgCol *color.SimpleColor + Tab int + Locked bool + Debug bool + Hide bool +} + +type AP struct { + irDOffL, irDYesL *types.IndirectRef + irNOffL, irNYesL *types.IndirectRef + irDOffR, irDYesR *types.IndirectRef + irNOffR, irNYesR *types.IndirectRef +} + +func (cb *CheckBox) validateID() error { + if cb.ID == "" { + return errors.New("pdfcpu: missing field id") + } + if cb.pdf.DuplicateField(cb.ID) { + return errors.Errorf("pdfcpu: duplicate form field: %s", cb.ID) + } + cb.pdf.FieldIDs[cb.ID] = true + return nil +} + +func (cb *CheckBox) validatePosition() error { + if cb.Position[0] < 0 || cb.Position[1] < 0 { + return errors.Errorf("pdfcpu: field: %s pos value < 0", cb.ID) + } + cb.x, cb.y = cb.Position[0], cb.Position[1] + return nil +} + +func (cb *CheckBox) validateMargin() error { + if cb.Margin != nil { + if err := cb.Margin.validate(); err != nil { + return err + } + } + return nil +} + +func (cb *CheckBox) validateWidth() error { + if cb.Width <= 0 { + return errors.Errorf("pdfcpu: field: %s width <= 0", cb.ID) + } + return nil +} + +func (cb *CheckBox) validateLabel() error { + if cb.Label != nil { + cb.Label.pdf = cb.pdf + if err := cb.Label.validate(); err != nil { + return err + } + } + return nil +} + +func (cb *CheckBox) validateTab() error { + if cb.Tab < 0 { + return errors.Errorf("pdfcpu: field: %s negative tab value", cb.ID) + } + if cb.Tab == 0 { + return nil + } + page := cb.content.page + if page.Tabs == nil { + page.Tabs = types.IntSet{} + } else { + if page.Tabs[cb.Tab] { + return errors.Errorf("pdfcpu: field: %s duplicate tab value %d", cb.ID, cb.Tab) + } + } + page.Tabs[cb.Tab] = true + return nil +} + +func (cb *CheckBox) validate() error { + + if err := cb.validateID(); err != nil { + return err + } + + if err := cb.validatePosition(); err != nil { + return err + } + + if err := cb.validateWidth(); err != nil { + return err + } + + if err := cb.validateMargin(); err != nil { + return err + } + + if err := cb.validateLabel(); err != nil { + return err + } + + return cb.validateTab() +} + +func (cb *CheckBox) margin(name string) *Margin { + return cb.content.namedMargin(name) +} + +func (cb *CheckBox) calcMargin() (float64, float64, float64, float64, error) { + mTop, mRight, mBottom, mLeft := 0., 0., 0., 0. + if cb.Margin != nil { + m := cb.Margin + if m.Name != "" && m.Name[0] == '$' { + // use named margin + mName := m.Name[1:] + m0 := cb.margin(mName) + if m0 == nil { + return mTop, mRight, mBottom, mLeft, errors.Errorf("pdfcpu: unknown named margin %s", mName) + } + m.mergeIn(m0) + } + + if m.Width > 0 { + mTop = m.Width + mRight = m.Width + mBottom = m.Width + mLeft = m.Width + } else { + mTop = m.Top + mRight = m.Right + mBottom = m.Bottom + mLeft = m.Left + } + } + return mTop, mRight, mBottom, mLeft, nil +} + +func (cb *CheckBox) labelPos(labelHeight, w, g float64) (float64, float64) { + + var x, y float64 + bb, horAlign := cb.boundingBox, cb.Label.HorAlign + + switch cb.Label.relPos { + + case types.RelPosLeft: + x = bb.LL.X - g + if horAlign == types.AlignLeft { + x -= w + if x < 0 { + x = 0 + } + } + y = bb.LL.Y + + case types.RelPosRight: + x = bb.UR.X + g + if horAlign == types.AlignRight { + x += w + } + y = bb.LL.Y + + case types.RelPosTop: + y = bb.UR.Y + g + x = bb.LL.X + if horAlign == types.AlignRight { + x += bb.Width() + } else if horAlign == types.AlignCenter { + x += bb.Width() / 2 + } + + case types.RelPosBottom: + y = bb.LL.Y - g - labelHeight + x = bb.LL.X + if horAlign == types.AlignRight { + x += bb.Width() + } else if horAlign == types.AlignCenter { + x += bb.Width() / 2 + } + } + + return x, y +} + +func (cb *CheckBox) ensureZapfDingbats(fonts model.FontMap) (*types.IndirectRef, error) { + // TODO Refactor + pdf := cb.pdf + fontName := "ZapfDingbats" + font, ok := fonts[fontName] + if ok { + if font.Res.IndRef != nil { + return font.Res.IndRef, nil + } + ir, err := pdffont.EnsureFontDict(pdf.XRefTable, fontName, "", "", false, nil) + if err != nil { + return nil, err + } + font.Res.IndRef = ir + fonts[fontName] = font + return ir, nil + } + + var ( + indRef *types.IndirectRef + err error + ) + + if pdf.Update() { + + for objNr, fo := range pdf.Optimize.FormFontObjects { + //fmt.Printf("searching for %s - obj:%d fontName:%s prefix:%s\n", fontName, objNr, fo.FontName, fo.Prefix) + if fontName == fo.FontName { + //fmt.Println("Match!") + indRef = types.NewIndirectRef(objNr, 0) + break + } + } + + if indRef == nil { + for objNr, fo := range pdf.Optimize.FontObjects { + if fontName == fo.FontName { + indRef = types.NewIndirectRef(objNr, 0) + break + } + } + } + } + + if indRef == nil { + indRef, err = pdffont.EnsureFontDict(pdf.XRefTable, fontName, "", "", false, nil) + if err != nil { + return nil, err + } + } + + font.Res = model.Resource{IndRef: indRef} + + fonts[fontName] = font + + return indRef, nil +} + +func (cb *CheckBox) calcFont() error { + + if cb.Label != nil { + f, err := cb.content.calcLabelFont(cb.Label.Font) + if err != nil { + return err + } + cb.Label.Font = f + } + + return nil +} + +func (cb *CheckBox) irNOff(bgCol *color.SimpleColor) (*types.IndirectRef, error) { + + pdf := cb.pdf + + ap, found := pdf.CheckBoxAPs[cb.Width] + if found && ap.irNOffL != nil { + return ap.irNOffL, nil + } + + buf := new(bytes.Buffer) + + fmt.Fprint(buf, "q ") + if bgCol != nil { + fmt.Fprintf(buf, "%.2f %.2f %.2f rg ", bgCol.R, bgCol.G, bgCol.B) + } else { + fmt.Fprint(buf, "1 g ") + } + + fmt.Fprintf(buf, "0 0 %.1f %.1f re f 0.5 0.5 %.1f %.1f re s Q ", cb.Width, cb.Width, cb.Width-1, cb.Width-1) + + sd, err := pdf.XRefTable.NewStreamDictForBuf(buf.Bytes()) + if err != nil { + return nil, err + } + + sd.InsertName("Type", "XObject") + sd.InsertName("Subtype", "Form") + sd.InsertInt("FormType", 1) + sd.Insert("BBox", types.NewNumberArray(0, 0, cb.Width, cb.Width)) + sd.Insert("Matrix", types.NewNumberArray(1, 0, 0, 1, 0, 0)) + + if err := sd.Encode(); err != nil { + return nil, err + } + + ir, err := pdf.XRefTable.IndRefForNewObject(*sd) + if err != nil { + return nil, err + } + + if !found { + ap = &AP{} + pdf.CheckBoxAPs[cb.Width] = ap + } + ap.irNOffL = ir + + return ir, nil +} + +func (cb *CheckBox) irNYes(fonts model.FontMap, bgCol *color.SimpleColor) (*types.IndirectRef, error) { + + pdf := cb.pdf + + ap, found := pdf.CheckBoxAPs[cb.Width] + if found && ap.irNYesL != nil { + return ap.irNYesL, nil + } + + buf := new(bytes.Buffer) + + fmt.Fprint(buf, "q ") + if bgCol != nil { + fmt.Fprintf(buf, "%.2f %.2f %.2f rg ", bgCol.R, bgCol.G, bgCol.B) + } else { + fmt.Fprint(buf, "1 g ") + } + + s, x, y := 14.532/18, 2.853/18, 4.081/18 + fmt.Fprintf(buf, "0 0 %.1f %.1f re f 0.5 0.5 %.1f %.1f re s Q ", cb.Width, cb.Width, cb.Width-1, cb.Width-1) + fmt.Fprintf(buf, "q 1 1 %.1f %.1f re W n BT /F0 %f Tf %f %f Td (4) Tj ET Q ", cb.Width-2, cb.Width-2, s*cb.Width, x*cb.Width, y*cb.Width) + sd, err := pdf.XRefTable.NewStreamDictForBuf(buf.Bytes()) + if err != nil { + return nil, err + } + + sd.InsertName("Type", "XObject") + sd.InsertName("Subtype", "Form") + sd.InsertInt("FormType", 1) + sd.Insert("BBox", types.NewNumberArray(0, 0, cb.Width, cb.Width)) + sd.Insert("Matrix", types.NewNumberArray(1, 0, 0, 1, 0, 0)) + + ir, err := cb.ensureZapfDingbats(fonts) + if err != nil { + return nil, err + } + + d := types.Dict( + map[string]types.Object{ + "Font": types.Dict( + map[string]types.Object{ + "F0": *ir, + }, + ), + }, + ) + + sd.Insert("Resources", d) + + if err := sd.Encode(); err != nil { + return nil, err + } + + ir, err = pdf.XRefTable.IndRefForNewObject(*sd) + if err != nil { + return nil, err + } + + if !found { + ap = &AP{} + pdf.CheckBoxAPs[cb.Width] = ap + } + ap.irNYesL = ir + + return ir, nil +} + +func (cb *CheckBox) irDOff(bgCol *color.SimpleColor) (*types.IndirectRef, error) { + + pdf := cb.pdf + + ap, found := cb.pdf.CheckBoxAPs[cb.Width] + if found && ap.irDOffL != nil { + return ap.irDOffL, nil + } + + buf := fmt.Sprintf("q 0.75293 g 0 0 %.1f %.1f re f 0.5 0.5 %.1f %.1f re se Q ", cb.Width, cb.Width, cb.Width-1, cb.Width-1) + sd, err := pdf.XRefTable.NewStreamDictForBuf([]byte(buf)) + if err != nil { + return nil, err + } + + sd.InsertName("Type", "XObject") + sd.InsertName("Subtype", "Form") + sd.InsertInt("FormType", 1) + sd.Insert("BBox", types.NewNumberArray(0, 0, cb.Width, cb.Width)) + sd.Insert("Matrix", types.NewNumberArray(1, 0, 0, 1, 0, 0)) + + if err := sd.Encode(); err != nil { + return nil, err + } + + ir, err := pdf.XRefTable.IndRefForNewObject(*sd) + if err != nil { + return nil, err + } + + if !found { + ap = &AP{} + pdf.CheckBoxAPs[cb.Width] = ap + } + ap.irDOffL = ir + + return ir, nil +} + +func (cb *CheckBox) irDYes(fonts model.FontMap, bgCol *color.SimpleColor) (*types.IndirectRef, error) { + + pdf := cb.pdf + + ap, found := pdf.CheckBoxAPs[cb.Width] + if found && ap.irDYesL != nil { + return ap.irDYesL, nil + } + + s, x, y := 14.532/18, 2.853/18, 4.081/18 + buf := fmt.Sprintf("q 0.75293 g 0 0 %.1f %.1f re f 0.5 0.5 %.1f %.1f re se Q ", cb.Width, cb.Width, cb.Width-1, cb.Width-1) + buf += fmt.Sprintf("q 1 1 %.1f %.1f re W n BT /F0 %f Tf %f %f Td (4) Tj ET Q ", cb.Width-2, cb.Width-2, s*cb.Width, x*cb.Width, y*cb.Width) + sd, _ := cb.pdf.XRefTable.NewStreamDictForBuf([]byte(buf)) + sd.InsertName("Type", "XObject") + sd.InsertName("Subtype", "Form") + sd.InsertInt("FormType", 1) + sd.Insert("BBox", types.NewNumberArray(0, 0, cb.Width, cb.Width)) + sd.Insert("Matrix", types.NewNumberArray(1, 0, 0, 1, 0, 0)) + + ir, err := cb.ensureZapfDingbats(fonts) + if err != nil { + return nil, err + } + + d := types.Dict( + map[string]types.Object{ + "Font": types.Dict( + map[string]types.Object{ + "F0": *ir, + }, + ), + }, + ) + + sd.Insert("Resources", d) + + if err := sd.Encode(); err != nil { + return nil, err + } + + ir, err = pdf.XRefTable.IndRefForNewObject(*sd) + if err != nil { + return nil, err + } + + if !found { + ap = &AP{} + pdf.CheckBoxAPs[cb.Width] = ap + } + ap.irDYesL = ir + + return ir, nil +} + +func (cb *CheckBox) appearanceIndRefs(fonts model.FontMap, bgCol *color.SimpleColor) ( + *types.IndirectRef, *types.IndirectRef, *types.IndirectRef, *types.IndirectRef, error) { + + irDOff, err := cb.irDOff(bgCol) + if err != nil { + return nil, nil, nil, nil, err + } + + irDYes, err := cb.irDYes(fonts, bgCol) + if err != nil { + return nil, nil, nil, nil, err + } + + irNOff, err := cb.irNOff(bgCol) + if err != nil { + return nil, nil, nil, nil, err + } + + irNYes, err := cb.irNYes(fonts, bgCol) + if err != nil { + return nil, nil, nil, nil, err + } + + return irDOff, irDYes, irNOff, irNYes, nil +} + +func (cb *CheckBox) prepareDict(fonts model.FontMap) (types.Dict, error) { + + id, err := types.EscapeUTF16String(cb.ID) + if err != nil { + return nil, err + } + + v := "Off" + if cb.Value { + v = "Yes" + } + + dv := "Off" + if cb.Default { + dv = "Yes" + if !cb.Value { + v = "Yes" + } + } + + bgCol := cb.bgCol + if bgCol == nil { + bgCol = cb.content.page.bgCol + if bgCol == nil { + bgCol = cb.pdf.bgCol + } + } + + irDOff, irDYes, irNOff, irNYes, err := cb.appearanceIndRefs(fonts, bgCol) + if err != nil { + return nil, err + } + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("Widget"), + "FT": types.Name("Btn"), + "Rect": cb.boundingBox.Array(), + "F": types.Integer(model.AnnPrint), + "T": types.StringLiteral(*id), + "V": types.Name(v), // -> extractValue: Off or Yes + "DV": types.Name(dv), + "AS": types.Name(v), + "AP": types.Dict( + map[string]types.Object{ + "D": types.Dict( + map[string]types.Object{ + "Off": *irDOff, + "Yes": *irDYes, + }, + ), + "N": types.Dict( + map[string]types.Object{ + "Off": *irNOff, + "Yes": *irNYes, + }, + ), + }, + ), + }, + ) + + if cb.Tip != "" { + tu, err := types.EscapeUTF16String(cb.Tip) + if err != nil { + return nil, err + } + d["TU"] = types.StringLiteral(*tu) + } + + if bgCol != nil { + appCharDict := types.Dict{} + if bgCol != nil { + appCharDict["BG"] = bgCol.Array() + } + d["MK"] = appCharDict + } + + if cb.Locked { + d["Ff"] = types.Integer(FieldReadOnly) + } + + return d, nil +} + +func (cb *CheckBox) bbox() *types.Rectangle { + if cb.Label == nil { + return cb.boundingBox.Clone() + } + + l := cb.Label + var r *types.Rectangle + x := l.td.X + + switch l.td.HAlign { + case types.AlignCenter: + x -= float64(l.Width) / 2 + case types.AlignRight: + x -= float64(l.Width) + } + + y := l.td.Y + if l.relPos == types.RelPosLeft || l.relPos == types.RelPosRight { + y -= cb.boundingBox.Height() / 2 + } + r = types.RectForWidthAndHeight(x, y, float64(l.Width), l.height) + + return model.CalcBoundingBoxForRects(cb.boundingBox, r) +} + +func (cb *CheckBox) prepareRectLL(mTop, mRight, mBottom, mLeft float64) (float64, float64) { + return cb.content.calcPosition(cb.x, cb.y, cb.Dx, cb.Dy, mTop, mRight, mBottom, mLeft) +} + +func (cb *CheckBox) prepLabel(p *model.Page, pageNr int, fonts model.FontMap) error { + + if cb.Label == nil { + return nil + } + + l := cb.Label + + v := "Default" + if l.Value != "" { + v = l.Value + } + + w := float64(l.Width) + g := float64(l.Gap) + + f := l.Font + fontName, fontLang, col := f.Name, f.Lang, f.col + + id, err := cb.pdf.idForFontName(fontName, fontLang, p.Fm, fonts, pageNr) + if err != nil { + return err + } + + td := model.TextDescriptor{ + Text: v, + FontName: fontName, + Embed: true, + FontKey: id, + FontSize: f.Size, + Scale: 1., + ScaleAbs: true, + RTL: l.RTL, + } + + if col != nil { + td.StrokeCol, td.FillCol = *col, *col + } + + if l.BgCol != nil { + td.ShowBackground, td.ShowTextBB, td.BackgroundCol = true, true, *l.BgCol + } + + bb := model.WriteMultiLine(cb.pdf.XRefTable, new(bytes.Buffer), types.RectForFormat("A4"), nil, td) + l.height = bb.Height() + if bb.Width() > w { + w = bb.Width() + l.Width = int(bb.Width()) + } + + td.X, td.Y = cb.labelPos(l.height, w, g) + td.HAlign, td.VAlign = l.HorAlign, types.AlignBottom + + if l.relPos == types.RelPosLeft || l.relPos == types.RelPosRight { + td.Y += cb.boundingBox.Height() / 2 + td.VAlign = types.AlignMiddle + } + + l.td = &td + + return nil +} + +func (cb *CheckBox) prepForRender(p *model.Page, pageNr int, fonts model.FontMap) error { + + mTop, mRight, mBottom, mLeft, err := cb.calcMargin() + if err != nil { + return err + } + + x, y := cb.prepareRectLL(mTop, mRight, mBottom, mLeft) + + if err := cb.calcFont(); err != nil { + return err + } + + cb.boundingBox = types.RectForWidthAndHeight(x, y, cb.Width, cb.Width) + + return cb.prepLabel(p, pageNr, fonts) +} + +func (cb *CheckBox) doRender(p *model.Page, fonts model.FontMap) error { + + d, err := cb.prepareDict(fonts) + if err != nil { + return err + } + + ann := model.FieldAnnotation{Dict: d} + if cb.Tab > 0 { + p.AnnotTabs[cb.Tab] = ann + } else { + p.Annots = append(p.Annots, ann) + } + + if cb.Label != nil { + model.WriteColumn(cb.pdf.XRefTable, p.Buf, p.MediaBox, nil, *cb.Label.td, 0) + } + + if cb.Debug || cb.pdf.Debug { + cb.pdf.highlightPos(p.Buf, cb.boundingBox.LL.X, cb.boundingBox.LL.Y, cb.content.Box()) + } + + return nil +} + +func (cb *CheckBox) render(p *model.Page, pageNr int, fonts model.FontMap) error { + + if err := cb.prepForRender(p, pageNr, fonts); err != nil { + return err + } + + return cb.doRender(p, fonts) +} + +func CalcCheckBoxASNames(d types.Dict) (types.Name, types.Name) { + apDict := d.DictEntry("AP") + d1 := apDict.DictEntry("D") + if d1 == nil { + d1 = apDict.DictEntry("N") + } + offName, yesName := "Off", "Yes" + for k := range d1 { + if k != "Off" { + yesName = k + } + } + return types.Name(offName), types.Name(yesName) +} diff --git a/pkg/pdfcpu/primitives/comboBox.go b/pkg/pdfcpu/primitives/comboBox.go new file mode 100644 index 0000000000000000000000000000000000000000..f3764f7cafd4b9ed5d7db26f155a43a21624e8ac --- /dev/null +++ b/pkg/pdfcpu/primitives/comboBox.go @@ -0,0 +1,811 @@ +/* + Copyright 2022 The pdfcpu Authors. + + 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 + + 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 primitives + +import ( + "bytes" + "fmt" + "unicode/utf8" + + "github.com/pdfcpu/pdfcpu/pkg/font" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + pdffont "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/font" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// ComboBox represents a specific choice form field including a positioned label. +type ComboBox struct { + pdf *PDF + content *Content + Label *TextFieldLabel + ID string + Tip string + Default string + Value string + Options []string + Position [2]float64 `json:"pos"` + x, y float64 + Width float64 + Dx, Dy float64 + BoundingBox *types.Rectangle `json:"-"` + Edit bool + Font *FormFont + fontID string `json:"-"` + Margin *Margin + Border *Border + BackgroundColor string `json:"bgCol"` + BgCol *color.SimpleColor `json:"-"` + Alignment string `json:"align"` // "Left", "Center", "Right" + HorAlign types.HAlignment `json:"-"` + RTL bool + Tab int + Locked bool + Debug bool + Hide bool +} + +func (cb *ComboBox) SetFontID(s string) { + cb.fontID = s +} + +func (cb *ComboBox) validateID() error { + if cb.ID == "" { + return errors.New("pdfcpu: missing field id") + } + if cb.pdf.DuplicateField(cb.ID) { + return errors.Errorf("pdfcpu: duplicate form field: %s", cb.ID) + } + cb.pdf.FieldIDs[cb.ID] = true + return nil +} + +func (cb *ComboBox) validatePosition() error { + if cb.Position[0] < 0 || cb.Position[1] < 0 { + return errors.Errorf("pdfcpu: field: %s pos value < 0", cb.ID) + } + cb.x, cb.y = cb.Position[0], cb.Position[1] + return nil +} + +func (cb *ComboBox) validateWidth() error { + if cb.Width == 0 { + return errors.Errorf("pdfcpu: field: %s width == 0", cb.ID) + } + return nil +} + +func (cb *ComboBox) validateOptionsValueAndDefault() error { + if len(cb.Options) == 0 { + return errors.Errorf("pdfcpu: field: %s missing options", cb.ID) + } + + if len(cb.Value) > 0 && !types.MemberOf(cb.Value, cb.Options) { + return errors.Errorf("pdfcpu: field: %s invalid value: %s", cb.ID, cb.Value) + } + + if len(cb.Default) > 0 && !types.MemberOf(cb.Default, cb.Options) { + return errors.Errorf("pdfcpu: field: %s invalid default: %s", cb.ID, cb.Default) + } + + return nil +} + +func (cb *ComboBox) validateFont() error { + if cb.Font != nil { + cb.Font.pdf = cb.pdf + if err := cb.Font.validate(); err != nil { + return err + } + } + return nil +} + +func (cb *ComboBox) validateMargin() error { + if cb.Margin != nil { + if err := cb.Margin.validate(); err != nil { + return err + } + } + return nil +} + +func (cb *ComboBox) validateBorder() error { + if cb.Border != nil { + cb.Border.pdf = cb.pdf + if err := cb.Border.validate(); err != nil { + return err + } + } + return nil +} + +func (cb *ComboBox) validateBackgroundColor() error { + if cb.BackgroundColor != "" { + sc, err := cb.pdf.parseColor(cb.BackgroundColor) + if err != nil { + return err + } + cb.BgCol = sc + } + return nil +} + +func (cb *ComboBox) validateHorAlign() error { + cb.HorAlign = types.AlignLeft + if cb.Alignment != "" { + ha, err := types.ParseHorAlignment(cb.Alignment) + if err != nil { + return err + } + cb.HorAlign = ha + } + return nil +} + +func (cb *ComboBox) validateLabel() error { + if cb.Label != nil { + cb.Label.pdf = cb.pdf + if err := cb.Label.validate(); err != nil { + return err + } + } + return nil +} + +func (cb *ComboBox) validateTab() error { + if cb.Tab < 0 { + return errors.Errorf("pdfcpu: field: %s negative tab value", cb.ID) + } + if cb.Tab == 0 { + return nil + } + page := cb.content.page + if page.Tabs == nil { + page.Tabs = types.IntSet{} + } else { + if page.Tabs[cb.Tab] { + return errors.Errorf("pdfcpu: field: %s duplicate tab value %d", cb.ID, cb.Tab) + } + } + page.Tabs[cb.Tab] = true + return nil +} + +func (cb *ComboBox) validate() error { + + if err := cb.validateID(); err != nil { + return err + } + + if err := cb.validatePosition(); err != nil { + return err + } + + if err := cb.validateWidth(); err != nil { + return err + } + + if err := cb.validateOptionsValueAndDefault(); err != nil { + return err + } + + if err := cb.validateFont(); err != nil { + return err + } + + if err := cb.validateMargin(); err != nil { + return err + } + + if err := cb.validateBorder(); err != nil { + return err + } + + if err := cb.validateBackgroundColor(); err != nil { + return err + } + + if err := cb.validateHorAlign(); err != nil { + return err + } + + if err := cb.validateLabel(); err != nil { + return err + } + + return cb.validateTab() +} + +func (cb *ComboBox) calcFontFromDA(ctx *model.Context, d types.Dict, fonts map[string]types.IndirectRef) (*types.IndirectRef, error) { + + s := d.StringEntry("DA") + if s == nil { + s = ctx.Form.StringEntry("DA") + if s == nil { + return nil, errors.New("pdfcpu: combobox missing \"DA\"") + } + } + + fontID, f, err := fontFromDA(*s) + if err != nil { + return nil, err + } + + cb.Font, cb.fontID = &f, fontID + + id, name, lang, fontIndRef, err := extractFormFontDetails(ctx, cb.fontID, fonts) + if err != nil { + return nil, err + } + if fontIndRef == nil { + return nil, errors.New("pdfcpu: unable to detect indirect reference for font") + } + + cb.fontID = id + cb.Font.Name = name + cb.Font.Lang = lang + cb.RTL = pdffont.RTL(lang) + + return fontIndRef, nil +} + +func (cb *ComboBox) calcFont() error { + f, err := cb.content.calcInputFont(cb.Font) + if err != nil { + return err + } + cb.Font = f + + if cb.Label != nil { + f, err = cb.content.calcLabelFont(cb.Label.Font) + if err != nil { + return err + } + cb.Label.Font = f + } + + return nil +} + +func (cb *ComboBox) margin(name string) *Margin { + return cb.content.namedMargin(name) +} + +func (cb *ComboBox) calcMargin() (float64, float64, float64, float64, error) { + mTop, mRight, mBottom, mLeft := 0., 0., 0., 0. + if cb.Margin != nil { + m := cb.Margin + if m.Name != "" && m.Name[0] == '$' { + // use named margin + mName := m.Name[1:] + m0 := cb.margin(mName) + if m0 == nil { + return mTop, mRight, mBottom, mLeft, errors.Errorf("pdfcpu: unknown named margin %s", mName) + } + m.mergeIn(m0) + } + if m.Width > 0 { + mTop = m.Width + mRight = m.Width + mBottom = m.Width + mLeft = m.Width + } else { + mTop = m.Top + mRight = m.Right + mBottom = m.Bottom + mLeft = m.Left + } + } + return mTop, mRight, mBottom, mLeft, nil +} + +func (cb *ComboBox) labelPos(labelHeight, w, g float64) (float64, float64) { + + var x, y float64 + bb, horAlign := cb.BoundingBox, cb.Label.HorAlign + + switch cb.Label.relPos { + + case types.RelPosLeft, types.RelPosBottom: + x = bb.LL.X - g + if horAlign == types.AlignLeft { + x -= w + if x < 0 { + x = 0 + } + } + y = bb.LL.Y + + case types.RelPosRight: + x = bb.UR.X + g + if horAlign == types.AlignRight { + x += w + } + y = bb.LL.Y + + case types.RelPosTop: + y = bb.UR.Y + g + x = bb.LL.X + if horAlign == types.AlignRight { + x += bb.Width() + } else if horAlign == types.AlignCenter { + x += bb.Width() / 2 + } + + } + + return x, y +} + +func (cb *ComboBox) renderN(xRefTable *model.XRefTable) ([]byte, error) { + w, h := cb.BoundingBox.Width(), cb.BoundingBox.Height() + bgCol := cb.BgCol + boWidth, boCol := cb.calcBorder() + buf := new(bytes.Buffer) + + if bgCol != nil || boCol != nil { + fmt.Fprint(buf, "q ") + if bgCol != nil { + fmt.Fprintf(buf, "%.2f %.2f %.2f rg 0 0 %.2f %.2f re f ", bgCol.R, bgCol.G, bgCol.B, w, h) + } + if boCol != nil { + fmt.Fprintf(buf, "%.2f %.2f %.2f RG %.2f w %.2f %.2f %.2f %.2f re s ", + boCol.R, boCol.G, boCol.B, boWidth, boWidth/2, boWidth/2, w-boWidth, h-boWidth) + } + fmt.Fprint(buf, "Q ") + } + + fmt.Fprint(buf, "/Tx BMC q ") + fmt.Fprintf(buf, "1 1 %.2f %.2f re W n ", w-2, h-2) + + f := cb.Font + + v := cb.Default + if cb.Value != "" { + v = cb.Value + } + + //cjk := fo.CJK(f.Script, f.Lang) + if font.IsCoreFont(f.Name) && utf8.ValidString(v) { + v = model.DecodeUTF8ToByte(v) + } + lineBB := model.CalcBoundingBox(v, 0, 0, f.Name, f.Size) + s := model.PrepBytes(xRefTable, v, f.Name, true, cb.RTL) + x := 2 * boWidth + if x == 0 { + x = 2 + } + switch cb.HorAlign { + case types.AlignCenter: + x = w/2 - lineBB.Width()/2 + case types.AlignRight: + x = w - lineBB.Width() - 2 + } + + y := (cb.BoundingBox.Height()-font.LineHeight(f.Name, f.Size))/2 + font.Descent(f.Name, f.Size) + + fmt.Fprintf(buf, "BT /%s %d Tf ", cb.fontID, f.Size) + fmt.Fprintf(buf, "%.2f %.2f %.2f RG %.2f %.2f %.2f rg %.2f %.2f Td (%s) Tj ET ", + f.col.R, f.col.G, f.col.B, + f.col.R, f.col.G, f.col.B, x, y, s) + + fmt.Fprint(buf, "Q EMC ") + + if boCol != nil && boWidth > 0 { + fmt.Fprintf(buf, "q %.2f %.2f %.2f RG %.2f w %.2f %.2f %.2f %.2f re s Q ", + boCol.R, boCol.G, boCol.B, boWidth-1, boWidth/2, boWidth/2, w-boWidth, h-boWidth) + } + + return buf.Bytes(), nil +} + +func (cb *ComboBox) calcBorder() (boWidth float64, boCol *color.SimpleColor) { + if cb.Border == nil { + return 0, nil + } + return cb.Border.calc() +} + +func (cb *ComboBox) prepareFF() FieldFlags { + ff := FieldFlags(0) + ff += FieldCombo + if cb.Edit { + // Note: unsupported in Mac Preview + ff += FieldEdit + FieldDoNotSpellCheck + } + if cb.Locked { + // Note: unsupported in Mac Preview + ff += FieldReadOnly + } + return ff +} + +func (cb *ComboBox) handleBorderAndMK(d types.Dict) { + bgCol := cb.BgCol + if bgCol == nil { + bgCol = cb.content.page.bgCol + if bgCol == nil { + bgCol = cb.pdf.bgCol + } + } + cb.BgCol = bgCol + + boWidth, boCol := cb.calcBorder() + + if bgCol != nil || boCol != nil { + appCharDict := types.Dict{} + if bgCol != nil { + appCharDict["BG"] = bgCol.Array() + } + if boCol != nil && cb.Border.Width > 0 { + appCharDict["BC"] = boCol.Array() + } + d["MK"] = appCharDict + } + + if boWidth > 0 { + d["Border"] = types.NewNumberArray(0, 0, boWidth) + } +} + +func (cb *ComboBox) prepareDict(fonts model.FontMap) (types.Dict, error) { + pdf := cb.pdf + + id, err := types.EscapeUTF16String(cb.ID) + if err != nil { + return nil, err + } + + opt := types.Array{} + for _, s := range cb.Options { + s, err := types.EscapeUTF16String(s) + if err != nil { + return nil, err + } + opt = append(opt, types.StringLiteral(*s)) + } + + ff := cb.prepareFF() + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("Widget"), + "FT": types.Name("Ch"), + "Rect": cb.BoundingBox.Array(), + "F": types.Integer(model.AnnPrint), + "Ff": types.Integer(ff), + "Opt": opt, + "Q": types.Integer(cb.HorAlign), + "T": types.StringLiteral(*id), + }, + ) + + if cb.Tip != "" { + tu, err := types.EscapeUTF16String(cb.Tip) + if err != nil { + return nil, err + } + d["TU"] = types.StringLiteral(*tu) + } + + cb.handleBorderAndMK(d) + + v := cb.Value + if cb.Default != "" { + s, err := types.EscapeUTF16String(cb.Default) + if err != nil { + return nil, err + } + d["DV"] = types.StringLiteral(*s) + if v == "" { + v = cb.Default + } + } + + ind := types.Array{} + for i, o := range cb.Options { + if o == v { + ind = append(ind, types.Integer(i)) + break + } + } + s, err := types.EscapeUTF16String(v) + if err != nil { + return nil, err + } + d["V"] = types.StringLiteral(*s) + d["I"] = ind + + if pdf.InheritedDA != "" { + d["DA"] = types.StringLiteral(pdf.InheritedDA) + } + + f := cb.Font + fCol := f.col + + fontID, err := pdf.ensureFormFont(f) + if err != nil { + return d, err + } + cb.fontID = fontID + + da := fmt.Sprintf("/%s %d Tf %.2f %.2f %.2f rg", fontID, f.Size, fCol.R, fCol.G, fCol.B) + // Note: Mac Preview does not honour inherited "DA" + d["DA"] = types.StringLiteral(da) + + return d, nil +} + +func (cb *ComboBox) bbox() *types.Rectangle { + if cb.Label == nil { + return cb.BoundingBox.Clone() + } + + l := cb.Label + var r *types.Rectangle + x := l.td.X + + switch l.td.HAlign { + case types.AlignCenter: + x -= float64(l.Width) / 2 + case types.AlignRight: + x -= float64(l.Width) + } + + r = types.RectForWidthAndHeight(x, l.td.Y, float64(l.Width), l.height) + + return model.CalcBoundingBoxForRects(cb.BoundingBox, r) +} + +func (cb *ComboBox) prepareRectLL(mTop, mRight, mBottom, mLeft float64) (float64, float64) { + return cb.content.calcPosition(cb.x, cb.y, cb.Dx, cb.Dy, mTop, mRight, mBottom, mLeft) +} + +func (cb *ComboBox) prepLabel(p *model.Page, pageNr int, fonts model.FontMap) error { + + if cb.Label == nil { + return nil + } + + l := cb.Label + v := l.Value + w := float64(l.Width) + g := float64(l.Gap) + + f := l.Font + fontName, fontLang, col := f.Name, f.Lang, f.col + + id, err := cb.pdf.idForFontName(fontName, fontLang, p.Fm, fonts, pageNr) + if err != nil { + return err + } + + td := model.TextDescriptor{ + Text: v, + FontName: fontName, + Embed: true, + FontKey: id, + FontSize: f.Size, + Scale: 1., + ScaleAbs: true, + RTL: l.RTL, + } + + if col != nil { + td.StrokeCol, td.FillCol = *col, *col + } + + if l.BgCol != nil { + td.ShowBackground, td.ShowTextBB, td.BackgroundCol = true, true, *l.BgCol + } + + bb := model.WriteMultiLine(cb.pdf.XRefTable, new(bytes.Buffer), types.RectForFormat("A4"), nil, td) + l.height = bb.Height() + 10 + + // Weird heuristic for vertical alignment with label + if f.Size >= 24 { + td.MTop, td.MBot = 6, 4 + } else if f.Size >= 12 { + td.MTop, td.MBot = 5, 5 + } else { + td.MTop, td.MBot = 6, 4 + } + + if bb.Width() > w { + w = bb.Width() + l.Width = int(bb.Width()) + } + + td.X, td.Y = cb.labelPos(l.height, w, g) + td.HAlign, td.VAlign = l.HorAlign, types.AlignBottom + + l.td = &td + + return nil +} + +func (cb *ComboBox) prepForRender(p *model.Page, pageNr int, fonts model.FontMap) error { + + mTop, mRight, mBottom, mLeft, err := cb.calcMargin() + if err != nil { + return err + } + + x, y := cb.prepareRectLL(mTop, mRight, mBottom, mLeft) + + if err := cb.calcFont(); err != nil { + return err + } + + td := model.TextDescriptor{ + Text: "Xy", + FontName: cb.Font.Name, + Embed: true, + FontSize: cb.Font.Size, + Scale: 1., + ScaleAbs: true, + } + + bb := model.WriteMultiLine(cb.pdf.XRefTable, new(bytes.Buffer), types.RectForFormat("A4"), nil, td) + + if cb.Width < 0 { + // Extend width to maxWidth. + r := cb.content.Box().CroppedCopy(0) + r.LL.X += mLeft + r.LL.Y += mBottom + r.UR.X -= mRight + r.UR.Y -= mTop + cb.Width = r.Width() - cb.x + + } + + cb.BoundingBox = types.RectForWidthAndHeight(x, y, cb.Width, bb.Height()+10) + + return cb.prepLabel(p, pageNr, fonts) +} + +func (cb *ComboBox) doRender(p *model.Page, fonts model.FontMap) error { + + d, err := cb.prepareDict(fonts) + if err != nil { + return err + } + + ann := model.FieldAnnotation{Dict: d} + if cb.Tab > 0 { + p.AnnotTabs[cb.Tab] = ann + } else { + p.Annots = append(p.Annots, ann) + } + + if cb.Label != nil { + model.WriteColumn(cb.pdf.XRefTable, p.Buf, p.MediaBox, nil, *cb.Label.td, 0) + } + + if cb.Debug || cb.pdf.Debug { + cb.pdf.highlightPos(p.Buf, cb.BoundingBox.LL.X, cb.BoundingBox.LL.Y, cb.content.Box()) + } + + return nil +} + +func (cb *ComboBox) render(p *model.Page, pageNr int, fonts model.FontMap) error { + + if err := cb.prepForRender(p, pageNr, fonts); err != nil { + return err + } + + return cb.doRender(p, fonts) +} + +// NewComboBox creates a new combobox for d. +func NewComboBox( + ctx *model.Context, + d types.Dict, + v string, + fonts map[string]types.IndirectRef) (*ComboBox, *types.IndirectRef, error) { + + cb := &ComboBox{Value: v} + + bb, err := ctx.RectForArray(d.ArrayEntry("Rect")) + if err != nil { + return nil, nil, err + } + + cb.BoundingBox = types.RectForDim(bb.Width(), bb.Height()) + + fontIndRef, err := cb.calcFontFromDA(ctx, d, fonts) + if err != nil { + return nil, nil, err + } + + cb.HorAlign = types.AlignLeft + if q := d.IntEntry("Q"); q != nil { + cb.HorAlign = types.HAlignment(*q) + } + + bgCol, boCol, err := calcColsFromMK(ctx, d) + if err != nil { + return nil, nil, err + } + cb.BgCol = bgCol + + var b Border + boWidth := calcBorderWidth(d) + if boWidth > 0 { + b.Width = boWidth + b.col = boCol + } + cb.Border = &b + + return cb, fontIndRef, nil +} + +func renderComboBoxAP(ctx *model.Context, d types.Dict, v string, fonts map[string]types.IndirectRef) error { + + cb, fontIndRef, err := NewComboBox(ctx, d, v, fonts) + if err != nil { + return err + } + + bb, err := cb.renderN(ctx.XRefTable) + if err != nil { + return err + } + + irN, err := NewForm(ctx.XRefTable, bb, cb.fontID, fontIndRef, cb.BoundingBox) + if err != nil { + return err + } + + d["AP"] = types.Dict(map[string]types.Object{"N": *irN}) + + return nil +} + +func refreshComboBoxAP(ctx *model.Context, d types.Dict, v string, fonts map[string]types.IndirectRef, irN *types.IndirectRef) error { + + cb, _, err := NewComboBox(ctx, d, v, fonts) + if err != nil { + return err + } + + bb, err := cb.renderN(ctx.XRefTable) + if err != nil { + return err + } + + return updateForm(ctx.XRefTable, bb, irN) +} + +func EnsureComboBoxAP(ctx *model.Context, d types.Dict, v string, fonts map[string]types.IndirectRef) error { + + apd := d.DictEntry("AP") + if apd == nil { + return renderComboBoxAP(ctx, d, v, fonts) + } + + irN := apd.IndirectRefEntry("N") + if irN == nil { + return nil + } + + return refreshComboBoxAP(ctx, d, v, fonts, irN) +} diff --git a/pkg/pdfcpu/primitives/content.go b/pkg/pdfcpu/primitives/content.go new file mode 100644 index 0000000000000000000000000000000000000000..4765103fdf798112c39afcf89c42468d89a70c82 --- /dev/null +++ b/pkg/pdfcpu/primitives/content.go @@ -0,0 +1,1320 @@ +/* + Copyright 2021 The pdfcpu Authors. + + 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 + + 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 primitives + +import ( + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// Content represents page content. +type Content struct { + parent *Content + page *PDFPage + BackgroundColor string `json:"bgCol"` + bgCol *color.SimpleColor // page background color + Fonts map[string]*FormFont // named fonts + Margins map[string]*Margin // named margins + Borders map[string]*Border // named borders + Paddings map[string]*Padding // named paddings + Margin *Margin // content margin + Border *Border // content border + Padding *Padding // content padding + Regions *Regions + mediaBox *types.Rectangle + borderRect *types.Rectangle + box *types.Rectangle + Guides []*Guide // hor/vert guidelines for layout + Bars []*Bar `json:"bar"` + SimpleBoxes []*SimpleBox `json:"box"` + SimpleBoxPool map[string]*SimpleBox `json:"boxes"` + TextBoxes []*TextBox `json:"text"` + TextBoxPool map[string]*TextBox `json:"texts"` + ImageBoxes []*ImageBox `json:"image"` + ImageBoxPool map[string]*ImageBox `json:"images"` + Tables []*Table `json:"table"` + TablePool map[string]*Table `json:"tables"` + // Form elements + TextFields []*TextField `json:"textfield"` // input text fields with optional label + DateFields []*DateField `json:"datefield"` // input date fields with optional label + CheckBoxes []*CheckBox `json:"checkbox"` // input checkboxes with optional label + RadioButtonGroups []*RadioButtonGroup `json:"radiobuttongroup"` // input radiobutton groups with optional label + ComboBoxes []*ComboBox `json:"combobox"` + ListBoxes []*ListBox `json:"listbox"` + FieldGroups []*FieldGroup `json:"fieldgroup"` // rectangular container holding form elements + FieldGroupPool map[string]*FieldGroup `json:"fieldgroups"` +} + +func (c *Content) validateBackgroundColor() error { + if c.BackgroundColor != "" { + sc, err := c.page.pdf.parseColor(c.BackgroundColor) + if err != nil { + return err + } + c.bgCol = sc + } + return nil +} + +func (c *Content) validateBorders() error { + pdf := c.page.pdf + if c.Border != nil { + if len(c.Borders) > 0 { + return errors.New("pdfcpu: Please supply either content \"border\" or \"borders\"") + } + c.Border.pdf = pdf + if err := c.Border.validate(); err != nil { + return err + } + c.Borders = map[string]*Border{} + c.Borders["border"] = c.Border + } + for _, b := range c.Borders { + b.pdf = pdf + if err := b.validate(); err != nil { + return err + } + } + return nil +} + +func (c *Content) validateMargins() error { + if c.Margin != nil { + if len(c.Margins) > 0 { + return errors.New("pdfcpu: Please supply either page \"margin\" or \"margins\"") + } + if err := c.Margin.validate(); err != nil { + return err + } + c.Margins = map[string]*Margin{} + c.Margins["margin"] = c.Margin + } + for _, m := range c.Margins { + if err := m.validate(); err != nil { + return err + } + } + return nil +} + +func (c *Content) validatePaddings() error { + if c.Padding != nil { + if len(c.Paddings) > 0 { + return errors.New("pdfcpu: Please supply either page \"padding\" or \"paddings\"") + } + if err := c.Padding.validate(); err != nil { + return err + } + c.Paddings = map[string]*Padding{} + c.Paddings["padding"] = c.Padding + } + for _, p := range c.Paddings { + if err := p.validate(); err != nil { + return err + } + } + return nil +} + +func (c *Content) validatePrimitives(s string) error { + if len(c.SimpleBoxPool) > 0 { + return errors.Errorf("pdfcpu: \"boxes\" %s", s) + } + if len(c.SimpleBoxes) > 0 { + return errors.Errorf("pdfcpu: \"box\" %s", s) + } + if len(c.TextBoxPool) > 0 { + return errors.Errorf("pdfcpu: \"texts\" %s", s) + } + if len(c.TextBoxes) > 0 { + return errors.Errorf("pdfcpu: \"text\" %s", s) + } + if len(c.ImageBoxPool) > 0 { + return errors.Errorf("pdfcpu: \"images\" %s", s) + } + if len(c.ImageBoxes) > 0 { + return errors.Errorf("pdfcpu: \"image\" %s", s) + } + if len(c.TablePool) > 0 { + return errors.Errorf("pdfcpu: \"tables\" %s", s) + } + if len(c.Tables) > 0 { + return errors.Errorf("pdfcpu: \"table\" %s", s) + } + return nil +} + +func (c *Content) validateFormPrimitives(s string) error { + if len(c.FieldGroupPool) > 0 { + return errors.Errorf("pdfcpu: \"fieldgroups\" %s", s) + } + if len(c.FieldGroups) > 0 { + return errors.Errorf("pdfcpu: \"fieldgroup\" %s", s) + } + if len(c.TextFields) > 0 { + return errors.Errorf("pdfcpu: \"textfield\" %s", s) + } + if len(c.DateFields) > 0 { + return errors.Errorf("pdfcpu: \"datefield\" %s", s) + } + if len(c.CheckBoxes) > 0 { + return errors.Errorf("pdfcpu: \"checkbox\" %s", s) + } + if len(c.RadioButtonGroups) > 0 { + return errors.Errorf("pdfcpu: \"radiobuttongroup\" %s", s) + } + if len(c.ComboBoxes) > 0 { + return errors.Errorf("pdfcpu: \"combobox\" %s", s) + } + if len(c.ListBoxes) > 0 { + return errors.Errorf("pdfcpu: \"listbox\" %s", s) + } + return nil +} + +func (c *Content) validateRegions() error { + s := "must be defined within region content" + if err := c.validatePrimitives(s); err != nil { + return err + } + if err := c.validateFormPrimitives(s); err != nil { + return err + } + c.Regions.page = c.page + c.Regions.parent = c + return c.Regions.validate() +} + +func (c *Content) validateBars() error { + // bars + for _, b := range c.Bars { + b.pdf = c.page.pdf + b.content = c + if err := b.validate(); err != nil { + return err + } + } + return nil +} + +func (c *Content) validateSimpleBoxPool() error { + // boxes + for _, sb := range c.SimpleBoxPool { + sb.pdf = c.page.pdf + sb.content = c + if err := sb.validate(); err != nil { + return err + } + } + for _, sb := range c.SimpleBoxes { + sb.pdf = c.page.pdf + sb.content = c + if err := sb.validate(); err != nil { + return err + } + } + return nil +} + +func (c *Content) validateTextBoxPool() error { + // text + for _, tb := range c.TextBoxPool { + tb.pdf = c.page.pdf + tb.content = c + if err := tb.validate(); err != nil { + return err + } + } + for _, tb := range c.TextBoxes { + tb.pdf = c.page.pdf + tb.content = c + if err := tb.validate(); err != nil { + return err + } + } + return nil +} + +func (c *Content) validateImageBoxPool() error { + // images + for _, ib := range c.ImageBoxPool { + ib.pdf = c.page.pdf + ib.content = c + if err := ib.validate(); err != nil { + return err + } + } + for _, ib := range c.ImageBoxes { + ib.pdf = c.page.pdf + ib.content = c + if err := ib.validate(); err != nil { + return err + } + } + return nil +} + +func (c *Content) validateTablePool() error { + // tables + for _, t := range c.TablePool { + t.pdf = c.page.pdf + t.content = c + if err := t.validate(); err != nil { + return err + } + } + for _, t := range c.Tables { + t.pdf = c.page.pdf + t.content = c + if err := t.validate(); err != nil { + return err + } + } + return nil +} + +func (c *Content) validateFieldGroupPool() error { + // textfield groups + for _, fg := range c.FieldGroupPool { + fg.pdf = c.page.pdf + fg.content = c + if err := fg.validate(); err != nil { + return err + } + } + return c.validateFieldGroups() +} + +func (c *Content) validatePools() error { + if err := c.validateSimpleBoxPool(); err != nil { + return err + } + if err := c.validateTextBoxPool(); err != nil { + return err + } + if err := c.validateImageBoxPool(); err != nil { + return err + } + if err := c.validateTablePool(); err != nil { + return err + } + return c.validateFieldGroupPool() +} + +func (c *Content) validateTextFields() error { + pdf := c.page.pdf + if len(c.TextFields) > 0 { + for _, tf := range c.TextFields { + tf.pdf = pdf + tf.content = c + if err := tf.validate(); err != nil { + return err + } + } + } + return nil +} + +func (c *Content) validateDateFields() error { + pdf := c.page.pdf + if len(c.DateFields) > 0 { + for _, df := range c.DateFields { + df.pdf = pdf + df.content = c + if err := df.validate(); err != nil { + return err + } + } + } + return nil +} + +func (c *Content) validateFieldGroups() error { + pdf := c.page.pdf + if len(c.FieldGroups) > 0 { + for _, fg := range c.FieldGroups { + fg.pdf = pdf + fg.content = c + if err := fg.validate(); err != nil { + return err + } + } + } + return nil +} + +func (c *Content) validateCheckBoxes() error { + pdf := c.page.pdf + if len(c.CheckBoxes) > 0 { + for _, cb := range c.CheckBoxes { + cb.pdf = pdf + cb.content = c + if err := cb.validate(); err != nil { + return err + } + } + } + return nil +} + +func (c *Content) validateRadioButtonGroups() error { + pdf := c.page.pdf + if len(c.RadioButtonGroups) > 0 { + for _, rbg := range c.RadioButtonGroups { + rbg.pdf = pdf + rbg.content = c + if err := rbg.validate(); err != nil { + return err + } + } + } + return nil +} + +func (c *Content) validateComboBoxes() error { + pdf := c.page.pdf + if len(c.ComboBoxes) > 0 { + for _, cb := range c.ComboBoxes { + cb.pdf = pdf + cb.content = c + if err := cb.validate(); err != nil { + return err + } + } + } + return nil +} + +func (c *Content) validateListBoxes() error { + pdf := c.page.pdf + if len(c.ListBoxes) > 0 { + for _, lb := range c.ListBoxes { + lb.pdf = pdf + lb.content = c + if err := lb.validate(); err != nil { + return err + } + } + } + return nil +} + +func (c *Content) validate() error { + + if err := c.validateBackgroundColor(); err != nil { + return err + } + + for _, g := range c.Guides { + g.validate() + } + + if err := c.validateBorders(); err != nil { + return err + } + + if err := c.validateMargins(); err != nil { + return err + } + + if err := c.validatePaddings(); err != nil { + return err + } + + if c.Regions != nil { + return c.validateRegions() + } + + if err := c.validateBars(); err != nil { + return err + } + + if err := c.validatePools(); err != nil { + return err + } + + if err := c.validateTextFields(); err != nil { + return err + } + + if err := c.validateDateFields(); err != nil { + return err + } + + if err := c.validateCheckBoxes(); err != nil { + return err + } + + if err := c.validateRadioButtonGroups(); err != nil { + return err + } + + if err := c.validateComboBoxes(); err != nil { + return err + } + + return c.validateListBoxes() +} + +func (c *Content) namedFont(id string) *FormFont { + f := c.Fonts[id] + if f != nil { + return f + } + if c.parent != nil { + return c.parent.namedFont(id) + } + return c.page.namedFont(id) +} + +func (c *Content) namedMargin(id string) *Margin { + m := c.Margins[id] + if m != nil { + return m + } + if c.parent != nil { + return c.parent.namedMargin(id) + } + return c.page.namedMargin(id) +} + +func (c *Content) margin() *Margin { + return c.namedMargin("margin") +} + +func (c *Content) namedBorder(id string) *Border { + b := c.Borders[id] + if b != nil { + return b + } + if c.parent != nil { + return c.parent.namedBorder(id) + } + return c.page.namedBorder(id) +} + +func (c *Content) border() *Border { + return c.namedBorder("border") +} + +func (c *Content) namedPadding(id string) *Padding { + p := c.Paddings[id] + if p != nil { + return p + } + if c.parent != nil { + return c.parent.namedPadding(id) + } + return c.page.namedPadding(id) +} + +func (c *Content) padding() *Padding { + return c.namedPadding("padding") +} + +func (c *Content) namedSimpleBox(id string) *SimpleBox { + sb := c.SimpleBoxPool[id] + if sb != nil { + return sb + } + if c.parent != nil { + return c.parent.namedSimpleBox(id) + } + return c.page.namedSimpleBox(id) +} + +func (c *Content) namedImageBox(id string) *ImageBox { + ib := c.ImageBoxPool[id] + if ib != nil { + return ib + } + if c.parent != nil { + return c.parent.namedImageBox(id) + } + return c.page.namedImageBox(id) +} + +func (c *Content) namedTextBox(id string) *TextBox { + tb := c.TextBoxPool[id] + if tb != nil { + return tb + } + if c.parent != nil { + return c.parent.namedTextBox(id) + } + return c.page.namedTextBox(id) +} + +func (c *Content) namedTable(id string) *Table { + t := c.TablePool[id] + if t != nil { + return t + } + if c.parent != nil { + return c.parent.namedTable(id) + } + return c.page.namedTable(id) +} + +func (c *Content) namedFieldGroup(id string) *FieldGroup { + fg := c.FieldGroupPool[id] + if fg != nil { + return fg + } + if c.parent != nil { + return c.parent.namedFieldGroup(id) + } + return c.page.namedFieldGroup(id) +} + +func (c *Content) calcFont(ff map[string]*FormFont) { + + fff := map[string]*FormFont{} + for id, f0 := range ff { + fff[id] = f0 + f1 := c.Fonts[id] + if f1 != nil { + f1.mergeIn(f0) + fff[id] = f1 + } + } + + if c.Regions != nil { + if c.Regions.horizontal { + c.Regions.Left.calcFont(fff) + c.Regions.Right.calcFont(fff) + } else { + c.Regions.Top.calcFont(fff) + c.Regions.Bottom.calcFont(fff) + } + } +} + +func (c *Content) mergeIn(fName string, f *FormFont) error { + f0 := c.namedFont(fName) + if f0 == nil { + return errors.Errorf("pdfcpu: missing named font \"input\"") + } + f.Name = f0.Name + if f.Size == 0 { + f.Size = f0.Size + } + if f.col == nil { + f.col = f0.col + } + if f.Lang == "" { + f.Lang = f0.Lang + } + if f.Script == "" { + f.Script = f0.Script + } + return nil +} + +func (c *Content) calcInputFont(f *FormFont) (*FormFont, error) { + + if f != nil { + if f.Name == "" { + // Inherited named font "input". + if err := c.mergeIn("input", f); err != nil { + return nil, err + } + } + if f.Name != "" && f.Name[0] == '$' { + // Inherit some other named font. + fName := f.Name[1:] + if err := c.mergeIn(fName, f); err != nil { + return nil, err + } + } + } else { + // Use inherited named font "input". + f = c.namedFont("input") + if f == nil { + return nil, errors.Errorf("pdfcpu: missing named font \"input\"") + } + } + + if f.col == nil { + f.col = &color.Black + } + + return f, nil +} + +func (c *Content) calcLabelFont(f *FormFont) (*FormFont, error) { + + if f != nil { + var f0 *FormFont + if f.Name == "" { + // Use inherited named font "label". + f0 = c.namedFont("label") + if f0 == nil { + return nil, errors.Errorf("pdfcpu: missing named font \"label\"") + } + f.Name = f0.Name + if f.Size == 0 { + f.Size = f0.Size + } + if f.col == nil { + f.col = f0.col + } + if f.Script == "" { + f.Script = f0.Script + } + } + if f.Name != "" && f.Name[0] == '$' { + // use named font + fName := f.Name[1:] + f0 := c.namedFont(fName) + if f0 == nil { + return nil, errors.Errorf("pdfcpu: unknown font name %s", fName) + } + f.Name = f0.Name + if f.Size == 0 { + f.Size = f0.Size + } + if f.col == nil { + f.col = f0.col + } + if f.Script == "" { + f.Script = f0.Script + } + } + } else { + // Use inherited named font "label". + f = c.namedFont("label") + if f == nil { + return nil, errors.Errorf("pdfcpu: missing named font \"label\"") + } + } + + if f.col == nil { + f.col = &color.Black + } + + return f, nil +} + +func (c *Content) calcBorder(bb map[string]*Border) { + + bbb := map[string]*Border{} + for id, b0 := range bb { + bbb[id] = b0 + b1 := c.Borders[id] + if b1 != nil { + b1.mergeIn(b0) + bbb[id] = b1 + } + } + + if c.Regions != nil { + if c.Regions.horizontal { + c.Regions.Left.calcBorder(bbb) + c.Regions.Right.calcBorder(bb) + } else { + c.Regions.Top.calcBorder(bbb) + c.Regions.Bottom.calcBorder(bbb) + } + } +} + +func (c *Content) calcMargin(mm map[string]*Margin) { + + mmm := map[string]*Margin{} + for id, m0 := range mm { + mmm[id] = m0 + m1 := c.Margins[id] + if m1 != nil { + m1.mergeIn(m0) + mmm[id] = m1 + } + } + + if c.Regions != nil { + if c.Regions.horizontal { + c.Regions.Left.calcMargin(mmm) + c.Regions.Right.calcMargin(mmm) + } else { + c.Regions.Top.calcMargin(mmm) + c.Regions.Bottom.calcMargin(mmm) + } + } +} + +func (c *Content) calcPadding(pp map[string]*Padding) { + + ppp := map[string]*Padding{} + for id, p0 := range pp { + ppp[id] = p0 + p1 := c.Paddings[id] + if p1 != nil { + p1.mergeIn(p0) + ppp[id] = p1 + } + } + + if c.Regions != nil { + if c.Regions.horizontal { + c.Regions.Left.calcPadding(ppp) + c.Regions.Right.calcPadding(ppp) + } else { + c.Regions.Top.calcPadding(ppp) + c.Regions.Bottom.calcPadding(ppp) + } + } +} + +func (c *Content) calcSimpleBoxes(bb map[string]*SimpleBox) { + + bbb := map[string]*SimpleBox{} + for id, sb0 := range bb { + bbb[id] = sb0 + sb1 := c.SimpleBoxPool[id] + if sb1 != nil { + sb1.mergeIn(sb0) + bbb[id] = sb1 + } + } + + if c.Regions != nil { + if c.Regions.horizontal { + c.Regions.Left.calcSimpleBoxes(bbb) + c.Regions.Right.calcSimpleBoxes(bbb) + } else { + c.Regions.Top.calcSimpleBoxes(bbb) + c.Regions.Bottom.calcSimpleBoxes(bbb) + } + } +} + +func (c *Content) calcTextBoxes(bb map[string]*TextBox) { + + bbb := map[string]*TextBox{} + for id, tb0 := range bb { + bbb[id] = tb0 + tb1 := c.TextBoxPool[id] + if tb1 != nil { + tb1.mergeIn(tb0) + bbb[id] = tb1 + } + } + + if c.Regions != nil { + if c.Regions.horizontal { + c.Regions.Left.calcTextBoxes(bbb) + c.Regions.Right.calcTextBoxes(bbb) + } else { + c.Regions.Top.calcTextBoxes(bbb) + c.Regions.Bottom.calcTextBoxes(bbb) + } + } +} + +func (c *Content) calcImageBoxes(bb map[string]*ImageBox) { + + bbb := map[string]*ImageBox{} + for id, ib0 := range bb { + bbb[id] = ib0 + ib1 := c.ImageBoxPool[id] + if ib1 != nil { + ib1.mergeIn(ib0) + bbb[id] = ib1 + } + } + + if c.Regions != nil { + if c.Regions.horizontal { + c.Regions.Left.calcImageBoxes(bbb) + c.Regions.Right.calcImageBoxes(bbb) + } else { + c.Regions.Top.calcImageBoxes(bbb) + c.Regions.Bottom.calcImageBoxes(bbb) + } + } +} + +func (c *Content) calcTables(bb map[string]*Table) { + + bbb := map[string]*Table{} + for id, t0 := range bb { + bbb[id] = t0 + t1 := c.TablePool[id] + if t1 != nil { + t1.mergeIn(t0) + bbb[id] = t1 + } + } + + if c.Regions != nil { + if c.Regions.horizontal { + c.Regions.Left.calcTables(bbb) + c.Regions.Right.calcTables(bbb) + } else { + c.Regions.Top.calcTables(bbb) + c.Regions.Bottom.calcTables(bbb) + } + } +} + +func (c *Content) calcFieldGroups(bb map[string]*FieldGroup) { + + bbb := map[string]*FieldGroup{} + for id, fg0 := range bb { + bbb[id] = fg0 + fg1 := c.FieldGroupPool[id] + if fg1 != nil { + fg1.mergeIn(fg0) + bbb[id] = fg1 + } + } + + if c.Regions != nil { + if c.Regions.horizontal { + c.Regions.Left.calcFieldGroups(bbb) + c.Regions.Right.calcFieldGroups(bbb) + } else { + c.Regions.Top.calcFieldGroups(bbb) + c.Regions.Bottom.calcFieldGroups(bbb) + } + } +} + +// BorderRect returns the border rect for c. +func (c *Content) BorderRect() *types.Rectangle { + + if c.borderRect == nil { + + mLeft, mRight, mTop, mBottom, borderWidth := 0., 0., 0., 0., 0. + + m := c.margin() + if m != nil { + mTop = m.Top + mRight = m.Right + mBottom = m.Bottom + mLeft = m.Left + } + + b := c.border() + if b != nil && b.col != nil && b.Width >= 0 { + borderWidth = float64(b.Width) + } + + c.borderRect = types.RectForWidthAndHeight( + c.mediaBox.LL.X+mLeft+borderWidth/2, + c.mediaBox.LL.Y+mBottom+borderWidth/2, + c.mediaBox.Width()-mLeft-mRight-borderWidth, + c.mediaBox.Height()-mTop-mBottom-borderWidth) + + } + + return c.borderRect +} + +func (c *Content) Box() *types.Rectangle { + + if c.box == nil { + + var mTop, mRight, mBottom, mLeft float64 + var pTop, pRight, pBottom, pLeft float64 + var borderWidth float64 + + m := c.margin() + if m != nil { + if m.Width > 0 { + mTop, mRight, mBottom, mLeft = m.Width, m.Width, m.Width, m.Width + } else { + mTop = m.Top + mRight = m.Right + mBottom = m.Bottom + mLeft = m.Left + } + } + + b := c.border() + if b != nil && b.col != nil && b.Width >= 0 { + borderWidth = float64(b.Width) + } + + p := c.padding() + if p != nil { + if p.Width > 0 { + pTop, pRight, pBottom, pLeft = p.Width, p.Width, p.Width, p.Width + } else { + pTop = p.Top + pRight = p.Right + pBottom = p.Bottom + pLeft = p.Left + } + } + + llx := c.mediaBox.LL.X + mLeft + borderWidth + pLeft + lly := c.mediaBox.LL.Y + mBottom + borderWidth + pBottom + w := c.mediaBox.Width() - mLeft - mRight - 2*borderWidth - pLeft - pRight + h := c.mediaBox.Height() - mTop - mBottom - 2*borderWidth - pTop - pBottom + c.box = types.RectForWidthAndHeight(llx, lly, w, h) + } + + return c.box +} + +func (c *Content) calcPosition(x, y, dx, dy, mTop, mRight, mBottom, mLeft float64) (float64, float64) { + + cBox := c.Box() + + r := cBox.CroppedCopy(0) + r.LL.X += mLeft + r.LL.Y += mBottom + r.UR.X -= mRight + r.UR.Y -= mTop + + pdf := c.page.pdf + x, y = types.NormalizeCoord(x, y, cBox, pdf.origin, false) + + if x == -1 { + // Center horizontally + x = cBox.Center().X - r.LL.X + } else if x > 0 { + x -= mLeft + if x < 0 { + x = 0 + } + } + + if y == -1 { + // Center vertically + y = cBox.Center().Y - r.LL.Y + } else if y > 0 { + y -= mBottom + if y < 0 { + y = 0 + } + } + + if x >= 0 { + x = r.LL.X + x + } + if y >= 0 { + y = r.LL.Y + y + } + + // Position text horizontally centered for x < 0. + if x < 0 { + x = r.LL.X + r.Width()/2 + } + + // Position text vertically centered for y < 0. + if y < 0 { + y = r.LL.Y + r.Height()/2 + } + + // Apply offset. + x += dx + y += dy + + return x, y + +} + +func (c *Content) renderBars(p *model.Page) error { + for _, b := range c.Bars { + if b.Hide { + continue + } + if err := b.render(p); err != nil { + return err + } + } + return nil +} + +func (c *Content) renderSimpleBoxes(p *model.Page) error { + for _, sb := range c.SimpleBoxes { + if sb.Hide { + continue + } + if sb.Name != "" && sb.Name[0] == '$' { + // Use named simplebox + sbName := sb.Name[1:] + sb0 := c.namedSimpleBox(sbName) + if sb0 == nil { + return errors.Errorf("pdfcpu: unknown named box %s", sbName) + } + sb.mergeIn(sb0) + } + if err := sb.render(p); err != nil { + return err + } + } + return nil +} + +func (c *Content) renderTextBoxes(p *model.Page, pageNr int, fonts model.FontMap) error { + for _, tb := range c.TextBoxes { + if tb.Hide { + continue + } + if tb.Name != "" && tb.Name[0] == '$' { + // Use named textbox + tbName := tb.Name[1:] + tb0 := c.namedTextBox(tbName) + if tb0 == nil { + return errors.Errorf("pdfcpu: unknown named text %s", tbName) + } + tb.mergeIn(tb0) + } + if err := tb.render(p, pageNr, fonts); err != nil { + return err + } + } + return nil +} + +func (c *Content) renderImageBoxes(p *model.Page, pageNr int, images model.ImageMap) error { + for _, ib := range c.ImageBoxes { + if ib.Hide { + continue + } + if ib.Name != "" && ib.Name[0] == '$' { + // Use named imagebox + ibName := ib.Name[1:] + ib0 := c.namedImageBox(ibName) + if ib0 == nil { + return errors.Errorf("pdfcpu: unknown named image %s", ibName) + } + ib.mergeIn(ib0) + } + if err := ib.render(p, pageNr, images); err != nil { + return err + } + } + return nil +} + +func (c *Content) renderTables(p *model.Page, pageNr int, fonts model.FontMap) error { + for _, t := range c.Tables { + if t.Hide { + continue + } + if t.Name != "" && t.Name[0] == '$' { + // Use named table + tName := t.Name[1:] + t0 := c.namedTable(tName) + if t0 == nil { + return errors.Errorf("pdfcpu: unknown named table %s", tName) + } + t.mergeIn(t0) + } + if err := t.render(p, pageNr, fonts); err != nil { + return err + } + } + return nil +} + +func (c *Content) renderTextFields(p *model.Page, pageNr int, fonts model.FontMap) error { + for _, tf := range c.TextFields { + if tf.Hide { + continue + } + if err := tf.render(p, pageNr, fonts); err != nil { + return err + } + } + return nil +} + +func (c *Content) renderDateFields(p *model.Page, pageNr int, fonts model.FontMap) error { + for _, df := range c.DateFields { + if df.Hide { + continue + } + if err := df.render(p, pageNr, fonts); err != nil { + return err + } + } + return nil +} + +func (c *Content) renderCheckBoxes(p *model.Page, pageNr int, fonts model.FontMap) error { + for _, cb := range c.CheckBoxes { + if cb.Hide { + continue + } + if err := cb.render(p, pageNr, fonts); err != nil { + return err + } + } + return nil +} + +func (c *Content) renderRadioButtonGroups(p *model.Page, pageNr int, fonts model.FontMap) error { + for _, rbg := range c.RadioButtonGroups { + if rbg.Hide { + continue + } + if err := rbg.render(p, pageNr, fonts); err != nil { + return err + } + } + return nil +} + +func (c *Content) renderComboBoxes(p *model.Page, pageNr int, fonts model.FontMap) error { + for _, cb := range c.ComboBoxes { + if cb.Hide { + continue + } + if err := cb.render(p, pageNr, fonts); err != nil { + return err + } + } + return nil +} + +func (c *Content) renderListBoxes(p *model.Page, pageNr int, fonts model.FontMap) error { + for _, lb := range c.ListBoxes { + if lb.Hide { + continue + } + if err := lb.render(p, pageNr, fonts); err != nil { + return err + } + } + return nil +} + +func (c *Content) renderFieldGroups(p *model.Page, pageNr int, fonts model.FontMap) error { + for _, fg := range c.FieldGroups { + if fg.Hide { + continue + } + if fg.Name != "" && fg.Name[0] == '$' { + // Use named field group + fgName := fg.Name[1:] + fg0 := c.namedFieldGroup(fgName) + if fg0 == nil { + return errors.Errorf("pdfcpu: unknown named field group %s", fgName) + } + fg.mergeIn(fg0) + } + if err := fg.render(p, pageNr, fonts); err != nil { + return err + } + } + return nil +} + +func (c *Content) renderBoxesAndGuides(p *model.Page) { + pdf := c.page.pdf + + // Render mediaBox & contentBox + if pdf.ContentBox { + draw.DrawRect(p.Buf, c.mediaBox, 0, &color.Green, nil) + draw.DrawRect(p.Buf, c.Box(), 0, &color.Red, nil) + } + + // Render guides + if pdf.Guides { + for _, g := range c.Guides { + g.render(p.Buf, c.Box(), pdf) + } + } +} + +func (c *Content) renderPrimitives(p *model.Page, pageNr int, fonts model.FontMap, images model.ImageMap) error { + if err := c.renderBars(p); err != nil { + return err + } + + if err := c.renderSimpleBoxes(p); err != nil { + return err + } + + if err := c.renderTextBoxes(p, pageNr, fonts); err != nil { + return err + } + + if err := c.renderImageBoxes(p, pageNr, images); err != nil { + return err + } + + return c.renderTables(p, pageNr, fonts) +} + +func (c *Content) renderFormPrimitives(p *model.Page, pageNr int, fonts model.FontMap) error { + if err := c.renderTextFields(p, pageNr, fonts); err != nil { + return err + } + + if err := c.renderDateFields(p, pageNr, fonts); err != nil { + return err + } + + if err := c.renderCheckBoxes(p, pageNr, fonts); err != nil { + return err + } + + if err := c.renderRadioButtonGroups(p, pageNr, fonts); err != nil { + return err + } + + if err := c.renderComboBoxes(p, pageNr, fonts); err != nil { + return err + } + + if err := c.renderListBoxes(p, pageNr, fonts); err != nil { + return err + } + + return c.renderFieldGroups(p, pageNr, fonts) +} + +func (c *Content) render(p *model.Page, pageNr int, fonts model.FontMap, images model.ImageMap) error { + + if c.Regions != nil { + c.Regions.mediaBox = c.mediaBox + c.Regions.page = c.page + return c.Regions.render(p, pageNr, fonts, images) + } + + // Render background + if c.bgCol != nil { + draw.FillRectNoBorder(p.Buf, c.BorderRect(), *c.bgCol) + } + + // Render border + b := c.border() + if b != nil && b.col != nil && b.Width >= 0 { + draw.DrawRect(p.Buf, c.BorderRect(), float64(b.Width), b.col, &b.style) + } + + if err := c.renderPrimitives(p, pageNr, fonts, images); err != nil { + return err + } + + if err := c.renderFormPrimitives(p, pageNr, fonts); err != nil { + return err + } + + c.renderBoxesAndGuides(p) + + return nil +} diff --git a/pkg/pdfcpu/primitives/date.go b/pkg/pdfcpu/primitives/date.go new file mode 100644 index 0000000000000000000000000000000000000000..4080e98f813e2fc9ea5d40e1cac005a337090223 --- /dev/null +++ b/pkg/pdfcpu/primitives/date.go @@ -0,0 +1,100 @@ +/* + Copyright 2022 The pdfcpu Authors. + + 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 + + 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 primitives + +import ( + "strings" + "time" + + "github.com/pkg/errors" +) + +// DateFormat represents a supported date format. +// It consists of an internal and an external form. +type DateFormat struct { + Int string + Ext string +} + +var dateFormats = []DateFormat{ + + // based on separator '-' + {"2006-1-2", "yyyy-m-d"}, + {"2006-2-1", "yyyy-d-m"}, + {"2006-01-02", "yyyy-mm-dd"}, + {"2006-02-01", "yyyy-dd-mm"}, + {"02-01-2006", "dd-mm-yyyy"}, + {"01-02-2006", "mm-dd-yyyy"}, + {"2-1-2006", "d-m-yyyy"}, + {"1-2-2006", "m-d-yyyy"}, + + // based on separator '/' + {"2006/1/2", "yyyy/m/d"}, + {"2006/2/1", "yyyy/d/m"}, + {"2006/01/02", "yyyy/mm/dd"}, + {"2006/02/01", "yyyy/dd/mm"}, + {"02/01/2006", "dd/mm/yyyy"}, + {"01/02/2006", "mm/dd/yyyy"}, + {"2/1/2006", "d/m/yyyy"}, + {"1/2/2006", "m/d/yyyy"}, + + // based on separator '.' + {"2006.1.2", "yyyy.m.d"}, + {"2006.2.1", "yyyy.d.m"}, + {"2006.01.02", "yyyy.mm.dd"}, + {"2006.02.01", "yyyy.dd.mm"}, + {"02.01.2006", "dd.mm.yyyy"}, + {"01.02.2006", "mm.dd.yyyy"}, + {"2.1.2006", "d.m.yyyy"}, + {"1.2.2006", "m.d.yyyy"}, +} + +// DateFormatForFmtInt returns the date format for an internal format string. +func DateFormatForFmtInt(fmtInt string) (*DateFormat, error) { + for _, df := range dateFormats { + if df.Int == fmtInt { + return &df, nil + } + } + return nil, errors.Errorf("pdfcpu: \"%s\": unknown internal date format", fmtInt) +} + +// DateFormatForFmtInt returns the date format for an external format string. +func DateFormatForFmtExt(fmtExt string) (*DateFormat, error) { + s := strings.ToLower(fmtExt) + for _, df := range dateFormats { + if df.Ext == s { + return &df, nil + } + } + return nil, errors.Errorf("pdfcpu: \"%s\": unknown external date format", fmtExt) +} + +// DateFormatForDate returns the date format for given date string. +func DateFormatForDate(date string) (*DateFormat, error) { + for _, df := range dateFormats { + if _, err := time.Parse(df.Int, date); err == nil { + return &df, nil + } + } + return nil, errors.Errorf("pdfcpu: \"%s\": using unknown date format", date) +} + +func (df DateFormat) validate(date string) error { + _, err := time.Parse(df.Int, date) + return err +} diff --git a/pkg/pdfcpu/primitives/dateField.go b/pkg/pdfcpu/primitives/dateField.go new file mode 100644 index 0000000000000000000000000000000000000000..9371b0ee163fe254403d0a34ca6ecc9ff518e094 --- /dev/null +++ b/pkg/pdfcpu/primitives/dateField.go @@ -0,0 +1,948 @@ +/* + Copyright 2022 The pdfcpu Authors. + + 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 + + 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 primitives + +import ( + "bytes" + "fmt" + "io" + + "github.com/pdfcpu/pdfcpu/pkg/font" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/format" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// Note: Mac Preview does not support validating date fields. + +// DateField is a form field accepting date strings according to DateFormat including a positioned label. +type DateField struct { + pdf *PDF + content *Content + Label *TextFieldLabel + ID string + Tip string + Value string + Default string + DateFormat string `json:"format"` + dateFormat *DateFormat + Position [2]float64 `json:"pos"` // x,y + x, y float64 + Width float64 + Dx, Dy float64 + BoundingBox *types.Rectangle `json:"-"` + Font *FormFont + fontID string + Margin *Margin // applied to content box + Border *Border + BackgroundColor string `json:"bgCol"` + BgCol *color.SimpleColor `json:"-"` + Alignment string `json:"align"` // "Left", "Center", "Right" + HorAlign types.HAlignment `json:"-"` + Tab int + Locked bool + Debug bool + Hide bool +} + +func (df *DateField) SetFontID(s string) { + df.fontID = s +} + +func (df *DateField) validateID() error { + if df.ID == "" { + return errors.New("pdfcpu: missing field id") + } + if df.pdf.DuplicateField(df.ID) { + return errors.Errorf("pdfcpu: duplicate form field: %s", df.ID) + } + df.pdf.FieldIDs[df.ID] = true + return nil +} + +func (df *DateField) validatePosition() error { + if df.Position[0] < 0 || df.Position[1] < 0 { + return errors.Errorf("pdfcpu: field: %s pos value < 0", df.ID) + } + df.x, df.y = df.Position[0], df.Position[1] + return nil +} + +func (df *DateField) validateWidth() error { + if df.Width <= 0 { + return errors.Errorf("pdfcpu: field: %s width <= 0", df.ID) + } + return nil +} + +func (df *DateField) validateFont() error { + if df.Font != nil { + df.Font.pdf = df.pdf + if err := df.Font.validate(); err != nil { + return err + } + } + return nil +} + +func (df *DateField) validateMargin() error { + if df.Margin != nil { + if err := df.Margin.validate(); err != nil { + return err + } + } + return nil +} + +func (df *DateField) validateBorder() error { + if df.Border != nil { + df.Border.pdf = df.pdf + if err := df.Border.validate(); err != nil { + return err + } + } + return nil +} + +func (df *DateField) validateBackgroundColor() error { + if df.BackgroundColor != "" { + sc, err := df.pdf.parseColor(df.BackgroundColor) + if err != nil { + return err + } + df.BgCol = sc + } + return nil +} + +func (df *DateField) validateHorAlign() error { + df.HorAlign = types.AlignLeft + if df.Alignment != "" { + ha, err := types.ParseHorAlignment(df.Alignment) + if err != nil { + return err + } + df.HorAlign = ha + } + return nil +} + +func (df *DateField) validateLabel() error { + if df.Label != nil { + df.Label.pdf = df.pdf + if err := df.Label.validate(); err != nil { + return err + } + } + return nil +} + +func (df *DateField) validateDateFormat() error { + if len(df.DateFormat) == 0 { + return nil + } + dFormat, err := DateFormatForFmtExt(df.DateFormat) + if err != nil { + return err + } + df.dateFormat = dFormat + return nil +} + +func (df *DateField) validateDefault() error { + if df.Default == "" { + return nil + } + if df.dateFormat != nil { + if err := df.dateFormat.validate(df.Default); err != nil { + return errors.Errorf("pdfcpu: field: %s date format failure, \"%s\" incompatible with \"%s\"", df.ID, df.Default, df.dateFormat.Ext) + } + return nil + } + dFormat, err := DateFormatForDate(df.Default) + if err != nil { + return err + } + df.dateFormat = dFormat + return nil +} + +func (df *DateField) validateValue() error { + if df.Value == "" { + return nil + } + if df.dateFormat != nil { + if err := df.dateFormat.validate(df.Value); err != nil { + return errors.Errorf("pdfcpu: field: %s date format failure, \"%s\" incompatible with \"%s\"", df.ID, df.Value, df.dateFormat.Ext) + } + return nil + } + dFormat, err := DateFormatForDate(df.Value) + if err != nil { + return err + } + df.dateFormat = dFormat + return nil +} + +func (df *DateField) validateTab() error { + if df.Tab < 0 { + return errors.Errorf("pdfcpu: field: %s negative tab value", df.ID) + } + if df.Tab == 0 { + return nil + } + page := df.content.page + if page.Tabs == nil { + page.Tabs = types.IntSet{} + } else { + if page.Tabs[df.Tab] { + return errors.Errorf("pdfcpu: field: %s duplicate tab value %d", df.ID, df.Tab) + } + } + page.Tabs[df.Tab] = true + return nil +} + +func (df *DateField) validate() error { + + if err := df.validateID(); err != nil { + return err + } + + if err := df.validatePosition(); err != nil { + return err + } + + if err := df.validateWidth(); err != nil { + return err + } + + if err := df.validateFont(); err != nil { + return err + } + + if err := df.validateMargin(); err != nil { + return err + } + + if err := df.validateBorder(); err != nil { + return err + } + + if err := df.validateBackgroundColor(); err != nil { + return err + } + + if err := df.validateHorAlign(); err != nil { + return err + } + + if err := df.validateLabel(); err != nil { + return err + } + + if err := df.validateDateFormat(); err != nil { + return err + } + + if err := df.validateDefault(); err != nil { + return err + } + + if err := df.validateValue(); err != nil { + return err + } + + if df.dateFormat == nil { + dFormat, err := DateFormatForFmtInt(df.pdf.DateFormat) + if err != nil { + return err + } + df.dateFormat = dFormat + } + + return df.validateTab() +} + +func (df *DateField) calcFontFromDA(ctx *model.Context, d types.Dict, fonts map[string]types.IndirectRef) (*types.IndirectRef, error) { + + s := d.StringEntry("DA") + if s == nil { + s = ctx.Form.StringEntry("DA") + if s == nil { + return nil, errors.New("pdfcpu: datefield missing \"DA\"") + } + } + + fontID, f, err := fontFromDA(*s) + if err != nil { + return nil, err + } + + df.Font, df.fontID = &f, fontID + + id, name, lang, fontIndRef, err := extractFormFontDetails(ctx, df.fontID, fonts) + if err != nil { + return nil, err + } + if fontIndRef == nil { + return nil, errors.New("pdfcpu: unable to detect indirect reference for font") + } + + df.fontID = id + df.Font.Name = name + df.Font.Lang = lang + //df.RTL = pdffont.RTL(lang) + + return fontIndRef, nil +} + +func (df *DateField) calcFont() error { + f, err := df.content.calcInputFont(df.Font) + if err != nil { + return err + } + df.Font = f + + if df.Label != nil { + f, err = df.content.calcLabelFont(df.Label.Font) + if err != nil { + return err + } + df.Label.Font = f + } + + return nil +} + +func (df *DateField) margin(name string) *Margin { + return df.content.namedMargin(name) +} + +func (df *DateField) calcMargin() (float64, float64, float64, float64, error) { + mTop, mRight, mBottom, mLeft := 0., 0., 0., 0. + if df.Margin != nil { + m := df.Margin + if m.Name != "" && m.Name[0] == '$' { + // use named margin + mName := m.Name[1:] + m0 := df.margin(mName) + if m0 == nil { + return mTop, mRight, mBottom, mLeft, errors.Errorf("pdfcpu: unknown named margin %s", mName) + } + m.mergeIn(m0) + } + if m.Width > 0 { + mTop = m.Width + mRight = m.Width + mBottom = m.Width + mLeft = m.Width + } else { + mTop = m.Top + mRight = m.Right + mBottom = m.Bottom + mLeft = m.Left + } + } + return mTop, mRight, mBottom, mLeft, nil +} + +func (df *DateField) labelPos(labelHeight, w, g float64) (float64, float64) { + + var x, y float64 + bb, horAlign := df.BoundingBox, df.Label.HorAlign + + switch df.Label.relPos { + + case types.RelPosLeft: + x = bb.LL.X - g + if horAlign == types.AlignLeft { + x -= w + if x < 0 { + x = 0 + } + } + y = bb.LL.Y + + case types.RelPosRight: + x = bb.UR.X + g + if horAlign == types.AlignRight { + x += w + } + y = bb.LL.Y + + case types.RelPosTop: + y = bb.UR.Y + g + x = bb.LL.X + if horAlign == types.AlignRight { + x += bb.Width() + } else if horAlign == types.AlignCenter { + x += bb.Width() / 2 + } + + case types.RelPosBottom: + y = bb.LL.Y - g - labelHeight + x = bb.LL.X + if horAlign == types.AlignRight { + x += bb.Width() + } else if horAlign == types.AlignCenter { + x += bb.Width() / 2 + } + + } + + return x, y +} + +func (tf *DateField) renderBackground(w io.Writer, bgCol, boCol *color.SimpleColor, boWidth, width, height float64) { + if bgCol != nil || (boCol != nil && boWidth > 0) { + fmt.Fprint(w, "q ") + if bgCol != nil { + fmt.Fprintf(w, "%.2f %.2f %.2f rg 0 0 %.2f %.2f re f ", bgCol.R, bgCol.G, bgCol.B, width, height) + } + if boCol != nil && boWidth > 0 { + fmt.Fprintf(w, "%.2f %.2f %.2f RG %.2f w %.2f %.2f %.2f %.2f re s ", + boCol.R, boCol.G, boCol.B, boWidth, boWidth/2, boWidth/2, width-boWidth, height-boWidth) + } + fmt.Fprint(w, "Q ") + } +} + +func (df *DateField) renderN(xRefTable *model.XRefTable) ([]byte, error) { + + w, h := df.BoundingBox.Width(), df.BoundingBox.Height() + bgCol := df.BgCol + boWidth, boCol := df.calcBorder() + buf := new(bytes.Buffer) + + df.renderBackground(buf, bgCol, boCol, boWidth, w, h) + + fmt.Fprint(buf, "/Tx BMC q ") + fmt.Fprintf(buf, "1 1 %.1f %.1f re W n ", w-2, h-2) + + v := "" + if df.dateFormat != nil { + v = df.dateFormat.Ext + } + if len(df.Default) > 0 { + v = df.Default + } + if len(df.Value) > 0 { + v = df.Value + } + + f := df.Font + if float64(f.Size) > h { + f.Size = font.SizeForLineHeight(f.Name, h) + } + + lineBB := model.CalcBoundingBox(v, 0, 0, f.Name, f.Size) + s := model.PrepBytes(xRefTable, v, f.Name, true, false) + x := 2 * boWidth + if x == 0 { + x = 2 + } + switch df.HorAlign { + case types.AlignCenter: + x = w/2 - lineBB.Width()/2 + case types.AlignRight: + x = w - lineBB.Width() - 2 + } + + y := (df.BoundingBox.Height()-font.LineHeight(f.Name, f.Size))/2 + font.Descent(f.Name, f.Size) + + fmt.Fprintf(buf, "BT /%s %d Tf ", df.fontID, f.Size) + fmt.Fprintf(buf, "%.2f %.2f %.2f RG %.2f %.2f %.2f rg %.2f %.2f Td (%s) Tj ET ", + f.col.R, f.col.G, f.col.B, + f.col.R, f.col.G, f.col.B, x, y, s) + + fmt.Fprint(buf, "Q EMC ") + + if boCol != nil && boWidth > 0 { + fmt.Fprintf(buf, "q %.2f %.2f %.2f RG %.2f w %.2f %.2f %.2f %.2f re s Q ", + boCol.R, boCol.G, boCol.B, boWidth-1, boWidth/2, boWidth/2, w-boWidth, h-boWidth) + } + + return buf.Bytes(), nil +} + +// RefreshN updates the normal appearance referred to by indRef according to df. +func (df *DateField) RefreshN(xRefTable *model.XRefTable, indRef *types.IndirectRef) error { + bb, err := df.renderN(xRefTable) + if err != nil { + return err + } + + entry, _ := xRefTable.FindTableEntryForIndRef(indRef) + sd, _ := entry.Object.(types.StreamDict) + + sd.Content = bb + if err := sd.Encode(); err != nil { + return err + } + + entry.Object = sd + + return nil +} + +func (df *DateField) irN(fonts model.FontMap) (*types.IndirectRef, error) { + + bb, err := df.renderN(df.pdf.XRefTable) + if err != nil { + return nil, err + } + + sd, err := df.pdf.XRefTable.NewStreamDictForBuf(bb) + if err != nil { + return nil, err + } + + sd.InsertName("Type", "XObject") + sd.InsertName("Subtype", "Form") + sd.InsertInt("FormType", 1) + sd.Insert("BBox", types.NewNumberArray(0, 0, df.BoundingBox.Width(), df.BoundingBox.Height())) + sd.Insert("Matrix", types.NewNumberArray(1, 0, 0, 1, 0, 0)) + + // f := df.Font + + // fName := f.Name + // if fo.CJK(df.Font.Script, df.Font.Lang) { + // fName = "cjk:" + fName + // } + + ir, err := df.pdf.ensureFont(df.fontID, df.Font.Name, df.Font.Lang, fonts) + if err != nil { + return nil, err + } + + d := types.Dict( + map[string]types.Object{ + "Font": types.Dict( + map[string]types.Object{ + df.fontID: *ir, + }, + ), + }, + ) + + sd.Insert("Resources", d) + + if err := sd.Encode(); err != nil { + return nil, err + } + + return df.pdf.XRefTable.IndRefForNewObject(*sd) +} + +func (df *DateField) calcBorder() (boWidth float64, boCol *color.SimpleColor) { + if df.Border == nil { + return 0, nil + } + return df.Border.calc() +} + +func (df *DateField) prepareFF() FieldFlags { + ff := FieldDoNotSpellCheck + ff += FieldDoNotScroll + if df.Locked { + ff += FieldReadOnly + } + return ff +} + +func (df *DateField) handleBorderAndMK(d types.Dict) { + bgCol := df.BgCol + if bgCol == nil { + bgCol = df.content.page.bgCol + if bgCol == nil { + bgCol = df.pdf.bgCol + } + } + df.BgCol = bgCol + + boWidth, boCol := df.calcBorder() + + if bgCol != nil || boCol != nil { + appCharDict := types.Dict{} + if bgCol != nil { + appCharDict["BG"] = bgCol.Array() + } + if boCol != nil && df.Border.Width > 0 { + appCharDict["BC"] = boCol.Array() + } + d["MK"] = appCharDict + } + + if boWidth > 0 { + d["Border"] = types.NewNumberArray(0, 0, boWidth) + } +} + +func (df *DateField) prepareDict(fonts model.FontMap) (types.Dict, error) { + pdf := df.pdf + + id, err := types.EscapeUTF16String(df.ID) + if err != nil { + return nil, err + } + + ff := df.prepareFF() + + format, err := types.Escape(fmt.Sprintf("AFDate_FormatEx(\"%s\");", df.dateFormat.Ext)) + if err != nil { + return nil, err + } + + keystroke, err := types.Escape(fmt.Sprintf("AFDate_KeystrokeEx(\"%s\");", df.dateFormat.Ext)) + if err != nil { + return nil, err + } + + aa := types.Dict( + map[string]types.Object{ + "F": types.Dict( + map[string]types.Object{ + "JS": types.StringLiteral(*format), + "S": types.Name("JavaScript"), + }, + ), + "K": types.Dict( + map[string]types.Object{ + "JS": types.StringLiteral(*keystroke), + "S": types.Name("JavaScript"), + }, + ), + }, + ) + + tu := types.StringLiteral(df.dateFormat.Ext) + if df.Tip != "" { + tu = types.StringLiteral(types.EncodeUTF16String(df.Tip)) + } + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("Widget"), + "FT": types.Name("Tx"), + "Rect": df.BoundingBox.Array(), + "F": types.Integer(model.AnnPrint), + "Ff": types.Integer(ff), + "T": types.StringLiteral(*id), + "Q": types.Integer(df.HorAlign), + "TU": tu, + "AA": aa, + }, + ) + + df.handleBorderAndMK(d) + + if df.Value != "" { + s, err := types.EscapeUTF16String(df.Value) + if err != nil { + return nil, err + } + d["V"] = types.StringLiteral(*s) + } + + if df.Default != "" { + s, err := types.EscapeUTF16String(df.Default) + if err != nil { + return nil, err + } + d["DV"] = types.StringLiteral(*s) + if df.Value == "" { + d["V"] = types.StringLiteral(*s) + } + } + + if pdf.InheritedDA != "" { + d["DA"] = types.StringLiteral(pdf.InheritedDA) + } + + f := df.Font + fCol := f.col + + fontID, err := pdf.ensureFormFont(f) + if err != nil { + return d, err + } + df.fontID = fontID + + da := fmt.Sprintf("/%s %d Tf %.2f %.2f %.2f rg", fontID, f.Size, fCol.R, fCol.G, fCol.B) + // Note: Mac Preview does not honour inherited "DA" + d["DA"] = types.StringLiteral(da) + + irN, err := df.irN(fonts) + if err != nil { + return nil, err + } + + d["AP"] = types.Dict(map[string]types.Object{"N": *irN}) + + return d, nil +} + +func (df *DateField) bbox() *types.Rectangle { + if df.Label == nil { + return df.BoundingBox.Clone() + } + + l := df.Label + var r *types.Rectangle + x := l.td.X + + switch l.td.HAlign { + case types.AlignCenter: + x -= float64(l.Width) / 2 + case types.AlignRight: + x -= float64(l.Width) + } + + r = types.RectForWidthAndHeight(x, l.td.Y, float64(l.Width), l.height) + + return model.CalcBoundingBoxForRects(df.BoundingBox, r) +} + +func (df *DateField) prepareRectLL(mTop, mRight, mBottom, mLeft float64) (float64, float64) { + return df.content.calcPosition(df.x, df.y, df.Dx, df.Dy, mTop, mRight, mBottom, mLeft) +} + +func (df *DateField) prepLabel(p *model.Page, pageNr int, fonts model.FontMap) error { + + if df.Label == nil { + return nil + } + + l := df.Label + pdf := df.pdf + + t := "Default" + if l.Value != "" { + t, _ = format.Text(l.Value, pdf.TimestampFormat, pageNr, pdf.pageCount()) + } + + w := float64(l.Width) + g := float64(l.Gap) + + f := l.Font + fontName, fontLang, col := f.Name, f.Lang, f.col + + id, err := df.pdf.idForFontName(fontName, fontLang, p.Fm, fonts, pageNr) + if err != nil { + return err + } + + td := model.TextDescriptor{ + Text: t, + FontName: fontName, + Embed: true, + FontKey: id, + FontSize: f.Size, + Scale: 1., + ScaleAbs: true, + RTL: l.RTL, + } + + if col != nil { + td.StrokeCol, td.FillCol = *col, *col + } + + if l.BgCol != nil { + td.ShowBackground, td.ShowTextBB, td.BackgroundCol = true, true, *l.BgCol + } + + bb := model.WriteMultiLine(df.pdf.XRefTable, new(bytes.Buffer), types.RectForFormat("A4"), nil, td) + l.height = bb.Height() + if bb.Width() > w { + w = bb.Width() + l.Width = int(bb.Width()) + } + + td.X, td.Y = df.labelPos(l.height, w, g) + + if bb.Height() < df.BoundingBox.Height() && + (l.relPos == types.RelPosLeft || l.relPos == types.RelPosRight) { + td.MBot = (df.BoundingBox.Height() - bb.Height()) / 2 + td.MTop = td.MBot + } + + td.HAlign, td.VAlign = l.HorAlign, types.AlignBottom + + l.td = &td + + return nil +} + +func (df *DateField) prepForRender(p *model.Page, pageNr int, fonts model.FontMap) error { + + mTop, mRight, mBottom, mLeft, err := df.calcMargin() + if err != nil { + return err + } + + x, y := df.prepareRectLL(mTop, mRight, mBottom, mLeft) + + if err := df.calcFont(); err != nil { + return err + } + + var boWidth int + if df.Border != nil { + if df.Border.col != nil { + boWidth = df.Border.Width + } + } + + h := float64(df.Font.Size)*1.2 + 2*float64(boWidth) + + df.BoundingBox = types.RectForWidthAndHeight(x, y, df.Width, h) + + return df.prepLabel(p, pageNr, fonts) +} + +func (df *DateField) doRender(p *model.Page, fonts model.FontMap) error { + + d, err := df.prepareDict(fonts) + if err != nil { + return err + } + + ann := model.FieldAnnotation{Dict: d} + if df.Tab > 0 { + p.AnnotTabs[df.Tab] = ann + } else { + p.Annots = append(p.Annots, ann) + } + + if df.Label != nil { + model.WriteColumn(df.pdf.XRefTable, p.Buf, p.MediaBox, nil, *df.Label.td, 0) + } + + if df.Debug || df.pdf.Debug { + df.pdf.highlightPos(p.Buf, df.BoundingBox.LL.X, df.BoundingBox.LL.Y, df.content.Box()) + } + + return nil +} + +func (df *DateField) render(p *model.Page, pageNr int, fonts model.FontMap) error { + + if err := df.prepForRender(p, pageNr, fonts); err != nil { + return err + } + + return df.doRender(p, fonts) +} + +// NewDateField returns a new date field for d. +func NewDateField( + ctx *model.Context, + d types.Dict, + v string, + fonts map[string]types.IndirectRef) (*DateField, *types.IndirectRef, error) { + + df := &DateField{Value: v} + + bb, err := ctx.RectForArray(d.ArrayEntry("Rect")) + if err != nil { + return nil, nil, err + } + + df.BoundingBox = types.RectForDim(bb.Width(), bb.Height()) + + fontIndRef, err := df.calcFontFromDA(ctx, d, fonts) + if err != nil { + return nil, nil, err + } + + df.HorAlign = types.AlignLeft + if q := d.IntEntry("Q"); q != nil { + df.HorAlign = types.HAlignment(*q) + } + + bgCol, boCol, err := calcColsFromMK(ctx, d) + if err != nil { + return nil, nil, err + } + df.BgCol = bgCol + + var b Border + boWidth := calcBorderWidth(d) + if boWidth > 0 { + b.Width = boWidth + b.col = boCol + } + df.Border = &b + + return df, fontIndRef, nil +} + +func renderDateFieldAP(ctx *model.Context, d types.Dict, v string, fonts map[string]types.IndirectRef) error { + + df, fontIndRef, err := NewDateField(ctx, d, v, fonts) + if err != nil { + return err + } + + bb, err := df.renderN(ctx.XRefTable) + if err != nil { + return err + } + + irN, err := NewForm(ctx.XRefTable, bb, df.fontID, fontIndRef, df.BoundingBox) + if err != nil { + return err + } + + d["AP"] = types.Dict(map[string]types.Object{"N": *irN}) + + return nil +} + +func refreshDateFieldAP(ctx *model.Context, d types.Dict, v string, fonts map[string]types.IndirectRef, irN *types.IndirectRef) error { + + df, _, err := NewDateField(ctx, d, v, fonts) + if err != nil { + return err + } + + bb, err := df.renderN(ctx.XRefTable) + if err != nil { + return err + } + + return updateForm(ctx.XRefTable, bb, irN) +} + +func EnsureDateFieldAP(ctx *model.Context, d types.Dict, v string, fonts map[string]types.IndirectRef) error { + apd := d.DictEntry("AP") + if apd == nil { + return renderDateFieldAP(ctx, d, v, fonts) + } + + irN := apd.IndirectRefEntry("N") + if irN == nil { + return nil + } + + return refreshDateFieldAP(ctx, d, v, fonts, irN) +} diff --git a/pkg/pdfcpu/primitives/divider.go b/pkg/pdfcpu/primitives/divider.go new file mode 100644 index 0000000000000000000000000000000000000000..abcc6288dad8858c32bb5b81e0cec495d18a65ed --- /dev/null +++ b/pkg/pdfcpu/primitives/divider.go @@ -0,0 +1,63 @@ +/* + Copyright 2021 The pdfcpu Authors. + + 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 + + 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 primitives + +import ( + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// Divider is a positioned separator between two regions from p to q. +type Divider struct { + pdf *PDF + Pos float64 `json:"at"` // fraction 0..1 + p, q types.Point // Endpoints + Width int // 1..10 + Color string `json:"col"` + col *color.SimpleColor +} + +func (d *Divider) validate() error { + if d.Pos <= 0 || d.Pos >= 1 { + return errors.Errorf("pdfcpu: div at(%.1f) needs to be between 0 and 1", d.Pos) + } + if d.Width < 0 || d.Width > 10 { + return errors.Errorf("pdfcpu: div width(%d) needs to be between 0 and 10", d.Width) + } + if d.Color != "" { + sc, err := d.pdf.parseColor(d.Color) + if err != nil { + return err + } + d.col = sc + } + return nil +} + +func (d *Divider) render(p *model.Page) error { + + if d.col == nil { + return nil + } + + draw.DrawLine(p.Buf, d.p.X, d.p.Y, d.q.X, d.q.Y, float64(d.Width), d.col, nil) + + return nil +} diff --git a/pkg/pdfcpu/primitives/fieldGroup.go b/pkg/pdfcpu/primitives/fieldGroup.go new file mode 100644 index 0000000000000000000000000000000000000000..b10ebb692e47eb912095733743119ac9f31d8109 --- /dev/null +++ b/pkg/pdfcpu/primitives/fieldGroup.go @@ -0,0 +1,404 @@ +/* + Copyright 2022 The pdfcpu Authors. + + 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 + + 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 primitives + +import ( + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +// FieldGroup is a container for fields. +type FieldGroup struct { + pdf *PDF + content *Content + Name string + Value string + Border *Border + Padding *Padding + BackgroundColor string `json:"bgCol"` + bgCol *color.SimpleColor + TextFields []*TextField `json:"textfield"` // text fields with optional label + DateFields []*DateField `json:"datefield"` // date fields with optional label + CheckBoxes []*CheckBox `json:"checkbox"` // checkboxes with optional label + RadioButtonGroups []*RadioButtonGroup `json:"radiobuttongroup"` // radiobutton groups with optional label + ComboBoxes []*ComboBox `json:"combobox"` // comboboxes with optional label + ListBoxes []*ListBox `json:"listbox"` // listboxes with optional label + Hide bool +} + +func (fg *FieldGroup) validateBorder() error { + if fg.Border != nil { + fg.Border.pdf = fg.pdf + if err := fg.Border.validate(); err != nil { + return err + } + } + return nil +} + +func (fg *FieldGroup) validatePadding() error { + if fg.Padding != nil { + if err := fg.Padding.validate(); err != nil { + return err + } + } + return nil +} + +func (fg *FieldGroup) validateBackgroundColor() error { + if fg.BackgroundColor != "" { + sc, err := fg.pdf.parseColor(fg.BackgroundColor) + if err != nil { + return err + } + fg.bgCol = sc + } + return nil +} + +func (fg *FieldGroup) validateBorderPaddingBgCol() error { + if err := fg.validateBorder(); err != nil { + return err + } + + if err := fg.validatePadding(); err != nil { + return err + } + + return fg.validateBackgroundColor() +} + +func (fg *FieldGroup) validate() error { + + if err := fg.validateBorderPaddingBgCol(); err != nil { + return err + } + + for _, tf := range fg.TextFields { + tf.pdf = fg.pdf + tf.content = fg.content + if err := tf.validate(); err != nil { + return err + } + } + + for _, df := range fg.DateFields { + df.pdf = fg.pdf + df.content = fg.content + if err := df.validate(); err != nil { + return err + } + } + + for _, cb := range fg.CheckBoxes { + cb.pdf = fg.pdf + cb.content = fg.content + if err := cb.validate(); err != nil { + return err + } + } + + for _, rbg := range fg.RadioButtonGroups { + rbg.pdf = fg.pdf + rbg.content = fg.content + if err := rbg.validate(); err != nil { + return err + } + } + + for _, cb := range fg.ComboBoxes { + cb.pdf = fg.pdf + cb.content = fg.content + if err := cb.validate(); err != nil { + return err + } + } + + for _, lb := range fg.ListBoxes { + lb.pdf = fg.pdf + lb.content = fg.content + if err := lb.validate(); err != nil { + return err + } + } + + return nil +} + +func (fg *FieldGroup) mergeIn(fg0 *FieldGroup) { + if fg.Border == nil { + fg.Border = fg0.Border + } + + if fg.Padding == nil { + fg.Padding = fg0.Padding + } + + if fg.bgCol == nil { + fg.bgCol = fg0.bgCol + } + + if !fg.Hide { + fg.Hide = fg0.Hide + } +} + +func (fg *FieldGroup) calcBBoxFromTextFields(bbox **types.Rectangle, p *model.Page, pageNr int, fonts model.FontMap) error { + for _, tf := range fg.TextFields { + if err := tf.prepForRender(p, pageNr, fonts); err != nil { + return err + } + *bbox = model.CalcBoundingBoxForRects(*bbox, tf.bbox()) + } + return nil +} + +func (fg *FieldGroup) calcBBoxFromDateFields(bbox **types.Rectangle, p *model.Page, pageNr int, fonts model.FontMap) error { + for _, df := range fg.DateFields { + if err := df.prepForRender(p, pageNr, fonts); err != nil { + return err + } + *bbox = model.CalcBoundingBoxForRects(*bbox, df.bbox()) + } + return nil +} + +func (fg *FieldGroup) calcBBoxFromCheckBoxes(bbox **types.Rectangle, p *model.Page, pageNr int, fonts model.FontMap) error { + for _, cb := range fg.CheckBoxes { + if err := cb.prepForRender(p, pageNr, fonts); err != nil { + return err + } + *bbox = model.CalcBoundingBoxForRects(*bbox, cb.bbox()) + } + return nil +} + +func (fg *FieldGroup) calcBBoxFromRadioButtonGroups(bbox **types.Rectangle, p *model.Page, pageNr int, fonts model.FontMap) error { + for _, rbg := range fg.RadioButtonGroups { + if err := rbg.prepForRender(p, pageNr, fonts); err != nil { + return err + } + *bbox = model.CalcBoundingBoxForRects(*bbox, rbg.bbox()) + } + return nil +} + +func (fg *FieldGroup) calcBBoxFromComboBoxes(bbox **types.Rectangle, p *model.Page, pageNr int, fonts model.FontMap) error { + for _, cb := range fg.ComboBoxes { + if err := cb.prepForRender(p, pageNr, fonts); err != nil { + return err + } + *bbox = model.CalcBoundingBoxForRects(*bbox, cb.bbox()) + } + return nil +} + +func (fg *FieldGroup) calcBBoxFromListBoxes(bbox **types.Rectangle, p *model.Page, pageNr int, fonts model.FontMap) error { + for _, lb := range fg.ListBoxes { + if err := lb.prepForRender(p, pageNr, fonts); err != nil { + return err + } + *bbox = model.CalcBoundingBoxForRects(*bbox, lb.bbox()) + } + return nil +} + +func (fg *FieldGroup) calcBBox(p *model.Page, pageNr int, fonts model.FontMap) (*types.Rectangle, error) { + var bbox *types.Rectangle + + if err := fg.calcBBoxFromTextFields(&bbox, p, pageNr, fonts); err != nil { + return nil, err + } + + if err := fg.calcBBoxFromDateFields(&bbox, p, pageNr, fonts); err != nil { + return nil, err + } + + if err := fg.calcBBoxFromCheckBoxes(&bbox, p, pageNr, fonts); err != nil { + return nil, err + } + + if err := fg.calcBBoxFromRadioButtonGroups(&bbox, p, pageNr, fonts); err != nil { + return nil, err + } + + if err := fg.calcBBoxFromComboBoxes(&bbox, p, pageNr, fonts); err != nil { + return nil, err + } + + if err := fg.calcBBoxFromListBoxes(&bbox, p, pageNr, fonts); err != nil { + return nil, err + } + + return bbox, nil +} + +func (fg *FieldGroup) renderBBox(bbox *types.Rectangle, p *model.Page) error { + r := fg.content.Box() + var bw float64 + if fg.Border != nil { + bw = float64(fg.Border.Width) + } + + var pTop, pRight, pBottom, pLeft float64 + if fg.Padding != nil { + p := fg.Padding + pTop, pRight, pBottom, pLeft = p.Top, p.Right, p.Bottom, p.Left + } + + x := bbox.LL.X - r.LL.X - bw - pLeft + w := bbox.Width() + 2*bw + pLeft + pRight + if x < 0 { + w += x + x = 0 + } + + y := bbox.LL.Y - r.LL.Y - bw - pBottom + h := bbox.Height() + 2*bw + pBottom + pTop + if y < 0 { + h += y + y = 0 + } + + sb := SimpleBox{ + x: x, + y: y, + Width: w, + Height: h, + fillCol: fg.bgCol, + Border: fg.Border, + } + + sb.pdf = fg.pdf + sb.content = fg.content + + return sb.render(p) +} + +func (fg *FieldGroup) renderTextFields(p *model.Page, fonts model.FontMap) error { + for _, tf := range fg.TextFields { + if tf.Hide { + continue + } + if err := tf.doRender(p, fonts); err != nil { + return err + } + } + return nil +} + +func (fg *FieldGroup) renderDateFields(p *model.Page, fonts model.FontMap) error { + for _, df := range fg.DateFields { + if df.Hide { + continue + } + if err := df.doRender(p, fonts); err != nil { + return err + } + } + return nil +} + +func (fg *FieldGroup) renderCheckBoxes(p *model.Page, fonts model.FontMap) error { + for _, cb := range fg.CheckBoxes { + if cb.Hide { + continue + } + if err := cb.doRender(p, fonts); err != nil { + return err + } + } + return nil +} + +func (fg *FieldGroup) renderRadioButtonGroups(p *model.Page, pageNr int, fonts model.FontMap) error { + for _, rbg := range fg.RadioButtonGroups { + if rbg.Hide { + continue + } + if err := rbg.doRender(p, pageNr, fonts); err != nil { + return err + } + } + return nil +} + +func (fg *FieldGroup) renderComboBoxes(p *model.Page, fonts model.FontMap) error { + for _, cb := range fg.ComboBoxes { + if cb.Hide { + continue + } + if err := cb.doRender(p, fonts); err != nil { + return err + } + } + return nil +} + +func (fg *FieldGroup) renderListBoxes(p *model.Page, fonts model.FontMap) error { + for _, cb := range fg.ListBoxes { + if cb.Hide { + continue + } + if err := cb.doRender(p, fonts); err != nil { + return err + } + } + return nil +} + +func (fg *FieldGroup) renderFields(p *model.Page, pageNr int, fonts model.FontMap) error { + if err := fg.renderTextFields(p, fonts); err != nil { + return err + } + if err := fg.renderDateFields(p, fonts); err != nil { + return err + } + if err := fg.renderCheckBoxes(p, fonts); err != nil { + return err + } + if err := fg.renderRadioButtonGroups(p, pageNr, fonts); err != nil { + return err + } + if err := fg.renderComboBoxes(p, fonts); err != nil { + return err + } + return fg.renderListBoxes(p, fonts) +} + +func (fg *FieldGroup) render(p *model.Page, pageNr int, fonts model.FontMap) error { + bbox, err := fg.calcBBox(p, pageNr, fonts) + if err != nil { + return err + } + + // Render simpleBox containing all fields of this group. + if err := fg.renderBBox(bbox, p); err != nil { + return err + } + + if err := fg.renderFields(p, pageNr, fonts); err != nil { + return err + } + + if fg.pdf.Debug { + fg.pdf.highlightPos(p.Buf, bbox.LL.X, bbox.LL.Y, fg.content.Box()) + } + + return nil +} diff --git a/pkg/pdfcpu/primitives/font.go b/pkg/pdfcpu/primitives/font.go new file mode 100644 index 0000000000000000000000000000000000000000..d5c36fa466a3589789efe735c3f58652eb3cd4db --- /dev/null +++ b/pkg/pdfcpu/primitives/font.go @@ -0,0 +1,339 @@ +/* + Copyright 2021 The pdfcpu Authors. + + 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 + + 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 primitives + +import ( + "strconv" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/font" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + pdffont "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/font" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +type FormFont struct { + pdf *PDF + Name string + Lang string // ISO-639 + Script string // ISO-15924 + Size int + Color string `json:"col"` + col *color.SimpleColor +} + +// ISO-639 country codes +// See https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes +var ISO639Codes = []string{"ab", "aa", "af", "ak", "sq", "am", "ar", "an", "hy", "as", "av", "ae", "ay", "az", "bm", "ba", "eu", "be", "bn", "bi", "bs", "br", "bg", + "my", "ca", "ch", "ce", "ny", "zh", "cu", "cv", "kw", "co", "cr", "hr", "cs", "da", "dv", "nl", "dz", "en", "eo", "et", "ee", "fo", "fj", "fi", "fr", "fy", "ff", + "gd", "gl", "lg", "ka", "de", "el", "kl", "gn", "gu", "ht", "ha", "he", "hz", "hi", "ho", "hu", "is", "io", "ig", "id", "ia", "ie", "iu", "ik", "ga", "it", "ja", + "jv", "kn", "kr", "ks", "kk", "km", "ki", "rw", "ky", "kv", "kg", "ko", "kj", "ku", "lo", "la", "lv", "li", "ln", "lt", "lu", "lb", "mk", "mg", "ms", "ml", "mt", + "gv", "mi", "mr", "mh", "mn", "na", "nv", "nd", "nr", "ng", "ne", "no", "nb", "nn", "ii", "oc", "oj", "or", "om", "os", "pi", "ps", "fa", "pl", "pt", "pa", "qu", + "ro", "rm", "rn", "ru", "se", "sm", "sg", "sa", "sc", "sr", "sn", "sd", "si", "sk", "sl", "so", "st", "es", "su", "sw", "ss", "sv", "tl", "ty", "tg", "ta", "tt", + "te", "th", "bo", "ti", "to", "ts", "tn", "tr", "tk", "tw", "ug", "uk", "ur", "uz", "ve", "vi", "vo", "wa", "cy", "wo", "xh", "yi", "yo", "za", "zu"} + +func (f *FormFont) validateISO639() error { + if !types.MemberOf(f.Lang, ISO639Codes) { + return errors.Errorf("pdfcpu: invalid ISO-639 code: %s", f.Lang) + } + return nil +} + +func (f *FormFont) validateScriptSupport() error { + font.UserFontMetricsLock.RLock() + fd, ok := font.UserFontMetrics[f.Name] + font.UserFontMetricsLock.RUnlock() + if !ok { + return errors.Errorf("pdfcpu: userfont %s not available", f.Name) + } + ok, err := fd.SupportsScript(f.Script) + if err != nil { + return err + } + if !ok { + return errors.Errorf("pdfcpu: userfont (%s) does not support script: %s", f.Name, f.Script) + } + return nil +} + +func (f *FormFont) validate() error { + if f.Name == "$" { + return errors.New("pdfcpu: invalid font reference $") + } + + if f.Name != "" && f.Name[0] != '$' { + if !font.SupportedFont(f.Name) { + return errors.Errorf("pdfcpu: font %s is unsupported, please refer to \"pdfcpu fonts list\".\n", f.Name) + } + if font.IsUserFont(f.Name) { + if f.Lang != "" { + f.Lang = strings.ToLower(f.Lang) + if err := f.validateISO639(); err != nil { + return err + } + } + if f.Script != "" { + f.Script = strings.ToUpper(f.Script) + if err := f.validateScriptSupport(); err != nil { + return err + } + } + } + if f.Size <= 0 { + return errors.Errorf("pdfcpu: invalid font size: %d", f.Size) + } + } + + if f.Color != "" { + sc, err := f.pdf.parseColor(f.Color) + if err != nil { + return err + } + f.col = sc + } + + return nil +} + +func (f *FormFont) mergeIn(f0 *FormFont) { + if f.Name == "" { + f.Name = f0.Name + } + if f.Size == 0 { + f.Size = f0.Size + } + if f.col == nil { + f.col = f0.col + } + if f.Lang == "" { + f.Lang = f0.Lang + } + if f.Script == "" { + f.Script = f0.Script + } +} + +func (f *FormFont) SetCol(c color.SimpleColor) { + f.col = &c +} + +func (f FormFont) RTL() bool { + return types.MemberOf(f.Script, []string{"Arab", "Hebr"}) || types.MemberOf(f.Lang, []string{"ar", "fa", "he"}) +} + +func FormFontNameAndLangForID(xRefTable *model.XRefTable, indRef types.IndirectRef) (string, string, error) { + + objNr := int(indRef.ObjectNumber) + fontDict, err := xRefTable.DereferenceDict(indRef) + if err != nil || fontDict == nil { + return "", "", err + } + + _, fName, err := pdffont.Name(xRefTable, fontDict, objNr) + if err != nil { + return "", "", err + } + + var fLang string + if font.IsUserFont(fName) { + fLang, err = pdffont.Lang(xRefTable, fontDict) + if err != nil { + return "", "", err + } + } + + return fName, fLang, nil +} + +// FormFontResDict returns form dict's font resource dict. +func FormFontResDict(xRefTable *model.XRefTable) (types.Dict, error) { + + d := xRefTable.Form + if len(d) == 0 { + return nil, nil + } + + o, found := d.Find("DR") + if !found { + return nil, nil + } + + resDict, err := xRefTable.DereferenceDict(o) + if err != nil || len(resDict) == 0 { + return nil, err + } + + o, found = resDict.Find("Font") + if !found { + return nil, nil + } + + return xRefTable.DereferenceDict(o) +} + +func formFontIndRef(xRefTable *model.XRefTable, fontID string) *types.IndirectRef { + + indRef, ok := xRefTable.FillFonts[fontID] + if ok { + return &indRef + } + + for k, v := range xRefTable.FillFonts { + if strings.HasPrefix(k, fontID) || strings.HasPrefix(fontID, k) { + return &v + } + } + + return nil +} + +func FontIndRef(fName string, ctx *model.Context, fonts map[string]types.IndirectRef) (*types.IndirectRef, error) { + + indRef, ok := fonts[fName] + if ok { + d, err := ctx.DereferenceDict(indRef) + if err != nil { + return nil, err + } + if enc := d.NameEntry("Encoding"); *enc == "Identity-H" { + return &indRef, nil + } + } + + for objNr, fo := range ctx.Optimize.FontObjects { + if fo.FontName == fName { + indRef := types.NewIndirectRef(objNr, 0) + d, err := ctx.DereferenceDict(*indRef) + if err != nil { + return nil, err + } + if enc := d.NameEntry("Encoding"); *enc == "Identity-H" { + fonts[fName] = *indRef + return indRef, nil + } + } + } + + return nil, nil +} + +func ensureUTF8FormFont(ctx *model.Context, fonts map[string]types.IndirectRef) (string, string, string, *types.IndirectRef, error) { + + // TODO Make name of UTF-8 userfont part of pdfcpu configs. + + fontID, fontName := "F0", "Roboto-Regular" + + if indRef, ok := fonts[fontName]; ok { + return fontID, fontName, "", &indRef, nil + } + + for objNr, fo := range ctx.Optimize.FontObjects { + if fo.FontName == fontName && fo.Prefix != "" { + indRef := types.NewIndirectRef(objNr, 0) + fonts[fontName] = *indRef + return fontID, fontName, "", indRef, nil + } + } + + indRef, err := pdffont.EnsureFontDict(ctx.XRefTable, fontName, "", "", false, nil) + if err != nil { + return "", "", "", nil, err + } + fonts[fontName] = *indRef + + return fontID, fontName, "", indRef, nil +} + +func extractFormFontDetails( + ctx *model.Context, + fontID string, + fonts map[string]types.IndirectRef) (string, string, string, *types.IndirectRef, error) { + + xRefTable := ctx.XRefTable + + var ( + fName, fLang string + fontIndRef *types.IndirectRef + err error + ) + + if len(fontID) > 0 { + + fontIndRef = formFontIndRef(xRefTable, fontID) + if fontIndRef != nil { + fName, fLang, err = FormFontNameAndLangForID(xRefTable, *fontIndRef) + if err != nil { + return "", "", "", nil, err + } + + if fName == "" { + return "", "", "", nil, errors.Errorf("pdfcpu: Unable to detect fontName for: %s", fontID) + } + } + + } + + if fontIndRef == nil { + return ensureUTF8FormFont(ctx, fonts) + } + + return fontID, fName, fLang, fontIndRef, err +} + +func fontFromDA(s string) (string, FormFont, error) { + + da := strings.Fields(s) + + var ( + f FormFont + fontID string + ) + + f.SetCol(color.Black) + + for i := 0; i < len(da); i++ { + if da[i] == "Tf" { + fontID = da[i-2][1:] + //tf.SetFontID(fontID) + fl, err := strconv.ParseFloat(da[i-1], 64) + if err != nil { + return fontID, f, err + } + if fl == 0 { + // TODO derive size from acroDict DA and then use a default form font size (add to pdfcpu config) + fl = 12 + } + f.Size = int(fl) + continue + } + if da[i] == "rg" { + r, _ := strconv.ParseFloat(da[i-3], 32) + g, _ := strconv.ParseFloat(da[i-2], 32) + b, _ := strconv.ParseFloat(da[i-1], 32) + f.SetCol(color.SimpleColor{R: float32(r), G: float32(g), B: float32(b)}) + continue + } + if da[i] == "g" { + g, _ := strconv.ParseFloat(da[i-1], 32) + f.SetCol(color.SimpleColor{R: float32(g), G: float32(g), B: float32(g)}) + } + } + + return fontID, f, nil +} diff --git a/pkg/pdfcpu/primitives/guide.go b/pkg/pdfcpu/primitives/guide.go new file mode 100644 index 0000000000000000000000000000000000000000..9bb7d775a53de37608c17c09d08c5253d410e150 --- /dev/null +++ b/pkg/pdfcpu/primitives/guide.go @@ -0,0 +1,46 @@ +/* + Copyright 2021 The pdfcpu Authors. + + 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 + + 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 primitives + +import ( + "io" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +// Guide represents horizontal and vertical lines at (x,y) for layout purposes. +type Guide struct { + Position [2]float64 `json:"pos"` // x,y + x, y float64 +} + +func (g *Guide) validate() { + g.x = g.Position[0] + g.y = g.Position[1] +} + +func (g *Guide) render(w io.Writer, r *types.Rectangle, pdf *PDF) { + x, y := types.NormalizeCoord(g.x, g.y, r, pdf.origin, true) + if g.x < 0 { + x = 0 + } + if g.y < 0 { + y = 0 + } + draw.DrawHairCross(w, x, y, r) +} diff --git a/pkg/pdfcpu/primitives/imageBox.go b/pkg/pdfcpu/primitives/imageBox.go new file mode 100644 index 0000000000000000000000000000000000000000..6dd6af173af4a911c43a8090d0686de3fe982e02 --- /dev/null +++ b/pkg/pdfcpu/primitives/imageBox.go @@ -0,0 +1,679 @@ +/* + Copyright 2021 The pdfcpu Authors. + + 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 + + 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 primitives + +import ( + "fmt" + "io" + "math" + "net/http" + "os" + "strconv" + "strings" + "time" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/matrix" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// ImageData represents a more direct way for providing image data for form filling scenarios. +type ImageData struct { + Payload string // base64 encoded image data + Format string // jpeg, png, webp, tiff, ccitt + Width, Height int +} + +// ImageBox is a rectangular region within content containing an image. +type ImageBox struct { + pdf *PDF + content *Content + Name string + Src string `json:"src"` // path of image file name + Data *ImageData // TODO Implement + Position [2]float64 `json:"pos"` // x,y + x, y float64 + Dx, Dy float64 + dest *types.Rectangle + Anchor string + anchor types.Anchor + anchored bool + Width float64 + Height float64 + Margin *Margin + Border *Border + Padding *Padding + BackgroundColor string `json:"bgCol"` + bgCol *color.SimpleColor + Rotation float64 `json:"rot"` + Url string + Hide bool + PageNr string `json:"-"` +} + +func (ib *ImageBox) resolveFileName(s string) (string, error) { + if s[0] != '$' { + return s, nil + } + + varName := s[1:] + if ib.content != nil { + return ib.content.page.resolveFileName(varName) + } + + return ib.pdf.resolveFileName(varName) +} + +func (ib *ImageBox) parseAnchor() (types.Anchor, error) { + if ib.Position[0] != 0 || ib.Position[1] != 0 { + var a types.Anchor + return a, errors.New("pdfcpu: Please supply \"pos\" or \"anchor\"") + } + return types.ParseAnchor(ib.Anchor) +} + +func (ib *ImageBox) validate() error { + + ib.x = ib.Position[0] + ib.y = ib.Position[1] + + if ib.Name == "$" { + return errors.New("pdfcpu: invalid image reference $") + } + + // TODO Validate width, height inside content box + + if ib.Src != "" { + s, err := ib.resolveFileName(ib.Src) + if err != nil { + return err + } + ib.Src = s + } + + if ib.Anchor != "" { + a, err := ib.parseAnchor() + if err != nil { + return err + } + ib.anchor = a + ib.anchored = true + } + + if ib.Margin != nil { + if err := ib.Margin.validate(); err != nil { + return err + } + } + + if ib.Border != nil { + ib.Border.pdf = ib.pdf + if err := ib.Border.validate(); err != nil { + return err + } + } + + if ib.Padding != nil { + if err := ib.Padding.validate(); err != nil { + return err + } + } + + if ib.BackgroundColor != "" { + sc, err := ib.pdf.parseColor(ib.BackgroundColor) + if err != nil { + return err + } + ib.bgCol = sc + } + + return nil +} + +func (ib *ImageBox) margin(name string) *Margin { + return ib.content.namedMargin(name) +} + +func (ib *ImageBox) border(name string) *Border { + return ib.content.namedBorder(name) +} + +func (ib *ImageBox) padding(name string) *Padding { + return ib.content.namedPadding(name) +} + +func (ib *ImageBox) missingPosition() bool { + return ib.x == 0 && ib.y == 0 +} + +func (ib *ImageBox) mergeIn(ib0 *ImageBox) { + + if !ib.anchored && ib.missingPosition() { + ib.x = ib0.x + ib.y = ib0.y + ib.anchor = ib0.anchor + ib.anchored = ib0.anchored + } + + if ib.Dx == 0 { + ib.Dx = ib0.Dx + } + if ib.Dy == 0 { + ib.Dy = ib0.Dy + } + + if ib.Width == 0 { + ib.Width = ib0.Width + } + + if ib.Height == 0 { + ib.Height = ib0.Height + } + + if ib.Margin == nil { + ib.Margin = ib0.Margin + } + + if ib.Border == nil { + ib.Border = ib0.Border + } + + if ib.Padding == nil { + ib.Padding = ib0.Padding + } + + if ib.Src == "" && ib.Data == nil { + ib.Src = ib0.Src + ib.Data = ib0.Data + } + + if ib.bgCol == nil { + ib.bgCol = ib0.bgCol + } + + if ib.Rotation == 0 { + ib.Rotation = ib0.Rotation + } + + if !ib.Hide { + ib.Hide = ib0.Hide + } + +} + +func (ib *ImageBox) cachedImg(img model.ImageResource, pageImages model.ImageMap, pageNr int) (int, int, string, error) { + imgResIDs := ib.pdf.XObjectResIDs[pageNr] + img.Res.ID = "Im" + strconv.Itoa(len(pageImages)) + if ib.pdf.Update() { + var id string + for k, v := range imgResIDs { + if v == img.Res.IndRef { + id = k + break + } + } + if id == "" { + id = imgResIDs.NewIDForPrefix("Im", len(pageImages)) + } + img.Res.ID = id + } + pageImages[ib.Src] = img + + return img.Width, img.Height, img.Res.ID, nil +} + +func (ib *ImageBox) checkForExistingImage(sd *types.StreamDict, w, h int) (*types.IndirectRef, error) { + // For each existing image in xRefTable with matching w,h check for byte level identity. + for objNr, io := range ib.pdf.Optimize.ImageObjects { + d := io.ImageDict.Dict + if w != *d.IntEntry("Width") || h != *d.IntEntry("Height") { + continue + } + // compare decoded content from sd and io.ImageDict + ok, err := model.EqualObjects(*sd, *io.ImageDict, ib.pdf.XRefTable) + if err != nil { + return nil, err + } + if ok { + // If identical create indRef for objNr + return types.NewIndirectRef(objNr, 0), nil + } + } + return nil, nil +} + +func (ib *ImageBox) resource() (io.ReadCloser, error) { + pdf := ib.pdf + var f io.ReadCloser + if strings.HasPrefix(ib.Src, "http") { + client := pdf.httpClient + if client == nil { + pdf.httpClient = &http.Client{ + Timeout: 10 * time.Second, + } + client = pdf.httpClient + } + resp, err := client.Get(ib.Src) + if err != nil { + if log.CLIEnabled() { + log.CLI.Printf("%v: %s\n", err, ib.Src) + } + return nil, err + } + if resp.StatusCode != 200 { + if log.CLIEnabled() { + log.CLI.Printf("http status %d: %s\n", resp.StatusCode, ib.Src) + } + return nil, nil + } + f = resp.Body + } else { + var err error + f, err = os.Open(ib.Src) + if err != nil { + return nil, err + } + } + return f, nil +} + +func (ib *ImageBox) imageResource(pageImages, images model.ImageMap, pageNr int) (*model.ImageResource, error) { + + f, err := ib.resource() + if err != nil || f == nil { + return nil, err + } + + defer f.Close() + + var ( + w, h int + id string + indRef *types.IndirectRef + sd *types.StreamDict + ) + + pdf := ib.pdf + imgResIDs := pdf.XObjectResIDs[pageNr] + + if ib.pdf.Update() { + + sd, w, h, err = model.CreateImageStreamDict(pdf.XRefTable, f, false, false) + if err != nil { + return nil, err + } + + indRef, err := ib.checkForExistingImage(sd, w, h) + if err != nil { + return nil, err + } + + if indRef != nil { + for k, v := range imgResIDs { + if v == *indRef { + id = k + break + } + } + if id == "" { + id = imgResIDs.NewIDForPrefix("Im", len(images)) + } + } + } + + if indRef == nil { + if pdf.Update() { + indRef, err = pdf.XRefTable.IndRefForNewObject(*sd) + if err != nil { + return nil, err + } + id = imgResIDs.NewIDForPrefix("Im", len(pageImages)) + } else { + indRef, w, h, err = model.CreateImageResource(pdf.XRefTable, f, false, false) + if err != nil { + return nil, err + } + id = "Im" + strconv.Itoa(len(pageImages)) + } + } + + res := model.Resource{ID: id, IndRef: indRef} + + return &model.ImageResource{Res: res, Width: w, Height: h}, nil +} + +func (ib *ImageBox) image(pageImages, images model.ImageMap, pageNr int) (int, int, string, error) { + + img, ok := pageImages[ib.Src] + if ok { + return img.Width, img.Height, img.Res.ID, nil + } + + img, ok = images[ib.Src] + if ok { + return ib.cachedImg(img, pageImages, pageNr) + } + + imgRes, err := ib.imageResource(pageImages, images, pageNr) + if err != nil || imgRes == nil { + return 0, 0, "", nil + } + + images[ib.Src] = *imgRes + pageImages[ib.Src] = *imgRes + + return imgRes.Width, imgRes.Height, imgRes.Res.ID, nil +} + +func (ib *ImageBox) createLink(p *model.Page, pageNr int, r *types.Rectangle, m matrix.Matrix) { + + p1 := m.Transform(types.Point{X: r.LL.X, Y: r.LL.Y}) + p2 := m.Transform(types.Point{X: r.UR.X, Y: r.LL.X}) + p3 := m.Transform(types.Point{X: r.UR.X, Y: r.UR.Y}) + p4 := m.Transform(types.Point{X: r.LL.X, Y: r.UR.Y}) + + ql := types.QuadLiteral{P1: p1, P2: p2, P3: p3, P4: p4} + + id := fmt.Sprintf("l%d%d", pageNr, len(p.LinkAnnots)) + ann := model.NewLinkAnnotation( + *ql.EnclosingRectangle(5.0), // rect + "", // contents + id, // id + "", // modDate + 0, // f + &color.Red, // borderCol + nil, // dest + ib.Url, // uri + types.QuadPoints{ql}, // quad + false, // border + 0, // borderWidth + model.BSSolid, // borderStyle + ) + + p.LinkAnnots = append(p.LinkAnnots, ann) +} + +func (ib *ImageBox) prepareMargin() (float64, float64, float64, float64, error) { + + mTop, mRight, mBot, mLeft := 0., 0., 0., 0. + + if ib.Margin != nil { + + m := ib.Margin + if m.Name != "" && m.Name[0] == '$' { + // use named margin + mName := m.Name[1:] + m0 := ib.margin(mName) + if m0 == nil { + return mTop, mRight, mBot, mLeft, errors.Errorf("pdfcpu: unknown named margin %s", mName) + } + m.mergeIn(m0) + } + + if m.Width > 0 { + mTop = m.Width + mRight = m.Width + mBot = m.Width + mLeft = m.Width + } else { + mTop = m.Top + mRight = m.Right + mBot = m.Bottom + mLeft = m.Left + } + } + + return mTop, mRight, mBot, mLeft, nil +} + +func (ib *ImageBox) prepareBorder() (float64, *color.SimpleColor, types.LineJoinStyle, error) { + + bWidth := 0. + var bCol *color.SimpleColor + bStyle := types.LJMiter + + if ib.Border != nil { + + b := ib.Border + if b.Name != "" && b.Name[0] == '$' { + // Use named border + bName := b.Name[1:] + b0 := ib.border(bName) + if b0 == nil { + return bWidth, bCol, bStyle, errors.Errorf("pdfcpu: unknown named border %s", bName) + } + b.mergeIn(b0) + } + + if b.Width >= 0 { + bWidth = float64(b.Width) + if b.col != nil { + bCol = b.col + } + bStyle = b.style + } + + if bWidth > 0 && bCol == nil { + bWidth = 0 + } + } + + return bWidth, bCol, bStyle, nil +} + +func (ib *ImageBox) preparePadding() (float64, float64, float64, float64, error) { + + pTop, pRight, pBot, pLeft := 0., 0., 0., 0. + + if ib.Padding != nil { + + p := ib.Padding + if p.Name != "" && p.Name[0] == '$' { + // use named padding + pName := p.Name[1:] + p0 := ib.padding(pName) + if p0 == nil { + return pTop, pRight, pBot, pLeft, errors.Errorf("pdfcpu: unknown named padding %s", pName) + } + p.mergeIn(p0) + } + + pTop, pRight, pBot, pLeft = p.Top, p.Right, p.Bottom, p.Left + if p.Width > 0 { + pTop, pRight, pBot, pLeft = p.Width, p.Width, p.Width, p.Width + } + + } + + return pTop, pRight, pBot, pLeft, nil +} + +func (ib *ImageBox) calcDim(rSrc, r *types.Rectangle, bWidth, pTop, pBot, pLeft, pRight float64) { + if ib.Width == 0 && ib.Height == 0 { + if rSrc.Width() <= r.Width() && rSrc.Height() <= r.Height() { + ib.Width = rSrc.Width() + ib.Height = rSrc.Height() + } else { + ib.Height = r.Height() + ib.Width = rSrc.ScaledWidth(ib.Height-2*bWidth-pTop-pBot) + 2*bWidth + pLeft + pRight + } + } else if ib.Width == 0 { + ib.Width = rSrc.ScaledWidth(ib.Height-2*bWidth-pTop-pBot) + 2*bWidth + pLeft + pRight + } else if ib.Height == 0 { + ib.Height = rSrc.ScaledHeight(ib.Width-2*bWidth-pLeft-pRight) + 2*bWidth + pTop + pBot + } +} + +func (ib *ImageBox) calcTransform( + mLeft, mBot, mRight, mTop, + pLeft, pBot, pRight, pTop, + bWidth float64, rSrc *types.Rectangle) (matrix.Matrix, float64, float64, float64, float64, *types.Rectangle) { + + cBox := ib.dest + if ib.content != nil { + cBox = ib.content.Box() + } + r := cBox.CroppedCopy(0) + r.LL.X += mLeft + r.LL.Y += mBot + r.UR.X -= mRight + r.UR.Y -= mTop + + ib.calcDim(rSrc, r, bWidth, pTop, pBot, pLeft, pRight) + + var x, y float64 + if ib.anchored { + x, y = types.AnchorPosition(ib.anchor, r, ib.Width, ib.Height) + } else { + x, y = types.NormalizeCoord(ib.x, ib.y, cBox, ib.pdf.origin, false) + if y < 0 { + y = cBox.Center().Y - ib.Height/2 - r.LL.Y + } else if y > 0 { + y -= mBot + } + if x < 0 { + x = cBox.Center().X - ib.Width/2 - r.LL.X + } else if x > 0 { + x -= mLeft + } + } + + dx, dy := types.NormalizeOffset(ib.Dx, ib.Dy, ib.pdf.origin) + x += r.LL.X + dx + y += r.LL.Y + dy + + if x < r.LL.X { + x = r.LL.X + } else if x > r.UR.X-ib.Width { + x = r.UR.X - ib.Width + } + + if y < r.LL.Y { + y = r.LL.Y + } else if y > r.UR.Y-ib.Height { + y = r.UR.Y - ib.Height + } + + r = types.RectForWidthAndHeight(x, y, ib.Width, ib.Height) + r.LL.X += bWidth / 2 + r.LL.Y += bWidth / 2 + r.UR.X -= bWidth / 2 + r.UR.Y -= bWidth / 2 + + sin := math.Sin(float64(ib.Rotation) * float64(matrix.DegToRad)) + cos := math.Cos(float64(ib.Rotation) * float64(matrix.DegToRad)) + + dx = r.LL.X + dy = r.LL.Y + r.Translate(-r.LL.X, -r.LL.Y) + + dx += r.Width()/2 + sin*(r.Height()/2) - cos*r.Width()/2 + dy += r.Height()/2 - cos*(r.Height()/2) - sin*r.Width()/2 + + m := matrix.CalcTransformMatrix(1, 1, sin, cos, dx, dy) + + return m, x, y, sin, cos, r +} + +func (ib *ImageBox) render(p *model.Page, pageNr int, images model.ImageMap) error { + + mTop, mRight, mBot, mLeft, err := ib.prepareMargin() + if err != nil { + return err + } + + bWidth, bCol, bStyle, err := ib.prepareBorder() + if err != nil { + return err + } + if bCol == nil { + bCol = &color.Black + } + + pTop, pRight, pBot, pLeft, err := ib.preparePadding() + if err != nil { + return err + } + + w, h, id, err := ib.image(p.Im, images, pageNr) + if err != nil { + return err + } + + missingImg := w == 0 && h == 0 + if missingImg { + w = 200 + } + + rSrc := types.RectForDim(float64(w), float64(h)) + + m, x, y, sin, cos, r := ib.calcTransform(mLeft, mBot, mRight, mTop, pLeft, pBot, pRight, pTop, bWidth, rSrc) + + fmt.Fprintf(p.Buf, "q %.5f %.5f %.5f %.5f %.5f %.5f cm ", m[0][0], m[0][1], m[1][0], m[1][1], m[2][0], m[2][1]) + + if ib.Url != "" { + ib.createLink(p, pageNr, r, m) + } + + // Render border + if ib.bgCol != nil { + if bWidth == 0 { + bCol = ib.bgCol + } + draw.FillRect(p.Buf, r, bWidth, bCol, *ib.bgCol, &bStyle) + } else if ib.Border != nil { + draw.DrawRect(p.Buf, r, bWidth, bCol, &bStyle) + } + if ib.pdf.Debug { + draw.DrawCircle(p.Buf, r.LL.X, r.LL.Y, 5, color.Black, &color.Red) + } + fmt.Fprint(p.Buf, "Q ") + + if !missingImg { + // Render image + rDest := types.RectForWidthAndHeight(x+bWidth+pLeft, y+bWidth+pBot, ib.Width-2*bWidth-pLeft-pRight, ib.Height-2*bWidth-pTop-pBot) + sx, sy, dx, dy, _ := types.BestFitRectIntoRect(rSrc, rDest, false, false) + dx += rDest.LL.X + dy += rDest.LL.Y + + dx += sx/2 + sin*(sy/2) - cos*sx/2 + dy += sy/2 - cos*(sy/2) - sin*sx/2 + + m = matrix.CalcTransformMatrix(sx, sy, sin, cos, dx, dy) + fmt.Fprintf(p.Buf, "q %.5f %.5f %.5f %.5f %.5f %.5f cm /%s Do Q ", m[0][0], m[0][1], m[1][0], m[1][1], m[2][0], m[2][1], id) + } + + return nil +} + +// RenderForFill renders ib during form filling. +func (ib *ImageBox) RenderForFill(pdf *PDF, p *model.Page, pageNr int, imageMap model.ImageMap) error { + + ib.pdf = pdf + + if err := ib.validate(); err != nil { + return err + } + + ib.dest = p.CropBox + + return ib.render(p, pageNr, imageMap) +} diff --git a/pkg/pdfcpu/primitives/listBox.go b/pkg/pdfcpu/primitives/listBox.go new file mode 100644 index 0000000000000000000000000000000000000000..baccdda19c86d0d2d27b96e6f29f5e775905cc9d --- /dev/null +++ b/pkg/pdfcpu/primitives/listBox.go @@ -0,0 +1,1015 @@ +/* + Copyright 2022 The pdfcpu Authors. + + 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 + + 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 primitives + +import ( + "bytes" + "fmt" + "io" + "unicode/utf8" + + "github.com/pdfcpu/pdfcpu/pkg/font" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + pdffont "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/font" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// ListBox represents a specific choice form field including a positioned label. +type ListBox struct { + pdf *PDF + content *Content + Label *TextFieldLabel + ID string + Tip string + Default string + Defaults []string + Value string + Values []string + Ind types.Array `json:"-"` + Options []string + Position [2]float64 `json:"pos"` + x, y float64 + Width float64 + Height float64 + Dx, Dy float64 + BoundingBox *types.Rectangle `json:"-"` + Multi bool `json:"multi"` + Font *FormFont + fontID string + Margin *Margin + Border *Border + BackgroundColor string `json:"bgCol"` + BgCol *color.SimpleColor `json:"-"` + Alignment string `json:"align"` // "Left", "Center", "Right" + HorAlign types.HAlignment `json:"-"` + RTL bool + Tab int + Locked bool + Debug bool + Hide bool +} + +func (lb *ListBox) SetFontID(s string) { + lb.fontID = s +} + +func (lb *ListBox) validateDefault() error { + if len(lb.Options) == 0 { + return errors.Errorf("pdfcpu: field: %s missing options", lb.ID) + } + + dv := []string{} + if lb.Default != "" { + dv = append(dv, lb.Default) + } + for _, v := range lb.Defaults { + if !types.MemberOf(v, dv) { + dv = append(dv, v) + } + } + if len(dv) == 0 { + return nil + } + + for _, v := range dv { + if !types.MemberOf(v, lb.Options) { + return errors.Errorf("pdfcpu: field: %s invalid default: %s", lb.ID, v) + } + } + + if !lb.Multi && len(dv) > 1 { + return errors.Errorf("pdfcpu: field %s only 1 value allowed", lb.ID) + } + + lb.Defaults = dv + + return nil +} + +func (lb *ListBox) validateValue() error { + if len(lb.Options) == 0 { + return errors.Errorf("pdfcpu: field: %s missing options", lb.ID) + } + + vv := []string{} + if lb.Value != "" { + vv = append(vv, lb.Value) + } + for _, v1 := range lb.Values { + if !types.MemberOf(v1, vv) { + vv = append(vv, v1) + } + } + if len(vv) == 0 { + return nil + } + + for _, v := range vv { + if !types.MemberOf(v, lb.Options) { + return errors.Errorf("pdfcpu: field: %s invalid default: %s", lb.ID, v) + } + } + + if !lb.Multi && len(vv) > 1 { + return errors.Errorf("pdfcpu: field %s only 1 value allowed", lb.ID) + } + + lb.Values = vv + + return nil +} + +func (lb *ListBox) validateID() error { + if lb.ID == "" { + return errors.New("pdfcpu: missing field id") + } + if lb.pdf.DuplicateField(lb.ID) { + return errors.Errorf("pdfcpu: duplicate form field: %s", lb.ID) + } + lb.pdf.FieldIDs[lb.ID] = true + return nil +} + +func (lb *ListBox) validatePosition() error { + if lb.Position[0] < 0 || lb.Position[1] < 0 { + return errors.Errorf("pdfcpu: field: %s pos value < 0", lb.ID) + } + lb.x, lb.y = lb.Position[0], lb.Position[1] + return nil +} + +func (lb *ListBox) validateWidth() error { + if lb.Width == 0 { + return errors.Errorf("pdfcpu: field: %s width == 0", lb.ID) + } + return nil +} + +func (lb *ListBox) validateHeight() error { + if lb.Height < 0 { + return errors.Errorf("pdfcpu: field: %s height < 0", lb.ID) + } + return nil +} + +func (lb *ListBox) validateFont() error { + if lb.Font != nil { + lb.Font.pdf = lb.pdf + if err := lb.Font.validate(); err != nil { + return err + } + } + return nil +} + +func (lb *ListBox) validateMargin() error { + if lb.Margin != nil { + if err := lb.Margin.validate(); err != nil { + return err + } + } + return nil +} + +func (lb *ListBox) validateBorder() error { + if lb.Border != nil { + lb.Border.pdf = lb.pdf + if err := lb.Border.validate(); err != nil { + return err + } + } + return nil +} + +func (lb *ListBox) validateBackgroundColor() error { + if lb.BackgroundColor != "" { + sc, err := lb.pdf.parseColor(lb.BackgroundColor) + if err != nil { + return err + } + lb.BgCol = sc + } + return nil +} + +func (lb *ListBox) validateHorAlign() error { + lb.HorAlign = types.AlignLeft + if lb.Alignment != "" { + ha, err := types.ParseHorAlignment(lb.Alignment) + if err != nil { + return err + } + lb.HorAlign = ha + } + return nil +} + +func (lb *ListBox) validateLabel() error { + if lb.Label != nil { + lb.Label.pdf = lb.pdf + if err := lb.Label.validate(); err != nil { + return err + } + } + return nil +} + +func (lb *ListBox) validateTab() error { + if lb.Tab < 0 { + return errors.Errorf("pdfcpu: field: %s negative tab value", lb.ID) + } + if lb.Tab == 0 { + return nil + } + page := lb.content.page + if page.Tabs == nil { + page.Tabs = types.IntSet{} + } else { + if page.Tabs[lb.Tab] { + return errors.Errorf("pdfcpu: field: %s duplicate tab value %d", lb.ID, lb.Tab) + } + } + page.Tabs[lb.Tab] = true + return nil +} + +func (lb *ListBox) validate() error { + + if err := lb.validateID(); err != nil { + return err + } + + if err := lb.validatePosition(); err != nil { + return err + } + + if err := lb.validateWidth(); err != nil { + return err + } + + if err := lb.validateHeight(); err != nil { + return err + } + + if err := lb.validateDefault(); err != nil { + return err + } + + if err := lb.validateValue(); err != nil { + return err + } + + if err := lb.validateFont(); err != nil { + return err + } + + if err := lb.validateMargin(); err != nil { + return err + } + + if err := lb.validateBorder(); err != nil { + return err + } + + if err := lb.validateBackgroundColor(); err != nil { + return err + } + + if err := lb.validateHorAlign(); err != nil { + return err + } + + if err := lb.validateLabel(); err != nil { + return err + } + + return lb.validateTab() +} + +func (lb *ListBox) calcFontFromDA(ctx *model.Context, d types.Dict, fonts map[string]types.IndirectRef) (*types.IndirectRef, error) { + + s := d.StringEntry("DA") + if s == nil { + s = ctx.Form.StringEntry("DA") + if s == nil { + return nil, errors.New("pdfcpu: listbox missing \"DA\"") + } + } + + fontID, f, err := fontFromDA(*s) + if err != nil { + return nil, err + } + + lb.Font, lb.fontID = &f, fontID + + id, name, lang, fontIndRef, err := extractFormFontDetails(ctx, lb.fontID, fonts) + if err != nil { + return nil, err + } + if fontIndRef == nil { + return nil, errors.New("pdfcpu: unable to detect indirect reference for font") + } + + lb.fontID = id + lb.Font.Name = name + lb.Font.Lang = lang + lb.RTL = pdffont.RTL(lang) + + return fontIndRef, nil +} + +func (lb *ListBox) calcFont() error { + f, err := lb.content.calcInputFont(lb.Font) + if err != nil { + return err + } + lb.Font = f + + if lb.Label != nil { + f, err = lb.content.calcLabelFont(lb.Label.Font) + if err != nil { + return err + } + lb.Label.Font = f + } + + return nil +} + +func (lb *ListBox) margin(name string) *Margin { + return lb.content.namedMargin(name) +} + +func (lb *ListBox) calcMargin() (float64, float64, float64, float64, error) { + mTop, mRight, mBottom, mLeft := 0., 0., 0., 0. + if lb.Margin != nil { + m := lb.Margin + if m.Name != "" && m.Name[0] == '$' { + // use named margin + mName := m.Name[1:] + m0 := lb.margin(mName) + if m0 == nil { + return mTop, mRight, mBottom, mLeft, errors.Errorf("pdfcpu: unknown named margin %s", mName) + } + m.mergeIn(m0) + } + if m.Width > 0 { + mTop = m.Width + mRight = m.Width + mBottom = m.Width + mLeft = m.Width + } else { + mTop = m.Top + mRight = m.Right + mBottom = m.Bottom + mLeft = m.Left + } + } + return mTop, mRight, mBottom, mLeft, nil +} + +func (lb *ListBox) labelPos(labelHeight, w, g float64) (float64, float64) { + + var x, y float64 + bb, horAlign := lb.BoundingBox, lb.Label.HorAlign + + switch lb.Label.relPos { + + case types.RelPosLeft: + x = bb.LL.X - g + if horAlign == types.AlignLeft { + x -= w + if x < 0 { + x = 0 + } + } + y = bb.UR.Y - labelHeight + + case types.RelPosRight: + x = bb.UR.X + g + if horAlign == types.AlignRight { + x += w + } + y = bb.UR.Y - labelHeight + + case types.RelPosTop: + y = bb.UR.Y + g + x = bb.LL.X + if horAlign == types.AlignRight { + x += bb.Width() + } else if horAlign == types.AlignCenter { + x += bb.Width() / 2 + } + + case types.RelPosBottom: + y = bb.LL.Y - g - labelHeight + x = bb.LL.X + if horAlign == types.AlignRight { + x += bb.Width() + } else if horAlign == types.AlignCenter { + x += bb.Width() / 2 + } + + } + + return x, y +} + +func selectItem(w io.Writer, i int, width, height float64, fontName string, fontSize int, boWidth float64, col color.SimpleColor) { + lh := font.LineHeight(fontName, fontSize) + fmt.Fprintf(w, "%.2f %.2f %.2f rg 1 %.2f %.2f %.2f re f ", + col.R, col.G, col.B, + height-boWidth-float64(i+1)*lh, width-2, lh) +} + +func (lb *ListBox) renderN(xRefTable *model.XRefTable) ([]byte, error) { + w, h := lb.BoundingBox.Width(), lb.BoundingBox.Height() + bgCol := lb.BgCol + boWidth, boCol := lb.calcBorder() + buf := new(bytes.Buffer) + + if bgCol != nil || boCol != nil { + fmt.Fprint(buf, "q ") + if bgCol != nil { + fmt.Fprintf(buf, "%.2f %.2f %.2f rg 0 0 %.2f %.2f re f ", bgCol.R, bgCol.G, bgCol.B, w, h) + } + if boCol != nil { + fmt.Fprintf(buf, "%.2f %.2f %.2f RG %.2f w %.2f %.2f %.2f %.2f re s ", + boCol.R, boCol.G, boCol.B, boWidth, boWidth/2, boWidth/2, w-boWidth, h-boWidth) + } + fmt.Fprint(buf, "Q ") + } + + fmt.Fprint(buf, "/Tx BMC q ") + fmt.Fprintf(buf, "1 1 %.2f %.2f re W n ", w-2, h-2) + + f, ind := lb.Font, lb.Ind + selCol := color.SimpleColor{R: 0.600006, G: 0.756866, B: 0.854904} + for i := 0; i < len(ind); i++ { + j := ind[i].(types.Integer).Value() + selectItem(buf, j, w, h, f.Name, f.Size, boWidth, selCol) + } + + x := 2 * boWidth + if x == 0 { + x = 2 + } + h0 := h + font.Descent(f.Name, f.Size) - boWidth + lh := font.LineHeight(f.Name, f.Size) + + opts := lb.Options + for i := 0; i < len(opts); i++ { + s := opts[i] + if font.IsCoreFont(f.Name) && utf8.ValidString(s) { + s = model.DecodeUTF8ToByte(s) + } + lineBB := model.CalcBoundingBox(s, 0, 0, f.Name, f.Size) + s = model.PrepBytes(xRefTable, s, f.Name, true, lb.RTL) + x := 2 * boWidth + if x == 0 { + x = 2 + } + switch lb.HorAlign { + case types.AlignCenter: + x = w/2 - lineBB.Width()/2 + case types.AlignRight: + x = w - lineBB.Width() - 2 + } + fmt.Fprint(buf, "BT ") + if i == 0 { + fmt.Fprintf(buf, "/%s %d Tf %.2f %.2f %.2f RG %.2f %.2f %.2f rg ", + lb.fontID, f.Size, + f.col.R, f.col.G, f.col.B, + f.col.R, f.col.G, f.col.B) + } + fmt.Fprintf(buf, "%.2f %.2f Td (%s) Tj ET ", x, h0-float64(i+1)*lh, s) + } + + fmt.Fprint(buf, "Q EMC ") + + if boCol != nil { + fmt.Fprintf(buf, "q %.2f %.2f %.2f RG %.2f w %.2f %.2f %.2f %.2f re s Q ", + boCol.R, boCol.G, boCol.B, boWidth, boWidth/2, boWidth/2, w-boWidth, h-boWidth) + } + + return buf.Bytes(), nil +} + +func (lb *ListBox) irN(fonts model.FontMap) (*types.IndirectRef, error) { + pdf := lb.pdf + + bb, err := lb.renderN(lb.pdf.XRefTable) + if err != nil { + return nil, err + } + + sd, err := pdf.XRefTable.NewStreamDictForBuf(bb) + if err != nil { + return nil, err + } + + sd.InsertName("Type", "XObject") + sd.InsertName("Subtype", "Form") + sd.InsertInt("FormType", 1) + sd.Insert("BBox", types.NewNumberArray(0, 0, lb.BoundingBox.Width(), lb.BoundingBox.Height())) + sd.Insert("Matrix", types.NewNumberArray(1, 0, 0, 1, 0, 0)) + + ir, err := pdf.ensureFont(lb.fontID, lb.Font.Name, lb.Font.Lang, fonts) + if err != nil { + return nil, err + } + + d := types.Dict( + map[string]types.Object{ + "Font": types.Dict( + map[string]types.Object{ + lb.fontID: *ir, + }, + ), + }, + ) + + sd.Insert("Resources", d) + + if err := sd.Encode(); err != nil { + return nil, err + } + + return pdf.XRefTable.IndRefForNewObject(*sd) +} + +func (lb *ListBox) calcBorder() (boWidth float64, boCol *color.SimpleColor) { + if lb.Border == nil { + return 0, nil + } + return lb.Border.calc() +} + +func (lb *ListBox) prepareFF() FieldFlags { + ff := FieldFlags(0) + if lb.Multi { + // Note: unsupported in Mac Preview + ff += FieldMultiselect + } + if lb.Locked { + ff += FieldReadOnly + } + return ff +} + +func (lb *ListBox) handleBorderAndMK(d types.Dict) { + bgCol := lb.BgCol + if bgCol == nil { + bgCol = lb.content.page.bgCol + if bgCol == nil { + bgCol = lb.pdf.bgCol + } + } + lb.BgCol = bgCol + + boWidth, boCol := lb.calcBorder() + + if bgCol != nil || boCol != nil { + appCharDict := types.Dict{} + if bgCol != nil { + appCharDict["BG"] = bgCol.Array() + } + if boCol != nil && boWidth > 0 { + appCharDict["BC"] = boCol.Array() + } + d["MK"] = appCharDict + } + + if boWidth > 0 { + d["Border"] = types.NewNumberArray(0, 0, boWidth) + } +} + +func (lb *ListBox) handleVAndDV(d types.Dict) error { + vv := lb.Values + if len(vv) == 0 { + vv = lb.Defaults + } + ind := types.Array{} + arr := types.Array{} + for _, v := range vv { + for i, o := range lb.Options { + if o == v { + ind = append(ind, types.Integer(i)) + } + } + s, err := types.EscapeUTF16String(v) + if err != nil { + return err + } + arr = append(arr, types.StringLiteral(*s)) + } + if len(arr) == 1 { + d["V"] = arr[0] + d["I"] = ind + lb.Ind = ind + } + if len(arr) > 1 { + d["V"] = arr + d["I"] = ind + lb.Ind = ind + } + + arr = types.Array{} + for _, v := range lb.Defaults { + s, err := types.EscapeUTF16String(v) + if err != nil { + return err + } + arr = append(arr, types.StringLiteral(*s)) + } + if len(arr) == 1 { + d["DV"] = arr[0] + } + if len(arr) > 1 { + d["DV"] = arr + } + + return nil +} + +func (lb *ListBox) prepareDict(fonts model.FontMap) (types.Dict, error) { + pdf := lb.pdf + + id, err := types.EscapeUTF16String(lb.ID) + if err != nil { + return nil, err + } + + opt := types.Array{} + for _, s := range lb.Options { + s1, err := types.EscapeUTF16String(s) + if err != nil { + return nil, err + } + opt = append(opt, types.StringLiteral(*s1)) + } + + ff := lb.prepareFF() + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("Widget"), + "FT": types.Name("Ch"), + "Rect": lb.BoundingBox.Array(), + "F": types.Integer(model.AnnPrint), + "Ff": types.Integer(ff), + "Opt": opt, + "Q": types.Integer(lb.HorAlign), + "T": types.StringLiteral(*id), + }, + ) + + if lb.Tip != "" { + tu, err := types.EscapeUTF16String(lb.Tip) + if err != nil { + return nil, err + } + d["TU"] = types.StringLiteral(*tu) + } + + lb.handleBorderAndMK(d) + + if err := lb.handleVAndDV(d); err != nil { + return nil, err + } + + if pdf.InheritedDA != "" { + d["DA"] = types.StringLiteral(pdf.InheritedDA) + } + + f := lb.Font + fCol := f.col + + fontID, err := pdf.ensureFormFont(f) + if err != nil { + return nil, err + } + lb.fontID = fontID + + da := fmt.Sprintf("/%s %d Tf %.2f %.2f %.2f rg", fontID, f.Size, fCol.R, fCol.G, fCol.B) + // Note: Mac Preview does not honour inherited "DA" + d["DA"] = types.StringLiteral(da) + + irN, err := lb.irN(fonts) + if err != nil { + return nil, err + } + + d["AP"] = types.Dict(map[string]types.Object{"N": *irN}) + + return d, nil +} + +func (lb *ListBox) bbox() *types.Rectangle { + if lb.Label == nil { + return lb.BoundingBox.Clone() + } + + l := lb.Label + var r *types.Rectangle + x := l.td.X + + switch l.td.HAlign { + case types.AlignCenter: + x -= float64(l.Width) / 2 + case types.AlignRight: + x -= float64(l.Width) + } + + r = types.RectForWidthAndHeight(x, l.td.Y, float64(l.Width), l.height) + + return model.CalcBoundingBoxForRects(lb.BoundingBox, r) +} + +func (lb *ListBox) prepareRectLL(mTop, mRight, mBottom, mLeft float64) (float64, float64) { + return lb.content.calcPosition(lb.x, lb.y, lb.Dx, lb.Dy, mTop, mRight, mBottom, mLeft) +} + +func (lb *ListBox) prepLabel(p *model.Page, pageNr int, fonts model.FontMap) error { + + if lb.Label == nil { + return nil + } + + l := lb.Label + v := l.Value + w := float64(l.Width) + g := float64(l.Gap) + + f := l.Font + fontName, fontLang, col := f.Name, f.Lang, f.col + + id, err := lb.pdf.idForFontName(fontName, fontLang, p.Fm, fonts, pageNr) + if err != nil { + return err + } + + td := model.TextDescriptor{ + Text: v, + FontName: fontName, + Embed: true, + FontKey: id, + FontSize: f.Size, + Scale: 1., + ScaleAbs: true, + RTL: l.RTL, + } + + if col != nil { + td.StrokeCol, td.FillCol = *col, *col + } + + if l.BgCol != nil { + td.ShowBackground, td.ShowTextBB, td.BackgroundCol = true, true, *l.BgCol + } + + bb := model.WriteMultiLine(lb.pdf.XRefTable, new(bytes.Buffer), types.RectForFormat("A4"), nil, td) + l.height = bb.Height() + if bb.Width() > w { + w = bb.Width() + l.Width = int(bb.Width()) + } + + td.X, td.Y = lb.labelPos(l.height, w, g) + td.HAlign, td.VAlign = l.HorAlign, types.AlignBottom + + l.td = &td + + return nil +} + +func (lb *ListBox) prepForRender(p *model.Page, pageNr int, fonts model.FontMap) error { + + mTop, mRight, mBottom, mLeft, err := lb.calcMargin() + if err != nil { + return err + } + + x, y := lb.prepareRectLL(mTop, mRight, mBottom, mLeft) + + if err := lb.calcFont(); err != nil { + return err + } + + if lb.Width < 0 { + // Extend width to maxWidth. + r := lb.content.Box().CroppedCopy(0) + r.LL.X += mLeft + r.LL.Y += mBottom + r.UR.X -= mRight + r.UR.Y -= mTop + lb.Width = r.Width() - lb.x + + } + + lb.BoundingBox = types.RectForWidthAndHeight(x, y, lb.Width, lb.Height) + + return lb.prepLabel(p, pageNr, fonts) +} + +func (lb *ListBox) doRender(p *model.Page, fonts model.FontMap) error { + + d, err := lb.prepareDict(fonts) + if err != nil { + return err + } + + ann := model.FieldAnnotation{Dict: d} + if lb.Tab > 0 { + p.AnnotTabs[lb.Tab] = ann + } else { + p.Annots = append(p.Annots, ann) + } + + if lb.Label != nil { + model.WriteColumn(lb.pdf.XRefTable, p.Buf, p.MediaBox, nil, *lb.Label.td, 0) + } + + if lb.Debug || lb.pdf.Debug { + lb.pdf.highlightPos(p.Buf, lb.BoundingBox.LL.X, lb.BoundingBox.LL.Y, lb.content.Box()) + } + + return nil +} + +func (lb *ListBox) render(p *model.Page, pageNr int, fonts model.FontMap) error { + + if err := lb.prepForRender(p, pageNr, fonts); err != nil { + return err + } + + return lb.doRender(p, fonts) +} + +// NewListBox creates a new listbox for d. +func NewListBox( + ctx *model.Context, + d types.Dict, + opts []string, + ind types.Array, + fonts map[string]types.IndirectRef) (*ListBox, *types.IndirectRef, error) { + + lb := &ListBox{Options: opts, Ind: ind} + + bb, err := ctx.RectForArray(d.ArrayEntry("Rect")) + if err != nil { + return nil, nil, err + } + + lb.BoundingBox = types.RectForDim(bb.Width(), bb.Height()) + + fontIndRef, err := lb.calcFontFromDA(ctx, d, fonts) + if err != nil { + return nil, nil, err + } + + lb.HorAlign = types.AlignLeft + if q := d.IntEntry("Q"); q != nil { + lb.HorAlign = types.HAlignment(*q) + } + + bgCol, boCol, err := calcColsFromMK(ctx, d) + if err != nil { + return nil, nil, err + } + lb.BgCol = bgCol + + var b Border + boWidth := calcBorderWidth(d) + if boWidth > 0 { + b.Width = boWidth + b.col = boCol + } + lb.Border = &b + + return lb, fontIndRef, nil +} + +func NewForm( + xRefTable *model.XRefTable, + bb []byte, + fontID string, + fontIndRef *types.IndirectRef, + boundingBox *types.Rectangle) (*types.IndirectRef, error) { + + sd, err := xRefTable.NewStreamDictForBuf(bb) + if err != nil { + return nil, err + } + + sd.InsertName("Type", "XObject") + sd.InsertName("Subtype", "Form") + sd.InsertInt("FormType", 1) + sd.Insert("BBox", types.NewNumberArray(0, 0, boundingBox.Width(), boundingBox.Height())) + sd.Insert("Matrix", types.NewNumberArray(1, 0, 0, 1, 0, 0)) + + d := types.Dict( + map[string]types.Object{ + "Font": types.Dict( + map[string]types.Object{ + fontID: *fontIndRef, + }, + ), + }, + ) + + sd.Insert("Resources", d) + + if err := sd.Encode(); err != nil { + return nil, err + } + + return xRefTable.IndRefForNewObject(*sd) +} + +func updateForm(xRefTable *model.XRefTable, bb []byte, indRef *types.IndirectRef) error { + + entry, _ := xRefTable.FindTableEntryForIndRef(indRef) + + sd := entry.Object.(types.StreamDict) + + sd.Content = bb + if err := sd.Encode(); err != nil { + return err + } + + entry.Object = sd + + return nil +} + +func renderListBoxAP(ctx *model.Context, d types.Dict, opts []string, ind types.Array, fonts map[string]types.IndirectRef) error { + + lb, fontIndRef, err := NewListBox(ctx, d, opts, ind, fonts) + if err != nil { + return err + } + + bb, err := lb.renderN(ctx.XRefTable) + if err != nil { + return err + } + + irN, err := NewForm(ctx.XRefTable, bb, lb.fontID, fontIndRef, lb.BoundingBox) + if err != nil { + return err + } + + d["AP"] = types.Dict(map[string]types.Object{"N": *irN}) + + return nil +} + +func refreshListBoxAP(ctx *model.Context, d types.Dict, opts []string, ind types.Array, fonts map[string]types.IndirectRef, irN *types.IndirectRef) error { + + lb, _, err := NewListBox(ctx, d, opts, ind, fonts) + if err != nil { + return err + } + + bb, err := lb.renderN(ctx.XRefTable) + if err != nil { + return err + } + + return updateForm(ctx.XRefTable, bb, irN) +} + +func EnsureListBoxAP(ctx *model.Context, d types.Dict, opts []string, ind types.Array, fonts map[string]types.IndirectRef) error { + + apd := d.DictEntry("AP") + if apd == nil { + return renderListBoxAP(ctx, d, opts, ind, fonts) + } + + irN := apd.IndirectRefEntry("N") + if irN == nil { + return nil + } + + return refreshListBoxAP(ctx, d, opts, ind, fonts, irN) +} diff --git a/pkg/pdfcpu/primitives/margin.go b/pkg/pdfcpu/primitives/margin.go new file mode 100644 index 0000000000000000000000000000000000000000..c40e41ab909d1b938a746a039bd80ecfceaacbdd --- /dev/null +++ b/pkg/pdfcpu/primitives/margin.go @@ -0,0 +1,79 @@ +/* + Copyright 2021 The pdfcpu Authors. + + 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 + + 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 primitives + +import "github.com/pkg/errors" + +type Margin struct { + Name string + Width float64 + Top, Right, Bottom, Left float64 +} + +func (m *Margin) validate() error { + + if m.Name == "$" { + return errors.New("pdfcpu: invalid margin reference $") + } + + if m.Width < 0 { + if m.Top > 0 || m.Right > 0 || m.Bottom > 0 || m.Left > 0 { + return errors.Errorf("pdfcpu: individual margins not allowed for width: %f", m.Width) + } + } + + if m.Width > 0 { + m.Top, m.Right, m.Bottom, m.Left = m.Width, m.Width, m.Width, m.Width + return nil + } + + return nil +} + +func (m *Margin) mergeIn(m0 *Margin) { + if m.Width > 0 { + return + } + if m.Width < 0 { + m.Top, m.Right, m.Bottom, m.Left = 0, 0, 0, 0 + return + } + + if m.Top == 0 { + m.Top = m0.Top + } else if m.Top < 0 { + m.Top = 0. + } + + if m.Right == 0 { + m.Right = m0.Right + } else if m.Right < 0 { + m.Right = 0. + } + + if m.Bottom == 0 { + m.Bottom = m0.Bottom + } else if m.Bottom < 0 { + m.Bottom = 0. + } + + if m.Left == 0 { + m.Left = m0.Left + } else if m.Left < 0 { + m.Left = 0. + } +} diff --git a/pkg/pdfcpu/primitives/padding.go b/pkg/pdfcpu/primitives/padding.go new file mode 100644 index 0000000000000000000000000000000000000000..7fbd0f05e28e4f6792fc4b9b2fbd6bcdcfcf4781 --- /dev/null +++ b/pkg/pdfcpu/primitives/padding.go @@ -0,0 +1,79 @@ +/* + Copyright 2021 The pdfcpu Authors. + + 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 + + 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 primitives + +import "github.com/pkg/errors" + +type Padding struct { + Name string + Width float64 + Top, Right, Bottom, Left float64 +} + +func (p *Padding) validate() error { + + if p.Name == "$" { + return errors.New("pdfcpu: invalid padding reference $") + } + + if p.Width < 0 { + if p.Top > 0 || p.Right > 0 || p.Bottom > 0 || p.Left > 0 { + return errors.Errorf("pdfcpu: invalid padding width: %f", p.Width) + } + } + + if p.Width > 0 { + p.Top, p.Right, p.Bottom, p.Left = p.Width, p.Width, p.Width, p.Width + return nil + } + + return nil +} + +func (p *Padding) mergeIn(p0 *Padding) { + if p.Width > 0 { + return + } + if p.Width < 0 { + p.Top, p.Right, p.Bottom, p.Left = 0, 0, 0, 0 + return + } + + if p.Top == 0 { + p.Top = p0.Top + } else if p.Top < 0 { + p.Top = 0. + } + + if p.Right == 0 { + p.Right = p0.Right + } else if p.Right < 0 { + p.Right = 0. + } + + if p.Bottom == 0 { + p.Bottom = p0.Bottom + } else if p.Bottom < 0 { + p.Bottom = 0. + } + + if p.Left == 0 { + p.Left = p0.Left + } else if p.Left < 0 { + p.Left = 0. + } +} diff --git a/pkg/pdfcpu/primitives/page.go b/pkg/pdfcpu/primitives/page.go new file mode 100644 index 0000000000000000000000000000000000000000..a25d6217208a687fc28ce5c31a7995c79e2a5c02 --- /dev/null +++ b/pkg/pdfcpu/primitives/page.go @@ -0,0 +1,374 @@ +/* + Copyright 2021 The pdfcpu Authors. + + 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 + + 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 primitives + +import ( + "path/filepath" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// PDFPage represents a PDF page with content for generation. +type PDFPage struct { + pdf *PDF + number int // page number + Paper string // page size + mediaBox *types.Rectangle // page media box + Crop string // page crop box + cropBox *types.Rectangle // page crop box + BackgroundColor string `json:"bgCol"` + bgCol *color.SimpleColor // page background color + Fonts map[string]*FormFont // default fonts + DA types.Object + Guides []*Guide // hor/vert guidelines for layout + Margin *Margin // page margin + Border *Border // page border + Padding *Padding // page padding + Margins map[string]*Margin // page scoped named margins + Borders map[string]*Border // page scoped named borders + Paddings map[string]*Padding // page scoped named paddings + SimpleBoxPool map[string]*SimpleBox `json:"boxes"` + TextBoxPool map[string]*TextBox `json:"texts"` + ImageBoxPool map[string]*ImageBox `json:"images"` + TablePool map[string]*Table `json:"tables"` + FieldGroupPool map[string]*FieldGroup `json:"fieldgroups"` + FileNames map[string]string `json:"files"` + Tabs types.IntSet `json:"-"` + Content *Content +} + +func (page *PDFPage) resolveFileName(s string) (string, error) { + filePath, ok := page.FileNames[s] + if !ok { + return page.pdf.resolveFileName(s) + } + if filePath[0] != '$' { + return filePath, nil + } + + filePath = filePath[1:] + i := strings.Index(filePath, "/") + if i <= 0 { + return "", errors.Errorf("pdfcpu: corrupt filename: %s", s) + } + + dirName := filePath[:i] + fileName := filePath[i:] + + dirPath, ok := page.pdf.DirNames[dirName] + if !ok { + return "", errors.Errorf("pdfcpu: can't resolve dirname: %s", dirName) + } + + s1 := filepath.Join(dirPath, fileName) + + return s1, nil +} + +func (page *PDFPage) validatePageBoundaries() error { + pdf := page.pdf + page.mediaBox = pdf.mediaBox + page.cropBox = pdf.cropBox + if page.Paper != "" { + dim, _, err := types.ParsePageFormat(page.Paper) + if err != nil { + return err + } + page.mediaBox = types.RectForDim(dim.Width, dim.Height) + page.cropBox = page.mediaBox.CroppedCopy(0) + } + if page.Crop != "" { + box, err := model.ParseBox(page.Crop, types.POINTS) + if err != nil { + return err + } + page.cropBox = model.ApplyBox("CropBox", box, nil, page.mediaBox) + } + return nil +} + +func (page *PDFPage) validateBackgroundColor() error { + // Default background color + if page.BackgroundColor != "" { + sc, err := page.pdf.parseColor(page.BackgroundColor) + if err != nil { + return err + } + page.bgCol = sc + } + return nil +} + +func (page *PDFPage) validateFonts() error { + // Default page fonts + for _, f := range page.Fonts { + f.pdf = page.pdf + if err := f.validate(); err != nil { + return err + } + } + return nil +} + +func (page *PDFPage) validateBorders() error { + if page.Border != nil { + if len(page.Borders) > 0 { + return errors.New("pdfcpu: Please supply either page \"border\" or \"borders\"") + } + page.Border.pdf = page.pdf + if err := page.Border.validate(); err != nil { + return err + } + page.Borders = map[string]*Border{} + page.Borders["border"] = page.Border + } + for _, b := range page.Borders { + b.pdf = page.pdf + if err := b.validate(); err != nil { + return err + } + } + return nil +} + +func (page *PDFPage) validateMargins() error { + if page.Margin != nil { + if len(page.Margins) > 0 { + return errors.New("pdfcpu: Please supply either page \"margin\" or \"margins\"") + } + if err := page.Margin.validate(); err != nil { + return err + } + page.Margins = map[string]*Margin{} + page.Margins["margin"] = page.Margin + } + for _, m := range page.Margins { + if err := m.validate(); err != nil { + return err + } + } + return nil +} + +func (page *PDFPage) validatePaddings() error { + if page.Padding != nil { + if len(page.Paddings) > 0 { + return errors.New("pdfcpu: Please supply either page \"padding\" or \"paddings\"") + } + if err := page.Padding.validate(); err != nil { + return err + } + page.Paddings = map[string]*Padding{} + page.Paddings["padding"] = page.Padding + } + for _, p := range page.Paddings { + if err := p.validate(); err != nil { + return err + } + } + return nil +} + +func (page *PDFPage) validateSimpleBoxPool() error { + // box templates + for _, sb := range page.SimpleBoxPool { + sb.pdf = page.pdf + if err := sb.validate(); err != nil { + return err + } + } + return nil +} + +func (page *PDFPage) validateTextBoxPool() error { + // text templates + for _, tb := range page.TextBoxPool { + tb.pdf = page.pdf + if err := tb.validate(); err != nil { + return err + } + } + return nil +} + +func (page *PDFPage) validateImageBoxPool() error { + // image templates + for _, ib := range page.ImageBoxPool { + ib.pdf = page.pdf + if err := ib.validate(); err != nil { + return err + } + } + return nil +} + +func (page *PDFPage) validateTablePool() error { + // table templates + for _, t := range page.TablePool { + t.pdf = page.pdf + if err := t.validate(); err != nil { + return err + } + } + return nil +} + +func (page *PDFPage) validateFieldGroupPool() error { + // textfield group templates + for _, fg := range page.FieldGroupPool { + fg.pdf = page.pdf + if err := fg.validate(); err != nil { + return err + } + } + return nil +} + +func (page *PDFPage) validatePools() error { + if err := page.validateSimpleBoxPool(); err != nil { + return err + } + if err := page.validateTextBoxPool(); err != nil { + return err + } + if err := page.validateImageBoxPool(); err != nil { + return err + } + if err := page.validateTablePool(); err != nil { + return err + } + return page.validateFieldGroupPool() +} + +func (page *PDFPage) validate() error { + + if err := page.validatePageBoundaries(); err != nil { + return err + } + + if err := page.validateBackgroundColor(); err != nil { + return err + } + + if err := page.validateFonts(); err != nil { + return err + } + + for _, g := range page.Guides { + g.validate() + } + + if err := page.validateBorders(); err != nil { + return err + } + + if err := page.validateMargins(); err != nil { + return err + } + + if err := page.validatePaddings(); err != nil { + return err + } + + if err := page.validatePools(); err != nil { + return err + } + + if page.Content == nil { + return errors.New("pdfcpu: Please supply page \"content\"") + } + + page.Content.page = page + + return page.Content.validate() +} + +func (page *PDFPage) namedFont(id string) *FormFont { + f := page.Fonts[id] + if f != nil { + return f + } + return page.pdf.Fonts[id] +} + +func (page *PDFPage) namedMargin(id string) *Margin { + m := page.Margins[id] + if m != nil { + return m + } + return page.pdf.Margins[id] +} + +func (page *PDFPage) namedBorder(id string) *Border { + b := page.Borders[id] + if b != nil { + return b + } + return page.pdf.Borders[id] +} + +func (page *PDFPage) namedPadding(id string) *Padding { + p := page.Paddings[id] + if p != nil { + return p + } + return page.pdf.Paddings[id] +} + +func (page *PDFPage) namedSimpleBox(id string) *SimpleBox { + sb := page.SimpleBoxPool[id] + if sb != nil { + return sb + } + return page.pdf.SimpleBoxPool[id] +} + +func (page *PDFPage) namedImageBox(id string) *ImageBox { + tb := page.ImageBoxPool[id] + if tb != nil { + return tb + } + return page.pdf.ImageBoxPool[id] +} + +func (page *PDFPage) namedTextBox(id string) *TextBox { + tb := page.TextBoxPool[id] + if tb != nil { + return tb + } + return page.pdf.TextBoxPool[id] +} + +func (page *PDFPage) namedTable(id string) *Table { + t := page.TablePool[id] + if t != nil { + return t + } + return page.pdf.TablePool[id] +} + +func (page *PDFPage) namedFieldGroup(id string) *FieldGroup { + fg := page.FieldGroupPool[id] + if fg != nil { + return fg + } + return page.pdf.FieldGroupPool[id] +} diff --git a/pkg/pdfcpu/primitives/pdf.go b/pkg/pdfcpu/primitives/pdf.go new file mode 100644 index 0000000000000000000000000000000000000000..a8667b6e956724402af28571f5dc1976118a537a --- /dev/null +++ b/pkg/pdfcpu/primitives/pdf.go @@ -0,0 +1,1133 @@ +/* + Copyright 2021 The pdfcpu Authors. + + 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 + + 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 primitives + +import ( + "io" + "net/http" + "path/filepath" + "sort" + "strconv" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/font" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw" + pdffont "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/font" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// FieldFlags represents the PDF form field flags. +// See table 221 et.al. +type FieldFlags int + +const ( + FieldReadOnly FieldFlags = 1 << iota + FieldRequired + FieldNoExport + UnusedFlag4 + UnusedFlag5 + UnusedFlag6 + UnusedFlag7 + UnusedFlag8 + UnusedFlag9 + UnusedFlag10 + UnusedFlag11 + UnusedFlag12 + FieldMultiline + FieldPassword + FieldNoToggleToOff + FieldRadio + FieldPushbutton + FieldCombo + FieldEdit + FieldSort + FieldFileSelect + FieldMultiselect + FieldDoNotSpellCheck + FieldDoNotScroll + FieldComb + FieldRichTextAndRadiosInUnison + FieldCommitOnSelChange +) + +// PDF is the central structure for PDF generation. +type PDF struct { + Paper string // default paper size + mediaBox *types.Rectangle // default media box + Crop string // default crop box + cropBox *types.Rectangle // default crop box + Origin string // origin of the coordinate system + origin types.Corner // one of 4 page corners + Guides bool // render guides for layouting + ContentBox bool // render contentBox = cropBox - header - footer + Debug bool // highlight element positions + BackgroundColor string `json:"bgCol"` + bgCol *color.SimpleColor // default background color + Fonts map[string]*FormFont // global fonts + FormFonts map[string]*FormFont + FieldIDs types.StringSet + Fields types.Array + InheritedDA string + Header *HorizontalBand + Footer *HorizontalBand + Pages map[string]*PDFPage + pages []*PDFPage + Margin *Margin // the global margin named "margin" + Border *Border // the global border named "border" + Padding *Padding // the global padding named "padding" + Margins map[string]*Margin // global named margins + Borders map[string]*Border // global named borders + Paddings map[string]*Padding // global named paddings + SimpleBoxPool map[string]*SimpleBox `json:"boxes"` + TextBoxPool map[string]*TextBox `json:"texts"` + ImageBoxPool map[string]*ImageBox `json:"images"` + TablePool map[string]*Table `json:"tables"` + FieldGroupPool map[string]*FieldGroup `json:"fieldgroups"` + Colors map[string]string + colors map[string]color.SimpleColor + DirNames map[string]string `json:"dirs"` + FileNames map[string]string `json:"files"` + TimestampFormat string `json:"timestamp"` + DateFormat string `json:"dateFormat"` + Conf *model.Configuration `json:"-"` + XRefTable *model.XRefTable `json:"-"` + Optimize *model.OptimizationContext `json:"-"` + FontResIDs map[int]types.Dict `json:"-"` + XObjectResIDs map[int]types.Dict `json:"-"` + CheckBoxAPs map[float64]*AP `json:"-"` + RadioBtnAPs map[float64]*AP `json:"-"` + HasForm bool `json:"-"` + OldFieldIDs types.StringSet `json:"-"` + httpClient *http.Client +} + +func (pdf *PDF) Update() bool { + return pdf.Optimize != nil +} + +func (pdf *PDF) pageCount() int { + return len(pdf.pages) +} + +func (pdf *PDF) color(s string) *color.SimpleColor { + sc, ok := pdf.colors[strings.ToLower(s)] + if !ok { + return nil + } + return &sc +} + +func (pdf *PDF) parseColor(s string) (*color.SimpleColor, error) { + sc, err := color.ParseColor(s) + if err == nil { + return &sc, nil + } + if err != color.ErrInvalidColor || s[0] != '$' { + return nil, err + } + sc1 := pdf.color(s[1:]) + if sc1 == nil { + return nil, color.ErrInvalidColor + } + return sc1, nil +} + +func (pdf *PDF) resolveFileName(s string) (string, error) { + filePath, ok := pdf.FileNames[s] + if !ok { + return "", errors.Errorf("pdfcpu: can't resolve filename: %s", s) + } + if filePath[0] != '$' { + return filePath, nil + } + + filePath = filePath[1:] + i := strings.Index(filePath, "/") + if i <= 0 { + return "", errors.Errorf("pdfcpu: corrupt filename: %s", s) + } + + dirName := filePath[:i] + fileName := filePath[i:] + + dirPath, ok := pdf.DirNames[dirName] + if !ok { + return "", errors.Errorf("pdfcpu: can't resolve dirname: %s", dirName) + } + + s1 := filepath.Join(dirPath, fileName) + + return s1, nil +} + +func (pdf *PDF) validatePageBoundaries() error { + // Default paper size + defaultPaperSize := "A4" + + // Default media box + pdf.mediaBox = types.RectForFormat(defaultPaperSize) + if pdf.Paper != "" { + dim, _, err := types.ParsePageFormat(pdf.Paper) + if err != nil { + return err + } + pdf.mediaBox = types.RectForDim(dim.Width, dim.Height) + } + pdf.cropBox = pdf.mediaBox.CroppedCopy(0) + + if pdf.Crop != "" { + box, err := model.ParseBox(pdf.Crop, types.POINTS) + if err != nil { + return err + } + pdf.cropBox = model.ApplyBox("CropBox", box, nil, pdf.mediaBox) + } + return nil +} + +func (pdf *PDF) validateOrigin() error { + // Layout coordinate system + pdf.origin = types.LowerLeft + if pdf.Origin != "" { + corner, err := types.ParseOrigin(pdf.Origin) + if err != nil { + return err + } + pdf.origin = corner + } + return nil +} + +func (pdf *PDF) validateColors() error { + // Custom colors + pdf.colors = map[string]color.SimpleColor{} + for n, c := range pdf.Colors { + if c == "" { + continue + } + sc, err := color.NewSimpleColorForHexCode(c) + if err != nil { + return err + } + pdf.colors[strings.ToLower(n)] = sc + } + + // Default background color + if pdf.BackgroundColor != "" { + sc, err := pdf.parseColor(pdf.BackgroundColor) + if err != nil { + return err + } + pdf.bgCol = sc + } + return nil +} + +func (pdf *PDF) validateFonts() error { + // Default fonts + for _, f := range pdf.Fonts { + f.pdf = pdf + if err := f.validate(); err != nil { + return err + } + } + return nil +} + +func (pdf *PDF) validateHeader() error { + if pdf.Header != nil { + if err := pdf.Header.validate(); err != nil { + return err + } + pdf.Header.position = types.TopCenter + pdf.Header.pdf = pdf + } + return nil +} + +func (pdf *PDF) validateFooter() error { + if pdf.Footer != nil { + if err := pdf.Footer.validate(); err != nil { + return err + } + pdf.Footer.position = types.BottomCenter + pdf.Footer.pdf = pdf + } + return nil +} + +func (pdf *PDF) validateBorders() error { + if pdf.Border != nil { + if len(pdf.Borders) > 0 { + return errors.New("pdfcpu: Please supply either \"border\" or \"borders\"") + } + pdf.Border.pdf = pdf + if err := pdf.Border.validate(); err != nil { + return err + } + pdf.Borders = map[string]*Border{} + pdf.Borders["border"] = pdf.Border + } + for _, b := range pdf.Borders { + b.pdf = pdf + if err := b.validate(); err != nil { + return err + } + } + return nil +} + +func (pdf *PDF) validateMargins() error { + if pdf.Margin != nil { + if len(pdf.Margins) > 0 { + return errors.New("pdfcpu: Please supply either \"margin\" or \"margins\"") + } + if err := pdf.Margin.validate(); err != nil { + return err + } + pdf.Margins = map[string]*Margin{} + pdf.Margins["margin"] = pdf.Margin + } + for _, m := range pdf.Margins { + if err := m.validate(); err != nil { + return err + } + } + return nil +} + +func (pdf *PDF) validatePaddings() error { + if pdf.Padding != nil { + if len(pdf.Paddings) > 0 { + return errors.New("pdfcpu: Please supply either \"padding\" or \"paddings\"") + } + if err := pdf.Padding.validate(); err != nil { + return err + } + pdf.Paddings = map[string]*Padding{} + pdf.Paddings["padding"] = pdf.Padding + } + for _, p := range pdf.Paddings { + if err := p.validate(); err != nil { + return err + } + } + return nil +} + +func (pdf *PDF) validateSimpleBoxPool() error { + // box templates + for _, sb := range pdf.SimpleBoxPool { + sb.pdf = pdf + if err := sb.validate(); err != nil { + return err + } + } + return nil +} + +func (pdf *PDF) validateTextBoxPool() error { + // text templates + for _, tb := range pdf.TextBoxPool { + tb.pdf = pdf + if err := tb.validate(); err != nil { + return err + } + } + return nil +} + +func (pdf *PDF) validateImageBoxPool() error { + // image templates + for _, ib := range pdf.ImageBoxPool { + ib.pdf = pdf + if err := ib.validate(); err != nil { + return err + } + } + return nil +} + +func (pdf *PDF) validateTablePool() error { + // table templates + for _, t := range pdf.TablePool { + t.pdf = pdf + if err := t.validate(); err != nil { + return err + } + } + return nil +} + +func (pdf *PDF) validateFieldGroupPool() error { + for _, fg := range pdf.FieldGroupPool { + fg.pdf = pdf + if err := fg.validate(); err != nil { + return err + } + } + return nil +} + +func (pdf *PDF) validatePools() error { + if err := pdf.validateSimpleBoxPool(); err != nil { + return err + } + if err := pdf.validateTextBoxPool(); err != nil { + return err + } + if err := pdf.validateImageBoxPool(); err != nil { + return err + } + if err := pdf.validateTablePool(); err != nil { + return err + } + return pdf.validateFieldGroupPool() +} + +func (pdf *PDF) validateBordersMarginsPaddings() error { + if err := pdf.validateBorders(); err != nil { + return err + } + + if err := pdf.validateMargins(); err != nil { + return err + } + + return pdf.validatePaddings() +} + +func (pdf *PDF) Validate() error { + + if err := pdf.validatePageBoundaries(); err != nil { + return err + } + + if err := pdf.validateOrigin(); err != nil { + return err + } + + if err := pdf.validateColors(); err != nil { + return err + } + + if err := pdf.validateFonts(); err != nil { + return err + } + + if err := pdf.validateHeader(); err != nil { + return err + } + + if err := pdf.validateFooter(); err != nil { + return err + } + + if pdf.TimestampFormat == "" { + pdf.TimestampFormat = pdf.Conf.TimestampFormat + } + + if pdf.DateFormat == "" { + pdf.DateFormat = pdf.Conf.DateFormat + } + + if len(pdf.Pages) == 0 { + return errors.New("pdfcpu: Please supply \"pages\"") + } + + // What follows is a quirky way of turning a map of pages into a sorted slice of pages + // including entries for pages that are missing in the map. + + var pageNrs []int + + for pageNr, p := range pdf.Pages { + nr, err := strconv.Atoi(pageNr) + if err != nil { + return errors.Errorf("pdfcpu: invalid page number: %s", pageNr) + } + pageNrs = append(pageNrs, nr) + p.number = nr + p.pdf = pdf + if err := p.validate(); err != nil { + return err + } + } + + sort.Ints(pageNrs) + + pp := []*PDFPage{} + + maxPageNr := pageNrs[len(pageNrs)-1] + for i := 1; i <= maxPageNr; i++ { + pp = append(pp, pdf.Pages[strconv.Itoa(i)]) + } + + pdf.pages = pp + + if err := pdf.validateBordersMarginsPaddings(); err != nil { + return err + } + + return pdf.validatePools() +} + +func (pdf *PDF) DuplicateField(ID string) bool { + if pdf.FieldIDs[ID] || pdf.OldFieldIDs[ID] { + return true + } + oldID, err := types.EscapeUTF16String(ID) + if err != nil { + return true + } + return pdf.OldFieldIDs[*oldID] +} + +func (pdf *PDF) calcFont(f *FormFont) error { + // called by non content primitives using fonts + if f.Name[0] != '$' { + return nil + } + fName := f.Name[1:] + f0 := pdf.Fonts[fName] + if f0 == nil { + return errors.Errorf("pdfcpu: unknown font %s", fName) + } + f.Name = f0.Name + if f.Size == 0 { + f.Size = f0.Size + } + if f.col == nil { + f.col = f0.col + } + if f.Lang == "" { + f.Lang = f0.Lang + } + if f.Script == "" { + f.Script = f0.Script + } + return nil +} + +func (pdf *PDF) newPageFontID(indRef *types.IndirectRef, nextInd, pageNr int) string { + id := "F" + strconv.Itoa(nextInd) + if pdf.Update() { + fontResIDs := pdf.FontResIDs[pageNr] + id = fontResIDs.NewIDForPrefix("F", nextInd) + if indRef == nil { + return id + } + for k, v := range fontResIDs { + if v == *indRef { + id = k + break + } + } + } + return id +} + +func (pdf *PDF) idForFontName(fontName, fontLang string, pageFonts, globalFonts model.FontMap, pageNr int) (string, error) { + + // Used for textdescriptor configuration. + + var ( + id string + indRef *types.IndirectRef + ) + + fr, ok := pageFonts[fontName] + if ok { + // Return id of known page fontResource. + return fr.Res.ID, nil + } + + fr, ok = globalFonts[fontName] + if ok { + // Add global fontResource to page fontResource and return its id. + fr.Res.ID = pdf.newPageFontID(fr.Res.IndRef, len(pageFonts), pageNr) + pageFonts[fontName] = fr + return fr.Res.ID, nil + } + + // Create new fontResource. + + fr = model.FontResource{} + + if pdf.Update() { + + for objNr, fo := range pdf.Optimize.FormFontObjects { + //fmt.Printf("searching for %s - obj:%d fontName:%s prefix:%s\n", fontName, objNr, fo.FontName, fo.Prefix) + if fontName == fo.FontName { + if font.IsCoreFont(fontName) { + indRef = types.NewIndirectRef(objNr, 0) + break + } + err := pdffont.IndRefsForUserfontUpdate(pdf.XRefTable, fo.FontDict, fontLang, &fr) + if err == nil { + indRef = types.NewIndirectRef(objNr, 0) + break + } + if err != pdffont.ErrCorruptFontDict { + return "", err + } + break + } + } + + if indRef == nil { + for objNr, fo := range pdf.Optimize.FontObjects { + //fmt.Printf("searching for %s - obj:%d fontName:%s prefix:%s\n", fontName, objNr, fo.FontName, fo.Prefix) + if fontName == fo.FontName { + indRef = types.NewIndirectRef(objNr, 0) + if font.IsUserFont(fontName) { + if err := pdffont.IndRefsForUserfontUpdate(pdf.XRefTable, fo.FontDict, fontLang, &fr); err != nil { + return "", err + } + } + break + } + } + } + + } + + id = pdf.newPageFontID(indRef, len(pageFonts), pageNr) + fr.Res = model.Resource{ID: id, IndRef: indRef} + fr.Lang = fontLang + + globalFonts[fontName] = fr // id unique for pageFonts only. + pageFonts[fontName] = fr + + return id, nil +} + +func fontIndRef(xRefTable *model.XRefTable, fontName, fontLang string) (*types.IndirectRef, error) { + fName := fontName + if strings.HasPrefix(fontName, "cjk:") { + fName = strings.TrimPrefix(fontName, "cjk:") + } + if font.IsUserFont(fName) { + // Postpone font creation. + return xRefTable.IndRefForNewObject(nil) + } + return pdffont.EnsureFontDict(xRefTable, fName, fontLang, "", false, nil) +} + +func (pdf *PDF) ensureFont(fontID, fontName, fontLang string, fonts model.FontMap) (*types.IndirectRef, error) { + + fr, ok := fonts[fontName] + if ok { + if fr.Res.IndRef != nil { + return fr.Res.IndRef, nil + } + var ( + ir *types.IndirectRef + err error + ) + if font.IsUserFont(fontName) { + // Postpone font creation. + ir, err = pdf.XRefTable.IndRefForNewObject(nil) + } else { + ir, err = pdffont.EnsureFontDict(pdf.XRefTable, fontName, fr.Lang, "", false, nil) + } + if err != nil { + return nil, err + } + fr.Res.IndRef = ir + fonts[fontName] = fr + return ir, nil + } + + var ( + indRef *types.IndirectRef + err error + ) + + if pdf.Update() { + + fName := fontName + if strings.HasPrefix(fontName, "cjk:") { + fName = strings.TrimPrefix(fontName, "cjk:") + } + + for objNr, fo := range pdf.Optimize.FormFontObjects { + if fName == fo.FontName { + indRef = types.NewIndirectRef(objNr, 0) + break + } + } + + if indRef == nil { + for objNr, fo := range pdf.Optimize.FontObjects { + if fontName == fo.FontName { + indRef = types.NewIndirectRef(objNr, 0) + break + } + } + } + } + + if indRef == nil { + if indRef, err = fontIndRef(pdf.XRefTable, fontName, fontLang); err != nil { + return nil, err + } + } + + fr.Res = model.Resource{IndRef: indRef} + fr.Lang = fontLang + + fonts[fontName] = fr + + return indRef, nil +} + +func (pdf *PDF) ensureFormFont(font *FormFont) (string, error) { + for id, f := range pdf.FormFonts { + if f.Name == font.Name { + return id, nil + } + } + + id := "F" + strconv.Itoa(len(pdf.FormFonts)) + + if pdf.Update() && pdf.HasForm { + + for _, fo := range pdf.Optimize.FormFontObjects { + if font.Name == fo.FontName { + id := fo.ResourceNames[0] + return id, nil + } + } + + // TODO Check for unique id + id = "F" + strconv.Itoa(len(pdf.Optimize.FormFontObjects)) + } + + pdf.FormFonts[id] = font + return id, nil +} + +func (pdf *PDF) calcTopLevelFonts() { + for _, f0 := range pdf.Fonts { + if f0.Name[0] == '$' { + fName := f0.Name[1:] + for id, f1 := range pdf.Fonts { + if id == fName { + f0.Name = f1.Name + if f0.Size == 0 { + f0.Size = f1.Size + } + if f0.col == nil { + f0.col = f1.col + } + if f0.Lang == "" { + f0.Lang = f1.Lang + } + if f0.Script == "" { + f0.Script = f1.Script + } + } + } + } + } +} + +func (pdf *PDF) calcInheritedPageFonts() { + for id, f0 := range pdf.Fonts { + for _, page := range pdf.pages { + if page == nil { + continue + } + f1 := page.Fonts[id] + if f1 != nil { + f1.mergeIn(f0) + } + } + } +} + +func (pdf *PDF) calcInheritedContentFonts() { + for _, page := range pdf.pages { + if page == nil { + continue + } + ff := map[string]*FormFont{} + for k, v := range pdf.Fonts { + ff[k] = v + } + for k, v := range page.Fonts { + ff[k] = v + } + page.Content.calcFont(ff) + } +} + +func (pdf *PDF) calcInheritedFonts() { + pdf.calcTopLevelFonts() + pdf.calcInheritedPageFonts() + pdf.calcInheritedContentFonts() +} + +func (pdf *PDF) calcInheritedMargins() { + // Calc inherited margins. + for id, m0 := range pdf.Margins { + for _, page := range pdf.pages { + if page == nil { + continue + } + m1 := page.Margins[id] + if m1 != nil { + m1.mergeIn(m0) + } + } + } + for _, page := range pdf.pages { + if page == nil { + continue + } + mm := map[string]*Margin{} + for k, v := range pdf.Margins { + mm[k] = v + } + for k, v := range page.Margins { + mm[k] = v + } + page.Content.calcMargin(mm) + } +} + +func (pdf *PDF) calcInheritedBorders() { + // Calc inherited borders. + for id, b0 := range pdf.Borders { + for _, page := range pdf.pages { + if page == nil { + continue + } + b1 := page.Borders[id] + if b1 != nil { + b1.mergeIn(b0) + } + } + } + for _, page := range pdf.pages { + if page == nil { + continue + } + bb := map[string]*Border{} + for k, v := range pdf.Borders { + bb[k] = v + } + for k, v := range page.Borders { + bb[k] = v + } + page.Content.calcBorder(bb) + } +} + +func (pdf *PDF) calcInheritedPaddings() { + // Calc inherited paddings. + for id, p0 := range pdf.Paddings { + for _, page := range pdf.pages { + if page == nil { + continue + } + p1 := page.Paddings[id] + if p1 != nil { + p1.mergeIn(p0) + } + } + } + for _, page := range pdf.pages { + if page == nil { + continue + } + pp := map[string]*Padding{} + for k, v := range pdf.Paddings { + pp[k] = v + } + for k, v := range page.Paddings { + pp[k] = v + } + page.Content.calcPadding(pp) + } +} + +func (pdf *PDF) calcInheritedSimpleBoxes() { + // Calc inherited SimpleBoxes. + for id, sb0 := range pdf.SimpleBoxPool { + for _, page := range pdf.pages { + if page == nil { + continue + } + sb1 := page.SimpleBoxPool[id] + if sb1 != nil { + sb1.mergeIn(sb0) + } + } + } + for _, page := range pdf.pages { + if page == nil { + continue + } + bb := map[string]*SimpleBox{} + for k, v := range pdf.SimpleBoxPool { + bb[k] = v + } + for k, v := range page.SimpleBoxPool { + bb[k] = v + } + page.Content.calcSimpleBoxes(bb) + } +} + +func (pdf *PDF) calcInheritedTextBoxes() { + // Calc inherited TextBoxes. + for id, tb0 := range pdf.TextBoxPool { + for _, page := range pdf.pages { + if page == nil { + continue + } + tb1 := page.TextBoxPool[id] + if tb1 != nil { + tb1.mergeIn(tb0) + } + } + } + for _, page := range pdf.pages { + if page == nil { + continue + } + tb := map[string]*TextBox{} + for k, v := range pdf.TextBoxPool { + tb[k] = v + } + for k, v := range page.TextBoxPool { + tb[k] = v + } + page.Content.calcTextBoxes(tb) + } +} + +func (pdf *PDF) calcInheritedImageBoxes() { + // Calc inherited ImageBoxes. + for id, ib0 := range pdf.ImageBoxPool { + for _, page := range pdf.pages { + if page == nil { + continue + } + ib1 := page.ImageBoxPool[id] + if ib1 != nil { + ib1.mergeIn(ib0) + } + } + } + for _, page := range pdf.pages { + if page == nil { + continue + } + ib := map[string]*ImageBox{} + for k, v := range pdf.ImageBoxPool { + ib[k] = v + } + for k, v := range page.ImageBoxPool { + ib[k] = v + } + page.Content.calcImageBoxes(ib) + } +} + +func (pdf *PDF) calcInheritedTables() { + // Calc inherited Tables. + for id, t0 := range pdf.TablePool { + for _, page := range pdf.pages { + if page == nil { + continue + } + t1 := page.TablePool[id] + if t1 != nil { + t1.mergeIn(t0) + } + } + } + for _, page := range pdf.pages { + if page == nil { + continue + } + t := map[string]*Table{} + for k, v := range pdf.TablePool { + t[k] = v + } + for k, v := range page.TablePool { + t[k] = v + } + page.Content.calcTables(t) + } +} + +func (pdf *PDF) calcInheritedFieldGroups() { + // Calc inherited field groups. + for id, fg0 := range pdf.FieldGroupPool { + for _, page := range pdf.pages { + if page == nil { + continue + } + fg1 := page.FieldGroupPool[id] + if fg1 != nil { + fg1.mergeIn(fg0) + } + } + } + for _, page := range pdf.pages { + if page == nil { + continue + } + fg := map[string]*FieldGroup{} + for k, v := range pdf.FieldGroupPool { + fg[k] = v + } + for k, v := range page.FieldGroupPool { + fg[k] = v + } + page.Content.calcFieldGroups(fg) + } +} + +func (pdf *PDF) calcInheritedAttrs() { + pdf.calcInheritedFonts() + pdf.calcInheritedMargins() + pdf.calcInheritedBorders() + pdf.calcInheritedPaddings() + pdf.calcInheritedSimpleBoxes() + pdf.calcInheritedTextBoxes() + pdf.calcInheritedImageBoxes() + pdf.calcInheritedTables() + pdf.calcInheritedFieldGroups() +} + +func (pdf *PDF) highlightPos(w io.Writer, x, y float64, cBox *types.Rectangle) { + draw.DrawCircle(w, x, y, 5, color.Black, &color.Red) + draw.DrawHairCross(w, x, y, cBox) +} + +func (pdf *PDF) renderPageBackground(page *PDFPage, w io.Writer) { + if page.bgCol == nil { + page.bgCol = pdf.bgCol + } + if page.bgCol != nil { + draw.FillRectNoBorder(w, page.cropBox, *page.bgCol) + } +} + +func (pdf *PDF) newModelPageforPDFPage(page *PDFPage) model.Page { + mediaBox := pdf.mediaBox + if page != nil && page.mediaBox != nil { + mediaBox = page.mediaBox + } + + cropBox := pdf.cropBox + if page != nil && page.cropBox != nil { + cropBox = page.cropBox + } + + return model.NewPage(mediaBox, cropBox) +} + +// RenderPages renders page content into model.Pages +func (pdf *PDF) RenderPages() ([]*model.Page, model.FontMap, error) { + + pdf.calcInheritedAttrs() + + pp := []*model.Page{} + fontMap := model.FontMap{} + imageMap := model.ImageMap{} + + for i, page := range pdf.pages { + + pageNr := i + 1 + + p := pdf.newModelPageforPDFPage(page) + + if page == nil { + if pageNr <= pdf.XRefTable.PageCount { + pp = append(pp, nil) + continue + } + + // Create blank page with optional background color. + if pdf.bgCol != nil { + draw.FillRectNoBorder(p.Buf, p.CropBox, *pdf.bgCol) + } + + // Render page header. + if pdf.Header != nil { + if err := pdf.Header.render(&p, pageNr, fontMap, imageMap, true); err != nil { + return nil, nil, err + } + } + + // Render page footer. + if pdf.Footer != nil { + if err := pdf.Footer.render(&p, pageNr, fontMap, imageMap, false); err != nil { + return nil, nil, err + } + } + + pp = append(pp, &p) + + continue + } + + pdf.renderPageBackground(page, p.Buf) + + var headerHeight, headerDy float64 + var footerHeight, footerDy float64 + + // Render page header. + if pdf.Header != nil { + if err := pdf.Header.render(&p, pageNr, fontMap, imageMap, true); err != nil { + return nil, nil, err + } + headerHeight = pdf.Header.Height + headerDy = float64(pdf.Header.Dy) + } + + // Render page footer. + if pdf.Footer != nil { + if err := pdf.Footer.render(&p, pageNr, fontMap, imageMap, false); err != nil { + return nil, nil, err + } + footerHeight = pdf.Footer.Height + footerDy = float64(pdf.Footer.Dy) + } + + // Render page content. + r := page.cropBox.CroppedCopy(0) + r.LL.Y += footerHeight + footerDy + r.UR.Y -= headerHeight + headerDy + page.Content.mediaBox = r + if err := page.Content.render(&p, pageNr, fontMap, imageMap); err != nil { + return nil, nil, err + } + + pp = append(pp, &p) + } + + return pp, fontMap, nil +} diff --git a/pkg/pdfcpu/primitives/radioButtonGroup.go b/pkg/pdfcpu/primitives/radioButtonGroup.go new file mode 100644 index 0000000000000000000000000000000000000000..3b283f70cf09ea34aebb03a02396f52d161be2f9 --- /dev/null +++ b/pkg/pdfcpu/primitives/radioButtonGroup.go @@ -0,0 +1,1088 @@ +/* + Copyright 2021 The pdfcpu Authors. + + 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 + + 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 primitives + +import ( + "bytes" + "fmt" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// Note: +// Mac Preview is unable to save modified radio buttons: +// The form field holding the kid terminal fields for each button does not get the current value assigned to V. +// Instead Preview sets V in the widget annotation that corresponds to the selected radio button. + +// RadioButtonGroup represents a set of radio buttons including positioned labels. +type RadioButtonGroup struct { + pdf *PDF + content *Content + Label *TextFieldLabel + ID string + Tip string + Value string // checked button + Default string + Position [2]float64 `json:"pos"` // x,y + x, y float64 + Width float64 + boundingBox *types.Rectangle + Orientation string + hor bool + Dx, Dy float64 + Margin *Margin // applied to content box + BackgroundColor string `json:"bgCol"` + bgCol *color.SimpleColor + Buttons *Buttons + RTL bool + Tab int + Locked bool + Debug bool + Hide bool +} + +func (rbg *RadioButtonGroup) Rtl() bool { + if rbg.Buttons == nil { + return false + } + return rbg.Buttons.Rtl() +} + +func (rbg *RadioButtonGroup) validateID() error { + if rbg.ID == "" { + return errors.New("pdfcpu: missing field id") + } + if rbg.pdf.DuplicateField(rbg.ID) { + return errors.Errorf("pdfcpu: duplicate form field: %s", rbg.ID) + } + rbg.pdf.FieldIDs[rbg.ID] = true + return nil +} + +func (rbg *RadioButtonGroup) validatePosition() error { + if rbg.Position[0] < 0 || rbg.Position[1] < 0 { + return errors.Errorf("pdfcpu: field: %s pos value < 0", rbg.ID) + } + rbg.x, rbg.y = rbg.Position[0], rbg.Position[1] + return nil +} + +func parseRadioButtonOrientation(s string) (types.Orientation, error) { + var o types.Orientation + switch strings.ToLower(s) { + case "h", "hor", "horizontal": + o = types.Horizontal + case "v", "vert", "vertical": + o = types.Vertical + default: + return o, errors.Errorf("pdfcpu: unknown radiobutton orientation (hor, vert): %s", s) + } + return o, nil +} + +func (rbg *RadioButtonGroup) validateOrientation() error { + rbg.hor = true + if rbg.Orientation != "" { + o, err := parseRadioButtonOrientation(rbg.Orientation) + if err != nil { + return err + } + rbg.hor = o == types.Horizontal + } + return nil +} + +func (rbg *RadioButtonGroup) validateWidth() error { + if rbg.Width <= 0 { + return errors.Errorf("pdfcpu: field: %s width <= 0", rbg.ID) + } + return nil +} + +func (rbg *RadioButtonGroup) validateMargin() error { + if rbg.Margin != nil { + if err := rbg.Margin.validate(); err != nil { + return err + } + } + return nil +} + +func (rbg *RadioButtonGroup) validateLabel() error { + if rbg.Label != nil { + rbg.Label.pdf = rbg.pdf + if err := rbg.Label.validate(); err != nil { + return err + } + } + return nil +} + +func (rbg *RadioButtonGroup) validateButtonsDefaultAndValue() error { + if rbg.Buttons == nil { + return errors.New("pdfcpu: radiobuttongroup missing buttons") + } + rbg.Buttons.pdf = rbg.pdf + return rbg.Buttons.validate(rbg.Default, rbg.Value) +} + +func (rbg *RadioButtonGroup) validateTab() error { + if rbg.Tab < 0 { + return errors.Errorf("pdfcpu: field: %s negative tab value", rbg.ID) + } + if rbg.Tab == 0 { + return nil + } + page := rbg.content.page + if page.Tabs == nil { + page.Tabs = types.IntSet{} + } else { + if page.Tabs[rbg.Tab] { + return errors.Errorf("pdfcpu: field: %s duplicate tab value %d", rbg.ID, rbg.Tab) + } + } + page.Tabs[rbg.Tab] = true + return nil +} + +func (rbg *RadioButtonGroup) validate() error { + + if err := rbg.validateID(); err != nil { + return err + } + + if err := rbg.validatePosition(); err != nil { + return err + } + + if err := rbg.validateOrientation(); err != nil { + return err + } + + if err := rbg.validateWidth(); err != nil { + return err + } + + if err := rbg.validateMargin(); err != nil { + return err + } + + if err := rbg.validateLabel(); err != nil { + return err + } + + if err := rbg.validateButtonsDefaultAndValue(); err != nil { + return err + } + + return rbg.validateTab() +} + +func (rbg *RadioButtonGroup) calcFont() error { + + if rbg.Label != nil { + f, err := rbg.content.calcLabelFont(rbg.Label.Font) + if err != nil { + return err + } + rbg.Label.Font = f + } + + if rbg.Buttons.Label != nil { + f, err := rbg.content.calcLabelFont(rbg.Buttons.Label.Font) + if err != nil { + return err + } + rbg.Buttons.Label.Font = f + } + + return nil +} + +func (rbg *RadioButtonGroup) margin(name string) *Margin { + return rbg.content.namedMargin(name) +} + +func (rbg *RadioButtonGroup) prepareMargin() (float64, float64, float64, float64, error) { + mTop, mRight, mBot, mLeft := 0., 0., 0., 0. + + if rbg.Margin != nil { + + m := rbg.Margin + if m.Name != "" && m.Name[0] == '$' { + // use named margin + mName := m.Name[1:] + m0 := rbg.margin(mName) + if m0 == nil { + return mTop, mRight, mBot, mLeft, errors.Errorf("pdfcpu: unknown named margin %s", mName) + } + m.mergeIn(m0) + } + + if m.Width > 0 { + mTop = m.Width + mRight = m.Width + mBot = m.Width + mLeft = m.Width + } else { + mTop = m.Top + mRight = m.Right + mBot = m.Bottom + mLeft = m.Left + } + } + + return mTop, mRight, mBot, mLeft, nil +} + +func (rbg *RadioButtonGroup) buttonLabelPosition(i int) (float64, float64) { + rbw := rbg.Width + g := float64(rbg.Buttons.Label.Gap) + w := float64(rbg.Buttons.Label.Width) + bg := float64(rbg.Buttons.Gap) + maxWidth := rbg.Buttons.maxWidth + + if rbg.hor { + if maxWidth+g > w { + w = maxWidth + g + } + var x float64 + if rbg.Buttons.Label.HorAlign == types.AlignLeft { + x = rbg.boundingBox.LL.X + rbw + bg + float64(i)*(rbw+w) + } + if rbg.Buttons.Label.HorAlign == types.AlignRight { + x = rbg.boundingBox.LL.X + rbg.Buttons.widths[0] + if i > 0 { + x += float64(i) * (rbw + w) + } + } + return x, rbg.boundingBox.LL.Y + rbg.boundingBox.Height()/2 + } + + if maxWidth > w { + w = maxWidth + } + var dx float64 + if rbg.Buttons.Label.HorAlign == types.AlignLeft { + dx += rbw + bg + } + if rbg.Buttons.Label.HorAlign == types.AlignRight { + dx += w + } + dy := float64(i) * (rbw + g) + return rbg.boundingBox.LL.X + dx, rbg.boundingBox.LL.Y - dy +} + +func (rbg *RadioButtonGroup) renderButtonLabels(p *model.Page, pageNr int, fonts model.FontMap) error { + l := rbg.Buttons.Label + + fontName := l.Font.Name + fontLang := l.Font.Lang + fontSize := l.Font.Size + col := l.Font.col + + id, err := rbg.pdf.idForFontName(fontName, fontLang, p.Fm, fonts, pageNr) + if err != nil { + return err + } + + td := model.TextDescriptor{ + FontName: fontName, + Embed: true, + FontKey: id, + FontSize: fontSize, + Scale: 1., + ScaleAbs: true, + RTL: l.RTL, + } + + if col != nil { + td.StrokeCol, td.FillCol = *col, *col + } + + if l.BgCol != nil { + td.ShowTextBB = true + td.ShowBackground, td.ShowTextBB, td.BackgroundCol = true, true, *l.BgCol + } + + td.HAlign, td.VAlign = l.HorAlign, types.AlignBottom + + for i, v := range rbg.Buttons.Values { + td.Text = v + td.X, td.Y = rbg.buttonLabelPosition(i) + if rbg.hor { + td.VAlign = types.AlignMiddle + } + model.WriteColumn(rbg.pdf.XRefTable, p.Buf, p.MediaBox, nil, td, 0) + } + + return nil +} + +func (rbg *RadioButtonGroup) buttonGroupBB() *types.Rectangle { + g := float64(rbg.Buttons.Label.Gap) + w := float64(rbg.Buttons.Label.Width) + bg := float64(rbg.Buttons.Gap) + maxWidth := rbg.Buttons.maxWidth + + rbSize := rbg.Width + rbCount := float64(len(rbg.Buttons.Values)) + + if rbg.hor { + if maxWidth+g > w { + w = maxWidth + g + } + width := (rbCount-1)*(w+rbSize+bg) + rbSize + if rbg.Buttons.Label.HorAlign == types.AlignRight { + width += rbg.Buttons.widths[0] + } + if rbg.Buttons.Label.HorAlign == types.AlignLeft { + width += rbg.Buttons.widths[len(rbg.Buttons.widths)-1] + } + return types.RectForWidthAndHeight(rbg.boundingBox.LL.X, rbg.boundingBox.LL.Y, width, rbSize) + } + + if maxWidth > w { + w = maxWidth + } + y := rbg.boundingBox.LL.Y - (rbCount-1)*(rbSize+g) + h := rbSize + (rbCount-1)*(rbSize+g) + + return types.RectForWidthAndHeight(rbg.boundingBox.LL.X, y, w+rbSize+bg, h) +} + +func labelPos( + relPos types.RelPosition, + horAlign types.HAlignment, + boundingBox *types.Rectangle, + labelHeight, w, g float64, multiline bool) (float64, float64) { + + var x, y float64 + + switch relPos { + + case types.RelPosLeft: + x = boundingBox.LL.X - g + if horAlign == types.AlignLeft { + x -= w + if x < 0 { + x = 0 + } + } + if multiline { + y = boundingBox.UR.Y - labelHeight + } else { + y = boundingBox.LL.Y + } + + case types.RelPosRight: + x = boundingBox.UR.X + g + if horAlign == types.AlignRight { + x += w + } + if multiline { + y = boundingBox.UR.Y - labelHeight + } else { + y = boundingBox.LL.Y + } + + case types.RelPosTop: + y = boundingBox.UR.Y + g + x = boundingBox.LL.X + if horAlign == types.AlignRight { + x += boundingBox.Width() + } else if horAlign == types.AlignCenter { + x += boundingBox.Width() / 2 + } + + case types.RelPosBottom: + y = boundingBox.LL.Y - g - labelHeight + x = boundingBox.LL.X + if horAlign == types.AlignRight { + x += boundingBox.Width() + } else if horAlign == types.AlignCenter { + x += boundingBox.Width() / 2 + } + + } + + return x, y +} + +func (rbg *RadioButtonGroup) rect(i int) *types.Rectangle { + rbw := rbg.Width + g := float64(rbg.Buttons.Label.Gap) + w := float64(rbg.Buttons.Label.Width) + bg := float64(rbg.Buttons.Gap) + + if rbg.hor { + if rbg.Buttons.maxWidth+g > w { + w = rbg.Buttons.maxWidth + g + } + var x float64 + if rbg.Buttons.Label.HorAlign == types.AlignLeft { + x = rbg.boundingBox.LL.X + float64(i)*(rbw+w) + } + if rbg.Buttons.Label.HorAlign == types.AlignRight { + x = rbg.boundingBox.LL.X + rbg.Buttons.widths[0] + bg + if i > 0 { + x += float64(i) * (rbw + w) + } + } + return types.RectForWidthAndHeight(x, rbg.boundingBox.LL.Y, rbw, rbw) + } + dx := 0. + if rbg.Buttons.Label.HorAlign == types.AlignRight { + if rbg.Buttons.maxWidth > w { + w = rbg.Buttons.maxWidth + } + dx = w + bg + } + dy := float64(i) * (rbw + g) + return types.RectForWidthAndHeight(rbg.boundingBox.LL.X+dx, rbg.boundingBox.LL.Y-dy, rbw, rbw) +} + +func (rbg *RadioButtonGroup) irDOff(asWidth float64, flip bool) (*types.IndirectRef, error) { + + w := rbg.Width + + ap, found := rbg.pdf.RadioBtnAPs[asWidth] + if found { + if !flip && ap.irDOffL != nil { + return ap.irDOffL, nil + } + if flip && ap.irDOffR != nil { + return ap.irDOffR, nil + } + } + + f := .5523 + r := w / 2 + r1 := r - .5 + dx := r + if flip { + dx = asWidth - r + } + + buf := new(bytes.Buffer) + + fmt.Fprintf(buf, "q 0.5 g 1 0 0 1 %.2f %.2f cm %.2f 0 m ", dx, r, r) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", r, f*r, f*r, r, 0., r) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", -f*r, r, -r, f*r, -r, 0.) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", -r, -f*r, -f*r, -r, .0, -r) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", f*r, -r, r, -f*r, r, 0.) + fmt.Fprintf(buf, "f Q q 1 0 0 1 %.2f %.2f cm %.2f 0 m ", dx, r, r1) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", r1, f*r1, f*r1, r1, 0., r1) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", -f*r1, r1, -r1, f*r1, -r1, 0.) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", -r1, -f*r1, -f*r1, -r1, .0, -r1) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", f*r1, -r1, r1, -f*r1, r1, 0.) + fmt.Fprint(buf, "s Q ") + + sd, err := rbg.pdf.XRefTable.NewStreamDictForBuf(buf.Bytes()) + if err != nil { + return nil, err + } + + sd.InsertName("Type", "XObject") + sd.InsertName("Subtype", "Form") + sd.InsertInt("FormType", 1) + sd.Insert("BBox", types.NewNumberArray(0, 0, asWidth, w)) + sd.Insert("Matrix", types.NewNumberArray(1, 0, 0, 1, 0, 0)) + + if err := sd.Encode(); err != nil { + return nil, err + } + + ir, err := rbg.pdf.XRefTable.IndRefForNewObject(*sd) + if err != nil { + return nil, err + } + + if !found { + ap = &AP{} + rbg.pdf.RadioBtnAPs[asWidth] = ap + } + if !flip { + ap.irDOffL = ir + } + if flip { + ap.irDOffR = ir + } + + return ir, nil +} + +func (rbg *RadioButtonGroup) irDYes(asWidth float64, flip bool) (*types.IndirectRef, error) { + + w := rbg.Width + + ap, found := rbg.pdf.RadioBtnAPs[asWidth] + if found { + if !flip && ap.irDYesL != nil { + return ap.irDYesL, nil + } + if flip && ap.irDYesR != nil { + return ap.irDYesR, nil + } + } + + f := .5523 + r := w / 2 + r1 := r - .5 + r2 := r / 2 + dx := r + if flip { + dx = asWidth - r + } + + buf := new(bytes.Buffer) + + fmt.Fprintf(buf, "q 0.5 g 1 0 0 1 %.2f %.2f cm %.2f 0 m ", dx, r, r) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", r, f*r, f*r, r, 0., r) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", -f*r, r, -r, f*r, -r, 0.) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", -r, -f*r, -f*r, -r, .0, -r) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", f*r, -r, r, -f*r, r, 0.) + fmt.Fprintf(buf, "f Q q 1 0 0 1 %.2f %.2f cm %.2f 0 m ", dx, r, r1) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", r1, f*r1, f*r1, r1, 0., r1) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", -f*r1, r1, -r1, f*r1, -r1, 0.) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", -r1, -f*r1, -f*r1, -r1, .0, -r1) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", f*r1, -r1, r1, -f*r1, r1, 0.) + fmt.Fprintf(buf, "s Q 0 g q 1 0 0 1 %.2f %.2f cm %.2f 0 m ", dx, r, r2) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", r2, f*r2, f*r2, r2, 0., r2) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", -f*r2, r2, -r2, f*r2, -r2, 0.) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", -r2, -f*r2, -f*r2, -r2, .0, -r2) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", f*r2, -r2, r2, -f*r2, r2, 0.) + fmt.Fprint(buf, "f Q ") + + sd, err := rbg.pdf.XRefTable.NewStreamDictForBuf(buf.Bytes()) + if err != nil { + return nil, err + } + + sd.InsertName("Type", "XObject") + sd.InsertName("Subtype", "Form") + sd.InsertInt("FormType", 1) + sd.Insert("BBox", types.NewNumberArray(0, 0, asWidth, w)) + sd.Insert("Matrix", types.NewNumberArray(1, 0, 0, 1, 0, 0)) + + if err := sd.Encode(); err != nil { + return nil, err + } + + ir, err := rbg.pdf.XRefTable.IndRefForNewObject(*sd) + if err != nil { + return nil, err + } + + if !found { + ap = &AP{} + rbg.pdf.RadioBtnAPs[asWidth] = ap + } + if !flip { + ap.irDYesL = ir + } + if flip { + ap.irDYesR = ir + } + + return ir, nil +} + +func (rbg *RadioButtonGroup) irNOff(asWidth float64, flip bool, bgCol *color.SimpleColor) (*types.IndirectRef, error) { + + w := rbg.Width + + ap, found := rbg.pdf.RadioBtnAPs[asWidth] + if found { + if !flip && ap.irNOffL != nil { + return ap.irNOffL, nil + } + if flip && ap.irNOffR != nil { + return ap.irNOffR, nil + } + } + + f := .5523 + r := w / 2 + r1 := r - .5 + dx := r + if flip { + dx = asWidth - r + } + + buf := new(bytes.Buffer) + + fmt.Fprintf(buf, "q ") + if bgCol != nil { + fmt.Fprintf(buf, "%.2f %.2f %.2f rg ", bgCol.R, bgCol.G, bgCol.B) + } else { + fmt.Fprint(buf, "1 g ") + } + + fmt.Fprintf(buf, "1 0 0 1 %.2f %.2f cm %.2f 0 m ", dx, r, r) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", r, f*r, f*r, r, 0., r) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", -f*r, r, -r, f*r, -r, 0.) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", -r, -f*r, -f*r, -r, .0, -r) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", f*r, -r, r, -f*r, r, 0.) + fmt.Fprintf(buf, "f Q q 1 0 0 1 %.2f %.2f cm %.2f 0 m ", dx, r, r1) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", r1, f*r1, f*r1, r1, 0., r1) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", -f*r1, r1, -r1, f*r1, -r1, 0.) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", -r1, -f*r1, -f*r1, -r1, .0, -r1) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", f*r1, -r1, r1, -f*r1, r1, 0.) + fmt.Fprint(buf, "s Q ") + + sd, err := rbg.pdf.XRefTable.NewStreamDictForBuf(buf.Bytes()) + if err != nil { + return nil, err + } + + sd.InsertName("Type", "XObject") + sd.InsertName("Subtype", "Form") + sd.InsertInt("FormType", 1) + sd.Insert("BBox", types.NewNumberArray(0, 0, asWidth, w)) + sd.Insert("Matrix", types.NewNumberArray(1, 0, 0, 1, 0, 0)) + + if err := sd.Encode(); err != nil { + return nil, err + } + + ir, err := rbg.pdf.XRefTable.IndRefForNewObject(*sd) + if err != nil { + return nil, err + } + + if !found { + ap = &AP{} + rbg.pdf.RadioBtnAPs[asWidth] = ap + } + if !flip { + ap.irNOffL = ir + } + if flip { + ap.irNOffR = ir + } + + return ir, nil +} + +func (rbg *RadioButtonGroup) irNYes(asWidth float64, flip bool, bgCol *color.SimpleColor) (*types.IndirectRef, error) { + + w := rbg.Width + + ap, found := rbg.pdf.RadioBtnAPs[asWidth] + if found { + if !flip && ap.irNYesL != nil { + return ap.irNYesL, nil + } + if flip && ap.irNYesR != nil { + return ap.irNYesR, nil + } + } + + f := .5523 + r := w / 2 + r1 := r - .5 + r2 := r / 2 + dx := r + if flip { + dx = asWidth - r + } + + buf := new(bytes.Buffer) + + fmt.Fprintf(buf, "q ") + if bgCol != nil { + fmt.Fprintf(buf, "%.2f %.2f %.2f rg ", bgCol.R, bgCol.G, bgCol.B) + } else { + fmt.Fprint(buf, "1 g ") + } + + fmt.Fprintf(buf, "1 0 0 1 %.2f %.2f cm %.2f 0 m ", dx, r, r) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", r, f*r, f*r, r, 0., r) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", -f*r, r, -r, f*r, -r, 0.) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", -r, -f*r, -f*r, -r, .0, -r) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", f*r, -r, r, -f*r, r, 0.) + fmt.Fprintf(buf, "f Q q 1 0 0 1 %.2f %.2f cm %.2f 0 m ", dx, r, r1) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", r1, f*r1, f*r1, r1, 0., r1) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", -f*r1, r1, -r1, f*r1, -r1, 0.) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", -r1, -f*r1, -f*r1, -r1, .0, -r1) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", f*r1, -r1, r1, -f*r1, r1, 0.) + fmt.Fprintf(buf, "s Q 0 g q 1 0 0 1 %.2f %.2f cm %.2f 0 m ", dx, r, r2) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", r2, f*r2, f*r2, r2, 0., r2) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", -f*r2, r2, -r2, f*r2, -r2, 0.) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", -r2, -f*r2, -f*r2, -r2, .0, -r2) + fmt.Fprintf(buf, "%.3f %.3f %.3f %.3f %.3f %.3f c ", f*r2, -r2, r2, -f*r2, r2, 0.) + fmt.Fprint(buf, "f Q ") + + sd, err := rbg.pdf.XRefTable.NewStreamDictForBuf(buf.Bytes()) + if err != nil { + return nil, err + } + + sd.InsertName("Type", "XObject") + sd.InsertName("Subtype", "Form") + sd.InsertInt("FormType", 1) + sd.Insert("BBox", types.NewNumberArray(0, 0, asWidth, w)) + sd.Insert("Matrix", types.NewNumberArray(1, 0, 0, 1, 0, 0)) + + if err := sd.Encode(); err != nil { + return nil, err + } + + ir, err := rbg.pdf.XRefTable.IndRefForNewObject(*sd) + if err != nil { + return nil, err + } + + if !found { + ap = &AP{} + rbg.pdf.RadioBtnAPs[asWidth] = ap + } + if !flip { + ap.irNYesL = ir + } + if flip { + ap.irNYesR = ir + } + + return ir, nil +} + +func (rbg *RadioButtonGroup) appearanceIndRefs(flip bool, bgCol *color.SimpleColor) ( + *types.IndirectRef, *types.IndirectRef, *types.IndirectRef, *types.IndirectRef, error) { + + w := rbg.Width + + irDOff, err := rbg.irDOff(w, flip) + if err != nil { + return nil, nil, nil, nil, err + } + + irDYes, err := rbg.irDYes(w, flip) + if err != nil { + return nil, nil, nil, nil, err + } + + irNOff, err := rbg.irNOff(w, flip, bgCol) + if err != nil { + return nil, nil, nil, nil, err + } + + irNYes, err := rbg.irNYes(w, flip, bgCol) + if err != nil { + return nil, nil, nil, nil, err + } + + return irDOff, irDYes, irNOff, irNYes, nil +} + +func (rbg *RadioButtonGroup) prepareButtonDict(r *types.Rectangle, v string, parent types.IndirectRef, irDOff, irDYes, irNOff, irNYes *types.IndirectRef) (*types.IndirectRef, types.Dict, error) { + + /* Note: Mac Preview seems to have a problem saving radio buttons. + 1) Once saved in Mac Preview selected radio buttons don't get rendered in Mac Preview whereas Adobe Reader renders them w/o problem. + 2) Preselected radio buttons remain sticky after saving across Mac Preview and Adobe Reader. + */ + + s := types.EncodeName(v) + + as := types.Name("Off") + + v1 := rbg.Default + if rbg.Value != "" { + v1 = rbg.Value + } + if v == v1 { + as = types.Name(s) + } + + d := types.Dict(map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("Widget"), + "F": types.Integer(model.AnnPrint), + "Parent": parent, + "AS": as, + "Rect": r.Array(), + "AP": types.Dict( + map[string]types.Object{ + "D": types.Dict( + map[string]types.Object{ + "Off": *irDOff, + s: *irDYes, + }, + ), + "N": types.Dict( + map[string]types.Object{ + "Off": *irNOff, + s: *irNYes, + }, + ), + }, + ), + "BS": types.Dict( + map[string]types.Object{ + "S": types.Name("I"), + "W": types.Integer(1), + }, + ), + }) + + ir, err := rbg.pdf.XRefTable.IndRefForNewObject(d) + + return ir, d, err +} + +func (rbg *RadioButtonGroup) renderRadioButtonFields(p *model.Page, parent types.IndirectRef) (types.Array, error) { + flip := rbg.Buttons.Label.HorAlign == types.AlignRight + kids := types.Array{} + + bgCol := rbg.bgCol + if bgCol == nil { + bgCol = rbg.content.page.bgCol + if bgCol == nil { + bgCol = rbg.pdf.bgCol + } + } + + for i := 0; i < len(rbg.Buttons.Values); i++ { + + irDOff, irDYes, irNOff, irNYes, err := rbg.appearanceIndRefs(flip, bgCol) + if err != nil { + return nil, err + } + + r := rbg.rect(i) + v := rbg.Buttons.Values[i] + + ir, _, err := rbg.prepareButtonDict(r, v, parent, irDOff, irDYes, irNOff, irNYes) + if err != nil { + return nil, err + } + + kids = append(kids, *ir) + } + + return kids, nil +} + +func (rbg *RadioButtonGroup) bbox() *types.Rectangle { + if rbg.Label == nil { + return rbg.Buttons.boundingBox.Clone() + } + + l := rbg.Label + var r *types.Rectangle + x := l.td.X + + switch l.td.HAlign { + case types.AlignCenter: + x -= float64(l.Width) / 2 + case types.AlignRight: + x -= float64(l.Width) + } + + y := l.td.Y + if rbg.hor { + y -= rbg.boundingBox.Height() / 2 + } + r = types.RectForWidthAndHeight(x, y, float64(l.Width), l.height) + + return model.CalcBoundingBoxForRects(rbg.Buttons.boundingBox, r) +} + +func (rbg *RadioButtonGroup) prepareRectLL(mTop, mRight, mBottom, mLeft float64) (float64, float64) { + return rbg.content.calcPosition(rbg.x, rbg.y, rbg.Dx, rbg.Dy, mTop, mRight, mBottom, mLeft) +} + +func (rbg *RadioButtonGroup) prepLabel(p *model.Page, pageNr int, fonts model.FontMap) error { + + if rbg.Label == nil { + return nil + } + + l := rbg.Label + v := l.Value + w := float64(l.Width) + g := float64(l.Gap) + + f := l.Font + fontName, fontLang, col := f.Name, f.Lang, f.col + + id, err := rbg.pdf.idForFontName(fontName, fontLang, p.Fm, fonts, pageNr) + if err != nil { + return err + } + + td := model.TextDescriptor{ + Text: v, + FontName: fontName, + Embed: true, + FontKey: id, + FontSize: f.Size, + HAlign: l.HorAlign, + Scale: 1., + ScaleAbs: true, + RTL: l.RTL, + } + + if col != nil { + td.StrokeCol, td.FillCol = *col, *col + } + + if l.BgCol != nil { + td.ShowTextBB = true + td.ShowBackground, td.ShowTextBB, td.BackgroundCol = true, true, *l.BgCol + } + + bb := model.WriteMultiLine(rbg.pdf.XRefTable, new(bytes.Buffer), types.RectForFormat("A4"), nil, td) + l.height = bb.Height() + if bb.Width() > w { + w = bb.Width() + l.Width = int(bb.Width()) + } + td.X, td.Y = labelPos(l.relPos, l.HorAlign, rbg.buttonGroupBB(), l.height, w, g, !rbg.hor) + td.VAlign = types.AlignBottom + if rbg.hor { + td.Y += rbg.boundingBox.Height() / 2 + td.VAlign = types.AlignMiddle + } + + l.td = &td + + return nil +} + +func (rbg *RadioButtonGroup) prepForRender(p *model.Page, pageNr int, fonts model.FontMap) error { + + if err := rbg.calcFont(); err != nil { + return err + } + + rbg.Buttons.calcLabelWidths(rbg.hor) + + mTop, mRight, mBottom, mLeft, err := rbg.prepareMargin() + if err != nil { + return err + } + + x, y := rbg.prepareRectLL(mTop, mRight, mBottom, mLeft) + + rbg.boundingBox = types.RectForWidthAndHeight(x, y, rbg.Width, rbg.Width) + + rbg.Buttons.boundingBox = rbg.buttonGroupBB() + + return rbg.prepLabel(p, pageNr, fonts) +} + +func (rbg *RadioButtonGroup) prepareDict(p *model.Page, pageNr int, fonts model.FontMap) (*types.IndirectRef, types.Array, error) { + + rbg.renderButtonLabels(p, pageNr, fonts) + + id, err := types.EscapeUTF16String(rbg.ID) + if err != nil { + return nil, nil, err + } + + ff := FieldNoToggleToOff + FieldRadio + if rbg.Locked { + // Note: unsupported in Mac Preview + ff += FieldReadOnly + } + + d := types.Dict( + map[string]types.Object{ + "FT": types.Name("Btn"), + "Ff": types.Integer(ff), + "T": types.StringLiteral(*id), + }, + ) + + v := types.Name("Off") + if rbg.Value != "" { + s := types.EncodeName(rbg.Value) + v = types.Name(s) + } + + if rbg.Default != "" { + s := types.EncodeName(rbg.Default) + d["DV"] = types.Name(s) + if rbg.Value == "" { + v = types.Name(s) + } + } + + d["V"] = v + + if rbg.Tip != "" { + tu, err := types.EscapeUTF16String(rbg.Tip) + if err != nil { + return nil, nil, err + } + d["TU"] = types.StringLiteral(*tu) + } + + ir, err := rbg.pdf.XRefTable.IndRefForNewObject(d) + if err != nil { + return nil, nil, err + } + + kids, err := rbg.renderRadioButtonFields(p, *ir) + if err != nil { + return nil, nil, err + } + + d["Kids"] = kids + + return ir, kids, nil +} + +func (rbg *RadioButtonGroup) doRender(p *model.Page, pageNr int, fonts model.FontMap) error { + + ir, kids, err := rbg.prepareDict(p, pageNr, fonts) + if err != nil { + return err + } + + ann := model.FieldAnnotation{IndRef: ir, Kids: kids} + if rbg.Tab > 0 { + p.AnnotTabs[rbg.Tab] = ann + } else { + p.Annots = append(p.Annots, ann) + } + + if rbg.Label != nil { + model.WriteColumn(rbg.pdf.XRefTable, p.Buf, p.MediaBox, nil, *rbg.Label.td, 0) + } + + if rbg.Debug || rbg.pdf.Debug { + rbg.pdf.highlightPos(p.Buf, rbg.boundingBox.LL.X, rbg.boundingBox.LL.Y, rbg.content.Box()) + } + + return nil +} + +func (rbg *RadioButtonGroup) render(p *model.Page, pageNr int, fonts model.FontMap) error { + + if err := rbg.prepForRender(p, pageNr, fonts); err != nil { + return err + } + + return rbg.doRender(p, pageNr, fonts) +} diff --git a/pkg/pdfcpu/primitives/regions.go b/pkg/pdfcpu/primitives/regions.go new file mode 100644 index 0000000000000000000000000000000000000000..82b2023a9ca502ef094e3e53e9cb6060312468e8 --- /dev/null +++ b/pkg/pdfcpu/primitives/regions.go @@ -0,0 +1,162 @@ +/* + Copyright 2021 The pdfcpu Authors. + + 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 + + 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 primitives + +import ( + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +type Regions struct { + page *PDFPage + parent *Content + Name string // unique + Orientation string `json:"orient"` + horizontal bool + Divider *Divider `json:"div"` + Left, Right *Content // 2 horizontal regions or + Top, Bottom *Content // 2 vertical regions + mediaBox *types.Rectangle +} + +func parseRegionOrientation(s string) (types.Orientation, error) { + var o types.Orientation + switch strings.ToLower(s) { + case "h", "hor", "horizontal": + o = types.Horizontal + case "v", "vert", "vertical": + o = types.Vertical + default: + return o, errors.Errorf("pdfcpu: unknown region orientation (hor, vert): %s", s) + } + return o, nil +} + +func (r *Regions) validate() error { + + pdf := r.page.pdf + + // trim json string necessary? + if r.Orientation == "" { + return errors.Errorf("pdfcpu: region is missing orientation") + } + o, err := parseRegionOrientation(r.Orientation) + if err != nil { + return err + } + r.horizontal = o == types.Horizontal + + if r.Divider == nil { + return errors.New("pdfcpu: region is missing divider") + } + r.Divider.pdf = pdf + if err := r.Divider.validate(); err != nil { + return err + } + + if r.horizontal { + if r.Left == nil { + return errors.Errorf("pdfcpu: regions %s is missing Left", r.Name) + } + r.Left.page = r.page + r.Left.parent = r.parent + if err := r.Left.validate(); err != nil { + return err + } + if r.Right == nil { + return errors.Errorf("pdfcpu: regions %s is missing Right", r.Name) + } + r.Right.page = r.page + r.Right.parent = r.parent + return r.Right.validate() + } + + if r.Top == nil { + return errors.Errorf("pdfcpu: regions %s is missing Top", r.Name) + } + r.Top.page = r.page + r.Top.parent = r.parent + if err := r.Top.validate(); err != nil { + return err + } + if r.Bottom == nil { + return errors.Errorf("pdfcpu: regions %s is missing Bottom", r.Name) + } + r.Bottom.page = r.page + r.Bottom.parent = r.parent + if err := r.Bottom.validate(); err != nil { + return err + } + + return nil +} + +func (r *Regions) render(p *model.Page, pageNr int, fonts model.FontMap, images model.ImageMap) error { + + if r.horizontal { + + // Calc divider. + dx := r.mediaBox.Width() * r.Divider.Pos + r.Divider.p.X, r.Divider.p.Y = types.NormalizeCoord(dx, 0, r.mediaBox, r.page.pdf.origin, true) + r.Divider.q.X, r.Divider.q.Y = types.NormalizeCoord(dx, r.mediaBox.Height(), r.mediaBox, r.page.pdf.origin, true) + + // Render left region. + r.Left.mediaBox = r.mediaBox.CroppedCopy(0) + r.Left.mediaBox.UR.X = r.Divider.p.X - float64(r.Divider.Width)/2 + r.Left.page = r.page + if err := r.Left.render(p, pageNr, fonts, images); err != nil { + return err + } + + // Render right region. + r.Right.mediaBox = r.mediaBox.CroppedCopy(0) + r.Right.mediaBox.LL.X = r.Divider.p.X + float64(r.Divider.Width)/2 + r.Right.page = r.page + if err := r.Right.render(p, pageNr, fonts, images); err != nil { + return err + } + + } else { + + // Calc divider. + dy := r.mediaBox.Height() * r.Divider.Pos + r.Divider.p.X, r.Divider.p.Y = types.NormalizeCoord(0, dy, r.mediaBox, r.page.pdf.origin, true) + r.Divider.q.X, r.Divider.q.Y = types.NormalizeCoord(r.mediaBox.Width(), dy, r.mediaBox, r.page.pdf.origin, true) + + // Render top region. + r.Top.mediaBox = r.mediaBox.CroppedCopy(0) + r.Top.mediaBox.LL.Y = r.Divider.p.Y + float64(r.Divider.Width)/2 + r.Top.page = r.page + if err := r.Top.render(p, pageNr, fonts, images); err != nil { + return err + } + + // Render bottom region. + r.Bottom.mediaBox = r.mediaBox.CroppedCopy(0) + r.Bottom.mediaBox.UR.Y = r.Divider.p.Y - float64(r.Divider.Width)/2 + r.Bottom.page = r.page + if err := r.Bottom.render(p, pageNr, fonts, images); err != nil { + return err + } + + } + + return r.Divider.render(p) +} diff --git a/pkg/pdfcpu/primitives/simpleBox.go b/pkg/pdfcpu/primitives/simpleBox.go new file mode 100644 index 0000000000000000000000000000000000000000..cc50d88b7e7cc8a263a3a6ec9a25ddb335b82582 --- /dev/null +++ b/pkg/pdfcpu/primitives/simpleBox.go @@ -0,0 +1,295 @@ +/* + Copyright 2021 The pdfcpu Authors. + + 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 + + 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 primitives + +import ( + "fmt" + "math" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/matrix" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// SimpleBox is a positioned rectangular region within content. +type SimpleBox struct { + pdf *PDF + content *Content + Name string + Position [2]float64 `json:"pos"` // x,y + x, y float64 + Dx, Dy float64 + Anchor string + anchor types.Anchor + anchored bool + Width float64 + Height float64 + Margin *Margin + Border *Border + FillColor string `json:"fillCol"` + fillCol *color.SimpleColor + Rotation float64 `json:"rot"` + Hide bool +} + +func (sb *SimpleBox) validate() error { + + sb.x = sb.Position[0] + sb.y = sb.Position[1] + + if sb.Name == "$" { + return errors.New("pdfcpu: invalid box reference $") + } + + if sb.Anchor != "" { + if sb.Position[0] != 0 || sb.Position[1] != 0 { + return errors.New("pdfcpu: Please supply \"pos\" or \"anchor\"") + } + a, err := types.ParseAnchor(sb.Anchor) + if err != nil { + return err + } + sb.anchor = a + sb.anchored = true + } + + if sb.Margin != nil { + if err := sb.Margin.validate(); err != nil { + return err + } + } + + if sb.Border != nil { + sb.Border.pdf = sb.pdf + if err := sb.Border.validate(); err != nil { + return err + } + } + + if sb.FillColor != "" { + sc, err := sb.pdf.parseColor(sb.FillColor) + if err != nil { + return err + } + sb.fillCol = sc + } + + return nil +} + +func (sb *SimpleBox) margin(name string) *Margin { + return sb.content.namedMargin(name) +} + +func (sb *SimpleBox) border(name string) *Border { + return sb.content.namedBorder(name) +} + +func (sb *SimpleBox) mergeIn(sb0 *SimpleBox) { + if sb.Width == 0 { + sb.Width = sb0.Width + } + if sb.Height == 0 { + sb.Height = sb0.Height + } + if !sb.anchored && sb.x == 0 && sb.y == 0 { + sb.x = sb0.x + sb.y = sb0.y + sb.anchor = sb0.anchor + sb.anchored = sb0.anchored + } + + if sb.Dx == 0 { + sb.Dx = sb0.Dx + } + if sb.Dy == 0 { + sb.Dy = sb0.Dy + } + + if sb.Margin == nil { + sb.Margin = sb0.Margin + } + + if sb.Border == nil { + sb.Border = sb0.Border + } + + if sb.fillCol == nil { + sb.fillCol = sb0.fillCol + } + + if sb.Rotation == 0 { + sb.Rotation = sb0.Rotation + } + + if !sb.Hide { + sb.Hide = sb0.Hide + } +} + +func (sb *SimpleBox) calcBorder() (float64, *color.SimpleColor, types.LineJoinStyle, error) { + bWidth := 0. + var bCol *color.SimpleColor + bStyle := types.LJMiter + if sb.Border != nil { + b := sb.Border + if b.Name != "" && b.Name[0] == '$' { + // Use named border + bName := b.Name[1:] + b0 := sb.border(bName) + if b0 == nil { + return bWidth, bCol, bStyle, errors.Errorf("pdfcpu: unknown named border %s", bName) + } + b.mergeIn(b0) + } + if b.Width >= 0 { + bWidth = float64(b.Width) + if b.col != nil { + bCol = b.col + } + bStyle = b.style + } + } + return bWidth, bCol, bStyle, nil +} + +func (sb *SimpleBox) calcMargin() (float64, float64, float64, float64, error) { + mTop, mRight, mBottom, mLeft := 0., 0., 0., 0. + if sb.Margin != nil { + m := sb.Margin + if m.Name != "" && m.Name[0] == '$' { + // use named margin + mName := m.Name[1:] + m0 := sb.margin(mName) + if m0 == nil { + return mTop, mRight, mBottom, mLeft, errors.Errorf("pdfcpu: unknown named margin %s", mName) + } + m.mergeIn(m0) + } + if m.Width > 0 { + mTop = m.Width + mRight = m.Width + mBottom = m.Width + mLeft = m.Width + } else { + mTop = m.Top + mRight = m.Right + mBottom = m.Bottom + mLeft = m.Left + } + } + return mTop, mRight, mBottom, mLeft, nil +} + +func (sb *SimpleBox) calcTransform(mLeft, mBottom, mRight, mTop, bWidth float64) (matrix.Matrix, *types.Rectangle) { + pdf := sb.content.page.pdf + cBox := sb.content.Box() + r := sb.content.Box().CroppedCopy(0) + r.LL.X += mLeft + r.LL.Y += mBottom + r.UR.X -= mRight + r.UR.Y -= mTop + + var x, y float64 + if sb.anchored { + x, y = types.AnchorPosition(sb.anchor, r, sb.Width, sb.Height) + } else { + x, y = types.NormalizeCoord(sb.x, sb.y, cBox, pdf.origin, false) + if y < 0 { + y = cBox.Center().Y - sb.Height/2 - r.LL.Y + } else if y > 0 { + y -= mBottom + } + if x < 0 { + x = cBox.Center().X - sb.Width/2 - r.LL.X + } else if x > 0 { + x -= mLeft + } + } + + dx, dy := types.NormalizeOffset(sb.Dx, sb.Dy, pdf.origin) + x += r.LL.X + dx + y += r.LL.Y + dy + + if x < r.LL.X { + x = r.LL.X + } else if x > r.UR.X-sb.Width { + x = r.UR.X - sb.Width + } + + if y < r.LL.Y { + y = r.LL.Y + } else if y > r.UR.Y-sb.Height { + y = r.UR.Y - sb.Height + } + + r = types.RectForWidthAndHeight(x, y, sb.Width, sb.Height) + r.LL.X += bWidth / 2 + r.LL.Y += bWidth / 2 + r.UR.X -= bWidth / 2 + r.UR.Y -= bWidth / 2 + + sin := math.Sin(float64(sb.Rotation) * float64(matrix.DegToRad)) + cos := math.Cos(float64(sb.Rotation) * float64(matrix.DegToRad)) + + dx = r.LL.X + dy = r.LL.Y + r.Translate(-r.LL.X, -r.LL.Y) + + dx += r.Width()/2 + sin*(r.Height()/2) - cos*r.Width()/2 + dy += r.Height()/2 - cos*(r.Height()/2) - sin*r.Width()/2 + + m := matrix.CalcTransformMatrix(1, 1, sin, cos, dx, dy) + + return m, r +} + +func (sb *SimpleBox) render(p *model.Page) error { + + bWidth, bCol, bStyle, err := sb.calcBorder() + if err != nil { + return err + } + + mTop, mRight, mBottom, mLeft, err := sb.calcMargin() + if err != nil { + return err + } + + m, r := sb.calcTransform(mTop, mRight, mBottom, mLeft, bWidth) + + fmt.Fprintf(p.Buf, "q %.5f %.5f %.5f %.5f %.5f %.5f cm ", m[0][0], m[0][1], m[1][0], m[1][1], m[2][0], m[2][1]) + + if sb.fillCol != nil { + draw.FillRect(p.Buf, r, bWidth, bCol, *sb.fillCol, &bStyle) + if sb.pdf.Debug { + draw.DrawCircle(p.Buf, r.LL.X, r.LL.Y, 5, color.Black, &color.Red) + } + fmt.Fprint(p.Buf, "Q ") + return nil + } + + draw.DrawRect(p.Buf, r, bWidth, bCol, &bStyle) + if sb.pdf.Debug { + draw.DrawCircle(p.Buf, r.LL.X, r.LL.Y, 5, color.Black, &color.Red) + } + fmt.Fprint(p.Buf, "Q ") + return nil +} diff --git a/pkg/pdfcpu/primitives/table.go b/pkg/pdfcpu/primitives/table.go new file mode 100644 index 0000000000000000000000000000000000000000..a277cbd7545eac2c873156e8388e9e51b11fb2e2 --- /dev/null +++ b/pkg/pdfcpu/primitives/table.go @@ -0,0 +1,963 @@ +/* +Copyright 2021 The pdfcpu Authors. + +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 + + 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 primitives + +import ( + "fmt" + + "math" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/format" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/matrix" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +type TableHeader struct { + Values []string + ColAnchors []string + colAnchors []types.Anchor + ColPaddings []*Padding + BackgroundColor string `json:"bgCol"` + bgCol *color.SimpleColor + Font *FormFont // defaults to table font + RTL bool +} + +func (th *TableHeader) validateColumnPaddings(cols int) error { + if len(th.ColPaddings) == 0 { + return nil + } + + if len(th.ColPaddings) != cols { + return errors.New("pdfcpu: table header colPaddings must be specified for each column.") + } + + for i, p := range th.ColPaddings { + if p == nil { + continue + } + if err := p.validate(); err != nil { + return errors.Errorf("%s on table header colPaddings index %d", err.Error(), i) + } + } + + return nil +} + +func (th *TableHeader) validate(pdf *PDF, cols int) error { + if th.Values == nil || len(th.Values) != cols { + return errors.Errorf("pdfcpu: wants %d table header values", cols) + } + + if len(th.ColAnchors) > 0 { + th.colAnchors = make([]types.Anchor, cols) + if len(th.ColAnchors) != cols { + return errors.New("pdfcpu: table header colAnchors must be specified for each column.") + } + for i, s := range th.ColAnchors { + a, err := types.ParseAnchor(s) + if err != nil { + return err + } + th.colAnchors[i] = a + } + } + + if err := th.validateColumnPaddings(cols); err != nil { + return err + } + + if th.Font != nil { + th.Font.pdf = pdf + if err := th.Font.validate(); err != nil { + return err + } + } + + if th.BackgroundColor != "" { + sc, err := pdf.parseColor(th.BackgroundColor) + if err != nil { + return err + } + th.bgCol = sc + } + return nil +} + +func applyTextDescriptorPadding(td *model.TextDescriptor, p *Padding) { + if p == nil { + return + } + + if p.Width > 0 { + td.MTop = p.Width + td.MRight = p.Width + td.MBot = p.Width + td.MLeft = p.Width + return + } + + td.MTop = p.Top + td.MRight = p.Right + td.MBot = p.Bottom + td.MLeft = p.Left +} + +func (th *TableHeader) calcColumnPadding(td *model.TextDescriptor, col int) { + if len(th.ColPaddings) > 0 && th.ColPaddings[col] != nil { + applyTextDescriptorPadding(td, th.ColPaddings[col]) + } +} + +// Table represents a positioned fillable data grid including a header row. +type Table struct { + pdf *PDF + content *Content + Name string + Values [][]string + Position [2]float64 `json:"pos"` // x,y + x, y float64 + Dx, Dy float64 + Anchor string + anchor types.Anchor + anchored bool + Width float64 // if < 1 then fraction of content width + Rows, Cols int + ColWidths []int // optional column width percentages + ColAnchors []string + colAnchors []types.Anchor + ColPaddings []*Padding + LineHeight int `json:"lheight"` + Font *FormFont + Margin *Margin + Border *Border + Padding *Padding + BackgroundColor string `json:"bgCol"` + OddColor string `json:"oddCol"` + EvenColor string `json:"evenCol"` + bgCol *color.SimpleColor + oddCol *color.SimpleColor + evenCol *color.SimpleColor + RTL bool + Rotation float64 `json:"rot"` + Grid bool + Hide bool + Header *TableHeader +} + +func (t *Table) Height() float64 { + i := t.Rows + if t.Header != nil { + i++ + } + return float64(i) * float64(t.LineHeight) +} + +func (t *Table) validateAnchor() error { + if t.Anchor != "" { + if t.Position[0] != 0 || t.Position[1] != 0 { + return errors.New("pdfcpu: Please supply \"pos\" or \"anchor\"") + } + a, err := types.ParseAnchor(t.Anchor) + if err != nil { + return err + } + t.anchor = a + t.anchored = true + } + return nil +} + +func (t *Table) validateColWidths() error { + // Missing colWidths results in uniform grid. + if len(t.ColWidths) > 0 { + if len(t.ColWidths) != t.Cols { + return errors.New("pdfcpu: colWidths must be specified for each column.") + } + total := 0 + for _, w := range t.ColWidths { + if w <= 0 || w >= 100 { + return errors.New("pdfcpu: colWidths 0 < wi < 1") + } + total += w + } + if total != 100 { + return errors.New("pdfcpu: colWidths % total must be 100.") + } + } + return nil +} + +func (t *Table) validateColAnchors() error { + t.colAnchors = make([]types.Anchor, t.Cols) + for i := range t.colAnchors { + t.colAnchors[i] = types.Center + } + if len(t.ColAnchors) > 0 { + if len(t.ColAnchors) != t.Cols { + return errors.New("pdfcpu: colAnchors must be specified for each column.") + } + for i, s := range t.ColAnchors { + a, err := types.ParseAnchor(s) + if err != nil { + return err + } + t.colAnchors[i] = a + } + } + return nil +} + +func (t *Table) validateColPaddings() error { + if len(t.ColPaddings) > 0 { + if len(t.ColPaddings) != t.Cols { + return errors.New("pdfcpu: colPaddings must be specified for each column.") + } + for i, p := range t.ColPaddings { + if p == nil { + continue + } + if err := p.validate(); err != nil { + return errors.Errorf("%s on colPaddings index %d", err.Error(), i) + } + } + } + return nil +} + +func (t *Table) validateColumns() error { + if err := t.validateColWidths(); err != nil { + return err + } + if err := t.validateColAnchors(); err != nil { + return err + } + return t.validateColPaddings() +} + +func (t *Table) validateValues() error { + if t.Values != nil { + if len(t.Values) > t.Rows { + return errors.Errorf("pdfcpu: values for more than %d rows", t.Rows) + } + for _, vv := range t.Values { + if len(vv) > t.Cols { + return errors.Errorf("pdfcpu: values for more than %d cols", t.Cols) + } + } + } + return nil +} + +func (t *Table) validateFont() error { + if t.Font != nil { + t.Font.pdf = t.pdf + if err := t.Font.validate(); err != nil { + return err + } + } else if !strings.HasPrefix(t.Name, "$") { + return errors.New("pdfcpu: table missing font definition") + } + return nil +} + +func (t *Table) validateMargin() error { + if t.Margin != nil { + if err := t.Margin.validate(); err != nil { + return err + } + } + return nil +} + +func (t *Table) validateBorder() error { + if t.Border != nil { + t.Border.pdf = t.pdf + if err := t.Border.validate(); err != nil { + return err + } + } + return nil +} + +func (t *Table) validatePadding() error { + if t.Padding != nil { + if err := t.Padding.validate(); err != nil { + return err + } + } + return nil +} + +func (t *Table) validateBackgroundColor() error { + if t.BackgroundColor != "" { + sc, err := t.pdf.parseColor(t.BackgroundColor) + if err != nil { + return err + } + t.bgCol = sc + } + return nil +} + +func (t *Table) validateOddColor() error { + if t.OddColor != "" { + sc, err := t.pdf.parseColor(t.OddColor) + if err != nil { + return err + } + t.oddCol = sc + } + return nil +} + +func (t *Table) validateEvenColor() error { + if t.EvenColor != "" { + sc, err := t.pdf.parseColor(t.EvenColor) + if err != nil { + return err + } + t.evenCol = sc + } + return nil +} + +func (t *Table) validateColors() error { + if err := t.validateBackgroundColor(); err != nil { + return err + } + if err := t.validateOddColor(); err != nil { + return err + } + return t.validateEvenColor() +} + +func (t *Table) validate() error { + + t.x = t.Position[0] + t.y = t.Position[1] + + if t.Name == "$" { + return errors.New("pdfcpu: invalid table reference $") + } + + if err := t.validateAnchor(); err != nil { + return nil + } + + // TODO validate width against content box width + + if t.Rows < 1 { + return errors.New("pdfcpu: table \"rows\" missing.") + } + if t.Cols < 1 { + return errors.New("pdfcpu: table \"cols\" missing.") + } + + if t.Header != nil { + if err := t.Header.validate(t.pdf, t.Cols); err != nil { + return err + } + } + + if err := t.validateColumns(); err != nil { + return err + } + + if t.LineHeight <= 0 { + return errors.New("pdfcpu: line height \"lheight\" missing.") + } + + if err := t.validateValues(); err != nil { + return err + } + + if err := t.validateFont(); err != nil { + return err + } + + if err := t.validateMargin(); err != nil { + return err + } + + if err := t.validateBorder(); err != nil { + return err + } + + if err := t.validatePadding(); err != nil { + return err + } + + return t.validateColors() +} + +func (t *Table) font(name string) *FormFont { + return t.content.namedFont(name) +} + +func (t *Table) margin(name string) *Margin { + return t.content.namedMargin(name) +} + +func (t *Table) border(name string) *Border { + return t.content.namedBorder(name) +} + +func (t *Table) padding(name string) *Padding { + return t.content.namedPadding(name) +} + +func (t *Table) mergeInAnchor(t0 *Table) { + if !t.anchored && t.x == 0 && t.y == 0 { + t.x = t0.x + t.y = t0.y + t.anchor = t0.anchor + t.anchored = t0.anchored + } +} + +func (t *Table) mergeIn(t0 *Table) { + + t.mergeInAnchor(t0) + + if t.Dx == 0 { + t.Dx = t0.Dx + } + if t.Dy == 0 { + t.Dy = t0.Dy + } + + if t.Width == 0 { + t.Width = t0.Width + } + + if t.Margin == nil { + t.Margin = t0.Margin + } + + if t.Border == nil { + t.Border = t0.Border + } + + if t.Padding == nil { + t.Padding = t0.Padding + } + + if t.Font == nil { + t.Font = t0.Font + } + + if t.bgCol == nil { + t.bgCol = t0.bgCol + } + + if t.oddCol == nil { + t.oddCol = t0.oddCol + } + + if t.evenCol == nil { + t.evenCol = t0.evenCol + } + + if t.Rotation == 0 { + t.Rotation = t0.Rotation + } + + if !t.Hide { + t.Hide = t0.Hide + } +} + +func (t *Table) calcFont() error { + f := t.Font + if f.Name[0] == '$' { + // use named font + fName := f.Name[1:] + f0 := t.font(fName) + if f0 == nil { + return errors.Errorf("pdfcpu: unknown font name %s", fName) + } + f.Name = f0.Name + if f.Size == 0 { + f.Size = f0.Size + } + if f.col == nil { + f.col = f0.col + } + if f.Lang == "" { + f.Lang = f0.Lang + } + if f.Script == "" { + f.Script = f0.Script + } + } + if f.col == nil { + f.col = &color.Black + } + return nil +} + +func (t *Table) calcBorder() (float64, *color.SimpleColor, types.LineJoinStyle, error) { + bWidth := 0. + var bCol *color.SimpleColor + bStyle := types.LJMiter + if t.Border != nil { + b := t.Border + if b.Name != "" && b.Name[0] == '$' { + // Use named border + bName := b.Name[1:] + b0 := t.border(bName) + if b0 == nil { + return bWidth, bCol, bStyle, errors.Errorf("pdfcpu: unknown named border %s", bName) + } + b.mergeIn(b0) + } + if b.Width >= 0 { + bWidth = float64(b.Width) + if b.col != nil { + bCol = b.col + } + bStyle = b.style + } + } + return bWidth, bCol, bStyle, nil +} + +func (t *Table) calcMargin() (float64, float64, float64, float64, error) { + mTop, mRight, mBottom, mLeft := 0., 0., 0., 0. + if t.Margin != nil { + m := t.Margin + if m.Name != "" && m.Name[0] == '$' { + // use named margin + mName := m.Name[1:] + m0 := t.margin(mName) + if m0 == nil { + return mTop, mRight, mBottom, mLeft, errors.Errorf("pdfcpu: unknown named margin %s", mName) + } + m.mergeIn(m0) + } + if m.Width > 0 { + mTop = m.Width + mRight = m.Width + mBottom = m.Width + mLeft = m.Width + } else { + mTop = m.Top + mRight = m.Right + mBottom = m.Bottom + mLeft = m.Left + } + } + return mTop, mRight, mBottom, mLeft, nil +} + +func (t *Table) calcTransform(mLeft, mBottom, mRight, mTop, bWidth float64) (matrix.Matrix, *types.Rectangle) { + pdf := t.content.page.pdf + cBox := t.content.Box() + r := t.content.Box().CroppedCopy(0) + r.LL.X += mLeft + r.LL.Y += mBottom + r.UR.X -= mRight + r.UR.Y -= mTop + + var x, y float64 + h := t.Height() + 2*bWidth + if t.anchored { + x, y = types.AnchorPosition(t.anchor, r, t.Width, h) + } else { + x, y = types.NormalizeCoord(t.x, t.y, r, pdf.origin, false) + if y < 0 { + y = cBox.Center().Y - h/2 - r.LL.Y + } else if y > 0 { + y -= mBottom + } + if x < 0 { + x = cBox.Center().X - t.Width/2 - r.LL.X + } else if x > 0 { + x -= mLeft + } + } + + x += r.LL.X + t.Dx + y += r.LL.Y + t.Dy + + if x < r.LL.X { + x = r.LL.X + } else if x > r.UR.X-t.Width { + x = r.UR.X - t.Width + } + + if y < r.LL.Y { + y = r.LL.Y + } else if y > r.UR.Y-h { + y = r.UR.Y - h + } + + r = types.RectForWidthAndHeight(x, y, t.Width, h) + r.LL.X += bWidth / 2 + r.LL.Y += bWidth / 2 + r.UR.X -= bWidth / 2 + r.UR.Y -= bWidth / 2 + + sin := math.Sin(float64(t.Rotation) * float64(matrix.DegToRad)) + cos := math.Cos(float64(t.Rotation) * float64(matrix.DegToRad)) + + dx := r.LL.X + dy := r.LL.Y + r.Translate(-r.LL.X, -r.LL.Y) + + dx += t.Dx + r.Width()/2 + sin*(r.Height()/2) - cos*r.Width()/2 + dy += t.Dy + r.Height()/2 - cos*(r.Height()/2) - sin*r.Width()/2 + + m := matrix.CalcTransformMatrix(1, 1, sin, cos, dx, dy) + + return m, r +} + +func (t *Table) renderBackground(p *model.Page, bWidth float64, r *types.Rectangle) { + x := r.LL.X + bWidth/2 + // Render odd,even row background. + if t.oddCol != nil || t.evenCol != nil { + x, w := x, t.Width-2*bWidth + if bWidth == 0 { + // Reduce artefacts. + x += .5 + w -= 1 + } + for i := 0; i < t.Rows; i++ { + col := t.evenCol + if i%2 > 0 { + col = t.oddCol + } + if col == nil { + continue + } + y := r.LL.Y + bWidth/2 + float64(i*t.LineHeight) + r1 := types.RectForWidthAndHeight(x, y, w, float64(t.LineHeight)) + draw.FillRect(p.Buf, r1, 0, nil, *col, nil) + } + } + + // Render header background. + if t.Header != nil && t.Header.bgCol != nil { + x, w := x, t.Width-2*bWidth + h := float64(t.LineHeight) + if bWidth == 0 { + // Reduce artefacts. + x += .5 + w -= 1 + h -= .5 + } + y := r.LL.Y + bWidth/2 + float64(t.Rows*t.LineHeight) + col := t.Header.bgCol + r1 := types.RectForWidthAndHeight(x, y, w, h) + draw.FillRect(p.Buf, r1, 0, nil, *col, nil) + } +} + +func (t *Table) prepareColWidths(bWidth float64) []float64 { + colWidths := make([]float64, t.Cols) + w := t.Width - 2*bWidth + + if len(t.ColWidths) > 0 { + for i := 0; i < t.Cols; i++ { + colWidths[i] = float64(t.ColWidths[i]) / 100 * w + } + return colWidths + } + + colw := w / float64(t.Cols) + for i := 0; i < t.Cols; i++ { + colWidths[i] = colw + } + return colWidths +} + +func (t *Table) renderGrid(p *model.Page, colWidths []float64, bWidth float64, bCol *color.SimpleColor, r *types.Rectangle) { + // Draw vertical lines. + x := r.LL.X + bWidth/2 + for i := 1; i < t.Cols; i++ { + x += colWidths[i-1] + draw.DrawLine(p.Buf, x, r.LL.Y, x, r.UR.Y, 0, bCol, nil) + } + + // Draw horizontal lines. + maxRows := t.Rows + if t.Header != nil { + maxRows++ + } + y := r.LL.Y + bWidth/2 + for i := 1; i < maxRows; i++ { + y += float64(t.LineHeight) + draw.DrawLine(p.Buf, r.LL.X, y, r.UR.X, y, 0, bCol, nil) + } +} + +func (t *Table) prepareTextDescriptor() (model.TextDescriptor, error) { + td := model.TextDescriptor{ + Scale: 1., + ScaleAbs: true, + ShowTextBB: false, + ShowBorder: false, + //ShowBackground: true, + //BackgroundCol: pdfcpu.White, + } + + applyTextDescriptorPadding(&td, t.Padding) + + return td, nil +} + +func (t *Table) calcTextDescriptorPadding(td *model.TextDescriptor, p *Padding) error { + if p.Name != "" && p.Name[0] == '$' { + // use named padding + pName := p.Name[1:] + p0 := t.padding(pName) + if p0 == nil { + return errors.Errorf("pdfcpu: unknown named padding %s", pName) + } + p.mergeIn(p0) + } + + applyTextDescriptorPadding(td, p) + + return nil +} + +func (t *Table) renderValues(p *model.Page, pageNr int, fonts model.FontMap, colWidths []float64, td model.TextDescriptor, ll func(row, col int) (float64, float64)) error { + pdf := t.pdf + + f := t.Font + id, err := pdf.idForFontName(f.Name, f.Lang, p.Fm, fonts, pageNr) + if err != nil { + return err + } + + td.FontName = f.Name + td.Embed = true + td.FontKey = id + td.FontSize = f.Size + td.RTL = t.RTL + td.StrokeCol = *f.col + td.FillCol = *f.col + + // Render values + for i := 0; i < t.Rows; i++ { + + if len(t.Values) < i+1 { + break + } + + for j := 0; j < t.Cols; j++ { + + if len(t.Values[i]) < j+1 { + break + } + + s := t.Values[i][j] + if len(strings.TrimSpace(s)) == 0 { + continue + } + + colTd := td + if len(t.ColPaddings) > 0 && t.ColPaddings[j] != nil { + if err = t.calcTextDescriptorPadding(&colTd, t.ColPaddings[j]); err != nil { + return err + } + } + + colTd.Text, _ = format.Text(s, pdf.TimestampFormat, pageNr, pdf.pageCount()) + + row := i + if t.Header != nil { + row++ + } + + x, y := ll(row, j) + r := types.RectForWidthAndHeight(x, y, colWidths[j], float64(t.LineHeight)) + + bb := model.WriteMultiLineAnchored(pdf.XRefTable, p.Buf, r, nil, colTd, t.colAnchors[j]) + + if bb.Width() > colWidths[j] { + return errors.Errorf("pdfcpu: table cell width overflow - reduce padding or text: %s", colTd.Text) + } + + if bb.Height() > float64(t.LineHeight) { + return errors.Errorf("pdfcpu: table cell height overflow - reduce padding or text: %s", colTd.Text) + } + } + } + return nil +} + +func (t *Table) renderHeader(p *model.Page, pageNr int, fonts model.FontMap, colWidths []float64, td model.TextDescriptor, ll func(row, col int) (float64, float64)) error { + pdf := t.pdf + th := t.Header + + f1 := *t.Font + if th.Font != nil { + f1 = *th.Font + } + if f1.Name[0] == '$' { + // use named font + fName := f1.Name[1:] + f0 := t.font(fName) + if f0 == nil { + return errors.Errorf("pdfcpu: unknown font name %s", fName) + } + f1.Name = f0.Name + f1.Script = f0.Script + if f1.Size == 0 { + f1.Size = f0.Size + } + if f1.col == nil { + f1.col = f0.col + } + } + if f1.col == nil { + f1.col = &color.Black + } + + id, err := t.pdf.idForFontName(f1.Name, f1.Lang, p.Fm, fonts, pageNr) + if err != nil { + return err + } + + td.FontName = f1.Name + td.Embed = true + td.FontKey = id + td.FontSize = f1.Size + td.RTL = th.RTL + td.StrokeCol = *f1.col + td.FillCol = *f1.col + + // Render header values. + for i, s := range th.Values { + + if len(strings.TrimSpace(s)) == 0 { + continue + } + + colTd := td + th.calcColumnPadding(&colTd, i) + colTd.Text, _ = format.Text(s, pdf.TimestampFormat, pageNr, pdf.pageCount()) + + x, y := ll(0, i) + r := types.RectForWidthAndHeight(x, y, colWidths[i], float64(t.LineHeight)) + + a := t.colAnchors[i] + if len(th.colAnchors) > 0 { + a = th.colAnchors[i] + } + + bb := model.WriteMultiLineAnchored(pdf.XRefTable, p.Buf, r, nil, colTd, a) + + if bb.Width() > colWidths[i] { + return errors.Errorf("pdfcpu: table header cell width overflow - reduce padding or text: %s", colTd.Text) + } + + if bb.Height() > float64(t.LineHeight) { + return errors.Errorf("pdfcpu: table header cell height overflow - reduce padding or text: %s", colTd.Text) + } + } + + return nil +} + +func (t *Table) render(p *model.Page, pageNr int, fonts model.FontMap) error { + + if err := t.calcFont(); err != nil { + return err + } + + bWidth, bCol, bStyle, err := t.calcBorder() + if err != nil { + return err + } + + mTop, mRight, mBottom, mLeft, err := t.calcMargin() + if err != nil { + return err + } + + m, r := t.calcTransform(mTop, mRight, mBottom, mLeft, bWidth) + + fmt.Fprintf(p.Buf, "q %.5f %.5f %.5f %.5f %.5f %.5f cm ", m[0][0], m[0][1], m[1][0], m[1][1], m[2][0], m[2][1]) + + if t.bgCol != nil { + x, w := r.LL.X+bWidth/2, t.Width-2*bWidth + if bWidth == 0 { + // Reduce artefacts. + x += .5 + w -= 1 + } + y := r.LL.Y + bWidth/2 + r1 := types.RectForWidthAndHeight(x, y, w, r.Height()-.5) + draw.FillRect(p.Buf, r1, 0, bCol, *t.bgCol, &bStyle) + } + + if t.Border != nil { + draw.DrawRect(p.Buf, r, bWidth, bCol, &bStyle) + } + + t.renderBackground(p, bWidth, r) + + colWidths := t.prepareColWidths(bWidth) + + if t.Grid { + t.renderGrid(p, colWidths, bWidth, bCol, r) + } + + td, err := t.prepareTextDescriptor() + if err != nil { + return err + } + + ll := func(row, col int) (float64, float64) { + var x float64 + for i := 0; i < col; i++ { + x += colWidths[i] + } + y := r.UR.Y - bWidth/2 - float64((row+1)*t.LineHeight) + return r.LL.X + bWidth/2 + x, y + } + + if len(t.Values) > 0 { + if err := t.renderValues(p, pageNr, fonts, colWidths, td, ll); err != nil { + return err + } + } + + if t.Header != nil { + if err := t.renderHeader(p, pageNr, fonts, colWidths, td, ll); err != nil { + return err + } + } + + if t.pdf.Debug { + draw.DrawCircle(p.Buf, r.LL.X, r.LL.Y, 5, color.Black, &color.Red) + } + + fmt.Fprint(p.Buf, "Q ") + + return nil +} diff --git a/pkg/pdfcpu/primitives/textBox.go b/pkg/pdfcpu/primitives/textBox.go new file mode 100644 index 0000000000000000000000000000000000000000..61fdd0fec9a76c61b4af058a1b2909a0f1b3b5dc --- /dev/null +++ b/pkg/pdfcpu/primitives/textBox.go @@ -0,0 +1,464 @@ +/* + Copyright 2021 The pdfcpu Authors. + + 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 + + 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 primitives + +import ( + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/format" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// TextBox represents a form text input field including a positioned label. +type TextBox struct { + pdf *PDF + content *Content + Name string + Value string // text, content + Position [2]float64 `json:"pos"` // x,y + x, y float64 + Dx, Dy float64 + Anchor string + anchor types.Anchor + anchored bool + Width float64 + + Font *FormFont + Margin *Margin // applied to content box + Border *Border + Padding *Padding // applied to TextDescriptor marginx + + BackgroundColor string `json:"bgCol"` + bgCol *color.SimpleColor + Alignment string `json:"align"` // "Left", "Center", "Right" + horAlign types.HAlignment + RTL bool + Rotation float64 `json:"rot"` + Hide bool +} + +func (tb *TextBox) validateAnchor() error { + if tb.Anchor != "" { + if tb.Position[0] != 0 || tb.Position[1] != 0 { + return errors.New("pdfcpu: Please supply \"pos\" or \"anchor\"") + } + a, err := types.ParseAnchor(tb.Anchor) + if err != nil { + return err + } + tb.anchor = a + tb.anchored = true + } + return nil +} + +func (tb *TextBox) validateFont() error { + if tb.Font != nil { + tb.Font.pdf = tb.pdf + if err := tb.Font.validate(); err != nil { + return err + } + } else if !strings.HasPrefix(tb.Name, "$") { + return errors.New("pdfcpu: textbox missing font definition") + } + return nil +} + +func (tb *TextBox) validateMargin() error { + if tb.Margin == nil { + return nil + } + return tb.Margin.validate() +} + +func (tb *TextBox) validateBorder() error { + if tb.Border != nil { + tb.Border.pdf = tb.pdf + if err := tb.Border.validate(); err != nil { + return err + } + } + return nil +} + +func (tb *TextBox) validatePadding() error { + if tb.Padding == nil { + return nil + } + return tb.Padding.validate() +} + +func (tb *TextBox) validateBackgroundColor() error { + if tb.BackgroundColor != "" { + sc, err := tb.pdf.parseColor(tb.BackgroundColor) + if err != nil { + return err + } + tb.bgCol = sc + } + return nil +} + +func (tb *TextBox) validateHorAlign() error { + tb.horAlign = types.AlignLeft + if tb.Alignment != "" { + ha, err := types.ParseHorAlignment(tb.Alignment) + if err != nil { + return err + } + tb.horAlign = ha + } + return nil +} + +func (tb *TextBox) validate() error { + + tb.x = tb.Position[0] + tb.y = tb.Position[1] + + if tb.Name == "$" { + return errors.New("pdfcpu: invalid text reference $") + } + + if err := tb.validateAnchor(); err != nil { + return err + } + + if err := tb.validateFont(); err != nil { + return err + } + + if err := tb.validateMargin(); err != nil { + return err + } + + if err := tb.validateBorder(); err != nil { + return err + } + + if err := tb.validatePadding(); err != nil { + return err + } + + if err := tb.validateBackgroundColor(); err != nil { + return err + } + + return tb.validateHorAlign() +} + +func (tb *TextBox) font(name string) *FormFont { + return tb.content.namedFont(name) +} + +func (tb *TextBox) margin(name string) *Margin { + return tb.content.namedMargin(name) +} + +func (tb *TextBox) border(name string) *Border { + return tb.content.namedBorder(name) +} + +func (tb *TextBox) padding(name string) *Padding { + return tb.content.namedPadding(name) +} + +func (tb *TextBox) mergeInPos(tb0 *TextBox) { + + if !tb.anchored && tb.x == 0 && tb.y == 0 { + tb.x = tb0.x + tb.y = tb0.y + tb.anchor = tb0.anchor + tb.anchored = tb0.anchored + } + + if tb.Dx == 0 { + tb.Dx = tb0.Dx + } + if tb.Dy == 0 { + tb.Dy = tb0.Dy + } +} + +func (tb *TextBox) mergeIn(tb0 *TextBox) { + + tb.mergeInPos(tb0) + + if tb.Value == "" { + tb.Value = tb0.Value + } + + if tb.Width == 0 { + tb.Width = tb0.Width + } + + if tb.Margin == nil { + tb.Margin = tb0.Margin + } + + if tb.Border == nil { + tb.Border = tb0.Border + } + + if tb.Padding == nil { + tb.Padding = tb0.Padding + } + + if tb.Font == nil { + tb.Font = tb0.Font + } + + if tb.horAlign == types.AlignLeft { + tb.horAlign = tb0.horAlign + } + + if tb.bgCol == nil { + tb.bgCol = tb0.bgCol + } + + if tb.Rotation == 0 { + tb.Rotation = tb0.Rotation + } + + if !tb.Hide { + tb.Hide = tb0.Hide + } +} + +func (tb *TextBox) calcFont() error { + f := tb.Font + if f.Name[0] == '$' { + // use named font + fName := f.Name[1:] + f0 := tb.font(fName) + if f0 == nil { + return errors.Errorf("pdfcpu: unknown font name %s", fName) + } + f.Name = f0.Name + if f.Size == 0 { + f.Size = f0.Size + } + if f.col == nil { + f.col = f0.col + } + if f.Lang == "" { + f.Lang = f0.Lang + } + if f.Script == "" { + f.Script = f0.Script + } + } + if f.col == nil { + f.col = &color.Black + } + return nil +} + +func tdMargin(p *Padding, td *model.TextDescriptor) { + // TODO TextDescriptor margin is actually a padding. + if p.Width > 0 { + td.MTop = p.Width + td.MRight = p.Width + td.MBot = p.Width + td.MLeft = p.Width + } else { + td.MTop = p.Top + td.MRight = p.Right + td.MBot = p.Bottom + td.MLeft = p.Left + } +} + +func (tb *TextBox) prepareTextDescriptor(p *model.Page, pageNr int, fonts model.FontMap) (*model.TextDescriptor, error) { + + pdf := tb.pdf + f := tb.Font + fontName := f.Name + fontLang := f.Lang + fontSize := f.Size + col := f.col + + t, _ := format.Text(tb.Value, pdf.TimestampFormat, pageNr, pdf.pageCount()) + + id, err := tb.pdf.idForFontName(fontName, fontLang, p.Fm, fonts, pageNr) + if err != nil { + return nil, err + } + + dx, dy := types.NormalizeOffset(tb.Dx, tb.Dy, pdf.origin) + + td := model.TextDescriptor{ + Text: t, + Dx: dx, + Dy: dy, + HAlign: tb.horAlign, + VAlign: types.AlignBottom, + FontName: fontName, + Embed: true, + FontKey: id, + FontSize: fontSize, + Scale: 1., + ScaleAbs: true, + Rotation: tb.Rotation, + RTL: tb.RTL, // for user fonts only! + } + + if col != nil { + td.StrokeCol, td.FillCol = *col, *col + } + + bgCol := tb.bgCol + if bgCol == nil { + bgCol = tb.content.bgCol + } + if bgCol != nil { + td.ShowBackground, td.ShowTextBB, td.BackgroundCol = true, true, *bgCol + } + + if tb.Border != nil { + b := tb.Border + if b.Name != "" && b.Name[0] == '$' { + // Use named border + bName := b.Name[1:] + b0 := tb.border(bName) + if b0 == nil { + return nil, errors.Errorf("pdfcpu: unknown named border %s", bName) + } + b.mergeIn(b0) + } + if b.Width >= 0 { + td.BorderWidth = float64(b.Width) + if b.col != nil { + td.BorderCol = *b.col + td.ShowBorder = true + } + td.BorderStyle = b.style + } + } + + if tb.Padding != nil { + p := tb.Padding + if p.Name != "" && p.Name[0] == '$' { + // use named padding + pName := p.Name[1:] + p0 := tb.padding(pName) + if p0 == nil { + return nil, errors.Errorf("pdfcpu: unknown named padding %s", pName) + } + p.mergeIn(p0) + } + tdMargin(p, &td) + } + + return &td, nil +} + +func (tb *TextBox) calcMargin() (float64, float64, float64, float64, error) { + mTop, mRight, mBottom, mLeft := 0., 0., 0., 0. + if tb.Margin != nil { + m := tb.Margin + if m.Name != "" && m.Name[0] == '$' { + // use named margin + mName := m.Name[1:] + m0 := tb.margin(mName) + if m0 == nil { + return mTop, mRight, mBottom, mLeft, errors.Errorf("pdfcpu: unknown named margin %s", mName) + } + m.mergeIn(m0) + } + if m.Width > 0 { + mTop = m.Width + mRight = m.Width + mBottom = m.Width + mLeft = m.Width + } else { + mTop = m.Top + mRight = m.Right + mBottom = m.Bottom + mLeft = m.Left + } + } + return mTop, mRight, mBottom, mLeft, nil +} + +func (tb *TextBox) render(p *model.Page, pageNr int, fonts model.FontMap) error { + + pdf := tb.pdf + + if err := tb.calcFont(); err != nil { + return err + } + + td, err := tb.prepareTextDescriptor(p, pageNr, fonts) + if err != nil { + return err + } + + mTop, mRight, mBottom, mLeft, err := tb.calcMargin() + if err != nil { + return err + } + + cBox := tb.content.Box() + r := cBox.CroppedCopy(0) + r.LL.X += mLeft + r.LL.Y += mBottom + r.UR.X -= mRight + r.UR.Y -= mTop + + if pdf.Debug { + td.ShowPosition = true + td.HairCross = true + td.ShowLineBB = true + } + + if tb.anchored { + model.WriteMultiLineAnchored(tb.pdf.XRefTable, p.Buf, r, nil, *td, tb.anchor) + return nil + } + + td.X, td.Y = types.NormalizeCoord(tb.x, tb.y, tb.content.Box(), pdf.origin, false) + + if td.X == -1 { + // Center horizontally + td.X = cBox.Center().X - r.LL.X + } else if td.X > 0 { + td.X -= mLeft + if td.X < 0 { + td.X = 0 + } + } + + if td.Y == -1 { + // Center vertically + td.Y = cBox.Center().Y - r.LL.Y + td.VAlign = types.AlignMiddle + } else if td.Y > 0 { + td.Y -= mBottom + if td.Y < 0 { + td.Y = 0 + } + r.LL.Y += td.BorderWidth + } + + model.WriteColumn(tb.pdf.XRefTable, p.Buf, r, nil, *td, float64(tb.Width)) + + return nil +} diff --git a/pkg/pdfcpu/primitives/textField.go b/pkg/pdfcpu/primitives/textField.go new file mode 100644 index 0000000000000000000000000000000000000000..48ec10eb3c68e3cbf08593c5493a66355f62e4f1 --- /dev/null +++ b/pkg/pdfcpu/primitives/textField.go @@ -0,0 +1,1063 @@ +/* + Copyright 2021 The pdfcpu Authors. + + 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 + + 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 primitives + +import ( + "bytes" + "fmt" + "io" + + "unicode/utf8" + + "github.com/pdfcpu/pdfcpu/pkg/font" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + pdffont "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/font" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/format" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +type TextField struct { + pdf *PDF + content *Content + Label *TextFieldLabel + ID string + Tip string + Value string + Default string + Position [2]float64 `json:"pos"` // x,y + x, y float64 + Width float64 + Height float64 + Dx, Dy float64 + BoundingBox *types.Rectangle `json:"-"` + Multiline bool + Font *FormFont + fontID string + Margin *Margin // applied to content box + Border *Border + BackgroundColor string `json:"bgCol"` + BgCol *color.SimpleColor `json:"-"` + Alignment string `json:"align"` // "Left", "Center", "Right" + HorAlign types.HAlignment `json:"-"` + RTL bool + Tab int + Locked bool + Debug bool + Hide bool +} + +func (tf *TextField) SetFontID(s string) { + tf.fontID = s +} + +func (tf *TextField) validateID() error { + if tf.ID == "" { + return errors.New("pdfcpu: missing field id") + } + if tf.pdf.DuplicateField(tf.ID) { + return errors.Errorf("pdfcpu: duplicate form field: %s", tf.ID) + } + tf.pdf.FieldIDs[tf.ID] = true + return nil +} + +func (tf *TextField) validatePosition() error { + if tf.Position[0] < 0 || tf.Position[1] < 0 { + return errors.Errorf("pdfcpu: field: %s pos value < 0", tf.ID) + } + tf.x, tf.y = tf.Position[0], tf.Position[1] + return nil +} + +func (tf *TextField) validateWidth() error { + if tf.Width == 0 { + return errors.Errorf("pdfcpu: field: %s width == 0", tf.ID) + } + return nil +} + +func (tf *TextField) validateHeight() error { + if tf.Height < 0 { + return errors.Errorf("pdfcpu: field: %s height < 0", tf.ID) + } + return nil +} + +func (tf *TextField) validateFont() error { + if tf.Font != nil { + tf.Font.pdf = tf.pdf + if err := tf.Font.validate(); err != nil { + return err + } + } + return nil +} + +func (tf *TextField) validateMargin() error { + if tf.Margin != nil { + if err := tf.Margin.validate(); err != nil { + return err + } + } + return nil +} + +func (tf *TextField) validateBorder() error { + if tf.Border != nil { + tf.Border.pdf = tf.pdf + if err := tf.Border.validate(); err != nil { + return err + } + } + return nil +} + +func (tf *TextField) validateBackgroundColor() error { + if tf.BackgroundColor != "" { + sc, err := tf.pdf.parseColor(tf.BackgroundColor) + if err != nil { + return err + } + tf.BgCol = sc + } + return nil +} + +func (tf *TextField) validateHorAlign() error { + tf.HorAlign = types.AlignLeft + if tf.Alignment != "" { + ha, err := types.ParseHorAlignment(tf.Alignment) + if err != nil { + return err + } + tf.HorAlign = ha + } + return nil +} + +func (tf *TextField) validateLabel() error { + if tf.Label != nil { + tf.Label.pdf = tf.pdf + if err := tf.Label.validate(); err != nil { + return err + } + } + return nil +} + +func (tf *TextField) validateTab() error { + if tf.Tab < 0 { + return errors.Errorf("pdfcpu: field: %s negative tab value", tf.ID) + } + if tf.Tab == 0 { + return nil + } + page := tf.content.page + if page.Tabs == nil { + page.Tabs = types.IntSet{} + } else { + if page.Tabs[tf.Tab] { + return errors.Errorf("pdfcpu: field: %s duplicate tab value %d", tf.ID, tf.Tab) + } + } + page.Tabs[tf.Tab] = true + return nil +} + +func (tf *TextField) validate() error { + if err := tf.validateID(); err != nil { + return err + } + + if err := tf.validatePosition(); err != nil { + return err + } + + if err := tf.validateWidth(); err != nil { + return err + } + + if err := tf.validateHeight(); err != nil { + return err + } + + if err := tf.validateFont(); err != nil { + return err + } + + if err := tf.validateMargin(); err != nil { + return err + } + + if err := tf.validateBorder(); err != nil { + return err + } + + if err := tf.validateBackgroundColor(); err != nil { + return err + } + + if err := tf.validateHorAlign(); err != nil { + return err + } + + if err := tf.validateLabel(); err != nil { + return err + } + + return tf.validateTab() +} + +func (tf *TextField) calcFontFromDA(ctx *model.Context, d types.Dict, needUTF8 bool, fonts map[string]types.IndirectRef) (*types.IndirectRef, error) { + s := d.StringEntry("DA") + if s == nil { + s = ctx.Form.StringEntry("DA") + if s == nil { + return nil, errors.New("pdfcpu: textfield missing \"DA\"") + } + } + + fontID, f, err := fontFromDA(*s) + if err != nil { + return nil, err + } + + tf.Font, tf.fontID = &f, fontID + + id, name, lang, fontIndRef, err := extractFormFontDetails(ctx, tf.fontID, fonts) + if err != nil { + return nil, err + } + if fontIndRef == nil { + return nil, errors.New("pdfcpu: unable to detect indirect reference for font") + } + + if needUTF8 && font.IsCoreFont(name) { + id, name, lang, fontIndRef, err = ensureUTF8FormFont(ctx, fonts) + if err != nil { + return nil, err + } + } + + tf.fontID = id + tf.Font.Name = name + tf.Font.Lang = lang + tf.RTL = pdffont.RTL(lang) + + return fontIndRef, nil +} + +func (tf *TextField) calcFont() error { + f, err := tf.content.calcInputFont(tf.Font) + if err != nil { + return err + } + tf.Font = f + + if tf.Label != nil { + f, err = tf.content.calcLabelFont(tf.Label.Font) + if err != nil { + return err + } + tf.Label.Font = f + } + + return nil +} + +func (tf *TextField) margin(name string) *Margin { + return tf.content.namedMargin(name) +} + +func (tf *TextField) calcMargin() (float64, float64, float64, float64, error) { + mTop, mRight, mBottom, mLeft := 0., 0., 0., 0. + if tf.Margin != nil { + m := tf.Margin + if m.Name != "" && m.Name[0] == '$' { + // use named margin + mName := m.Name[1:] + m0 := tf.margin(mName) + if m0 == nil { + return mTop, mRight, mBottom, mLeft, errors.Errorf("pdfcpu: unknown named margin %s", mName) + } + m.mergeIn(m0) + } + if m.Width > 0 { + mTop = m.Width + mRight = m.Width + mBottom = m.Width + mLeft = m.Width + } else { + mTop = m.Top + mRight = m.Right + mBottom = m.Bottom + mLeft = m.Left + } + } + return mTop, mRight, mBottom, mLeft, nil +} + +func (tf *TextField) labelPos(labelHeight, w, g float64) (float64, float64) { + var x, y float64 + bb, horAlign := tf.BoundingBox, tf.Label.HorAlign + + switch tf.Label.relPos { + + case types.RelPosLeft: + x = bb.LL.X - g + if horAlign == types.AlignLeft { + x -= w + if x < 0 { + x = 0 + } + } + if tf.Multiline { + y = bb.UR.Y - labelHeight + } else { + y = bb.LL.Y + } + + case types.RelPosRight: + x = bb.UR.X + g + if horAlign == types.AlignRight { + x += w + } + if tf.Multiline { + y = bb.UR.Y - labelHeight + } else { + y = bb.LL.Y + } + + case types.RelPosTop: + y = bb.UR.Y + g + x = bb.LL.X + if horAlign == types.AlignRight { + x += bb.Width() + } else if horAlign == types.AlignCenter { + x += bb.Width() / 2 + } + + case types.RelPosBottom: + y = bb.LL.Y - g - labelHeight + x = bb.LL.X + if horAlign == types.AlignRight { + x += bb.Width() + } else if horAlign == types.AlignCenter { + x += bb.Width() / 2 + } + + } + + return x, y +} + +func (tf *TextField) renderBackground(w io.Writer, bgCol, boCol *color.SimpleColor, boWidth, width, height float64) { + if bgCol != nil || (boCol != nil && boWidth > 0) { + fmt.Fprint(w, "q ") + if bgCol != nil { + fmt.Fprintf(w, "%.2f %.2f %.2f rg 0 0 %.2f %.2f re f ", bgCol.R, bgCol.G, bgCol.B, width, height) + } + if boCol != nil && boWidth > 0 { + fmt.Fprintf(w, "%.2f %.2f %.2f RG %.2f w %.2f %.2f %.2f %.2f re s ", + boCol.R, boCol.G, boCol.B, boWidth, boWidth/2, boWidth/2, width-boWidth, height-boWidth) + } + fmt.Fprint(w, "Q ") + } +} + +func (tf *TextField) renderLines(xRefTable *model.XRefTable, boWidth, lh, w, y float64, lines []string, buf io.Writer) { + f := tf.Font + cjk := pdffont.CJK(f.Script, f.Lang) + for i := 0; i < len(lines); i++ { + s := lines[i] + lineBB := model.CalcBoundingBox(s, 0, 0, f.Name, f.Size) + s = model.PrepBytes(xRefTable, s, f.Name, !cjk, f.RTL()) + x := 2 * boWidth + if x == 0 { + x = 2 + } + switch tf.HorAlign { + case types.AlignCenter: + x = w/2 - lineBB.Width()/2 + case types.AlignRight: + x = w - lineBB.Width() - 2 + } + fmt.Fprint(buf, "BT ") + if i == 0 { + fmt.Fprintf(buf, "/%s %d Tf %.2f %.2f %.2f RG %.2f %.2f %.2f rg ", + tf.fontID, f.Size, + f.col.R, f.col.G, f.col.B, + f.col.R, f.col.G, f.col.B) + } + fmt.Fprintf(buf, "%.2f %.2f Td (%s) Tj ET ", x, y, s) + y -= lh + } +} + +func (tf *TextField) renderN(xRefTable *model.XRefTable) ([]byte, error) { + w, h := tf.BoundingBox.Width(), tf.BoundingBox.Height() + bgCol := tf.BgCol + boWidth, boCol := tf.calcBorder() + buf := new(bytes.Buffer) + + tf.renderBackground(buf, bgCol, boCol, boWidth, w, h) + + f := tf.Font + + if !tf.Multiline && float64(f.Size) > h { + f.Size = font.SizeForLineHeight(f.Name, h) + } + + s := tf.Value + if s == "" { + s = tf.Default + } + + if font.IsCoreFont(f.Name) && utf8.ValidString(s) { + s = model.DecodeUTF8ToByte(s) + } + lines := model.SplitMultilineStr(s) + + fmt.Fprint(buf, "/Tx BMC ") + + lh := font.LineHeight(f.Name, f.Size) + y := (tf.BoundingBox.Height()-lh)/2 + font.Descent(f.Name, f.Size) + if tf.Multiline { + y = tf.BoundingBox.Height() - lh + } + + if len(lines) > 0 { + fmt.Fprintf(buf, "q 1 1 %.1f %.1f re W n ", w-2, h-2) + } + + tf.renderLines(xRefTable, boWidth, lh, w, y, lines, buf) + + if len(lines) > 0 { + fmt.Fprint(buf, "Q ") + } + + fmt.Fprint(buf, "EMC ") + + if boCol != nil && boWidth > 0 { + fmt.Fprintf(buf, "q %.2f %.2f %.2f RG %.2f w %.2f %.2f %.2f %.2f re s Q ", + boCol.R, boCol.G, boCol.B, boWidth-1, boWidth/2, boWidth/2, w-boWidth, h-boWidth) + } + + return buf.Bytes(), nil +} + +func (tf *TextField) RefreshN(xRefTable *model.XRefTable, indRef *types.IndirectRef) error { + bb, err := tf.renderN(xRefTable) + if err != nil { + return err + } + + entry, _ := xRefTable.FindTableEntryForIndRef(indRef) + sd, _ := entry.Object.(types.StreamDict) + + sd.Content = bb + if err := sd.Encode(); err != nil { + return err + } + + entry.Object = sd + + return nil +} + +func (tf *TextField) irN(fonts model.FontMap) (*types.IndirectRef, error) { + bb, err := tf.renderN(tf.pdf.XRefTable) + if err != nil { + return nil, err + } + + sd, err := tf.pdf.XRefTable.NewStreamDictForBuf(bb) + if err != nil { + return nil, err + } + + sd.InsertName("Type", "XObject") + sd.InsertName("Subtype", "Form") + sd.InsertInt("FormType", 1) + sd.Insert("BBox", types.NewNumberArray(0, 0, tf.BoundingBox.Width(), tf.BoundingBox.Height())) + sd.Insert("Matrix", types.NewNumberArray(1, 0, 0, 1, 0, 0)) + + f := tf.Font + + fName := f.Name + if pdffont.CJK(tf.Font.Script, tf.Font.Lang) { + fName = "cjk:" + fName + } + + ir, err := tf.pdf.ensureFont(tf.fontID, fName, tf.Font.Lang, fonts) + if err != nil { + return nil, err + } + + d := types.Dict( + map[string]types.Object{ + "Font": types.Dict( + map[string]types.Object{ + tf.fontID: *ir, + }, + ), + }, + ) + + sd.Insert("Resources", d) + + if err := sd.Encode(); err != nil { + return nil, err + } + + return tf.pdf.XRefTable.IndRefForNewObject(*sd) +} + +func (tf *TextField) calcBorder() (boWidth float64, boCol *color.SimpleColor) { + if tf.Border == nil { + return 0, nil + } + return tf.Border.calc() +} + +func (tf *TextField) prepareFF() FieldFlags { + ff := FieldDoNotSpellCheck + if tf.Multiline { + // If FieldMultiline set, the field may contain multiple lines of text; + // if clear, the field’s text shall be restricted to a single line. + // Adobe Reader ok, Mac Preview nope + ff += FieldMultiline + } else { + // If FieldDoNotScroll set, the field shall not scroll (horizontally for single-line fields, vertically for multiple-line fields) + // to accommodate more text than fits within its annotation rectangle. + // Once the field is full, no further text shall be accepted for interactive form filling; + // for non- interactive form filling, the filler should take care + // not to add more character than will visibly fit in the defined area. + // Adobe Reader ok, Mac Preview nope :( + ff += FieldDoNotScroll + } + + if tf.Locked { + ff += FieldReadOnly + } + + return ff +} + +func (tf *TextField) handleBorderAndMK(d types.Dict) { + bgCol := tf.BgCol + if bgCol == nil { + bgCol = tf.content.page.bgCol + if bgCol == nil { + bgCol = tf.pdf.bgCol + } + } + tf.BgCol = bgCol + + boWidth, boCol := tf.calcBorder() + + if bgCol != nil || boCol != nil { + appCharDict := types.Dict{} + if bgCol != nil { + appCharDict["BG"] = bgCol.Array() + } + if boCol != nil && tf.Border.Width > 0 { + appCharDict["BC"] = boCol.Array() + } + d["MK"] = appCharDict + } + + if boWidth > 0 { + d["Border"] = types.NewNumberArray(0, 0, boWidth) + } +} + +func (tf *TextField) prepareDict(fonts model.FontMap) (types.Dict, error) { + pdf := tf.pdf + + id, err := types.EscapeUTF16String(tf.ID) + if err != nil { + return nil, err + } + + ff := tf.prepareFF() + + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("Annot"), + "Subtype": types.Name("Widget"), + "FT": types.Name("Tx"), + "Rect": tf.BoundingBox.Array(), + "F": types.Integer(model.AnnPrint), + "Ff": types.Integer(ff), + "Q": types.Integer(tf.HorAlign), + "T": types.StringLiteral(*id), + }, + ) + + if tf.Tip != "" { + tu, err := types.EscapeUTF16String(tf.Tip) + if err != nil { + return nil, err + } + d["TU"] = types.StringLiteral(*tu) + } + + tf.handleBorderAndMK(d) + + if tf.Value != "" { + s, err := types.EscapeUTF16String(tf.Value) + if err != nil { + return nil, err + } + d["V"] = types.StringLiteral(*s) + } + + if tf.Default != "" { + s, err := types.EscapeUTF16String(tf.Default) + if err != nil { + return nil, err + } + d["DV"] = types.StringLiteral(*s) + if tf.Value == "" { + d["V"] = types.StringLiteral(*s) + } + } + + if pdf.InheritedDA != "" { + d["DA"] = types.StringLiteral(pdf.InheritedDA) + } + + f := tf.Font + fCol := f.col + + fontID, err := pdf.ensureFormFont(f) + if err != nil { + return d, err + } + tf.fontID = fontID + + da := fmt.Sprintf("/%s %d Tf %.2f %.2f %.2f rg", fontID, f.Size, fCol.R, fCol.G, fCol.B) + // Note: Mac Preview does not honour inherited "DA" + d["DA"] = types.StringLiteral(da) + + irN, err := tf.irN(fonts) + if err != nil { + return nil, err + } + + d["AP"] = types.Dict(map[string]types.Object{"N": *irN}) + + return d, nil +} + +func (tf *TextField) bbox() *types.Rectangle { + if tf.Label == nil { + return tf.BoundingBox.Clone() + } + + l := tf.Label + var r *types.Rectangle + x := l.td.X + + switch l.td.HAlign { + case types.AlignCenter: + x -= float64(l.Width) / 2 + case types.AlignRight: + x -= float64(l.Width) + } + + r = types.RectForWidthAndHeight(x, l.td.Y, float64(l.Width), l.height) + + return model.CalcBoundingBoxForRects(tf.BoundingBox, r) +} + +func (tf *TextField) prepareRectLL(mTop, mRight, mBottom, mLeft float64) (float64, float64) { + return tf.content.calcPosition(tf.x, tf.y, tf.Dx, tf.Dy, mTop, mRight, mBottom, mLeft) +} + +func (tf *TextField) prepLabel(p *model.Page, pageNr int, fonts model.FontMap) error { + if tf.Label == nil { + return nil + } + + l := tf.Label + pdf := tf.pdf + + t := "Default" + if l.Value != "" { + t, _ = format.Text(l.Value, pdf.TimestampFormat, pageNr, pdf.pageCount()) + } + + w := float64(l.Width) + g := float64(l.Gap) + + f := l.Font + fontName, fontLang, col := f.Name, f.Lang, f.col + + id, err := tf.pdf.idForFontName(fontName, fontLang, p.Fm, fonts, pageNr) + if err != nil { + return err + } + + td := model.TextDescriptor{ + Text: t, + FontName: fontName, + Embed: true, + FontKey: id, + FontSize: f.Size, + Scale: 1., + ScaleAbs: true, + RTL: l.RTL, + } + + if col != nil { + td.StrokeCol, td.FillCol = *col, *col + } + + if l.BgCol != nil { + td.ShowBackground, td.ShowTextBB, td.BackgroundCol = true, true, *l.BgCol + } + + bb := model.WriteMultiLine(tf.pdf.XRefTable, new(bytes.Buffer), types.RectForFormat("A4"), nil, td) + l.height = bb.Height() + if bb.Width() > w { + w = bb.Width() + l.Width = int(bb.Width()) + } + + td.X, td.Y = tf.labelPos(l.height, w, g) + + if !tf.Multiline && + (bb.Height() < tf.BoundingBox.Height()) && + (l.relPos == types.RelPosLeft || l.relPos == types.RelPosRight) { + td.MBot = (tf.BoundingBox.Height() - bb.Height()) / 2 + td.MTop = td.MBot + } + + td.HAlign, td.VAlign = l.HorAlign, types.AlignBottom + + l.td = &td + + return nil +} + +func (tf *TextField) prepForRender(p *model.Page, pageNr int, fonts model.FontMap) error { + mTop, mRight, mBottom, mLeft, err := tf.calcMargin() + if err != nil { + return err + } + + x, y := tf.prepareRectLL(mTop, mRight, mBottom, mLeft) + + if err := tf.calcFont(); err != nil { + return err + } + + var boWidth int + if tf.Border != nil { + if tf.Border.col != nil { + boWidth = tf.Border.Width + } + } + + h := float64(tf.Font.Size)*1.2 + 2*float64(boWidth) + + if tf.Multiline { + if tf.Height == 0 { + return errors.Errorf("pdfcpu: field: %s height == 0", tf.ID) + } + h = tf.Height + } + + if tf.Width < 0 { + // Extend width to maxWidth. + if tf.HorAlign == types.AlignLeft || tf.HorAlign == types.AlignCenter { + r := tf.content.Box().CroppedCopy(0) + r.LL.X += mLeft + r.LL.Y += mBottom + r.UR.X -= mRight + r.UR.Y -= mTop + tf.Width = r.Width() - tf.x + } + } + + tf.BoundingBox = types.RectForWidthAndHeight(x, y, tf.Width, h) + + return tf.prepLabel(p, pageNr, fonts) +} + +func (tf *TextField) doRender(p *model.Page, fonts model.FontMap) error { + d, err := tf.prepareDict(fonts) + if err != nil { + return err + } + + ann := model.FieldAnnotation{Dict: d} + if tf.Tab > 0 { + p.AnnotTabs[tf.Tab] = ann + } else { + p.Annots = append(p.Annots, ann) + } + + if tf.Label != nil { + model.WriteColumn(tf.pdf.XRefTable, p.Buf, p.MediaBox, nil, *tf.Label.td, 0) + } + + if tf.Debug || tf.pdf.Debug { + tf.pdf.highlightPos(p.Buf, tf.BoundingBox.LL.X, tf.BoundingBox.LL.Y, tf.content.Box()) + } + + return nil +} + +func (tf *TextField) render(p *model.Page, pageNr int, fonts model.FontMap) error { + if err := tf.prepForRender(p, pageNr, fonts); err != nil { + return err + } + + return tf.doRender(p, fonts) +} + +func calcColsFromMK(ctx *model.Context, d types.Dict) (*color.SimpleColor, *color.SimpleColor, error) { + var bgCol, boCol *color.SimpleColor + + if o, found := d.Find("MK"); found { + d1, err := ctx.DereferenceDict(o) + if err != nil { + return nil, nil, err + } + if len(d1) > 0 { + if arr := d1.ArrayEntry("BG"); len(arr) == 3 { + sc := color.NewSimpleColorForArray(arr) + bgCol = &sc + } + if arr := d1.ArrayEntry("BC"); len(arr) == 3 { + sc := color.NewSimpleColorForArray(arr) + boCol = &sc + } + } + } + + return bgCol, boCol, nil +} + +func calcBorderWidth(d types.Dict) int { + w := 0 + if arr := d.ArrayEntry("Border"); len(arr) == 3 { + // 0, 1 ?? + bw, ok := arr[2].(types.Integer) + if ok { + w = bw.Value() + } else { + w = int(arr[2].(types.Float).Value()) + } + } + return w +} + +func hasUTF(s string) bool { + for _, char := range s { + if char > 0xFF { + return true + } + } + return false +} + +func NewTextField( + ctx *model.Context, + d types.Dict, + v string, + multiLine bool, + fontIndRef *types.IndirectRef, + fonts map[string]types.IndirectRef) (*TextField, *types.IndirectRef, error) { + + tf := &TextField{Value: v, Multiline: multiLine} + + bb, err := ctx.RectForArray(d.ArrayEntry("Rect")) + if err != nil { + return nil, nil, err + } + + tf.BoundingBox = types.RectForDim(bb.Width(), bb.Height()) + + if fontIndRef == nil { + if fontIndRef, err = tf.calcFontFromDA(ctx, d, hasUTF(v), fonts); err != nil { + return nil, nil, err + } + } + + tf.HorAlign = types.AlignLeft + if q := d.IntEntry("Q"); q != nil { + tf.HorAlign = types.HAlignment(*q) + } + + bgCol, boCol, err := calcColsFromMK(ctx, d) + if err != nil { + return nil, nil, err + } + tf.BgCol = bgCol + + var b Border + boWidth := calcBorderWidth(d) + if boWidth > 0 { + b.Width = boWidth + b.col = boCol + } + tf.Border = &b + + return tf, fontIndRef, nil +} + +func renderTextFieldAP(ctx *model.Context, d types.Dict, v string, multiLine bool, fonts map[string]types.IndirectRef) error { + if ap := d.DictEntry("AP"); ap != nil { + if err := ctx.DeleteObject(ap); err != nil { + return err + } + } + + tf, fontIndRef, err := NewTextField(ctx, d, v, multiLine, nil, fonts) + if err != nil { + return err + } + + bb, err := tf.renderN(ctx.XRefTable) + if err != nil { + return err + } + + irN, err := NewForm(ctx.XRefTable, bb, tf.fontID, fontIndRef, tf.BoundingBox) + if err != nil { + return err + } + + d["AP"] = types.Dict(map[string]types.Object{"N": *irN}) + + return nil +} + +func fontAttrs(ctx *model.Context, fd types.Dict, fontID, text string, fonts map[string]types.IndirectRef) (string, string, string, *types.IndirectRef, error) { + var prefix, name, lang string + var err error + + fontIndRef := fd.IndirectRefEntry(fontID) + if fontIndRef == nil { + // create utf8 font * save as indRef + fontID, name, lang, fontIndRef, err = ensureUTF8FormFont(ctx, fonts) + if err != nil { + return "", "", "", nil, err + } + fd[fontID] = *fontIndRef + } else { + objNr := int(fontIndRef.ObjectNumber) + fontDict, err := ctx.DereferenceDict(*fontIndRef) + if err != nil { + return "", "", "", nil, err + } + if fontDict == nil { + // create utf8 font * save as indRef + fontID, name, lang, fontIndRef, err = ensureUTF8FormFont(ctx, fonts) + if err != nil { + return "", "", "", nil, err + } + fd[fontID] = *fontIndRef + } else { + prefix, name, err = pdffont.Name(ctx.XRefTable, fontDict, objNr) + if err != nil { + return "", "", "", nil, err + } + if len(prefix) == 0 && hasUTF(text) { + // create utf8 font * save as indRef + fontID, name, lang, fontIndRef, err = ensureUTF8FormFont(ctx, fonts) + if err != nil { + return "", "", "", nil, err + } + fd[fontID] = *fontIndRef + } else { + fonts[name] = *fontIndRef + } + } + } + + return fontID, name, lang, fontIndRef, nil +} + +func EnsureTextFieldAP(ctx *model.Context, d types.Dict, text string, multiLine bool, fonts map[string]types.IndirectRef) error { + ap := d.DictEntry("AP") + if ap == nil { + return renderTextFieldAP(ctx, d, text, multiLine, fonts) + } + + irN := ap.IndirectRefEntry("N") + if irN == nil { + return renderTextFieldAP(ctx, d, text, multiLine, fonts) + } + + sd, _, err := ctx.DereferenceStreamDict(*irN) + if err != nil { + return err + } + + d1 := sd.DictEntry("Resources") + if d1 == nil { + return renderTextFieldAP(ctx, d, text, multiLine, fonts) + } + + fd := d1.DictEntry("Font") + if fd == nil { + return renderTextFieldAP(ctx, d, text, multiLine, fonts) + } + + s := d.StringEntry("DA") + if s == nil { + s = ctx.Form.StringEntry("DA") + if s == nil { + return errors.New("pdfcpu: textfield missing \"DA\"") + } + } + + fontID, f, err := fontFromDA(*s) + if err != nil { + return err + } + + fontID, name, lang, fontIndRef, err := fontAttrs(ctx, fd, fontID, text, fonts) + if err != nil { + return err + } + + tf, _, err := NewTextField(ctx, d, text, multiLine, fontIndRef, fonts) + if err != nil { + return err + } + + tf.Font = &f + tf.fontID = fontID + tf.Font.Name = name + tf.Font.Lang = lang + tf.RTL = pdffont.RTL(lang) + + bb, err := tf.renderN(ctx.XRefTable) + if err != nil { + return err + } + + return updateForm(ctx.XRefTable, bb, irN) +} diff --git a/pkg/pdfcpu/primitives/textFieldLabel.go b/pkg/pdfcpu/primitives/textFieldLabel.go new file mode 100644 index 0000000000000000000000000000000000000000..3098b53fd24477f35fdf0a65d06644ec7157f1d3 --- /dev/null +++ b/pkg/pdfcpu/primitives/textFieldLabel.go @@ -0,0 +1,88 @@ +/* + Copyright 2021 The pdfcpu Authors. + + 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 + + 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 primitives + +import ( + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// TextFieldLabel represents a label for an input field. +type TextFieldLabel struct { + TextField + Width int + height float64 + Gap int // horizontal space between textfield and label + Position string `json:"pos"` // relative to textfield + relPos types.RelPosition + td *model.TextDescriptor +} + +func (tfl *TextFieldLabel) validate() error { + + if tfl.Value == "" { + return errors.New("pdfcpu: missing label value") + } + + if tfl.Width <= 0 { + // only for pos left align left or pos right align right! + return errors.Errorf("pdfcpu: invalid label width: %d", tfl.Width) + } + + tfl.relPos = types.RelPosLeft + if tfl.Position != "" { + rp, err := types.ParseRelPosition(tfl.Position) + if err != nil { + return err + } + tfl.relPos = rp + } + + if tfl.Font != nil { + tfl.Font.pdf = tfl.pdf + if err := tfl.Font.validate(); err != nil { + return err + } + } + + if tfl.Border != nil { + tfl.Border.pdf = tfl.pdf + if err := tfl.Border.validate(); err != nil { + return err + } + } + + if tfl.BackgroundColor != "" { + sc, err := tfl.pdf.parseColor(tfl.BackgroundColor) + if err != nil { + return err + } + tfl.BgCol = sc + } + + tfl.HorAlign = types.AlignLeft + if tfl.Alignment != "" { + ha, err := types.ParseHorAlignment(tfl.Alignment) + if err != nil { + return err + } + tfl.HorAlign = ha + } + + return nil +} diff --git a/pkg/pdfcpu/property.go b/pkg/pdfcpu/property.go new file mode 100644 index 0000000000000000000000000000000000000000..f15487feb78892b84e889e27487313e954ed0ba3 --- /dev/null +++ b/pkg/pdfcpu/property.go @@ -0,0 +1,97 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 pdfcpu + +import ( + "fmt" + "sort" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +// PropertiesList returns a list of document properties as recorded in the document info dict. +func PropertiesList(ctx *model.Context) ([]string, error) { + list := make([]string, 0, len(ctx.Properties)) + keys := make([]string, len(ctx.Properties)) + i := 0 + for k := range ctx.Properties { + keys[i] = k + i++ + } + sort.Strings(keys) + for _, k := range keys { + v := ctx.Properties[k] + list = append(list, fmt.Sprintf("%s = %s", k, v)) + } + return list, nil +} + +// PropertiesAdd adds properties into the document info dict. +// Returns true if at least one property was added. +func PropertiesAdd(ctx *model.Context, properties map[string]string) error { + if err := ensureInfoDictAndFileID(ctx); err != nil { + return err + } + + d, _ := ctx.DereferenceDict(*ctx.Info) + + for k, v := range properties { + s, err := types.EscapeUTF16String(v) + if err != nil { + return err + } + d[k] = types.StringLiteral(*s) + ctx.Properties[k] = *s + } + + return nil +} + +// PropertiesRemove deletes specified properties. +// Returns true if at least one property was removed. +func PropertiesRemove(ctx *model.Context, properties []string) (bool, error) { + if ctx.Info == nil { + return false, nil + } + + d, err := ctx.DereferenceDict(*ctx.Info) + if err != nil || d == nil { + return false, err + } + + if len(properties) == 0 { + // Remove all properties. + for k := range ctx.Properties { + delete(d, types.EncodeName(k)) + } + ctx.Properties = map[string]string{} + return true, nil + } + + var removed bool + for _, k := range properties { + _, ok := d[k] + if ok && !removed { + delete(d, k) + delete(ctx.Properties, k) + removed = true + } + } + + return removed, nil +} diff --git a/pkg/pdfcpu/read.go b/pkg/pdfcpu/read.go new file mode 100644 index 0000000000000000000000000000000000000000..c779cecf423c2c74da71d7b3a6f855590bcaf5e3 --- /dev/null +++ b/pkg/pdfcpu/read.go @@ -0,0 +1,3006 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 pdfcpu + +import ( + "bufio" + "bytes" + "context" + "io" + "os" + "sort" + "strconv" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/filter" + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/scan" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +const ( + defaultBufSize = 1024 +) + +var ( + ErrWrongPassword = errors.New("pdfcpu: please provide the correct password") + ErrCorruptHeader = errors.New("pdfcpu: no header version available") + ErrReferenceDoesNotExist = errors.New("pdfcpu: referenced object does not exist") + + zero int64 = 0 +) + +// ReadFile reads in a PDF file and builds an internal structure holding its cross reference table aka the PDF model context. +func ReadFile(inFile string, conf *model.Configuration) (*model.Context, error) { + return ReadFileWithContext(context.Background(), inFile, conf) +} + +// ReadFileContext reads in a PDF file and builds an internal structure holding its cross reference table aka the PDF model context. +// If the passed Go context is cancelled, reading will be interrupted. +func ReadFileWithContext(c context.Context, inFile string, conf *model.Configuration) (*model.Context, error) { + if log.InfoEnabled() { + log.Info.Printf("reading %s..\n", inFile) + } + + f, err := os.Open(inFile) + if err != nil { + return nil, errors.Wrapf(err, "can't open %q", inFile) + } + + defer func() { + f.Close() + }() + + return ReadWithContext(c, f, conf) +} + +// Read takes a readSeeker and generates a PDF model context, +// an in-memory representation containing a cross reference table. +func Read(rs io.ReadSeeker, conf *model.Configuration) (*model.Context, error) { + return ReadWithContext(context.Background(), rs, conf) +} + +// Read takes a readSeeker and generates a PDF model context, +// an in-memory representation containing a cross reference table. +// If the passed Go context is cancelled, reading will be interrupted. +func ReadWithContext(c context.Context, rs io.ReadSeeker, conf *model.Configuration) (*model.Context, error) { + if log.ReadEnabled() { + log.Read.Println("Read: begin") + } + + ctx, err := model.NewContext(rs, conf) + if err != nil { + return nil, err + } + + if log.InfoEnabled() { + if ctx.Reader15 { + log.Info.Println("PDF Version 1.5 conforming reader") + } else { + log.Info.Println("PDF Version 1.4 conforming reader - no object streams or xrefstreams allowed") + } + } + + // Populate xRefTable. + if err = readXRefTable(c, ctx); err != nil { + return nil, errors.Wrap(err, "Read: xRefTable failed") + } + + // Make all objects explicitly available (load into memory) in corresponding xRefTable entries. + // Also decode any involved object streams. + if err = dereferenceXRefTable(c, ctx, conf); err != nil { + return nil, err + } + + // Some PDFWriters write an incorrect Size into trailer. + if *ctx.XRefTable.Size < len(ctx.XRefTable.Table) { + *ctx.XRefTable.Size = len(ctx.XRefTable.Table) + } + + if log.ReadEnabled() { + log.Read.Println("Read: end") + } + + return ctx, nil +} + +// fillBuffer reads from r until buf is full or read returns an error. +// Unlike io.ReadAtLeast fillBuffer does not return ErrUnexpectedEOF +// if an EOF happens after reading some but not all the bytes. +// Special thanks go to Rene Kaufmann. +func fillBuffer(r io.Reader, buf []byte) (int, error) { + var n int + var err error + + for n < len(buf) && err == nil { + var nn int + nn, err = r.Read(buf[n:]) + n += nn + } + + if n > 0 && err == io.EOF { + return n, nil + } + + return n, err +} + +func newPositionedReader(rs io.ReadSeeker, offset *int64) (*bufio.Reader, error) { + if _, err := rs.Seek(*offset, io.SeekStart); err != nil { + return nil, err + } + + if log.ReadEnabled() { + log.Read.Printf("newPositionedReader: positioned to offset: %d\n", *offset) + } + + return bufio.NewReader(rs), nil +} + +// Get the file offset of the last XRefSection. +// Go to end of file and search backwards for the first occurrence of startxref {offset} %%EOF +func offsetLastXRefSection(ctx *model.Context, skip int64) (*int64, error) { + rs := ctx.Read.RS + + var ( + prevBuf, workBuf []byte + bufSize int64 = 512 + offset int64 + ) + + if ctx.Read.FileSize < bufSize { + bufSize = ctx.Read.FileSize + } + + for i := 1; offset == 0; i++ { + + off, err := rs.Seek(-int64(i)*bufSize-skip, io.SeekEnd) + if err != nil { + return nil, errors.New("pdfcpu: can't find last xref section") + } + + if log.ReadEnabled() { + log.Read.Printf("scanning for offsetLastXRefSection starting at %d\n", off) + } + + curBuf := make([]byte, bufSize) + + if _, err = fillBuffer(rs, curBuf); err != nil { + return nil, err + } + + workBuf = curBuf + if prevBuf != nil { + workBuf = append(curBuf, prevBuf...) + } + + j := strings.LastIndex(string(workBuf), "startxref") + if j == -1 { + prevBuf = curBuf + continue + } + + p := workBuf[j+len("startxref"):] + posEOF := strings.Index(string(p), "%%EOF") + if posEOF == -1 { + return nil, errors.New("pdfcpu: no matching %%EOF for startxref") + } + + p = p[:posEOF] + offset, err = strconv.ParseInt(strings.TrimSpace(string(p)), 10, 64) + if err != nil || offset >= ctx.Read.FileSize { + return nil, errors.New("pdfcpu: corrupted last xref section") + } + } + + if log.ReadEnabled() { + log.Read.Printf("Offset last xrefsection: %d\n", offset) + } + + return &offset, nil +} + +func createXRefTableEntry(entryType string, objNr int, offset, offExtra int64, generation int) (model.XRefTableEntry, bool) { + entry := model.XRefTableEntry{Offset: &offset, Generation: &generation} + + if entryType == "n" { + + // in use object + + if log.ReadEnabled() { + log.Read.Printf("createXRefTableEntry: Object #%d is in use at offset=%d, generation=%d\n", objNr, offset, generation) + } + + if offset == 0 { + if log.InfoEnabled() { + log.Info.Printf("createXRefTableEntry: Skip entry for in use object #%d with offset 0\n", objNr) + } + return entry, false + } + + *entry.Offset += offExtra + + return entry, true + } + + // free object + + if log.ReadEnabled() { + log.Read.Printf("createXRefTableEntry: Object #%d is unused, next free is object#%d, generation=%d\n", objNr, offset, generation) + } + + entry.Free = true + + return entry, true +} + +func decodeSubsection(fields []string, repairOff int) (int64, int, string, error) { + offset, err := strconv.ParseInt(fields[0], 10, 64) + if err != nil { + return 0, 0, "", err + } + offset += int64(repairOff) + + generation, err := strconv.Atoi(fields[1]) + if err != nil { + return 0, 0, "", err + } + + entryType := fields[2] + if entryType != "f" && entryType != "n" { + return 0, 0, "", errors.New("pdfcpu: decodeSubsection: corrupt xref subsection entryType") + } + + return offset, generation, entryType, nil +} + +// Read next subsection entry and generate corresponding xref table entry. +func parseXRefTableEntry(xRefTable *model.XRefTable, s *bufio.Scanner, objNr int, offExtra int64, repairOff int) error { + if log.ReadEnabled() { + log.Read.Println("parseXRefTableEntry: begin") + } + + line, err := scanLine(s) + if err != nil { + return err + } + + if xRefTable.Exists(objNr) { + if log.ReadEnabled() { + log.Read.Printf("parseXRefTableEntry: end - Skip entry %d - already assigned\n", objNr) + } + return nil + } + + fields := strings.Fields(line) + if len(fields) != 3 || + len(fields[0]) != 10 || len(fields[1]) != 5 || len(fields[2]) != 1 { + return errors.New("pdfcpu: parseXRefTableEntry: corrupt xref subsection header") + } + + offset, generation, entryType, err := decodeSubsection(fields, repairOff) + if err != nil { + return err + } + + entry, ok := createXRefTableEntry(entryType, objNr, offset, offExtra, generation) + if !ok { + return nil + } + + if log.ReadEnabled() { + log.Read.Printf("parseXRefTableEntry: Insert new xreftable entry for Object %d\n", objNr) + } + + xRefTable.Table[objNr] = &entry + + if log.ReadEnabled() { + log.Read.Println("parseXRefTableEntry: end") + } + + return nil +} + +// Process xRef table subsection and create corrresponding xRef table entries. +func parseXRefTableSubSection(xRefTable *model.XRefTable, s *bufio.Scanner, fields []string, offExtra int64, repairOff int) error { + if log.ReadEnabled() { + log.Read.Println("parseXRefTableSubSection: begin") + } + + startObjNumber, err := strconv.Atoi(fields[0]) + if err != nil { + return err + } + + objCount, err := strconv.Atoi(fields[1]) + if err != nil { + return err + } + + if log.ReadEnabled() { + log.Read.Printf("detected xref subsection, startObj=%d length=%d\n", startObjNumber, objCount) + } + + // Process all entries of this subsection into xRefTable entries. + for i := 0; i < objCount; i++ { + if err = parseXRefTableEntry(xRefTable, s, startObjNumber+i, offExtra, repairOff); err != nil { + return err + } + } + + if log.ReadEnabled() { + log.Read.Println("parseXRefTableSubSection: end") + } + + return nil +} + +// Parse compressed object. +func compressedObject(c context.Context, s string) (types.Object, error) { + if log.ReadEnabled() { + log.Read.Println("compressedObject: begin") + } + + o, err := model.ParseObjectContext(c, &s) + if err != nil { + return nil, err + } + + d, ok := o.(types.Dict) + if !ok { + // return trivial Object: Integer, Array, etc. + if log.ReadEnabled() { + log.Read.Println("compressedObject: end, any other than dict") + } + return o, nil + } + + streamLength, streamLengthRef := d.Length() + if streamLength == nil && streamLengthRef == nil { + // return Dict + if log.ReadEnabled() { + log.Read.Println("compressedObject: end, dict") + } + return d, nil + } + + return nil, errors.New("pdfcpu: compressedObject: stream objects are not to be stored in an object stream") +} + +// Parse all objects of an object stream and save them into objectStreamDict.ObjArray. +func parseObjectStream(c context.Context, osd *types.ObjectStreamDict) error { + if log.ReadEnabled() { + log.Read.Printf("parseObjectStream begin: decoding %d objects.\n", osd.ObjCount) + } + + decodedContent := osd.Content + if decodedContent == nil { + // The actual content will be decoded lazily, only decode the prolog here. + var err error + decodedContent, err = osd.DecodeLength(int64(osd.FirstObjOffset)) + if err != nil { + return err + } + } + prolog := decodedContent[:osd.FirstObjOffset] + + // The separator used in the prolog shall be white space + // but some PDF writers use 0x00. + prolog = bytes.ReplaceAll(prolog, []byte{0x00}, []byte{0x20}) + + objs := strings.Fields(string(prolog)) + if len(objs)%2 > 0 { + return errors.New("pdfcpu: parseObjectStream: corrupt object stream dict") + } + + // e.g., 10 0 11 25 = 2 Objects: #10 @ offset 0, #11 @ offset 25 + + var objArray types.Array + + var offsetOld int + + for i := 0; i < len(objs); i += 2 { + + if err := c.Err(); err != nil { + return err + } + + offset, err := strconv.Atoi(objs[i+1]) + if err != nil { + return err + } + + offset += osd.FirstObjOffset + + if i > 0 { + o := types.NewLazyObjectStreamObject(osd, offsetOld, offset, compressedObject) + objArray = append(objArray, o) + } + + if i == len(objs)-2 { + o := types.NewLazyObjectStreamObject(osd, offset, -1, compressedObject) + objArray = append(objArray, o) + } + + offsetOld = offset + } + + osd.ObjArray = objArray + + if log.ReadEnabled() { + log.Read.Println("parseObjectStream end") + } + + return nil +} + +func createXRefTableEntryFromXRefStream(entry byte, objNr int, c2, c3, offExtra int64, objStreams types.IntSet) model.XRefTableEntry { + var xRefTableEntry model.XRefTableEntry + + switch entry { + + case 0x00: + // free object + if log.ReadEnabled() { + log.Read.Printf("createXRefTableEntryFromXRefStream: Object #%d is unused, next free is object#%d, generation=%d\n", objNr, c2, c3) + } + g := int(c3) + + xRefTableEntry = + model.XRefTableEntry{ + Free: true, + Compressed: false, + Offset: &c2, + Generation: &g} + + case 0x01: + // in use object + if log.ReadEnabled() { + log.Read.Printf("createXRefTableEntryFromXRefStream: Object #%d is in use at offset=%d, generation=%d\n", objNr, c2, c3) + } + g := int(c3) + + c2 += offExtra + + xRefTableEntry = + model.XRefTableEntry{ + Free: false, + Compressed: false, + Offset: &c2, + Generation: &g} + + case 0x02: + // compressed object + // generation always 0. + if log.ReadEnabled() { + log.Read.Printf("createXRefTableEntryFromXRefStream: Object #%d is compressed at obj %5d[%d]\n", objNr, c2, c3) + } + objNumberRef := int(c2) + objIndex := int(c3) + + xRefTableEntry = + model.XRefTableEntry{ + Free: false, + Compressed: true, + ObjectStream: &objNumberRef, + ObjectStreamInd: &objIndex} + + objStreams[objNumberRef] = true + } + + return xRefTableEntry +} + +// For each object embedded in this xRefStream create the corresponding xRef table entry. +func extractXRefTableEntriesFromXRefStream(buf []byte, offExtra int64, xsd *types.XRefStreamDict, ctx *model.Context) error { + if log.ReadEnabled() { + log.Read.Printf("extractXRefTableEntriesFromXRefStream begin") + } + + // Note: + // A value of zero for an element in the W array indicates that the corresponding field shall not be present in the stream, + // and the default value shall be used, if there is one. + // If the first element is zero, the type field shall not be present, and shall default to type 1. + + i1 := xsd.W[0] + i2 := xsd.W[1] + i3 := xsd.W[2] + + xrefEntryLen := i1 + i2 + i3 + if log.ReadEnabled() { + log.Read.Printf("extractXRefTableEntriesFromXRefStream: begin xrefEntryLen = %d\n", xrefEntryLen) + } + + if len(buf)%xrefEntryLen > 0 { + return errors.New("pdfcpu: extractXRefTableEntriesFromXRefStream: corrupt xrefstream") + } + + objCount := len(xsd.Objects) + if log.ReadEnabled() { + log.Read.Printf("extractXRefTableEntriesFromXRefStream: objCount:%d %v\n", objCount, xsd.Objects) + log.Read.Printf("extractXRefTableEntriesFromXRefStream: len(buf):%d objCount*xrefEntryLen:%d\n", len(buf), objCount*xrefEntryLen) + } + if len(buf) < objCount*xrefEntryLen { + // Sometimes there is an additional xref entry not accounted for by "Index". + // We ignore such entries and do not treat this as an error. + return errors.New("pdfcpu: extractXRefTableEntriesFromXRefStream: corrupt xrefstream") + } + + j := 0 + + // bufToInt64 interprets the content of buf as an int64. + bufToInt64 := func(buf []byte) (i int64) { + for _, b := range buf { + i <<= 8 + i |= int64(b) + } + return + } + + for i := 0; i < len(buf) && j < len(xsd.Objects); i += xrefEntryLen { + + objNr := xsd.Objects[j] + i2Start := i + i1 + c2 := bufToInt64(buf[i2Start : i2Start+i2]) + c3 := bufToInt64(buf[i2Start+i2 : i2Start+i2+i3]) + + entry := createXRefTableEntryFromXRefStream(buf[i], objNr, c2, c3, offExtra, ctx.Read.ObjectStreams) + + if ctx.XRefTable.Exists(objNr) { + if log.ReadEnabled() { + log.Read.Printf("extractXRefTableEntriesFromXRefStream: Skip entry %d - already assigned\n", objNr) + } + } else { + ctx.Table[objNr] = &entry + } + + j++ + } + + if log.ReadEnabled() { + log.Read.Println("extractXRefTableEntriesFromXRefStream: end") + } + + return nil +} + +func xRefStreamDict(c context.Context, ctx *model.Context, o types.Object, objNr int, streamOffset int64) (*types.XRefStreamDict, error) { + d, ok := o.(types.Dict) + if !ok { + return nil, errors.New("pdfcpu: xRefStreamDict: no dict") + } + + // Parse attributes for stream object. + streamLength, streamLengthObjNr := d.Length() + if streamLength == nil && streamLengthObjNr == nil { + return nil, errors.New("pdfcpu: xRefStreamDict: no \"Length\" entry") + } + + filterPipeline, err := pdfFilterPipeline(c, ctx, d) + if err != nil { + return nil, err + } + + // We have a stream object. + if log.ReadEnabled() { + log.Read.Printf("xRefStreamDict: streamobject #%d\n", objNr) + } + sd := types.NewStreamDict(d, streamOffset, streamLength, streamLengthObjNr, filterPipeline) + + if err = loadEncodedStreamContent(c, ctx, &sd, false); err != nil { + return nil, err + } + + // Decode xrefstream content + if err = saveDecodedStreamContent(nil, &sd, 0, 0, true); err != nil { + return nil, errors.Wrapf(err, "xRefStreamDict: cannot decode stream for obj#:%d\n", objNr) + } + + return model.ParseXRefStreamDict(&sd) +} + +func processXRefStream(ctx *model.Context, xsd *types.XRefStreamDict, objNr, genNr *int, offset *int64, offExtra int64) (prevOffset *int64, err error) { + if log.ReadEnabled() { + log.Read.Println("processXRefStream: begin") + } + + if err = parseTrailer(ctx.XRefTable, xsd.Dict); err != nil { + return nil, err + } + + // Parse xRefStream and create xRefTable entries for embedded objects. + if err = extractXRefTableEntriesFromXRefStream(xsd.Content, offExtra, xsd, ctx); err != nil { + return nil, err + } + + *offset += offExtra + + if entry, ok := ctx.Table[*objNr]; ok && *entry.Offset == *offset { + entry.Object = *xsd + } + + ////////////////// + // entry := + // model.XRefTableEntry{ + // Free: false, + // Offset: offset, + // Generation: genNr, + // Object: *xsd} + + // if log.ReadEnabled() { + // log.Read.Printf("processXRefStream: Insert new xRefTable entry for Object %d\n", *objNr) + // } + + // ctx.Table[*objNr] = &entry + // ctx.Read.XRefStreams[*objNr] = true + /////////////////// + + prevOffset = xsd.PreviousOffset + + if log.ReadEnabled() { + log.Read.Println("processXRefStream: end") + } + + return prevOffset, nil +} + +// Parse xRef stream and setup xrefTable entries for all embedded objects and the xref stream dict. +func parseXRefStream(c context.Context, ctx *model.Context, rd io.Reader, offset *int64, offExtra int64) (prevOffset *int64, err error) { + if log.ReadEnabled() { + log.Read.Printf("parseXRefStream: begin at offset %d\n", *offset) + } + + buf, endInd, streamInd, streamOffset, err := buffer(c, rd) + if err != nil { + return nil, err + } + + if log.ReadEnabled() { + log.Read.Printf("parseXRefStream: endInd=%[1]d(%[1]x) streamInd=%[2]d(%[2]x)\n", endInd, streamInd) + } + + line := string(buf) + + // We expect a stream and therefore "stream" before "endobj" if "endobj" within buffer. + // There is no guarantee that "endobj" is contained in this buffer for large streams! + if streamInd < 0 || (endInd > 0 && endInd < streamInd) { + return nil, errors.New("pdfcpu: parseXRefStream: corrupt pdf file") + } + + // Init object parse buf. + l := line[:streamInd] + + objNr, genNr, err := model.ParseObjectAttributes(&l) + if err != nil { + return nil, err + } + + // parse this object + if log.ReadEnabled() { + log.Read.Printf("parseXRefStream: xrefstm obj#:%d gen:%d\n", *objNr, *genNr) + log.Read.Printf("parseXRefStream: dereferencing object %d\n", *objNr) + } + + o, err := model.ParseObjectContext(c, &l) + if err != nil { + return nil, errors.Wrapf(err, "parseXRefStream: no object") + } + + if log.ReadEnabled() { + log.Read.Printf("parseXRefStream: we have an object: %s\n", o) + } + + streamOffset += *offset + xsd, err := xRefStreamDict(c, ctx, o, *objNr, streamOffset) + if err != nil { + return nil, err + } + + return processXRefStream(ctx, xsd, objNr, genNr, offset, offExtra) +} + +// Parse an xRefStream for a hybrid PDF file. +func parseHybridXRefStream(c context.Context, ctx *model.Context, offset *int64, offExtra int64) error { + if log.ReadEnabled() { + log.Read.Println("parseHybridXRefStream: begin") + } + + rd, err := newPositionedReader(ctx.Read.RS, offset) + if err != nil { + return err + } + + if _, err = parseXRefStream(c, ctx, rd, offset, offExtra); err != nil { + return err + } + + if log.ReadEnabled() { + log.Read.Println("parseHybridXRefStream: end") + } + + return nil +} + +func parseTrailerSize(xRefTable *model.XRefTable, d types.Dict) error { + i := d.Size() + if i == nil { + return errors.New("pdfcpu: parseTrailerSize: missing entry \"Size\"") + } + // Not reliable! + // Patched after all read in. + xRefTable.Size = i + return nil +} + +func parseTrailerRoot(xRefTable *model.XRefTable, d types.Dict) error { + indRef := d.IndirectRefEntry("Root") + if indRef == nil { + return errors.New("pdfcpu: parseTrailerRoot: missing entry \"Root\"") + } + xRefTable.Root = indRef + if log.ReadEnabled() { + log.Read.Printf("parseTrailerRoot: Root object: %s\n", *xRefTable.Root) + } + return nil +} + +func parseTrailerInfo(xRefTable *model.XRefTable, d types.Dict) error { + indRef := d.IndirectRefEntry("Info") + if indRef != nil { + xRefTable.Info = indRef + if log.ReadEnabled() { + log.Read.Printf("parseTrailerInfo: Info object: %s\n", *xRefTable.Info) + } + } + return nil +} + +func parseTrailerID(xRefTable *model.XRefTable, d types.Dict) error { + arr := d.ArrayEntry("ID") + if arr != nil { + xRefTable.ID = arr + if log.ReadEnabled() { + log.Read.Printf("parseTrailerID: ID object: %s\n", xRefTable.ID) + } + return nil + } + + if xRefTable.Encrypt != nil { + return errors.New("pdfcpu: parseTrailerID: missing entry \"ID\"") + } + + return nil +} + +// Parse trailer dict and return any offset of a previous xref section. +func parseTrailer(xRefTable *model.XRefTable, d types.Dict) error { + if log.ReadEnabled() { + log.Read.Println("parseTrailer begin") + } + + if indRef := d.IndirectRefEntry("Encrypt"); indRef != nil { + xRefTable.Encrypt = indRef + if log.ReadEnabled() { + log.Read.Printf("parseTrailer: Encrypt object: %s\n", *xRefTable.Encrypt) + } + } + + if xRefTable.Size == nil { + if err := parseTrailerSize(xRefTable, d); err != nil { + return err + } + } + + if xRefTable.Root == nil { + if err := parseTrailerRoot(xRefTable, d); err != nil { + return err + } + } + + if xRefTable.Info == nil { + if err := parseTrailerInfo(xRefTable, d); err != nil { + return err + } + } + + if xRefTable.ID == nil { + if err := parseTrailerID(xRefTable, d); err != nil { + return err + } + } + + if log.ReadEnabled() { + log.Read.Println("parseTrailerf end") + } + + return nil +} + +func scanForPreviousXref(ctx *model.Context, offset *int64) *int64 { + var ( + prevBuf, workBuf []byte + bufSize int64 = 512 + off int64 + match1 []byte = []byte("startxref") + match2 []byte = []byte("xref") + ) + + m := match1 + + for i := int64(1); ; i++ { + off = *offset - i*bufSize + rd, err := newPositionedReader(ctx.Read.RS, &off) + if err != nil { + return nil + } + + curBuf := make([]byte, bufSize) + + n, err := fillBuffer(rd, curBuf) + if err != nil { + return nil + } + + workBuf = curBuf + if prevBuf != nil { + workBuf = append(curBuf, prevBuf...) + } + + j := bytes.LastIndex(workBuf, m) + if j == -1 { + if int64(n) < bufSize { + return nil + } + prevBuf = curBuf + continue + } + + if bytes.Equal(m, match1) { + m = match2 + continue + } + + off += int64(j) + break + } + + return &off +} + +func handleAdditionalStreams(trailerDict types.Dict, xRefTable *model.XRefTable) { + arr := trailerDict.ArrayEntry("AdditionalStreams") + if arr == nil { + return + } + + if log.ReadEnabled() { + log.Read.Printf("found AdditionalStreams: %s\n", arr) + } + + a := types.Array{} + for _, value := range arr { + if indRef, ok := value.(types.IndirectRef); ok { + a = append(a, indRef) + } + } + + xRefTable.AdditionalStreams = &a +} + +func offsetPrev(ctx *model.Context, trailerDict types.Dict, offCurXRef *int64) *int64 { + offset := trailerDict.Prev() + if offset != nil { + if log.ReadEnabled() { + log.Read.Printf("offsetPrev: previous xref table section offset:%d\n", *offset) + } + if *offset == 0 { + offset = nil + if offCurXRef != nil { + if off := scanForPreviousXref(ctx, offCurXRef); off != nil { + offset = off + } + } + } + } + return offset +} + +func parseTrailerDict(c context.Context, ctx *model.Context, trailerDict types.Dict, offCurXRef *int64, offExtra int64) (*int64, error) { + if log.ReadEnabled() { + log.Read.Println("parseTrailerDict begin") + } + + xRefTable := ctx.XRefTable + + if err := parseTrailer(xRefTable, trailerDict); err != nil { + return nil, err + } + + handleAdditionalStreams(trailerDict, xRefTable) + + offset := offsetPrev(ctx, trailerDict, offCurXRef) + + offsetXRefStream := trailerDict.Int64Entry("XRefStm") + if offsetXRefStream == nil { + // No cross reference stream. + if !ctx.Reader15 && xRefTable.Version() >= model.V14 && !ctx.Read.Hybrid { + return nil, errors.Errorf("parseTrailerDict: PDF1.4 conformant reader: found incompatible version: %s", xRefTable.VersionString()) + } + if log.ReadEnabled() { + log.Read.Println("parseTrailerDict end") + } + // continue to parse previous xref section, if there is any. + return offset, nil + } + + // This file is using cross reference streams. + + if !ctx.Read.Hybrid { + ctx.Read.Hybrid = true + ctx.Read.UsingXRefStreams = true + } + + // 1.5 conformant readers process hidden objects contained + // in XRefStm before continuing to process any previous XRefSection. + // Previous XRefSection is expected to have free entries for hidden entries. + // May appear in XRefSections only. + if ctx.Reader15 { + if err := parseHybridXRefStream(c, ctx, offsetXRefStream, offExtra); err != nil { + return nil, err + } + } + + if log.ReadEnabled() { + log.Read.Println("parseTrailerDict end") + } + + return offset, nil +} + +func scanLineRaw(s *bufio.Scanner) (string, error) { + if ok := s.Scan(); !ok { + if s.Err() != nil { + return "", s.Err() + } + return "", errors.New("pdfcpu: scanLineRaw: returning nothing") + } + return s.Text(), nil +} + +func scanLine(s *bufio.Scanner) (s1 string, err error) { + for i := 0; i <= 1; i++ { + s1, err = scanLineRaw(s) + if err != nil { + return "", err + } + if len(s1) > 0 { + break + } + } + + return s1, nil +} + +func isDict(s string) (bool, error) { + o, err := model.ParseObject(&s) + if err != nil { + return false, err + } + _, ok := o.(types.Dict) + return ok, nil +} + +func scanTrailerDictStart(s *bufio.Scanner, line *string) error { + l := *line + var err error + for { + i := strings.Index(l, "<<") + if i >= 0 { + *line = l[i:] + return nil + } + l, err = scanLine(s) + if log.ReadEnabled() { + log.Read.Printf("line: <%s>\n", l) + } + if err != nil { + return err + } + } +} + +func scanTrailerDictRemainder(s *bufio.Scanner, line string, buf bytes.Buffer) (string, error) { + var ( + i int + err error + ) + + for i = strings.Index(line, "startxref"); i < 0; { + if log.ReadEnabled() { + log.Read.Printf("line: <%s>\n", line) + } + buf.WriteString(line) + buf.WriteString("\x0a") + if line, err = scanLine(s); err != nil { + return "", err + } + i = strings.Index(line, "startxref") + } + + line = line[:i] + if log.ReadEnabled() { + log.Read.Printf("line: <%s>\n", line) + } + buf.WriteString(line[:i]) + buf.WriteString("\x0a") + + return buf.String(), nil +} + +func scanTrailer(s *bufio.Scanner, line string) (string, error) { + var buf bytes.Buffer + if log.ReadEnabled() { + log.Read.Printf("line: <%s>\n", line) + } + + if err := scanTrailerDictStart(s, &line); err != nil { + return "", err + } + + return scanTrailerDictRemainder(s, line, buf) +} + +func processTrailer(c context.Context, ctx *model.Context, s *bufio.Scanner, line string, offCurXRef *int64, offExtra int64) (*int64, error) { + var trailerString string + + if line != "trailer" { + trailerString = line[7:] + if log.ReadEnabled() { + log.Read.Printf("processTrailer: trailer leftover: <%s>\n", trailerString) + } + } else { + if log.ReadEnabled() { + log.Read.Printf("line (len %d) <%s>\n", len(line), line) + } + } + + trailerString, err := scanTrailer(s, trailerString) + if err != nil { + return nil, err + } + + if log.ReadEnabled() { + log.Read.Printf("processTrailer: trailerString: (len:%d) <%s>\n", len(trailerString), trailerString) + } + + o, err := model.ParseObjectContext(c, &trailerString) + if err != nil { + return nil, err + } + + trailerDict, ok := o.(types.Dict) + if !ok { + return nil, errors.New("pdfcpu: processTrailer: corrupt trailer dict") + } + + if log.ReadEnabled() { + log.Read.Printf("processTrailer: trailerDict:\n%s\n", trailerDict) + } + + return parseTrailerDict(c, ctx, trailerDict, offCurXRef, offExtra) +} + +// Parse xRef section into corresponding number of xRef table entries. +func parseXRefSection(c context.Context, ctx *model.Context, s *bufio.Scanner, fields []string, ssCount *int, offCurXRef *int64, offExtra int64, repairOff int) (*int64, error) { + if log.ReadEnabled() { + log.Read.Println("parseXRefSection begin") + } + + var ( + line string + err error + ) + + if len(fields) == 0 { + + line, err = scanLine(s) + if err != nil { + return nil, err + } + + if log.ReadEnabled() { + log.Read.Printf("parseXRefSection: <%s>\n", line) + } + + fields = strings.Fields(line) + } + + // Process all sub sections of this xRef section. + for !strings.HasPrefix(line, "trailer") && len(fields) == 2 { + + if err = parseXRefTableSubSection(ctx.XRefTable, s, fields, offExtra, repairOff); err != nil { + return nil, err + } + *ssCount++ + + // trailer or another xref table subsection ? + if line, err = scanLine(s); err != nil { + return nil, err + } + + // if empty line try next line for trailer + if len(line) == 0 { + if line, err = scanLine(s); err != nil { + return nil, err + } + } + + fields = strings.Fields(line) + } + + if log.ReadEnabled() { + log.Read.Println("parseXRefSection: All subsections read!") + } + + if !strings.HasPrefix(line, "trailer") { + return nil, errors.Errorf("xrefsection: missing trailer dict, line = <%s>", line) + } + + if log.ReadEnabled() { + log.Read.Println("parseXRefSection: parsing trailer dict..") + } + + return processTrailer(c, ctx, s, line, offCurXRef, offExtra) +} + +func scanForVersion(rs io.ReadSeeker, prefix string) ([]byte, int, error) { + bufSize := 100 + + if _, err := rs.Seek(0, io.SeekStart); err != nil { + return nil, 0, err + } + + buf := make([]byte, bufSize) + var curBuf []byte + + off := 0 + found := false + var buf2 []byte + + for !found { + n, err := fillBuffer(rs, buf) + if err != nil { + return nil, 0, ErrCorruptHeader + } + curBuf = buf[:n] + for { + i := bytes.IndexByte(curBuf, '%') + if i < 0 { + // no match, check next block + off += bufSize + break + } + + // Check all occurrences + if i < len(curBuf)-18 { + if !bytes.HasPrefix(curBuf[i:], []byte(prefix)) { + // No match, keep checking + curBuf = curBuf[i+1:] + continue + } + off += i + curBuf = curBuf[i:] + found = true + break + } + + // Partial match, need 2nd buffer + if len(buf2) == 0 { + buf2 = make([]byte, bufSize) + } + n, err := fillBuffer(rs, buf2) + if err != nil { + return nil, 0, ErrCorruptHeader + } + buf3 := append(curBuf[i:], buf2[:n]...) + if !bytes.HasPrefix(buf3, []byte(prefix)) { + // No match, keep checking + curBuf = buf2 + off += bufSize + continue + } + off += i + curBuf = buf3 + found = true + break + } + } + + return curBuf, off, nil +} + +// Get version from first line of file. +// Beginning with PDF 1.4, the Version entry in the document’s catalog dictionary +// (located via the Root entry in the file’s trailer, as described in 7.5.5, "File Trailer"), +// if present, shall be used instead of the version specified in the Header. +// The header version comes as the first line of the file. +// eolCount is the number of characters used for eol (1 or 2). +func headerVersion(rs io.ReadSeeker) (v *model.Version, eolCount int, offset int64, err error) { + if log.ReadEnabled() { + log.Read.Println("headerVersion begin") + } + + prefix := "%PDF-" + + s, off, err := scanForVersion(rs, prefix) + if err != nil { + return nil, 0, 0, err + } + + pdfVersion, err := model.PDFVersion(string(s[len(prefix) : len(prefix)+3])) + if err != nil { + return nil, 0, 0, errors.Wrapf(err, "headerVersion: unknown PDF Header Version") + } + + s = s[8:] + s = bytes.TrimLeft(s, "\t\f ") + + // Detect the used eol which should be 1 (0x00, 0x0D) or 2 chars (0x0D0A)long. + // %PDF-1.x{whiteSpace}{text}{eol} or + j := bytes.IndexAny(s, "\x0A\x0D") + if j < 0 { + return nil, 0, 0, ErrCorruptHeader + } + if s[j] == 0x0A { + eolCount = 1 + } else if s[j] == 0x0D { + eolCount = 1 + if (len(s) > j+1) && (s[j+1] == 0x0A) { + eolCount = 2 + } + } + + if log.ReadEnabled() { + log.Read.Printf("headerVersion: end, found header version: %s\n", pdfVersion) + } + + return &pdfVersion, eolCount, int64(off), nil +} + +func parseAndLoad(c context.Context, ctx *model.Context, line string, offset *int64) error { + l := line + objNr, generation, err := model.ParseObjectAttributes(&l) + if err != nil { + return err + } + + entry := model.XRefTableEntry{ + Free: false, + Offset: offset, + Generation: generation} + + if !ctx.XRefTable.Exists(*objNr) { + ctx.Table[*objNr] = &entry + } + + o, err := ParseObjectWithContext(c, ctx, *entry.Offset, *objNr, *entry.Generation) + if err != nil { + return err + } + + entry.Object = o + + sd, ok := o.(types.StreamDict) + if ok { + if err = loadStreamDict(c, ctx, &sd, *objNr, *generation, true); err != nil { + return err + } + entry.Object = sd + *offset = sd.StreamOffset + *sd.StreamLength + return nil + } + + *offset += int64(len(line) + ctx.Read.EolCount) + + return nil +} + +func processObject(c context.Context, ctx *model.Context, line string, offset *int64) (*bufio.Scanner, error) { + if err := parseAndLoad(c, ctx, line, offset); err != nil { + return nil, err + } + rd, err := newPositionedReader(ctx.Read.RS, offset) + if err != nil { + return nil, err + } + s := bufio.NewScanner(rd) + s.Split(scan.Lines) + return s, nil +} + +// bypassXrefSection is a fix for digesting corrupt xref sections. +// It populates the xRefTable by reading in all indirect objects line by line +// and works on the assumption of a single xref section - meaning no incremental updates. +func bypassXrefSection(c context.Context, ctx *model.Context, offExtra int64, wasErr error) error { + if log.ReadEnabled() { + log.Read.Printf("bypassXRefSection after %v\n", wasErr) + } + + var z int64 + g := types.FreeHeadGeneration + ctx.Table[0] = &model.XRefTableEntry{ + Free: true, + Offset: &z, + Generation: &g} + + rs := ctx.Read.RS + eolCount := ctx.Read.EolCount + var offset int64 + + rd, err := newPositionedReader(rs, &offset) + if err != nil { + return err + } + + s := bufio.NewScanner(rd) + s.Split(scan.Lines) + + bb := []byte{} + var ( + withinXref bool + withinTrailer bool + ) + + for { + line, err := scanLineRaw(s) + if err != nil { + break + } + if withinXref { + offset += int64(len(line) + eolCount) + if withinTrailer { + bb = append(bb, '\n') + bb = append(bb, line...) + i := strings.Index(line, "startxref") + if i >= 0 { + _, err = processTrailer(c, ctx, s, string(bb), nil, offExtra) + if err == nil { + model.ShowRepaired("xreftable") + } + return err + } + continue + } + i := strings.Index(line, "trailer") + if i >= 0 { + bb = append(bb, line...) + withinTrailer = true + } + continue + } + i := strings.Index(line, "xref") + if i >= 0 { + offset += int64(len(line) + eolCount) + withinXref = true + continue + } + i = strings.Index(line, "obj") + if i >= 0 { + if i > 2 && strings.Index(line, "endobj") != i-3 { + s, err = processObject(c, ctx, line, &offset) + if err != nil { + return err + } + continue + } + } + offset += int64(len(line) + eolCount) + continue + } + return nil +} + +func postProcess(ctx *model.Context, xrefSectionCount int) { + // Ensure free object #0 if exactly one xref subsection + // and in one of the following weird situations: + if xrefSectionCount == 1 && !ctx.Exists(0) { + if *ctx.Size == len(ctx.Table)+1 { + // Fix for #262 + // Create free object 0 from scratch if the free list head is missing. + g0 := types.FreeHeadGeneration + ctx.Table[0] = &model.XRefTableEntry{Free: true, Offset: &zero, Generation: &g0} + } else { + // Fix for #250 + // Create free object 0 by shifting down all objects by one. + for i := 1; i <= *ctx.Size; i++ { + ctx.Table[i-1] = ctx.Table[i] + } + delete(ctx.Table, *ctx.Size) + } + } +} + +func tryXRefSection(c context.Context, ctx *model.Context, rs io.ReadSeeker, offset *int64, offExtra int64, xrefSectionCount *int) (*int64, error) { + rd, err := newPositionedReader(rs, offset) + if err != nil { + return nil, err + } + + s := bufio.NewScanner(rd) + buf := make([]byte, 0, 4096) + s.Buffer(buf, 1024*1024) + s.Split(scan.Lines) + + line, err := scanLine(s) + if err != nil { + return nil, err + } + if log.ReadEnabled() { + log.Read.Printf("xref line 1: <%s>\n", line) + } + repairOff := len(line) + + if strings.TrimSpace(line) == "xref" { + if log.ReadEnabled() { + log.Read.Println("tryXRefSection: found xref section") + } + return parseXRefSection(c, ctx, s, nil, xrefSectionCount, offset, offExtra, 0) + } + + // Repair fix for #823 + if strings.HasPrefix(line, "xref") { + fields := strings.Fields(line) + if len(fields) == 3 { + return parseXRefSection(c, ctx, s, fields[1:], xrefSectionCount, offset, offExtra, 0) + } + } + + // Repair fix for #326 + if line, err = scanLine(s); err != nil { + return nil, err + } + if log.ReadEnabled() { + log.Read.Printf("xref line 2: <%s>\n", line) + } + + i := strings.Index(line, "xref") + if i >= 0 { + if log.ReadEnabled() { + log.Read.Println("tryXRefSection: found xref section") + } + repairOff += i + if log.ReadEnabled() { + log.Read.Printf("Repair offset: %d\n", repairOff) + } + return parseXRefSection(c, ctx, s, nil, xrefSectionCount, offset, offExtra, repairOff) + } + + return &zero, nil +} + +// Build XRefTable by reading XRef streams or XRef sections. +func buildXRefTableStartingAt(c context.Context, ctx *model.Context, offset *int64) error { + if log.ReadEnabled() { + log.Read.Println("buildXRefTableStartingAt: begin") + } + + rs := ctx.Read.RS + hv, eolCount, offExtra, err := headerVersion(rs) + if err != nil { + return err + } + *offset += offExtra + + ctx.HeaderVersion = hv + ctx.Read.EolCount = eolCount + offs := map[int64]bool{} + xrefSectionCount := 0 + + for offset != nil { + + if err := c.Err(); err != nil { + return err + } + + if offs[*offset] { + if offset, err = offsetLastXRefSection(ctx, ctx.Read.FileSize-*offset); err != nil { + return err + } + if offs[*offset] { + return nil + } + } + + offs[*offset] = true + + off, err := tryXRefSection(c, ctx, rs, offset, offExtra, &xrefSectionCount) + if err != nil { + return err + } + + if off == nil || *off != 0 { + offset = off + continue + } + + if log.ReadEnabled() { + log.Read.Println("buildXRefTableStartingAt: found xref stream") + } + ctx.Read.UsingXRefStreams = true + rd, err := newPositionedReader(rs, offset) + if err != nil { + return err + } + + if offset, err = parseXRefStream(c, ctx, rd, offset, offExtra); err != nil { + // Try fix for corrupt single xref section. + return bypassXrefSection(c, ctx, offExtra, err) + } + + } + + postProcess(ctx, xrefSectionCount) + + if log.ReadEnabled() { + log.Read.Println("buildXRefTableStartingAt: end") + } + + return nil +} + +// Populate the cross reference table for this PDF file. +// Goto offset of first xref table entry. +// Can be "xref" or indirect object reference eg. "34 0 obj" +// Keep digesting xref sections as long as there is a defined previous xref section +// and build up the xref table along the way. +func readXRefTable(c context.Context, ctx *model.Context) (err error) { + if log.ReadEnabled() { + log.Read.Println("readXRefTable: begin") + } + + offset, err := offsetLastXRefSection(ctx, 0) + if err != nil { + return + } + + ctx.Write.OffsetPrevXRef = offset + + err = buildXRefTableStartingAt(c, ctx, offset) + if err == io.EOF { + return errors.Wrap(err, "readXRefTable: unexpected eof") + } + if err != nil { + return + } + + //Log list of free objects (not the "free list"). + //log.Read.Printf("freelist: %v\n", ctx.freeObjects()) + + // Note: Acrobat 6.0 and later do not use the free list to recycle object numbers - pdfcpu does. + err = ctx.EnsureValidFreeList() + + if log.ReadEnabled() { + log.Read.Println("readXRefTable: end") + } + + return err +} + +func growBufBy(buf []byte, size int, rd io.Reader) ([]byte, error) { + b := make([]byte, size) + + if _, err := fillBuffer(rd, b); err != nil { + return nil, err + } + //log.Read.Printf("growBufBy: Read %d bytes\n", n) + + return append(buf, b...), nil +} + +func nextStreamOffset(line string, streamInd int) (off int) { + off = streamInd + len("stream") + + // Skip optional blanks. + // TODO Should we skip optional whitespace instead? + for ; line[off] == 0x20; off++ { + } + + // Skip 0A eol. + if line[off] == '\n' { + off++ + return + } + + // Skip 0D eol. + if line[off] == '\r' { + off++ + // Skip 0D0A eol. + if line[off] == '\n' { + off++ + } + } + + return +} + +func lastStreamMarker(streamInd *int, endInd int, line string) { + if *streamInd > len(line)-len("stream") { + // No space for another stream marker. + *streamInd = -1 + return + } + + // We start searching after this stream marker. + bufpos := *streamInd + len("stream") + + // Search for next stream marker. + i := strings.Index(line[bufpos:], "stream") + if i < 0 { + // No stream marker within line buffer. + *streamInd = -1 + return + } + + // We found the next stream marker. + *streamInd += len("stream") + i + + if endInd > 0 && *streamInd > endInd { + // We found a stream marker of another object + *streamInd = -1 + } + +} + +// Provide a PDF file buffer of sufficient size for parsing an object w/o stream. +func buffer(c context.Context, rd io.Reader) (buf []byte, endInd int, streamInd int, streamOffset int64, err error) { + // process: # gen obj ... obj dict ... {stream ... data ... endstream} ... endobj + // streamInd endInd + // -1 if absent -1 if absent + + //log.Read.Println("buffer: begin") + + endInd, streamInd = -1, -1 + + for endInd < 0 && streamInd < 0 { + if err := c.Err(); err != nil { + return nil, 0, 0, 0, err + } + + if buf, err = growBufBy(buf, defaultBufSize, rd); err != nil { + return nil, 0, 0, 0, err + } + + line := string(buf) + + endInd, streamInd, err = model.DetectKeywords(line) + if err != nil { + return nil, 0, 0, 0, err + } + + if endInd > 0 && (streamInd < 0 || streamInd > endInd) { + // No stream marker in buf detected. + break + } + + // For very rare cases where "stream" also occurs within obj dict + // we need to find the last "stream" marker before a possible end marker. + for streamInd > 0 && !keywordStreamRightAfterEndOfDict(line, streamInd) { + lastStreamMarker(&streamInd, endInd, line) + } + + if log.ReadEnabled() { + log.Read.Printf("buffer: endInd=%d streamInd=%d\n", endInd, streamInd) + } + + if streamInd > 0 { + + // streamOffset ... the offset where the actual stream data begins. + // is right after the eol after "stream". + + slack := 10 // for optional whitespace + eol (max 2 chars) + need := streamInd + len("stream") + slack + + if len(line) < need { + + // to prevent buffer overflow. + if buf, err = growBufBy(buf, need-len(line), rd); err != nil { + return nil, 0, 0, 0, err + } + + line = string(buf) + } + + streamOffset = int64(nextStreamOffset(line, streamInd)) + } + } + + //log.Read.Printf("buffer: end, returned bufsize=%d streamOffset=%d\n", len(buf), streamOffset) + + return buf, endInd, streamInd, streamOffset, nil +} + +// return true if 'stream' follows end of dict: >>{whitespace}stream +func keywordStreamRightAfterEndOfDict(buf string, streamInd int) bool { + //log.Read.Println("keywordStreamRightAfterEndOfDict: begin") + + // Get a slice of the chunk right in front of 'stream'. + b := buf[:streamInd] + + // Look for last end of dict marker. + eod := strings.LastIndex(b, ">>") + if eod < 0 { + // No end of dict in buf. + return false + } + + // We found the last >>. Return true if after end of dict only whitespace. + ok := strings.TrimSpace(b[eod:]) == ">>" + + //log.Read.Printf("keywordStreamRightAfterEndOfDict: end, %v\n", ok) + + return ok +} + +func buildFilterPipeline(c context.Context, ctx *model.Context, filterArray, decodeParmsArr types.Array) ([]types.PDFFilter, error) { + var filterPipeline []types.PDFFilter + + for i, f := range filterArray { + + filterName, ok := f.(types.Name) + if !ok { + return nil, errors.New("pdfcpu: buildFilterPipeline: filterArray elements corrupt") + } + if decodeParmsArr == nil || decodeParmsArr[i] == nil { + filterPipeline = append(filterPipeline, types.PDFFilter{Name: filterName.Value(), DecodeParms: nil}) + continue + } + + dict, ok := decodeParmsArr[i].(types.Dict) + if !ok { + indRef, ok := decodeParmsArr[i].(types.IndirectRef) + if !ok { + return nil, errors.Errorf("buildFilterPipeline: corrupt Dict: %s\n", dict) + } + d, err := dereferencedDict(c, ctx, indRef.ObjectNumber.Value()) + if err != nil { + return nil, err + } + dict = d + } + + filterPipeline = append(filterPipeline, types.PDFFilter{Name: filterName.String(), DecodeParms: dict}) + } + + return filterPipeline, nil +} + +func singleFilter(c context.Context, ctx *model.Context, filterName string, d types.Dict) ([]types.PDFFilter, error) { + o, found := d.Find("DecodeParms") + if !found { + // w/o decode parameters. + if log.ReadEnabled() { + log.Read.Println("singleFilter: end w/o decode parms") + } + return []types.PDFFilter{{Name: filterName}}, nil + } + + var err error + d, ok := o.(types.Dict) + if !ok { + indRef, ok := o.(types.IndirectRef) + if !ok { + return nil, errors.Errorf("singleFilter: corrupt Dict: %s\n", o) + } + if d, err = dereferencedDict(c, ctx, indRef.ObjectNumber.Value()); err != nil { + return nil, err + } + } + + // with decode parameters. + if log.ReadEnabled() { + log.Read.Println("singleFilter: end with decode parms") + } + + return []types.PDFFilter{{Name: filterName, DecodeParms: d}}, nil +} + +// Return the filter pipeline associated with this stream dict. +func pdfFilterPipeline(c context.Context, ctx *model.Context, dict types.Dict) ([]types.PDFFilter, error) { + if log.ReadEnabled() { + log.Read.Println("pdfFilterPipeline: begin") + } + + var err error + + o, found := dict.Find("Filter") + if !found { + // stream is not compressed. + return nil, nil + } + + // compressed stream. + + var filterPipeline []types.PDFFilter + + if indRef, ok := o.(types.IndirectRef); ok { + if o, err = dereferencedObject(c, ctx, indRef.ObjectNumber.Value()); err != nil { + return nil, err + } + } + + //fmt.Printf("dereferenced filter obj: %s\n", obj) + + if name, ok := o.(types.Name); ok { + return singleFilter(c, ctx, name.String(), dict) + } + + // filter pipeline. + + // Array of filternames + filterArray, ok := o.(types.Array) + if !ok { + return nil, errors.Errorf("pdfFilterPipeline: Expected filterArray corrupt, %v %T", o, o) + } + + // Optional array of decode parameter dicts. + var decodeParmsArr types.Array + decodeParms, found := dict.Find("DecodeParms") + if found { + decodeParmsArr, ok = decodeParms.(types.Array) + if !ok || len(decodeParmsArr) != len(filterArray) { + return nil, errors.New("pdfcpu: pdfFilterPipeline: expected decodeParms array corrupt") + } + } + + //fmt.Printf("decodeParmsArr: %s\n", decodeParmsArr) + + filterPipeline, err = buildFilterPipeline(c, ctx, filterArray, decodeParmsArr) + + if log.ReadEnabled() { + log.Read.Println("pdfFilterPipeline: end") + } + + return filterPipeline, err +} + +func streamDictForObject(c context.Context, ctx *model.Context, d types.Dict, objNr, streamInd int, streamOffset, offset int64) (sd types.StreamDict, err error) { + streamLength, streamLengthRef := d.Length() + + if streamInd <= 0 { + return sd, errors.New("pdfcpu: streamDictForObject: stream object without streamOffset") + } + + filterPipeline, err := pdfFilterPipeline(c, ctx, d) + if err != nil { + return sd, err + } + + streamOffset += offset + + // We have a stream object. + sd = types.NewStreamDict(d, streamOffset, streamLength, streamLengthRef, filterPipeline) + + if log.ReadEnabled() { + log.Read.Printf("streamDictForObject: end, Streamobject #%d\n", objNr) + } + + return sd, nil +} + +func dict(ctx *model.Context, d1 types.Dict, objNr, genNr, endInd, streamInd int) (d2 types.Dict, err error) { + if ctx.EncKey != nil { + if _, err := decryptDeepObject(d1, objNr, genNr, ctx.EncKey, ctx.AES4Strings, ctx.E.R); err != nil { + return nil, err + } + } + + if endInd >= 0 && (streamInd < 0 || streamInd > endInd) { + if log.ReadEnabled() { + log.Read.Printf("dict: end, #%d\n", objNr) + } + d2 = d1 + } + + return d2, nil +} + +func object(c context.Context, ctx *model.Context, offset int64, objNr, genNr int) (o types.Object, endInd, streamInd int, streamOffset int64, err error) { + var rd io.Reader + + if rd, err = newPositionedReader(ctx.Read.RS, &offset); err != nil { + return nil, 0, 0, 0, err + } + + //log.Read.Printf("object: seeked to offset:%d\n", offset) + + // process: # gen obj ... obj dict ... {stream ... data ... endstream} endobj + // streamInd endInd + // -1 if absent -1 if absent + var buf []byte + if buf, endInd, streamInd, streamOffset, err = buffer(c, rd); err != nil { + return nil, 0, 0, 0, err + } + + //log.Read.Printf("streamInd:%d(#%x) streamOffset:%d(#%x) endInd:%d(#%x)\n", streamInd, streamInd, streamOffset, streamOffset, endInd, endInd) + //log.Read.Printf("buflen=%d\n%s", len(buf), hex.Dump(buf)) + + line := string(buf) + + var l string + + if endInd < 0 { // && streamInd >= 0, streamdict + // buf: # gen obj ... obj dict ... stream ... data + // implies we detected no endobj and a stream starting at streamInd. + // big stream, we parse object until "stream" + if log.ReadEnabled() { + log.Read.Println("object: big stream, we parse object until stream") + } + l = line[:streamInd] + } else if streamInd < 0 { // dict + // buf: # gen obj ... obj dict ... endobj + // implies we detected endobj and no stream. + // small object w/o stream, parse until "endobj" + if log.ReadEnabled() { + log.Read.Println("object: small object w/o stream, parse until endobj") + } + l = line[:endInd] + } else if streamInd < endInd { // streamdict + // buf: # gen obj ... obj dict ... stream ... data ... endstream endobj + // implies we detected endobj and stream. + // small stream within buffer, parse until "stream" + if log.ReadEnabled() { + log.Read.Println("object: small stream within buffer, parse until stream") + } + l = line[:streamInd] + } else { // dict + // buf: # gen obj ... obj dict ... endobj # gen obj ... obj dict ... stream + // small obj w/o stream, parse until "endobj" + // stream in buf belongs to subsequent object. + if log.ReadEnabled() { + log.Read.Println("object: small obj w/o stream, parse until endobj") + } + l = line[:endInd] + } + + // Parse object number and object generation. + var objectNr, generationNr *int + if objectNr, generationNr, err = model.ParseObjectAttributes(&l); err != nil { + return nil, 0, 0, 0, err + } + + if objNr != *objectNr || genNr != *generationNr { + // This is suspicious, but ok if two object numbers point to same offset and only one of them is used + // (compare entry.RefCount) like for cases where the PDF Writer is MS Word 2013. + if log.ReadEnabled() { + log.Read.Printf("object %d: non matching objNr(%d) or generationNumber(%d) tags found.\n", objNr, *objectNr, *generationNr) + } + } + + l = strings.TrimSpace(l) + if len(l) == 0 { + // 7.3.9 + // Specifying the null object as the value of a dictionary entry (7.3.7, "Dictionary Objects") + // shall be equivalent to omitting the entry entirely. + return nil, endInd, streamInd, streamOffset, err + } + + o, err = model.ParseObjectContext(c, &l) + + return o, endInd, streamInd, streamOffset, err +} + +// ParseObject parses an object from file at given offset. +func ParseObject(ctx *model.Context, offset int64, objNr, genNr int) (types.Object, error) { + return ParseObjectWithContext(context.Background(), ctx, offset, objNr, genNr) +} + +func ParseObjectWithContext(c context.Context, ctx *model.Context, offset int64, objNr, genNr int) (types.Object, error) { + if log.ReadEnabled() { + log.Read.Printf("ParseObject: begin, obj#%d, offset:%d\n", objNr, offset) + } + + obj, endInd, streamInd, streamOffset, err := object(c, ctx, offset, objNr, genNr) + if err != nil { + return nil, err + } + + switch o := obj.(type) { + + case types.Dict: + d, err := dict(ctx, o, objNr, genNr, endInd, streamInd) + if err != nil || d != nil { + // Dict + return d, err + } + // StreamDict. + return streamDictForObject(c, ctx, o, objNr, streamInd, streamOffset, offset) + + case types.Array: + if ctx.EncKey != nil { + if _, err = decryptDeepObject(o, objNr, genNr, ctx.EncKey, ctx.AES4Strings, ctx.E.R); err != nil { + return nil, err + } + } + return o, nil + + case types.StringLiteral: + if ctx.EncKey != nil { + sl, err := decryptStringLiteral(o, objNr, genNr, ctx.EncKey, ctx.AES4Strings, ctx.E.R) + if err != nil { + return nil, err + } + return *sl, nil + } + return o, nil + + case types.HexLiteral: + if ctx.EncKey != nil { + hl, err := decryptHexLiteral(o, objNr, genNr, ctx.EncKey, ctx.AES4Strings, ctx.E.R) + if err != nil { + return nil, err + } + return *hl, nil + } + return o, nil + + default: + return o, nil + } +} + +func dereferencedObject(c context.Context, ctx *model.Context, objNr int) (types.Object, error) { + entry, ok := ctx.Find(objNr) + if !ok { + return nil, errors.Errorf("pdfcpu: dereferencedObject: unregistered object: %d", objNr) + } + + if entry.Compressed { + if err := decompressXRefTableEntry(ctx.XRefTable, objNr, entry); err != nil { + return nil, err + } + } + + if entry.Object == nil { + + if log.ReadEnabled() { + log.Read.Printf("dereferencedObject: dereferencing object %d\n", objNr) + } + + if entry.Free { + return nil, ErrReferenceDoesNotExist + } + + o, err := ParseObjectWithContext(c, ctx, *entry.Offset, objNr, *entry.Generation) + if err != nil { + return nil, errors.Wrapf(err, "dereferencedObject: problem dereferencing object %d", objNr) + } + + if o == nil { + return nil, errors.New("pdfcpu: dereferencedObject: object is nil") + } + + entry.Object = o + } else if l, ok := entry.Object.(types.LazyObjectStreamObject); ok { + o, err := l.DecodedObject(c) + if err != nil { + return nil, errors.Wrapf(err, "dereferencedObject: problem dereferencing object %d", objNr) + } + + model.ProcessRefCounts(ctx.XRefTable, o) + entry.Object = o + } + + return entry.Object, nil +} + +func dereferencedInteger(c context.Context, ctx *model.Context, objNr int) (*types.Integer, error) { + o, err := dereferencedObject(c, ctx, objNr) + if err != nil { + return nil, err + } + + i, ok := o.(types.Integer) + if !ok { + return nil, errors.New("pdfcpu: dereferencedInteger: corrupt integer") + } + + return &i, nil +} + +func dereferencedDict(c context.Context, ctx *model.Context, objNr int) (types.Dict, error) { + o, err := dereferencedObject(c, ctx, objNr) + if err != nil { + return nil, err + } + + d, ok := o.(types.Dict) + if !ok { + return nil, errors.New("pdfcpu: dereferencedDict: corrupt dict") + } + + return d, nil +} + +// dereference a Integer object representing an int64 value. +func int64Object(c context.Context, ctx *model.Context, objNr int) (*int64, error) { + if log.ReadEnabled() { + log.Read.Printf("int64Object begin: %d\n", objNr) + } + + i, err := dereferencedInteger(c, ctx, objNr) + if err != nil { + return nil, err + } + + i64 := int64(i.Value()) + + if log.ReadEnabled() { + log.Read.Printf("int64Object end: %d\n", objNr) + } + + return &i64, nil + +} + +func readStreamContentBlindly(rd io.Reader) (buf []byte, err error) { + // Weak heuristic for reading in stream data for cases where stream length is unknown. + // ...data...{eol}endstream{eol}endobj + + if buf, err = growBufBy(buf, defaultBufSize, rd); err != nil { + return nil, err + } + + i := bytes.Index(buf, []byte("endstream")) + if i < 0 { + for i = -1; i < 0; i = bytes.Index(buf, []byte("endstream")) { + buf, err = growBufBy(buf, defaultBufSize, rd) + if err != nil { + return nil, err + } + } + } + + buf = buf[:i] + + j := 0 + + // Cut off trailing eol's. + for i = len(buf) - 1; i >= 0 && (buf[i] == 0x0A || buf[i] == 0x0D); i-- { + j++ + } + + if j > 0 { + buf = buf[:len(buf)-j] + } + + return buf, nil +} + +// Reads and returns a file buffer with length = stream length using provided reader positioned at offset. +func readStreamContent(rd io.Reader, streamLength int) ([]byte, error) { + if log.ReadEnabled() { + log.Read.Printf("readStreamContent: begin streamLength:%d\n", streamLength) + } + + if streamLength == 0 { + // Read until "endstream" then fix "Length". + return readStreamContentBlindly(rd) + } + + buf := make([]byte, streamLength) + + for totalCount := 0; totalCount < streamLength; { + count, err := fillBuffer(rd, buf[totalCount:]) + if err != nil { + if err != io.EOF { + return nil, err + } + // Weak heuristic to detect the actual end of this stream + // once we have reached EOF due to incorrect streamLength. + eob := bytes.Index(buf, []byte("endstream")) + if eob < 0 { + return nil, err + } + return buf[:eob], nil + } + + if log.ReadEnabled() { + log.Read.Printf("readStreamContent: count=%d, buflen=%d(%X)\n", count, len(buf), len(buf)) + } + totalCount += count + } + + if log.ReadEnabled() { + log.Read.Printf("readStreamContent: end\n") + } + + return buf, nil +} + +func ensureStreamLength(sd *types.StreamDict, rawContent []byte, fixLength bool) { + l := int64(len(rawContent)) + if fixLength || sd.StreamLength == nil || l != *sd.StreamLength { + sd.StreamLength = &l + sd.Dict["Length"] = types.Integer(l) + } +} + +// loadEncodedStreamContent loads the encoded stream content into sd. +func loadEncodedStreamContent(c context.Context, ctx *model.Context, sd *types.StreamDict, fixLength bool) error { + if log.ReadEnabled() { + log.Read.Printf("loadEncodedStreamContent: begin\n%v\n", sd) + } + + var err error + + if sd.Raw != nil { + if log.ReadEnabled() { + log.Read.Println("loadEncodedStreamContent: end, already in memory.") + } + return nil + } + + // Read stream content encoded at offset with stream length. + + // Dereference stream length if stream length is an indirect object. + if !fixLength && sd.StreamLength == nil { + if sd.StreamLengthObjNr == nil { + return errors.New("pdfcpu: loadEncodedStreamContent: missing streamLength") + } + if sd.StreamLength, err = int64Object(c, ctx, *sd.StreamLengthObjNr); err != nil { + if err != ErrReferenceDoesNotExist { + return err + } + } + if log.ReadEnabled() { + log.Read.Printf("loadEncodedStreamContent: new indirect streamLength:%d\n", *sd.StreamLength) + } + } + + newOffset := sd.StreamOffset + rd, err := newPositionedReader(ctx.Read.RS, &newOffset) + if err != nil { + return err + } + + l1 := 0 + if sd.StreamLength != nil { + l1 = int(*sd.StreamLength) + } + rawContent, err := readStreamContent(rd, l1) + if err != nil { + return err + } + + ensureStreamLength(sd, rawContent, fixLength) + + sd.Raw = rawContent + + if log.ReadEnabled() { + log.Read.Printf("loadEncodedStreamContent: end: len(streamDictRaw)=%d\n", len(sd.Raw)) + } + + return nil +} + +// Decodes the raw encoded stream content and saves it to streamDict.Content. +func saveDecodedStreamContent(ctx *model.Context, sd *types.StreamDict, objNr, genNr int, decode bool) (err error) { + if log.ReadEnabled() { + log.Read.Printf("saveDecodedStreamContent: begin decode=%t\n", decode) + } + + // If the "Identity" crypt filter is used we do not need to decrypt. + if ctx != nil && ctx.EncKey != nil { + if len(sd.FilterPipeline) == 1 && sd.FilterPipeline[0].Name == "Crypt" { + sd.Content = sd.Raw + return nil + } + } + + // Special case: If the length of the encoded data is 0, we do not need to decode anything. + if len(sd.Raw) == 0 { + sd.Content = sd.Raw + return nil + } + + // ctx gets created after XRefStream parsing. + // XRefStreams are not encrypted. + if ctx != nil && ctx.EncKey != nil { + if sd.Raw, err = decryptStream(sd.Raw, objNr, genNr, ctx.EncKey, ctx.AES4Streams, ctx.E.R); err != nil { + return err + } + l := int64(len(sd.Raw)) + sd.StreamLength = &l + } + + if !decode { + return nil + } + + if sd.Image() { + return nil + } + + // Actual decoding of stream data. + err = sd.Decode() + if err == filter.ErrUnsupportedFilter { + err = nil + } + if err != nil { + return err + } + + if log.ReadEnabled() { + log.Read.Println("saveDecodedStreamContent: end") + } + + return nil +} + +// Resolve compressed xRefTableEntry +func decompressXRefTableEntry(xRefTable *model.XRefTable, objNr int, entry *model.XRefTableEntry) error { + if log.ReadEnabled() { + log.Read.Printf("decompressXRefTableEntry: compressed object %d at %d[%d]\n", objNr, *entry.ObjectStream, *entry.ObjectStreamInd) + } + + // Resolve xRefTable entry of referenced object stream. + objectStreamXRefTableEntry, ok := xRefTable.Find(*entry.ObjectStream) + if !ok { + return errors.Errorf("decompressXRefTableEntry: problem dereferencing object stream %d, no xref table entry", *entry.ObjectStream) + } + + // Object of this entry has to be a ObjectStreamDict. + sd, ok := objectStreamXRefTableEntry.Object.(types.ObjectStreamDict) + if !ok { + return errors.Errorf("decompressXRefTableEntry: problem dereferencing object stream %d, no object stream", *entry.ObjectStream) + } + + // Get indexed object from ObjectStreamDict. + o, err := sd.IndexedObject(*entry.ObjectStreamInd) + if err != nil { + return errors.Wrapf(err, "decompressXRefTableEntry: problem dereferencing object stream %d", *entry.ObjectStream) + } + + // Save object to XRefRableEntry. + g := 0 + entry.Object = o + entry.Generation = &g + entry.Compressed = false + + if log.ReadEnabled() { + log.Read.Printf("decompressXRefTableEntry: end, Obj %d[%d]:\n<%s>\n", *entry.ObjectStream, *entry.ObjectStreamInd, o) + } + + return nil +} + +// Log interesting stream content. +func logStream(o types.Object) { + if !log.ReadEnabled() { + return + } + + switch o := o.(type) { + + case types.StreamDict: + + if o.Content == nil { + log.Read.Println("logStream: no stream content") + } + + // if o.IsPageContent { + // //log.Read.Printf("content <%s>\n", StreamDict.Content) + // } + + case types.ObjectStreamDict: + + if o.Content == nil { + log.Read.Println("logStream: no object stream content") + } else { + log.Read.Printf("logStream: objectStream content = %s\n", o.Content) + } + + if o.ObjArray == nil { + log.Read.Println("logStream: no object stream obj arr") + } else { + log.Read.Printf("logStream: objectStream objArr = %s\n", o.ObjArray) + } + + default: + log.Read.Println("logStream: no ObjectStreamDict") + + } + +} + +func decodeObjectStreamObjects(c context.Context, sd *types.StreamDict, objNr int) (*types.ObjectStreamDict, error) { + osd, err := model.ObjectStreamDict(sd) + if err != nil { + return nil, errors.Wrapf(err, "decodeObjectStreamObjects: problem dereferencing object stream %d", objNr) + } + + if log.ReadEnabled() { + log.Read.Printf("decodeObjectStreamObjects: decoding object stream %d:\n", objNr) + } + + // Parse all objects of this object stream and save them to ObjectStreamDict.ObjArray. + if err = parseObjectStream(c, osd); err != nil { + return nil, errors.Wrapf(err, "decodeObjectStreamObjects: problem decoding object stream %d\n", objNr) + } + + if osd.ObjArray == nil { + return nil, errors.Wrap(err, "decodeObjectStreamObjects: objArray should be set!") + } + + if log.ReadEnabled() { + log.Read.Printf("decodeObjectStreamObjects: decoded object stream %d:\n", objNr) + } + + return osd, nil +} + +func decodeObjectStream(c context.Context, ctx *model.Context, objNr int) error { + entry := ctx.Table[objNr] + if entry == nil { + return errors.Errorf("decodeObjectStream: missing entry for obj#%d\n", objNr) + } + + if log.ReadEnabled() { + log.Read.Printf("decodeObjectStream: parsing object stream for obj#%d\n", objNr) + } + + // Parse object stream from file. + o, err := ParseObjectWithContext(c, ctx, *entry.Offset, objNr, *entry.Generation) + if err != nil || o == nil { + return errors.New("pdfcpu: decodeObjectStream: corrupt object stream") + } + + // Ensure StreamDict + sd, ok := o.(types.StreamDict) + if !ok { + return errors.New("pdfcpu: decodeObjectStream: corrupt object stream") + } + + // Load encoded stream content to xRefTable. + if err = loadEncodedStreamContent(c, ctx, &sd, false); err != nil { + return errors.Wrapf(err, "decodeObjectStream: problem dereferencing object stream %d", objNr) + } + + // Will only decrypt, the actual stream content is decoded later lazily. + if err = saveDecodedStreamContent(ctx, &sd, objNr, *entry.Generation, false); err != nil { + if log.ReadEnabled() { + log.Read.Printf("obj %d: %s", objNr, err) + } + return err + } + + // Ensure decoded objectArray for object stream dicts. + if !sd.IsObjStm() { + return errors.New("pdfcpu: decodeObjectStreams: corrupt object stream") + } + + // We have an object stream. + if log.ReadEnabled() { + log.Read.Printf("decodeObjectStreams: object stream #%d\n", objNr) + } + + ctx.Read.UsingObjectStreams = true + + osd, err := decodeObjectStreamObjects(c, &sd, objNr) + if err != nil { + return err + } + + // Save object stream dict to xRefTableEntry. + entry.Object = *osd + + return nil +} + +// Decode all object streams so contained objects are ready to be used. +func decodeObjectStreams(c context.Context, ctx *model.Context) error { + // Note: + // Entry "Extends" intentionally left out. + // No object stream collection validation necessary. + + if log.ReadEnabled() { + log.Read.Println("decodeObjectStreams: begin") + } + + // Get sorted slice of object numbers. + var keys []int + for k := range ctx.Read.ObjectStreams { + keys = append(keys, k) + } + sort.Ints(keys) + + for _, objNr := range keys { + + if err := c.Err(); err != nil { + return err + } + if err := decodeObjectStream(c, ctx, objNr); err != nil { + return err + } + } + + if log.ReadEnabled() { + log.Read.Println("decodeObjectStreams: end") + } + + return nil +} + +func handleLinearizationParmDict(ctx *model.Context, obj types.Object, objNr int) error { + if ctx.Read.Linearized { + // Linearization dict already processed. + return nil + } + + // handle linearization parm dict. + if d, ok := obj.(types.Dict); ok && d.IsLinearizationParmDict() { + + ctx.Read.Linearized = true + ctx.LinearizationObjs[objNr] = true + if log.ReadEnabled() { + log.Read.Printf("handleLinearizationParmDict: identified linearizationObj #%d\n", objNr) + } + + a := d.ArrayEntry("H") + + if a == nil { + return errors.Errorf("handleLinearizationParmDict: corrupt linearization dict at obj:%d - missing array entry H", objNr) + } + + if len(a) != 2 && len(a) != 4 { + return errors.Errorf("handleLinearizationParmDict: corrupt linearization dict at obj:%d - corrupt array entry H, needs length 2 or 4", objNr) + } + + offset, ok := a[0].(types.Integer) + if !ok { + return errors.Errorf("handleLinearizationParmDict: corrupt linearization dict at obj:%d - corrupt array entry H, needs Integer values", objNr) + } + + offset64 := int64(offset.Value()) + ctx.OffsetPrimaryHintTable = &offset64 + + if len(a) == 4 { + + offset, ok := a[2].(types.Integer) + if !ok { + return errors.Errorf("handleLinearizationParmDict: corrupt linearization dict at obj:%d - corrupt array entry H, needs Integer values", objNr) + } + + offset64 := int64(offset.Value()) + ctx.OffsetOverflowHintTable = &offset64 + } + } + + return nil +} + +func loadStreamDict(c context.Context, ctx *model.Context, sd *types.StreamDict, objNr, genNr int, fixLength bool) error { + // Load encoded stream content for stream dicts into xRefTable entry. + if err := loadEncodedStreamContent(c, ctx, sd, fixLength); err != nil { + return errors.Wrapf(err, "dereferenceObject: problem dereferencing stream %d", objNr) + } + + ctx.Read.BinaryTotalSize += *sd.StreamLength + + // Decode stream content. + return saveDecodedStreamContent(ctx, sd, objNr, genNr, ctx.DecodeAllStreams) +} + +func updateBinaryTotalSize(ctx *model.Context, o types.Object) { + switch o := o.(type) { + case types.StreamDict: + ctx.Read.BinaryTotalSize += *o.StreamLength + case types.ObjectStreamDict: + ctx.Read.BinaryTotalSize += *o.StreamLength + case types.XRefStreamDict: + ctx.Read.BinaryTotalSize += *o.StreamLength + } +} + +func dereferenceAndLoad(c context.Context, ctx *model.Context, objNr int, entry *model.XRefTableEntry) error { + if log.ReadEnabled() { + log.Read.Printf("dereferenceAndLoad: dereferencing object %d\n", objNr) + } + + // Parse object from ctx: anything goes dict, array, integer, float, streamdict... + o, err := ParseObjectWithContext(c, ctx, *entry.Offset, objNr, *entry.Generation) + if err != nil { + return errors.Wrapf(err, "dereferenceAndLoad: problem dereferencing object %d", objNr) + } + + entry.Object = o + + // Linearization dicts are validated and recorded for stats only. + if err = handleLinearizationParmDict(ctx, o, objNr); err != nil { + return err + } + + // Handle stream dicts. + + if _, ok := o.(types.ObjectStreamDict); ok { + return errors.Errorf("dereferenceAndLoad: object stream should already be dereferenced at obj:%d", objNr) + } + + if _, ok := o.(types.XRefStreamDict); ok { + return errors.Errorf("dereferenceAndLoad: xref stream should already be dereferenced at obj:%d", objNr) + } + + if sd, ok := o.(types.StreamDict); ok { + if err = loadStreamDict(c, ctx, &sd, objNr, *entry.Generation, false); err != nil { + return err + } + entry.Object = sd + } + + if log.ReadEnabled() { + log.Read.Printf("dereferenceAndLoad: end obj %d of %d\n<%s>\n", objNr, len(ctx.Table), entry.Object) + } + + return nil +} + +func dereferenceObject(c context.Context, ctx *model.Context, objNr int) error { + xRefTable := ctx.XRefTable + xRefTableSize := len(xRefTable.Table) + + if log.ReadEnabled() { + log.Read.Printf("dereferenceObject: begin, dereferencing object %d\n", objNr) + } + + entry := xRefTable.Table[objNr] + + if entry.Free { + if log.ReadEnabled() { + log.Read.Printf("free object %d\n", objNr) + } + return nil + } + + if entry.Compressed { + if err := decompressXRefTableEntry(xRefTable, objNr, entry); err != nil { + return err + } + //log.Read.Printf("dereferenceObject: decompressed entry, Compressed=%v\n%s\n", entry.Compressed, entry.Object) + return nil + } + + // entry is in use. + if log.ReadEnabled() { + log.Read.Printf("in use object %d\n", objNr) + } + + if entry.Offset == nil || *entry.Offset == 0 { + if log.ReadEnabled() { + log.Read.Printf("dereferenceObject: already decompressed or used object w/o offset -> ignored") + } + return nil + } + + o := entry.Object + + if o != nil { + // Already dereferenced. + logStream(entry.Object) + updateBinaryTotalSize(ctx, o) + if log.ReadEnabled() { + log.Read.Printf("dereferenceObject: using cached object %d of %d\n<%s>\n", objNr, xRefTableSize, entry.Object) + } + return nil + } + + if err := dereferenceAndLoad(c, ctx, objNr, entry); err != nil { + return err + } + + logStream(entry.Object) + + return nil +} + +func dereferenceObjectsSorted(c context.Context, ctx *model.Context) error { + xRefTable := ctx.XRefTable + var keys []int + for k := range xRefTable.Table { + keys = append(keys, k) + } + sort.Ints(keys) + + for _, objNr := range keys { + if err := c.Err(); err != nil { + return err + } + if err := dereferenceObject(c, ctx, objNr); err != nil { + return err + } + } + + for _, objNr := range keys { + entry := xRefTable.Table[objNr] + if entry.Free || entry.Compressed { + continue + } + if err := c.Err(); err != nil { + return err + } + model.ProcessRefCounts(xRefTable, entry.Object) + } + + return nil +} + +func dereferenceObjectsRaw(c context.Context, ctx *model.Context) error { + xRefTable := ctx.XRefTable + for objNr := range xRefTable.Table { + if err := c.Err(); err != nil { + return err + } + if err := dereferenceObject(c, ctx, objNr); err != nil { + return err + } + } + + for objNr := range xRefTable.Table { + entry := xRefTable.Table[objNr] + if entry.Free || entry.Compressed { + continue + } + if err := c.Err(); err != nil { + return err + } + model.ProcessRefCounts(xRefTable, entry.Object) + } + + return nil +} + +// Dereferences all objects including compressed objects from object streams. +func dereferenceObjects(c context.Context, ctx *model.Context) error { + if log.ReadEnabled() { + log.Read.Println("dereferenceObjects: begin") + } + + var err error + + if log.StatsEnabled() { + err = dereferenceObjectsSorted(c, ctx) + } else { + err = dereferenceObjectsRaw(c, ctx) + } + + if err != nil { + return err + } + + if log.ReadEnabled() { + log.Read.Println("dereferenceObjects: end") + } + + return nil +} + +// Locate a possible Version entry (since V1.4) in the catalog +// and record this as rootVersion (as opposed to headerVersion). +func identifyRootVersion(xRefTable *model.XRefTable) error { + if log.ReadEnabled() { + log.Read.Println("identifyRootVersion: begin") + } + + // Try to get Version from Root. + rootVersionStr, err := xRefTable.ParseRootVersion() + if err != nil { + return err + } + + if rootVersionStr == nil { + return nil + } + + // Validate version and save corresponding constant to xRefTable. + rootVersion, err := model.PDFVersion(*rootVersionStr) + if err != nil { + return errors.Wrapf(err, "identifyRootVersion: unknown PDF Root version: %s\n", *rootVersionStr) + } + + xRefTable.RootVersion = &rootVersion + + // since V1.4 the header version may be overridden by a Version entry in the catalog. + if *xRefTable.HeaderVersion < model.V14 { + if log.InfoEnabled() { + log.Info.Printf("identifyRootVersion: PDF version is %s - will ignore root version: %s\n", xRefTable.HeaderVersion, *rootVersionStr) + } + } + + if log.ReadEnabled() { + log.Read.Println("identifyRootVersion: end") + } + + return nil +} + +// Parse all Objects including stream content from file and save to the corresponding xRefTableEntries. +// This includes processing of object streams and linearization dicts. +func dereferenceXRefTable(c context.Context, ctx *model.Context, conf *model.Configuration) error { + if log.ReadEnabled() { + log.Read.Println("dereferenceXRefTable: begin") + } + + xRefTable := ctx.XRefTable + + // Note for encrypted files: + // Mandatory provide userpw to open & display file. + // Access may be restricted (Decode access privileges). + // Optionally provide ownerpw in order to gain unrestricted access. + if err := checkForEncryption(c, ctx); err != nil { + return err + } + //fmt.Println("pw authenticated") + + // Prepare decompressed objects. + if err := decodeObjectStreams(c, ctx); err != nil { + return err + } + + // For each xRefTableEntry assign a Object either by parsing from file or pointing to a decompressed object. + if err := dereferenceObjects(c, ctx); err != nil { + return err + } + + // Identify an optional Version entry in the root object/catalog. + if err := identifyRootVersion(xRefTable); err != nil { + return err + } + + if log.ReadEnabled() { + log.Read.Println("dereferenceXRefTable: end") + } + + return nil +} + +func handleUnencryptedFile(ctx *model.Context) error { + if ctx.Cmd == model.DECRYPT || ctx.Cmd == model.SETPERMISSIONS { + return errors.New("pdfcpu: this file is not encrypted") + } + + if ctx.Cmd != model.ENCRYPT { + return nil + } + + // Encrypt subcommand found. + + if ctx.OwnerPW == "" { + return errors.New("pdfcpu: please provide owner password and optional user password") + } + + return nil +} + +func needsOwnerAndUserPassword(cmd model.CommandMode) bool { + return cmd == model.CHANGEOPW || cmd == model.CHANGEUPW || cmd == model.SETPERMISSIONS +} + +func handlePermissions(ctx *model.Context) error { + // AES256 Validate permissions + ok, err := validatePermissions(ctx) + if err != nil { + return err + } + + if !ok { + return errors.New("pdfcpu: corrupted permissions after upw ok") + } + + if ctx.OwnerPW == "" && ctx.UserPW == "" { + return nil + } + + // Double check minimum permissions for pdfcpu processing. + if !hasNeededPermissions(ctx.Cmd, ctx.E) { + //return errors.New("pdfcpu: operation restriced via pdfcpu's permission bits setting") + return nil + } + + return nil +} + +func setupEncryptionKey(ctx *model.Context, d types.Dict) (err error) { + if ctx.E, err = supportedEncryption(ctx, d); err != nil { + return err + } + + if ctx.E.ID, err = ctx.IDFirstElement(); err != nil { + return err + } + + var ok bool + + //fmt.Printf("opw: <%s> upw: <%s> \n", ctx.OwnerPW, ctx.UserPW) + + // Validate the owner password aka. permissions/master password. + if ok, err = validateOwnerPassword(ctx); err != nil { + return err + } + + // If the owner password does not match we generally move on if the user password is correct + // unless we need to insist on a correct owner password due to the specific command in progress. + if !ok && needsOwnerAndUserPassword(ctx.Cmd) { + return errors.New("pdfcpu: please provide the owner password with -opw") + } + + // Generally the owner password, which is also regarded as the master password or set permissions password + // is sufficient for moving on. A password change is an exception since it requires both current passwords. + if ok && !needsOwnerAndUserPassword(ctx.Cmd) { + // AES256 Validate permissions + if ok, err = validatePermissions(ctx); err != nil { + return err + } + if !ok { + return errors.New("pdfcpu: corrupted permissions after opw ok") + } + return nil + } + + // Validate the user password aka. document open password. + if ok, err = validateUserPassword(ctx); err != nil { + return err + } + if !ok { + return ErrWrongPassword + } + + //fmt.Printf("upw ok: %t\n", ok) + + return handlePermissions(ctx) +} + +func checkForEncryption(c context.Context, ctx *model.Context) error { + indRef := ctx.Encrypt + if indRef == nil { + // This file is not encrypted. + return handleUnencryptedFile(ctx) + } + + // This file is encrypted. + if log.ReadEnabled() { + log.Read.Printf("Encryption: %v\n", indRef) + } + + if ctx.Cmd == model.ENCRYPT { + // We want to encrypt this file. + return errors.New("pdfcpu: this file is already encrypted") + } + + // Dereference encryptDict. + d, err := dereferencedDict(c, ctx, indRef.ObjectNumber.Value()) + if err != nil { + return err + } + + if log.ReadEnabled() { + log.Read.Printf("%s\n", d) + } + + // We need to decrypt this file in order to read it. + return setupEncryptionKey(ctx, d) +} diff --git a/pkg/pdfcpu/read_test.go b/pkg/pdfcpu/read_test.go new file mode 100644 index 0000000000000000000000000000000000000000..bd81d3d88a58fddb0dfcb5779ad84d67ab464e66 --- /dev/null +++ b/pkg/pdfcpu/read_test.go @@ -0,0 +1,57 @@ +/* +Copyright 2024 The pdfcpu Authors. + +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 + + 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 pdfcpu + +import ( + "context" + "errors" + "os" + "path/filepath" + "testing" +) + +func TestReadFileContext(t *testing.T) { + inFile := filepath.Join("..", "testdata", "test.pdf") + + ctx, cancel := context.WithTimeout(context.Background(), 0) + defer cancel() + + if doc, err := ReadFileWithContext(ctx, inFile, nil); err == nil { + t.Errorf("reading should have failed, got %+v", doc) + } else if !errors.Is(err, context.DeadlineExceeded) { + t.Errorf("should have failed with timeout, got %s", err) + } +} + +func TestReadContext(t *testing.T) { + inFile := filepath.Join("..", "testdata", "test.pdf") + + fp, err := os.Open(inFile) + if err != nil { + t.Fatal(err) + } + defer fp.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 0) + defer cancel() + + if doc, err := ReadWithContext(ctx, fp, nil); err == nil { + t.Errorf("reading should have failed, got %+v", doc) + } else if !errors.Is(err, context.DeadlineExceeded) { + t.Errorf("should have failed with timeout, got %s", err) + } +} diff --git a/pkg/pdfcpu/resize.go b/pkg/pdfcpu/resize.go new file mode 100644 index 0000000000000000000000000000000000000000..b4def1fc2651d49292f5817cc19e02efd1e9d7e7 --- /dev/null +++ b/pkg/pdfcpu/resize.go @@ -0,0 +1,253 @@ +/* +Copyright 2023 The pdfcpu Authors. + +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 + + 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 pdfcpu + +import ( + "bytes" + "fmt" + "math" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/matrix" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// ParseResizeConfig parses a Resize command string into an internal structure. +// "scale:.5, form:A4, dim:400 200 bgcol:#D00000" +func ParseResizeConfig(s string, u types.DisplayUnit) (*model.Resize, error) { + + if s == "" { + return nil, errors.New("pdfcpu: missing resize configuration string") + } + + res := &model.Resize{Unit: u} + + ss := strings.Split(s, ",") + + for _, s := range ss { + + ss1 := strings.Split(s, ":") + if len(ss1) != 2 { + return nil, errors.New("pdfcpu: Invalid resize configuration string. Please consult pdfcpu help resize") + } + + paramPrefix := strings.TrimSpace(ss1[0]) + paramValueStr := strings.TrimSpace(ss1[1]) + + if err := model.ResizeParamMap.Handle(paramPrefix, paramValueStr, res); err != nil { + return nil, err + } + } + + if res.Scale > 0 && res.PageSize != "" { + return nil, errors.New("pdfcpu: resize - please supply either scale factor or dimensions or form size ") + } + + if res.UserDim && res.PageSize != "" { + return nil, errors.New("pdfcpu: resize - please supply either dimensions or form size ") + } + + return res, nil +} + +func prepTransform(rSrc, rDest *types.Rectangle, enforce bool) (float64, float64, float64, float64, float64) { + + if !enforce && (rSrc.Portrait() && rDest.Landscape()) || (rSrc.Landscape() && rDest.Portrait()) { + w1 := rDest.Width() + rDest.UR.X = rDest.LL.X + rDest.Height() + rDest.UR.Y = rDest.LL.Y + w1 + } + + w, h, dx, dy, rot := types.BestFitRectIntoRect(rSrc, rDest, enforce, true) + + sc := w / rSrc.Width() + + sin := math.Sin(rot * float64(model.DegToRad)) + cos := math.Cos(rot * float64(model.DegToRad)) + + if rot == 90 { + dx += h + } + + dx += rDest.LL.X + dy += rDest.LL.Y + + return sc, sin, cos, dx, dy +} + +func prepResize(res *model.Resize, cropBox *types.Rectangle) (*types.Rectangle, float64, float64, float64, float64, float64) { + ar := cropBox.AspectRatio() + + var ( + sc, dx, dy float64 + r *types.Rectangle + ) + + sin, cos := 0., 1. + + if res.Scale > 0 { + sc = res.Scale + } else { + if res.PageDim != nil { + w := res.PageDim.Width + h := res.PageDim.Height + if w == 0 { + sc = h / cropBox.Height() + w = h * ar + r = types.RectForDim(w, h) + } else if h == 0 { + sc = w / cropBox.Width() + h = w / ar + r = types.RectForDim(w, h) + } else { + r = types.RectForDim(w, h) + sc, sin, cos, dx, dy = prepTransform(cropBox, r, res.EnforceOrientation()) + } + } + } + + return r, sc, sin, cos, dx, dy +} + +func handleBgColAndBorder(dx, dy float64, cropBox *types.Rectangle, bb *[]byte, res *model.Resize) { + if (dx > 0 || dy > 0) && (res.BgColor != nil || res.Border) { + w, h := cropBox.Width(), cropBox.Height() + if dx > 0 { + w -= 2 * dx + } + if dy > 0 { + h -= 2 * dy + } + r1 := types.RectForWidthAndHeight(dx, dy, w, h) + var buf bytes.Buffer + + if res.BgColor != nil { + draw.FillRectNoBorder(&buf, cropBox, *res.BgColor) + draw.FillRectNoBorder(&buf, r1, color.White) + } + + if res.Border { + draw.DrawRect(&buf, r1, 1, &color.Black, nil) + } + + *bb = append(buf.Bytes(), *bb...) + } +} + +func resizePage(ctx *model.Context, pageNr int, res *model.Resize) error { + + d, _, inhPAttrs, err := ctx.PageDict(pageNr, false) + if err != nil { + return err + } + + cropBox := inhPAttrs.MediaBox + if inhPAttrs.CropBox != nil { + cropBox = inhPAttrs.CropBox + } + + // Account for existing rotation. + if inhPAttrs.Rotate != 0 { + if types.IntMemberOf(inhPAttrs.Rotate, []int{+90, -90, +270, -270}) { + w := cropBox.Width() + cropBox.UR.X = cropBox.LL.X + cropBox.Height() + cropBox.UR.Y = cropBox.LL.Y + w + } + } + + r, sc, sin, cos, dx, dy := prepResize(res, cropBox) + + m := matrix.CalcTransformMatrix(sc, sc, sin, cos, dx, dy) + + var trans bytes.Buffer + fmt.Fprintf(&trans, "q %.5f %.5f %.5f %.5f %.5f %.5f cm ", m[0][0], m[0][1], m[1][0], m[1][1], m[2][0], m[2][1]) + + bb, err := ctx.PageContent(d) + if err == model.ErrNoContent { + return nil + } + if err != nil { + return err + } + + if inhPAttrs.Rotate != 0 { + bbInvRot := append([]byte(" q "), model.ContentBytesForPageRotation(inhPAttrs.Rotate, cropBox.Width(), cropBox.Height())...) + bb = append(bbInvRot, bb...) + bb = append(bb, []byte(" Q")...) + } + + bb = append(trans.Bytes(), bb...) + bb = append(bb, []byte(" Q")...) + + if res.Scale > 0 { + cropBox.UR.X = cropBox.LL.X + sc*cropBox.Width() + cropBox.UR.Y = cropBox.LL.Y + sc*cropBox.Height() + } else { + cropBox.UR.X = cropBox.LL.X + r.Width() + cropBox.UR.Y = cropBox.LL.Y + r.Height() + } + + handleBgColAndBorder(dx, dy, cropBox, &bb, res) + + sd, _ := ctx.NewStreamDictForBuf(bb) + if err := sd.Encode(); err != nil { + return err + } + + ir, err := ctx.IndRefForNewObject(*sd) + if err != nil { + return err + } + + d["Contents"] = *ir + + d.Update("MediaBox", cropBox.Array()) + d.Delete("Rotate") + d.Delete("CropBox") + + return nil +} + +func Resize(ctx *model.Context, selectedPages types.IntSet, res *model.Resize) error { + if log.DebugEnabled() { + log.Debug.Printf("Resize:\n%s\n", res) + } + + if len(selectedPages) == 0 { + selectedPages = types.IntSet{} + for i := 1; i <= ctx.PageCount; i++ { + selectedPages[i] = true + } + } + + for k, v := range selectedPages { + if v { + if err := resizePage(ctx, k, res); err != nil { + return err + } + } + } + + ctx.EnsureVersionForWriting() + + return nil +} diff --git a/pkg/pdfcpu/rotate.go b/pkg/pdfcpu/rotate.go new file mode 100644 index 0000000000000000000000000000000000000000..76ea8100b0abff473fb95f74663cf862376e57ca --- /dev/null +++ b/pkg/pdfcpu/rotate.go @@ -0,0 +1,54 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 pdfcpu + +import ( + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +func rotatePage(xRefTable *model.XRefTable, i, j int) error { + if log.DebugEnabled() { + log.Debug.Printf("rotate page:%d\n", i) + } + + consolidateRes := false + d, _, inhPAttrs, err := xRefTable.PageDict(i, consolidateRes) + if err != nil { + return err + } + + d.Update("Rotate", types.Integer((inhPAttrs.Rotate+j)%360)) + + return nil +} + +// RotatePages rotates all selected pages by a multiple of 90 degrees. +func RotatePages(ctx *model.Context, selectedPages types.IntSet, rotation int) error { + + for k, v := range selectedPages { + if v { + err := rotatePage(ctx.XRefTable, k, rotation) + if err != nil { + return err + } + } + } + + return nil +} diff --git a/pkg/pdfcpu/scan/scan.go b/pkg/pdfcpu/scan/scan.go new file mode 100644 index 0000000000000000000000000000000000000000..f7d9d2b726979e26b9b45753f910ced742e50b1d --- /dev/null +++ b/pkg/pdfcpu/scan/scan.go @@ -0,0 +1,65 @@ +/* +Copyright 2023 The pdfcpu Authors. + +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 + + 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 scan + +import "bytes" + +// Lines is a split function for a Scanner that returns each line of +// text, stripped of any trailing end-of-line marker. The returned line may +// be empty. The end-of-line marker is one carriage return followed +// by one newline or one carriage return or one newline. +// The last non-empty line of input will be returned even if it has no newline. +func Lines(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + + indCR := bytes.IndexByte(data, '\r') + indLF := bytes.IndexByte(data, '\n') + + switch { + + case indCR >= 0 && indLF >= 0: + if indCR < indLF { + if indCR+1 == indLF { + // \r\n + return indCR + 2, data[0:indCR], nil + } + // \r + return indCR + 1, data[0:indCR], nil + } + // \n + return indLF + 1, data[0:indLF], nil + + case indCR >= 0: + // \r + return indCR + 1, data[0:indCR], nil + + case indLF >= 0: + // \n + return indLF + 1, data[0:indLF], nil + + } + + // If we're at EOF, we have a final, non-terminated line. Return it. + if atEOF { + return len(data), data, nil + } + + // Request more data. + return 0, nil, nil +} diff --git a/pkg/pdfcpu/stamp.go b/pkg/pdfcpu/stamp.go new file mode 100644 index 0000000000000000000000000000000000000000..46f31c11d83a87624c218174882174dac089e6f8 --- /dev/null +++ b/pkg/pdfcpu/stamp.go @@ -0,0 +1,2186 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 pdfcpu + +import ( + "bytes" + "fmt" + "io" + "net/url" + "os" + "path/filepath" + "strconv" + "strings" + "unicode/utf16" + + "github.com/pdfcpu/pdfcpu/pkg/filter" + "github.com/pdfcpu/pdfcpu/pkg/font" + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw" + pdffont "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/font" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/format" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/matrix" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +const stampWithBBox = false + +var ( + errNoWatermark = errors.New("pdfcpu: no watermarks found") + errCorruptOCGs = errors.New("pdfcpu: OCProperties: corrupt OCGs element") + ErrUnsupportedVersion = errors.New("pdfcpu: PDF 2.0 unsupported for this operation") +) + +type watermarkParamMap map[string]func(string, *model.Watermark) error + +func textDescriptor(wm model.Watermark, timestampFormat string, pageNr, pageCount int) (model.TextDescriptor, bool) { + t, unique := format.Text(wm.TextString, timestampFormat, pageNr, pageCount) + td := model.TextDescriptor{ + Text: t, + FontName: wm.FontName, + FontSize: wm.FontSize, + Scale: wm.Scale, + ScaleAbs: wm.ScaleAbs, + RMode: wm.RenderMode, + StrokeCol: wm.StrokeColor, + FillCol: wm.FillColor, + ShowBackground: true, + } + if wm.BgColor != nil { + td.ShowTextBB = true + td.BackgroundCol = *wm.BgColor + } + return td, unique +} + +// Handle applies parameter completion and if successful +// parses the parameter values into import. +func (m watermarkParamMap) Handle(paramPrefix, paramValueStr string, imp *model.Watermark) error { + var param string + + // Completion support + for k := range m { + if !strings.HasPrefix(k, strings.ToLower(paramPrefix)) { + continue + } + if len(param) > 0 { + return errors.Errorf("pdfcpu: ambiguous parameter prefix \"%s\"", paramPrefix) + } + param = k + } + + if param == "" { + return errors.Errorf("pdfcpu: unknown parameter prefix \"%s\"", paramPrefix) + } + + return m[param](paramValueStr, imp) +} + +var wmParamMap = watermarkParamMap{ + "aligntext": parseTextHorAlignment, + "backgroundcolor": parseBackgroundColor, + "bgcolor": parseBackgroundColor, + "border": parseBorder, + "color": parseFillColor, + "diagonal": parseDiagonal, + "fillcolor": parseFillColor, + "fontname": parseFontName, + "scriptname": parseScriptName, + "margins": parseMargins, + "mode": parseRenderMode, + "offset": parsePositionOffsetWM, + "opacity": parseOpacity, + "points": parseFontSize, + "position": parsePositionAnchorWM, + "rendermode": parseRenderMode, + "rtl": parseRightToLeft, + "rotation": parseRotation, + "scalefactor": parseScaleFactorWM, + "strokecolor": parseStrokeColor, + "url": parseURL, +} + +func parseTextHorAlignment(s string, wm *model.Watermark) error { + var a types.HAlignment + switch s { + case "l", "left": + a = types.AlignLeft + case "r", "right": + a = types.AlignRight + case "c", "center": + a = types.AlignCenter + case "j", "justify": + a = types.AlignJustify + default: + return errors.Errorf("pdfcpu: unknown horizontal alignment (l,r,c,j): %s", s) + } + + wm.HAlign = &a + + return nil +} + +func parsePositionAnchorWM(s string, wm *model.Watermark) error { + a, err := types.ParsePositionAnchor(s) + if err != nil { + return err + } + if a == types.Full { + a = types.Center + } + wm.Pos = a + return nil +} + +func parsePositionOffsetWM(s string, wm *model.Watermark) error { + d := strings.Split(s, " ") + if len(d) != 2 { + return errors.Errorf("pdfcpu: illegal position offset string: need 2 numeric values, %s\n", s) + } + + f, err := strconv.ParseFloat(d[0], 64) + if err != nil { + return err + } + wm.Dx = types.ToUserSpace(f, wm.InpUnit) + + f, err = strconv.ParseFloat(d[1], 64) + if err != nil { + return err + } + wm.Dy = types.ToUserSpace(f, wm.InpUnit) + + return nil +} + +func parseScaleFactorWM(s string, wm *model.Watermark) (err error) { + wm.Scale, wm.ScaleAbs, err = parseScaleFactor(s) + return err +} + +func parseFontName(s string, wm *model.Watermark) error { + if !font.SupportedFont(s) { + return errors.Errorf("pdfcpu: %s is unsupported, please refer to \"pdfcpu fonts list\".\n", s) + } + wm.FontName = s + if strings.HasSuffix(strings.ToUpper(wm.FontName), "GB2312") { + wm.ScriptName = "HANS" + } + + return nil +} + +func parseScriptName(s string, wm *model.Watermark) error { + script := strings.ToUpper(s) + if !pdffont.SupportedScript(script) { + return errors.Errorf("pdfcpu: unsupported font script \"%s\" - Supported are: HANS, HANT, HIRA, KANA, JPAN, HANG, KORE \n", script) + } + + wm.ScriptName = script + + return nil +} + +func parseURL(s string, wm *model.Watermark) error { + if !wm.OnTop { + return errors.Errorf("pdfcpu: \"url\" supported for stamps only.\n") + } + if !strings.HasPrefix(s, "https://") { + s = "https://" + s + } + if _, err := url.ParseRequestURI(s); err != nil { + return err + } + wm.URL = s + return nil +} + +func parseFontSize(s string, wm *model.Watermark) error { + fs, err := strconv.Atoi(s) + if err != nil { + return err + } + + wm.FontSize = fs + + return nil +} + +func parseScaleFactor(s string) (float64, bool, error) { + ss := strings.Split(s, " ") + if len(ss) > 2 { + return 0, false, errors.Errorf("pdfcpu: invalid factor string %s: 0.0 < i <= 1.0 {rel} | 0.0 < i {abs}\n", s) + } + + sc, err := strconv.ParseFloat(ss[0], 64) + if err != nil { + return 0, false, errors.Errorf("pdfcpu: scale factor must be a float value: %s\n", ss[0]) + } + + if sc <= 0 { + return 0, false, errors.Errorf("pdfcpu: invalid scale factor %.2f: 0.0 < i <= 1.0 {rel} | 0.0 < i {abs}\n", sc) + } + + var scaleAbs bool + + if len(ss) == 1 { + // Assume relative scaling for sc <= 1 and absolute scaling for sc > 1. + scaleAbs = sc > 1 + return sc, scaleAbs, nil + } + + switch ss[1] { + case "a", "abs", "absolute": + scaleAbs = true + + case "r", "rel", "relative": + scaleAbs = false + + default: + return 0, false, errors.Errorf("pdfcpu: illegal scale mode: abs|rel, %s\n", ss[1]) + } + + if !scaleAbs && sc > 1 { + return 0, false, errors.Errorf("pdfcpu: invalid relative scale factor %.2f: 0.0 < i <= 1\n", sc) + } + + return sc, scaleAbs, nil +} + +func parseRightToLeft(s string, wm *model.Watermark) error { + switch strings.ToLower(s) { + case "on", "true", "t": + wm.RTL = true + case "off", "false", "f": + wm.RTL = false + default: + return errors.New("pdfcpu: rtl (right-to-left), please provide one of: on/off true/false t/f") + } + + return nil +} + +func parseStrokeColor(s string, wm *model.Watermark) error { + c, err := color.ParseColor(s) + if err != nil { + return err + } + wm.StrokeColor = c + return nil +} + +func parseFillColor(s string, wm *model.Watermark) error { + c, err := color.ParseColor(s) + if err != nil { + return err + } + wm.FillColor = c + return nil +} + +func parseBackgroundColor(s string, wm *model.Watermark) error { + c, err := color.ParseColor(s) + if err != nil { + return err + } + wm.BgColor = &c + return nil +} + +func parseRotation(s string, wm *model.Watermark) error { + if wm.UserRotOrDiagonal { + return errors.New("pdfcpu: please specify rotation or diagonal (r or d)") + } + + r, err := strconv.ParseFloat(s, 64) + if err != nil { + return errors.Errorf("pdfcpu: rotation must be a float value: %s\n", s) + } + if r < -180 || r > 180 { + return errors.Errorf("pdfcpu: illegal rotation: -180 <= r <= 180 degrees, %s\n", s) + } + + wm.Rotation = r + wm.Diagonal = model.NoDiagonal + wm.UserRotOrDiagonal = true + + return nil +} + +func parseDiagonal(s string, wm *model.Watermark) error { + if wm.UserRotOrDiagonal { + return errors.New("pdfcpu: please specify rotation or diagonal (r or d)") + } + + d, err := strconv.Atoi(s) + if err != nil { + return errors.Errorf("pdfcpu: illegal diagonal value: allowed 1 or 2, %s\n", s) + } + if d != model.DiagonalLLToUR && d != model.DiagonalULToLR { + return errors.New("pdfcpu: diagonal: 1..lower left to upper right, 2..upper left to lower right") + } + + wm.Diagonal = d + wm.Rotation = 0 + wm.UserRotOrDiagonal = true + + return nil +} + +func parseOpacity(s string, wm *model.Watermark) error { + o, err := strconv.ParseFloat(s, 64) + if err != nil { + return errors.Errorf("pdfcpu: opacity must be a float value: %s\n", s) + } + if o < 0 || o > 1 { + return errors.Errorf("pdfcpu: illegal opacity: 0.0 <= r <= 1.0, %s\n", s) + } + wm.Opacity = o + + return nil +} + +func parseRenderMode(s string, wm *model.Watermark) error { + m, err := strconv.Atoi(s) + if err != nil { + return errors.Errorf("pdfcpu: illegal render mode value: allowed 0,1,2, %s\n", s) + } + rm := draw.RenderMode(m) + if rm != draw.RMFill && rm != draw.RMStroke && rm != draw.RMFillAndStroke { + return errors.New("pdfcpu: valid rendermodes: 0..fill, 1..stroke, 2..fill&stroke") + } + wm.RenderMode = rm + + return nil +} + +func parseMargins(s string, wm *model.Watermark) error { + var err error + + m := strings.Split(s, " ") + if len(m) == 0 || len(m) > 4 { + return errors.Errorf("pdfcpu: margins: need 1,2,3 or 4 int values, %s\n", s) + } + + f1, err := strconv.ParseFloat(m[0], 64) + if err != nil { + return err + } + + if len(m) == 1 { + wm.MLeft = f1 + wm.MRight = f1 + wm.MTop = f1 + wm.MBot = f1 + return nil + } + + f2, err := strconv.ParseFloat(m[1], 64) + if err != nil { + return err + } + + if len(m) == 2 { + wm.MTop, wm.MBot = f1, f1 + wm.MLeft, wm.MRight = f2, f2 + return nil + } + + f3, err := strconv.ParseFloat(m[2], 64) + if err != nil { + return err + } + + if len(m) == 3 { + wm.MTop = f1 + wm.MLeft, wm.MRight = f2, f2 + wm.MBot = f3 + return nil + } + + f4, err := strconv.ParseFloat(m[3], 64) + if err != nil { + return err + } + + wm.MTop = f1 + wm.MRight = f2 + wm.MBot = f3 + wm.MLeft = f4 + return nil +} + +func parseBorder(s string, wm *model.Watermark) error { + // w + // w r g b + // w #c + // w round + // w round r g b + // w round #c + + var err error + + b := strings.Split(s, " ") + if len(b) == 0 || len(b) > 5 { + return errors.Errorf("pdfcpu: borders: need 1,2,3,4 or 5 int values, %s\n", s) + } + + wm.BorderWidth, err = strconv.ParseFloat(b[0], 64) + if err != nil { + return err + } + if wm.BorderWidth == 0 { + return errors.New("pdfcpu: borders: need width > 0") + } + + if len(b) == 1 { + return nil + } + + if strings.HasPrefix("round", b[1]) { + wm.BorderStyle = types.LJRound + if len(b) == 2 { + return nil + } + c, err := color.ParseColor(strings.Join(b[2:], " ")) + wm.BorderColor = &c + return err + } + + c, err := color.ParseColor(strings.Join(b[1:], " ")) + wm.BorderColor = &c + return err +} + +func parseWatermarkDetails(mode int, modeParm, s string, onTop bool, u types.DisplayUnit) (*model.Watermark, error) { + wm := model.DefaultWatermarkConfig() + wm.OnTop = onTop + wm.InpUnit = u + + ss := strings.Split(s, ",") + if len(ss) > 0 && len(ss[0]) == 0 { + return wm, setWatermarkType(mode, modeParm, wm) + } + + for _, s := range ss { + ss1 := strings.Split(s, ":") + if len(ss1) != 2 { + return nil, parseWatermarkError(onTop) + } + + paramPrefix := strings.TrimSpace(ss1[0]) + paramValueStr := strings.TrimSpace(ss1[1]) + + if err := wmParamMap.Handle(paramPrefix, paramValueStr, wm); err != nil { + return nil, err + } + } + + return wm, setWatermarkType(mode, modeParm, wm) +} + +// ParseTextWatermarkDetails parses a text Watermark/Stamp command string into an internal structure. +func ParseTextWatermarkDetails(text, desc string, onTop bool, u types.DisplayUnit) (*model.Watermark, error) { + return parseWatermarkDetails(model.WMText, text, desc, onTop, u) +} + +// ParseImageWatermarkDetails parses an image Watermark/Stamp command string into an internal structure. +func ParseImageWatermarkDetails(fileName, desc string, onTop bool, u types.DisplayUnit) (*model.Watermark, error) { + return parseWatermarkDetails(model.WMImage, fileName, desc, onTop, u) +} + +// ParsePDFWatermarkDetails parses a PDF Watermark/Stamp command string into an internal structure. +func ParsePDFWatermarkDetails(fileName, desc string, onTop bool, u types.DisplayUnit) (*model.Watermark, error) { + return parseWatermarkDetails(model.WMPDF, fileName, desc, onTop, u) +} + +func onTopString(onTop bool) string { + e := "watermark" + if onTop { + e = "stamp" + } + return e +} + +func parseWatermarkError(onTop bool) error { + s := onTopString(onTop) + return errors.Errorf("Invalid %s configuration string. Please consult pdfcpu help %s.\n", s, s) +} + +func setTextWatermark(s string, wm *model.Watermark) { + wm.TextString = s + if font.IsCoreFont(wm.FontName) { + bb := []byte{} + for _, r := range s { + // Unicode => char code + b := byte(0x20) // better use glyph: .notdef + if r <= 0xff { + b = byte(r) + } + bb = append(bb, b) + } + s = string(bb) + } else { + bb := []byte{} + u := utf16.Encode([]rune(s)) + for _, i := range u { + bb = append(bb, byte((i>>8)&0xFF)) + bb = append(bb, byte(i&0xFF)) + } + s = string(bb) + } + s = strings.ReplaceAll(s, "\\n", "\n") + wm.TextLines = append(wm.TextLines, strings.FieldsFunc(s, func(c rune) bool { return c == 0x0a })...) +} + +func setImageWatermark(s string, wm *model.Watermark) error { + if len(s) == 0 { + // The caller is expected to provide: wm.Image (see api.ImageWatermarkForReader) + return nil + } + if !model.ImageFileName(s) { + return errors.New("imageFileName has to have one of these extensions: .jpg, .jpeg, .png, .tif, .tiff, .webp") + } + wm.FileName = s + f, err := os.Open(wm.FileName) + if err != nil { + return err + } + defer f.Close() + + var buf bytes.Buffer + if _, err := io.Copy(&buf, f); err != nil { + return err + } + + wm.Image = bytes.NewReader(buf.Bytes()) + return nil +} + +func setPDFWatermark(s string, wm *model.Watermark) error { + if len(s) == 0 { + /* + The caller is expected to provide: + wm.PDF and optionally wm.PdfPageNrSrc (see api.PDFWatermarkForReadSeeker) + or + wm.PDF and wm.PdfMultiStartPageNrSrc and wm.PdfMultiStartPageNrDest (see api.PDFMultiWatermarkForReadSeeker) + + Supported usecases: + + pdfcpu stamp add -mode pdf -- "stamp.pdf:m" "" in.pdf out.pdf ... single stamp using page n of source for selected pages of in.pdf + + pdfcpu stamp add -mode pdf -- "stamp.pdf" "" in.pdf out.pdf ... multi stamp starting at the beginning of source and dest + + pdfcpu stamp add -mode pdf -- "stamp.pdf:m:n" "" in.pdf out.pdf ... multi stamp starting at source page m and dest page n + + */ + return nil + } + i := strings.LastIndex(s, ":") + if i < 1 { + // No colon => multi stamp + if strings.ToLower(filepath.Ext(s)) != ".pdf" { + return errors.Errorf("%s is not a PDF file", s) + } + wm.FileName = s + return nil + } + // We have at least one Colon. + if strings.ToLower(filepath.Ext(s)) == ".pdf" { + // We have an absolute DOS filename eg. C:\test.pdf => multi stamp + wm.FileName = s + return nil + } + + pageNumberStr := s[i+1:] + j, err := strconv.Atoi(pageNumberStr) + if err != nil { + return errors.Errorf("unable to detect PDF page number: %s\n", pageNumberStr) + } + + s = s[:i] + i = strings.LastIndex(s, ":") + if i < 1 { + // single stamp + wm.PdfPageNrSrc = j + if strings.ToLower(filepath.Ext(s)) != ".pdf" { + return errors.Errorf("%s is not a PDF file", s) + } + wm.FileName = s + return nil + } + + // multi stamp + + wm.PdfMultiStartPageNrDest = j + pageNumberStr = s[i+1:] + wm.PdfMultiStartPageNrSrc, err = strconv.Atoi(pageNumberStr) + if err != nil { + return errors.Errorf("unable to detect PDF page number: %s\n", pageNumberStr) + } + + s = s[:i] + if strings.ToLower(filepath.Ext(s)) != ".pdf" { + return errors.Errorf("%s is not a PDF file", s) + } + wm.FileName = s + + return nil +} + +func setWatermarkType(mode int, s string, wm *model.Watermark) (err error) { + wm.Mode = mode + switch wm.Mode { + case model.WMText: + setTextWatermark(s, wm) + + case model.WMImage: + err = setImageWatermark(s, wm) + + case model.WMPDF: + err = setPDFWatermark(s, wm) + } + return err +} + +func createPDFRes(ctx, otherCtx *model.Context, pageNrSrc, pageNrDest int, migrated map[int]int, wm *model.Watermark) error { + pdfRes := model.PdfResources{} + xRefTable := ctx.XRefTable + otherXRefTable := otherCtx.XRefTable + + // Locate page dict & resource dict of PDF stamp. + consolidateRes := true + d, _, inhPAttrs, err := otherXRefTable.PageDict(pageNrSrc, consolidateRes) + if err != nil { + return err + } + if d == nil { + return errors.Errorf("pdfcpu: unknown page number: %d\n", pageNrSrc) + } + + // Retrieve content stream bytes of page dict. + pdfRes.Content, err = otherXRefTable.PageContent(d) + if err != nil && err != model.ErrNoContent { + return err + } + + // Migrate external resource dict into ctx. + if _, err = migrateObject(inhPAttrs.Resources, otherCtx, ctx, migrated); err != nil { + return err + } + + // Create an object for resource dict in xRefTable. + if inhPAttrs.Resources != nil { + ir, err := xRefTable.IndRefForNewObject(inhPAttrs.Resources) + if err != nil { + return err + } + pdfRes.ResDict = ir + } + + pdfRes.Bb = viewPort(inhPAttrs) + wm.PdfRes[pageNrDest] = pdfRes + + return nil +} + +func createPDFResForWM(ctx *model.Context, wm *model.Watermark) error { + // Note: The stamp pdf is assumed to be valid! + var ( + otherCtx *model.Context + err error + ) + if wm.PDF != nil { + otherCtx, err = Read(wm.PDF, nil) + } else { + otherCtx, err = ReadFile(wm.FileName, nil) + } + if err != nil { + return err + } + if otherCtx.Version() == model.V20 { + return ErrUnsupportedVersion + } + + if err := otherCtx.EnsurePageCount(); err != nil { + return nil + } + + migrated := map[int]int{} + + if !wm.MultiStamp() { + return createPDFRes(ctx, otherCtx, wm.PdfPageNrSrc, wm.PdfPageNrSrc, migrated, wm) + } + + j := otherCtx.PageCount + if ctx.PageCount < otherCtx.PageCount { + j = ctx.PageCount + } + + destPageNr := wm.PdfMultiStartPageNrDest + for srcPageNr := wm.PdfMultiStartPageNrSrc; srcPageNr <= j; srcPageNr++ { + if err := createPDFRes(ctx, otherCtx, srcPageNr, destPageNr, migrated, wm); err != nil { + return err + } + destPageNr++ + } + + return nil +} + +func createImageResForWM(ctx *model.Context, wm *model.Watermark) (err error) { + wm.Img, wm.Width, wm.Height, err = model.CreateImageResource(ctx.XRefTable, wm.Image, false, false) + return err +} + +func createFontResForWM(ctx *model.Context, wm *model.Watermark) (err error) { + // TODO Reuse font dict. + if font.IsUserFont(wm.FontName) { + td, _ := setupTextDescriptor(*wm, "", 123456789, 0) + model.WriteMultiLine(ctx.XRefTable, new(bytes.Buffer), types.RectForFormat("A4"), nil, td) + } + wm.Font, err = pdffont.EnsureFontDict(ctx.XRefTable, wm.FontName, "", wm.ScriptName, false, nil) + return err +} + +func createResourcesForWM(ctx *model.Context, wm *model.Watermark) error { + if wm.IsPDF() { + return createPDFResForWM(ctx, wm) + } + if wm.IsImage() { + return createImageResForWM(ctx, wm) + } + return createFontResForWM(ctx, wm) +} + +func ensureOCG(ctx *model.Context, onTop bool) (*types.IndirectRef, error) { + name := "Background" + subt := "BG" + if onTop { + name = "Watermark" + subt = "FG" + } + + d := types.Dict( + map[string]types.Object{ + "Name": types.StringLiteral(name), + "Type": types.Name("OCG"), + "Usage": types.Dict( + map[string]types.Object{ + "PageElement": types.Dict(map[string]types.Object{"Subtype": types.Name(subt)}), + "View": types.Dict(map[string]types.Object{"ViewState": types.Name("ON")}), + "Print": types.Dict(map[string]types.Object{"PrintState": types.Name("ON")}), + "Export": types.Dict(map[string]types.Object{"ExportState": types.Name("ON")}), + }, + ), + }, + ) + + return ctx.IndRefForNewObject(d) +} + +func prepareOCPropertiesInRoot(ctx *model.Context, onTop bool) (*types.IndirectRef, error) { + rootDict, err := ctx.Catalog() + if err != nil { + return nil, err + } + + if o, ok := rootDict.Find("OCProperties"); ok { + + d, err := ctx.DereferenceDict(o) + if err != nil { + return nil, err + } + + o, found := d.Find("OCGs") + if found { + a, err := ctx.DereferenceArray(o) + if err != nil { + return nil, errCorruptOCGs + } + if len(a) > 0 { + ir, ok := a[0].(types.IndirectRef) + if !ok { + return nil, errCorruptOCGs + } + return &ir, nil + } + } + } + + ir, err := ensureOCG(ctx, onTop) + if err != nil { + return nil, err + } + + optionalContentConfigDict := types.Dict( + map[string]types.Object{ + "AS": types.Array{ + types.Dict( + map[string]types.Object{ + "Category": types.NewNameArray("View"), + "Event": types.Name("View"), + "OCGs": types.Array{*ir}, + }, + ), + types.Dict( + map[string]types.Object{ + "Category": types.NewNameArray("Print"), + "Event": types.Name("Print"), + "OCGs": types.Array{*ir}, + }, + ), + types.Dict( + map[string]types.Object{ + "Category": types.NewNameArray("Export"), + "Event": types.Name("Export"), + "OCGs": types.Array{*ir}, + }, + ), + }, + "ON": types.Array{*ir}, + "Order": types.Array{}, + "RBGroups": types.Array{}, + }, + ) + + d := types.Dict( + map[string]types.Object{ + "OCGs": types.Array{*ir}, + "D": optionalContentConfigDict, + }, + ) + + rootDict.Update("OCProperties", d) + return ir, nil +} + +func createFormResDict(ctx *model.Context, pageNr int, wm *model.Watermark) (*types.IndirectRef, error) { + if wm.IsPDF() { + i := wm.PdfResIndex(pageNr) + return wm.PdfRes[i].ResDict, nil + } + + if wm.IsImage() { + d := types.Dict( + map[string]types.Object{ + "ProcSet": types.NewNameArray("PDF", "Text", "ImageB", "ImageC", "ImageI"), + "XObject": types.Dict(map[string]types.Object{"Im0": *wm.Img}), + }, + ) + return ctx.IndRefForNewObject(d) + } + + d := types.Dict( + map[string]types.Object{ + "Font": types.Dict(map[string]types.Object{"F1": *wm.Font}), + "ProcSet": types.NewNameArray("PDF", "Text", "ImageB", "ImageC", "ImageI"), + }, + ) + + return ctx.IndRefForNewObject(d) +} + +func cachedForm(wm model.Watermark) bool { + return !wm.IsPDF() || !wm.MultiStamp() +} + +func pdfFormContent(w io.Writer, pageNr int, wm model.Watermark) error { + i := wm.PdfResIndex(pageNr) + cs := wm.PdfRes[i].Content + + sc := wm.Scale + if !wm.ScaleAbs { + sc = wm.Bb.Width() / float64(wm.Width) + } + + // Scale & translate into origin + + m1 := matrix.IdentMatrix + m1[0][0] = sc + m1[1][1] = sc + + m2 := matrix.IdentMatrix + m2[2][0] = -wm.Bb.LL.X * wm.ScaleEff + m2[2][1] = -wm.Bb.LL.Y * wm.ScaleEff + + m := m1.Multiply(m2) + + fmt.Fprintf(w, "%.5f %.5f %.5f %.5f %.5f %.5f cm ", m[0][0], m[0][1], m[1][0], m[1][1], m[2][0], m[2][1]) + + _, err := w.Write(cs) + return err +} + +func imageFormContent(w io.Writer, wm model.Watermark) { + fmt.Fprintf(w, "q %f 0 0 %f 0 0 cm /Im0 Do Q", wm.Bb.Width(), wm.Bb.Height()) // TODO dont need Q +} + +func formContent(w io.Writer, pageNr int, wm model.Watermark) error { + switch true { + case wm.IsPDF(): + return pdfFormContent(w, pageNr, wm) + case wm.IsImage(): + imageFormContent(w, wm) + } + return nil +} + +func setupTextDescriptor(wm model.Watermark, timestampFormat string, pageNr, pageCount int) (model.TextDescriptor, bool) { + // Set horizontal alignment. + var hAlign types.HAlignment + if wm.HAlign == nil { + // Use alignment implied by anchor. + _, _, hAlign, _ = model.AnchorPosAndAlign(wm.Pos, types.RectForDim(0, 0)) + } else { + // Use manual alignment. + hAlign = *wm.HAlign + } + + // Set effective position and vertical alignment. + x, y, _, vAlign := model.AnchorPosAndAlign(types.BottomLeft, wm.Vp) + td, unique := textDescriptor(wm, timestampFormat, pageNr, pageCount) + td.X, td.Y, td.HAlign, td.VAlign, td.FontKey = x, y, hAlign, vAlign, "F1" + + // Set right to left rendering. + td.RTL = wm.RTL + + td.Embed = wm.ScriptName == "" + + // Set margins. + td.MLeft = wm.MLeft + td.MRight = wm.MRight + td.MTop = wm.MTop + td.MBot = wm.MBot + + // Set border. + td.BorderWidth = wm.BorderWidth + td.BorderStyle = wm.BorderStyle + if wm.BorderColor != nil { + td.ShowBorder = true + td.BorderCol = *wm.BorderColor + } + return td, unique +} + +func drawBoundingBox(b *bytes.Buffer, wm model.Watermark, bb *types.Rectangle) { + urx := bb.UR.X + ury := bb.UR.Y + if wm.IsPDF() { + sc := wm.Scale + if !wm.ScaleAbs { + sc = bb.Width() / float64(wm.Width) + } + urx /= sc + ury /= sc + } + fmt.Fprintf(b, "[]0 d 2 w %.2f %.2f m %.2f %.2f l %.2f %.2f l %.2f %.2f l s ", + bb.LL.X, bb.LL.Y, + urx, bb.LL.Y, + urx, ury, + bb.LL.X, ury, + ) +} + +func calcFormBoundingBox(xRefTable *model.XRefTable, w io.Writer, timestampFormat string, pageNr, pageCount int, wm *model.Watermark) bool { + var unique bool + if wm.IsImage() || wm.IsPDF() { + wm.CalcBoundingBox(pageNr) + } else { + var td model.TextDescriptor + td, unique = setupTextDescriptor(*wm, timestampFormat, pageNr, pageCount) + // Render td into b and return the bounding box. + wm.Bb = model.WriteMultiLine(xRefTable, w, types.RectForDim(wm.Vp.Width(), wm.Vp.Height()), nil, td) + } + return unique +} + +func createForm(ctx *model.Context, pageNr, pageCount int, wm *model.Watermark, withBB bool) error { + var b bytes.Buffer + unique := calcFormBoundingBox(ctx.XRefTable, &b, ctx.Configuration.TimestampFormat, pageNr, pageCount, wm) + + // The forms bounding box is dependent on the page dimensions. + bb := wm.Bb + + maxStampPageNr := wm.PdfMultiStartPageNrDest + len(wm.PdfRes) - 1 + + if !unique && (cachedForm(*wm) || pageNr > maxStampPageNr) { + // Use cached form. + ir, ok := wm.FCache[*bb] + if ok { + wm.Form = ir + return nil + } + } + + if wm.IsImage() || wm.IsPDF() { + if err := formContent(&b, pageNr, *wm); err != nil { + return err + } + } + + ir, err := createFormResDict(ctx, pageNr, wm) + if err != nil { + return err + } + + bbox := bb.CroppedCopy(0) + bbox.Translate(-bb.LL.X, -bb.LL.Y) + + // Paint bounding box + if withBB { + drawBoundingBox(&b, *wm, bbox) + } + + sd := types.StreamDict{ + Dict: types.Dict( + map[string]types.Object{ + "Type": types.Name("XObject"), + "Subtype": types.Name("Form"), + "BBox": bbox.Array(), + "Matrix": types.NewNumberArray(1, 0, 0, 1, 0, 0), + "OC": *wm.Ocg, + }, + ), + Content: b.Bytes(), + FilterPipeline: []types.PDFFilter{{Name: filter.Flate, DecodeParms: nil}}, + } + + if ir != nil { + sd.Insert("Resources", *ir) + } + + sd.InsertName("Filter", filter.Flate) + + if err = sd.Encode(); err != nil { + return err + } + + ir, err = ctx.IndRefForNewObject(sd) + if err != nil { + return err + } + + wm.Form = ir + + if cachedForm(*wm) || pageNr >= len(wm.PdfRes) { + // Cache form. + wm.FCache[*wm.Bb] = ir + } + + return nil +} + +func createExtGStateForStamp(ctx *model.Context, opacity float64) (*types.IndirectRef, error) { + d := types.Dict( + map[string]types.Object{ + "Type": types.Name("ExtGState"), + "CA": types.Float(opacity), + "ca": types.Float(opacity), + }, + ) + + return ctx.IndRefForNewObject(d) +} + +func insertPageResourcesForWM(ctx *model.Context, pageDict types.Dict, wm model.Watermark, gsID, xoID string) error { + resourceDict := types.Dict( + map[string]types.Object{ + "ExtGState": types.Dict(map[string]types.Object{gsID: *wm.ExtGState}), + "XObject": types.Dict(map[string]types.Object{xoID: *wm.Form}), + }, + ) + + pageDict.Insert("Resources", resourceDict) + + return nil +} + +func updatePageResourcesForWM(ctx *model.Context, resDict types.Dict, wm model.Watermark, gsID, xoID *string) error { + o, ok := resDict.Find("ExtGState") + if !ok { + resDict.Insert("ExtGState", types.Dict(map[string]types.Object{*gsID: *wm.ExtGState})) + } else { + d, _ := ctx.DereferenceDict(o) + for i := 0; i < 10000000; i++ { + *gsID = "GS" + strconv.Itoa(i) + if _, found := d.Find(*gsID); !found { + break + } + } + d.Insert(*gsID, *wm.ExtGState) + } + + o, ok = resDict.Find("XObject") + if !ok { + resDict.Insert("XObject", types.Dict(map[string]types.Object{*xoID: *wm.Form})) + } else { + d, _ := ctx.DereferenceDict(o) + for i := 0; i < 10000000; i++ { + *xoID = "Fm" + strconv.Itoa(i) + if _, found := d.Find(*xoID); !found { + break + } + } + d.Insert(*xoID, *wm.Form) + } + + return nil +} + +func wmContent(wm *model.Watermark, gsID, xoID string) []byte { + m := wm.CalcTransformMatrix() + p1 := m.Transform(types.Point{X: wm.Bb.LL.X, Y: wm.Bb.LL.Y}) + p2 := m.Transform(types.Point{X: wm.Bb.UR.X, Y: wm.Bb.LL.Y}) + p3 := m.Transform(types.Point{X: wm.Bb.UR.X, Y: wm.Bb.UR.Y}) + p4 := m.Transform(types.Point{X: wm.Bb.LL.X, Y: wm.Bb.UR.Y}) + wm.BbTrans = types.QuadLiteral{P1: p1, P2: p2, P3: p3, P4: p4} + insertOCG := " /Artifact <>BDC q %.5f %.5f %.5f %.5f %.5f %.5f cm /%s gs /%s Do Q EMC " + var b bytes.Buffer + fmt.Fprintf(&b, insertOCG, m[0][0], m[0][1], m[1][0], m[1][1], m[2][0], m[2][1], gsID, xoID) + return b.Bytes() +} + +func insertPageContentsForWM(ctx *model.Context, pageDict types.Dict, wm *model.Watermark, gsID, xoID string) error { + sd, _ := ctx.NewStreamDictForBuf(wmContent(wm, gsID, xoID)) + if err := sd.Encode(); err != nil { + return err + } + + ir, err := ctx.IndRefForNewObject(*sd) + if err != nil { + return err + } + + pageDict.Insert("Contents", *ir) + + return nil +} + +func patchFirstContentStreamForWatermark(sd *types.StreamDict, gsID, xoID string, wm *model.Watermark, isLast bool) error { + err := sd.Decode() + if err == filter.ErrUnsupportedFilter { + if log.InfoEnabled() { + log.Info.Println("unsupported filter: unable to patch content with watermark.") + } + return nil + } + if err != nil { + return err + } + + wmbb := wmContent(wm, gsID, xoID) + + // stamp + if wm.OnTop { + bb := []byte(" q ") + if wm.PageRot != 0 { + bb = append(bb, model.ContentBytesForPageRotation(wm.PageRot, wm.Vp.Width(), wm.Vp.Height())...) + } + sd.Content = append(bb, sd.Content...) + if !isLast { + return sd.Encode() + } + sd.Content = append(sd.Content, []byte(" Q ")...) + sd.Content = append(sd.Content, wmbb...) + return sd.Encode() + } + + // watermark + if wm.PageRot == 0 { + sd.Content = append(wmbb, sd.Content...) + return sd.Encode() + } + + bb := append([]byte(" q "), model.ContentBytesForPageRotation(wm.PageRot, wm.Vp.Width(), wm.Vp.Height())...) + sd.Content = append(bb, sd.Content...) + if isLast { + sd.Content = append(sd.Content, []byte(" Q")...) + } + return sd.Encode() +} + +func patchLastContentStreamForWatermark(sd *types.StreamDict, gsID, xoID string, wm *model.Watermark) error { + err := sd.Decode() + if err == filter.ErrUnsupportedFilter { + if log.InfoEnabled() { + log.Info.Println("unsupported filter: unable to patch content with watermark.") + } + return nil + } + if err != nil { + return err + } + + // stamp + if wm.OnTop { + sd.Content = append(sd.Content, []byte(" Q ")...) + sd.Content = append(sd.Content, wmContent(wm, gsID, xoID)...) + return sd.Encode() + } + + // watermark + if wm.PageRot != 0 { + sd.Content = append(sd.Content, []byte(" Q")...) + return sd.Encode() + } + + return nil +} + +func updatePageContentsForWM(ctx *model.Context, obj types.Object, wm *model.Watermark, gsID, xoID string) error { + var entry *model.XRefTableEntry + var objNr int + + ir, ok := obj.(types.IndirectRef) + if ok { + objNr = ir.ObjectNumber.Value() + if wm.Objs[objNr] { + // wm already applied to this content stream. + return nil + } + genNr := ir.GenerationNumber.Value() + entry, _ = ctx.FindTableEntry(objNr, genNr) + obj = entry.Object + } + + switch o := obj.(type) { + + case types.StreamDict: + + err := patchFirstContentStreamForWatermark(&o, gsID, xoID, wm, true) + if err != nil { + return err + } + + entry.Object = o + wm.Objs[objNr] = true + + case types.Array: + // Get stream dict for first array element. + if len(o) == 0 { + return nil + } + o1 := o[0] + ir, _ := o1.(types.IndirectRef) + objNr = ir.ObjectNumber.Value() + genNr := ir.GenerationNumber.Value() + entry, _ := ctx.FindTableEntry(objNr, genNr) + sd, _ := (entry.Object).(types.StreamDict) + + if wm.Objs[objNr] { + // wm already applied to this content stream. + return nil + } + + err := patchFirstContentStreamForWatermark(&sd, gsID, xoID, wm, len(o) == 1) + if err != nil { + return err + } + + entry.Object = sd + wm.Objs[objNr] = true + if len(o) == 1 { + return nil + } + + // Get stream dict for last array element. + o1 = o[len(o)-1] + + ir, _ = o1.(types.IndirectRef) + objNr = ir.ObjectNumber.Value() + if wm.Objs[objNr] { + // wm already applied to this content stream. + return nil + } + + genNr = ir.GenerationNumber.Value() + entry, _ = ctx.FindTableEntry(objNr, genNr) + sd, _ = (entry.Object).(types.StreamDict) + + err = patchLastContentStreamForWatermark(&sd, gsID, xoID, wm) + if err != nil { + return err + } + + entry.Object = sd + wm.Objs[objNr] = true + } + + return nil +} + +func viewPort(a *model.InheritedPageAttrs) *types.Rectangle { + visibleRegion := a.MediaBox + if a.CropBox != nil { + visibleRegion = a.CropBox + } + return visibleRegion +} + +func handleLink(ctx *model.Context, pageIndRef *types.IndirectRef, d types.Dict, pageNr int, wm model.Watermark) error { + if !wm.OnTop || wm.URL == "" { + return nil + } + + ann := model.NewLinkAnnotation( + *wm.BbTrans.EnclosingRectangle(5.0), // rect + "", // contents + "pdfcpu", // id + "", // modDate + model.AnnNoZoom+model.AnnNoRotate, // f + &color.Red, // borderCol + nil, // dest + wm.URL, // uri + types.QuadPoints{wm.BbTrans}, // quad + false, // border + 0, // borderWidth + model.BSSolid, // borderStyle + ) + + _, _, err := AddAnnotation(ctx, pageIndRef, d, pageNr, ann, false) + + return err +} + +func addPageWatermark(ctx *model.Context, pageNr int, wm model.Watermark) error { + if pageNr > ctx.PageCount { + return errors.Errorf("pdfcpu: invalid page number: %d", pageNr) + } + + if log.DebugEnabled() { + log.Debug.Printf("addPageWatermark page:%d\n", pageNr) + } + + if wm.Update { + if log.DebugEnabled() { + log.Debug.Println("Updating") + } + if _, err := removePageWatermark(ctx, pageNr); err != nil { + return err + } + } + + consolidateRes := false + d, pageIndRef, inhPAttrs, err := ctx.PageDict(pageNr, consolidateRes) + if err != nil { + return err + } + + // Internalize page rotation into content stream. + wm.PageRot = inhPAttrs.Rotate + + wm.Vp = viewPort(inhPAttrs) + + // Reset page rotation in page dict. + if wm.PageRot != 0 { + if types.IntMemberOf(wm.PageRot, []int{+90, -90, +270, -270}) { + w := wm.Vp.Width() + wm.Vp.UR.X = wm.Vp.LL.X + wm.Vp.Height() + wm.Vp.UR.Y = wm.Vp.LL.Y + w + } + d.Update("MediaBox", wm.Vp.Array()) + d.Update("CropBox", wm.Vp.Array()) + d.Delete("Rotate") + } + + if err = createForm(ctx, pageNr, ctx.PageCount, &wm, stampWithBBox); err != nil { + return err + } + + if log.DebugEnabled() { + log.Debug.Printf("\n%s\n", wm) + } + + gsID := "GS0" + xoID := "Fm0" + + if inhPAttrs.Resources != nil { + err = updatePageResourcesForWM(ctx, inhPAttrs.Resources, wm, &gsID, &xoID) + d.Update("Resources", inhPAttrs.Resources) + } else { + err = insertPageResourcesForWM(ctx, d, wm, gsID, xoID) + } + if err != nil { + return err + } + + obj, found := d.Find("Contents") + if found { + err = updatePageContentsForWM(ctx, obj, &wm, gsID, xoID) + } else { + err = insertPageContentsForWM(ctx, d, &wm, gsID, xoID) + } + if err != nil { + return err + } + + return handleLink(ctx, pageIndRef, d, pageNr, wm) +} + +func createResourcesForPageNr( + ctx *model.Context, + wm *model.Watermark, + pageNr int, + fm map[string]types.IntSet, + ocgIndRef, extGStateIndRef *types.IndirectRef, + onTop bool, opacity float64) error { + + wm.Ocg = ocgIndRef + wm.ExtGState = extGStateIndRef + wm.OnTop = onTop + wm.Opacity = opacity + + if wm.IsImage() { + return createImageResForWM(ctx, wm) + } + + if wm.IsPDF() { + return createPDFResForWM(ctx, wm) + } + + // Text watermark + + if font.IsUserFont(wm.FontName) { + td, _ := setupTextDescriptor(*wm, "", 123456789, 0) + model.WriteMultiLine(ctx.XRefTable, new(bytes.Buffer), types.RectForFormat("A4"), nil, td) + } + + pageSet, found := fm[wm.FontName] + if !found { + fm[wm.FontName] = types.IntSet{pageNr: true} + } else { + pageSet[pageNr] = true + } + + return nil +} + +func createResourcesForWMMap( + ctx *model.Context, + m map[int]*model.Watermark, + ocgIndRef, extGStateIndRef *types.IndirectRef, + onTop bool, + opacity float64) (map[string]types.IntSet, error) { + + fm := map[string]types.IntSet{} + for pageNr, wm := range m { + if err := createResourcesForPageNr(ctx, wm, pageNr, fm, ocgIndRef, extGStateIndRef, onTop, opacity); err != nil { + return nil, err + } + } + + return fm, nil +} + +func createResourcesForWMSliceMap( + ctx *model.Context, + m map[int][]*model.Watermark, + ocgIndRef, extGStateIndRef *types.IndirectRef, + onTop bool, + opacity float64) (map[string]types.IntSet, error) { + + fm := map[string]types.IntSet{} + for pageNr, wms := range m { + for _, wm := range wms { + if err := createResourcesForPageNr(ctx, wm, pageNr, fm, ocgIndRef, extGStateIndRef, onTop, opacity); err != nil { + return nil, err + } + } + } + + return fm, nil +} + +// AddWatermarksMap adds watermarks in m to corresponding pages. +func AddWatermarksMap(ctx *model.Context, m map[int]*model.Watermark) error { + var ( + onTop bool + opacity float64 + ) + for _, wm := range m { + onTop = wm.OnTop + opacity = wm.Opacity + break + } + + ocgIndRef, err := prepareOCPropertiesInRoot(ctx, onTop) + if err != nil { + return err + } + + extGStateIndRef, err := createExtGStateForStamp(ctx, opacity) + if err != nil { + return err + } + + fm, err := createResourcesForWMMap(ctx, m, ocgIndRef, extGStateIndRef, onTop, opacity) + if err != nil { + return err + } + + // TODO Reuse font dict. + for fontName, pageSet := range fm { + ir, err := pdffont.EnsureFontDict(ctx.XRefTable, fontName, "", "", false, nil) + if err != nil { + return err + } + for pageNr, v := range pageSet { + if !v { + continue + } + wm := m[pageNr] + if wm.IsText() && wm.FontName == fontName { + m[pageNr].Font = ir + } + } + } + + for k, wm := range m { + if err := addPageWatermark(ctx, k, *wm); err != nil { + return err + } + } + + ctx.EnsureVersionForWriting() + return nil +} + +// AddWatermarksSliceMap adds watermarks in m to corresponding pages. +func AddWatermarksSliceMap(ctx *model.Context, m map[int][]*model.Watermark) error { + var ( + onTop bool + opacity float64 + ) + for _, wms := range m { + onTop = wms[0].OnTop + opacity = wms[0].Opacity + break + } + + ocgIndRef, err := prepareOCPropertiesInRoot(ctx, onTop) + if err != nil { + return err + } + + extGStateIndRef, err := createExtGStateForStamp(ctx, opacity) + if err != nil { + return err + } + + fm, err := createResourcesForWMSliceMap(ctx, m, ocgIndRef, extGStateIndRef, onTop, opacity) + if err != nil { + return err + } + + // TODO Take existing font dicts in xref into account. + for fontName, pageSet := range fm { + ir, err := pdffont.EnsureFontDict(ctx.XRefTable, fontName, "", "", false, nil) + if err != nil { + return err + } + for pageNr, v := range pageSet { + if !v { + continue + } + for _, wm := range m[pageNr] { + if wm.IsText() && wm.FontName == fontName { + wm.Font = ir + } + } + } + } + + for k, wms := range m { + for _, wm := range wms { + if err := addPageWatermark(ctx, k, *wm); err != nil { + return err + } + } + } + + ctx.EnsureVersionForWriting() + return nil +} + +// AddWatermarks adds watermarks to all pages selected. +func AddWatermarks(ctx *model.Context, selectedPages types.IntSet, wm *model.Watermark) error { + if log.DebugEnabled() { + log.Debug.Printf("AddWatermarks wm:\n%s\n", wm) + } + var err error + if wm.Ocg, err = prepareOCPropertiesInRoot(ctx, wm.OnTop); err != nil { + return err + } + + if err = createResourcesForWM(ctx, wm); err != nil { + return err + } + + if wm.ExtGState, err = createExtGStateForStamp(ctx, wm.Opacity); err != nil { + return err + } + + for i := wm.PdfMultiStartPageNrDest; i <= ctx.PageCount; i++ { + if len(selectedPages) == 0 || selectedPages[i] { + if err = addPageWatermark(ctx, i, *wm); err != nil { + return err + } + } + } + + ctx.EnsureVersionForWriting() + return nil +} + +func removeResDictEntry(ctx *model.Context, d types.Dict, entry string, ids []string, i int) error { + o, ok := d.Find(entry) + if !ok { + return errors.Errorf("pdfcpu: page %d: corrupt resource dict", i) + } + + d1, err := ctx.DereferenceDict(o) + if err != nil { + return err + } + + for _, id := range ids { + o, ok := d1.Find(id) + if ok { + err = ctx.DeleteObject(o) + if err != nil { + return err + } + d1.Delete(id) + } + } + + if d1.Len() == 0 { + d.Delete(entry) + } + + return nil +} + +func removeExtGStates(ctx *model.Context, d types.Dict, ids []string, i int) error { + return removeResDictEntry(ctx, d, "ExtGState", ids, i) +} + +func removeForms(ctx *model.Context, d types.Dict, ids []string, i int) error { + return removeResDictEntry(ctx, d, "XObject", ids, i) +} + +func removeArtifacts(sd *types.StreamDict, i int) (ok bool, extGStates []string, forms []string, err error) { + err = sd.Decode() + if err == filter.ErrUnsupportedFilter { + if log.InfoEnabled() { + log.Info.Printf("unsupported filter: unable to patch content with watermark for page %d\n", i) + } + return false, nil, nil, nil + } + if err != nil { + return false, nil, nil, err + } + + var patched bool + + // Watermarks may begin or end the content stream. + + for { + s := string(sd.Content) + beg := strings.Index(s, "/Artifact <>BDC") + if beg < 0 { + break + } + + end := strings.Index(s[beg:], "EMC") + if end < 0 { + break + } + + // Check for usage of resources. + t := s[beg : beg+end] + + i := strings.Index(t, "/GS") + if i > 0 { + j := i + 3 + k := strings.Index(t[j:], " gs") + if k > 0 { + extGStates = append(extGStates, "GS"+t[j:j+k]) + } + } + + i = strings.Index(t, "/Fm") + if i > 0 { + j := i + 3 + k := strings.Index(t[j:], " Do") + if k > 0 { + forms = append(forms, "Fm"+t[j:j+k]) + } + } + + // TODO Remove whitespace until 0x0a + sd.Content = append(sd.Content[:beg], sd.Content[beg+end+3:]...) + patched = true + } + + if patched { + err = sd.Encode() + } + + return patched, extGStates, forms, err +} + +func removeArtifactsFromPage(ctx *model.Context, sd *types.StreamDict, resDict types.Dict, i int) (bool, error) { + // Remove watermark artifacts and locate id's + // of used extGStates and forms. + ok, extGStates, forms, err := removeArtifacts(sd, i) + if err != nil { + return false, err + } + if !ok { + return false, nil + } + + // Remove obsolete extGStates from page resource dict. + err = removeExtGStates(ctx, resDict, extGStates, i) + if err != nil { + return false, err + } + + // Remove obsolete forms from page resource dict. + return true, removeForms(ctx, resDict, forms, i) +} + +func locatePageContentAndResourceDict(ctx *model.Context, pageNr int) (types.Object, *types.IndirectRef, types.Dict, error) { + consolidateRes := false + d, pageDictIndRef, _, err := ctx.PageDict(pageNr, consolidateRes) + if err != nil { + return nil, nil, nil, err + } + + o, found := d.Find("Resources") + if !found { + return nil, nil, nil, errors.Errorf("pdfcpu: page %d: no resource dict found\n", pageNr) + } + + resDict, err := ctx.DereferenceDict(o) + if err != nil { + return nil, nil, nil, err + } + + o, found = d.Find("Contents") + if !found { + return nil, nil, nil, errors.Errorf("pdfcpu: page %d: no page watermark found", pageNr) + } + + return o, pageDictIndRef, resDict, nil +} + +func removeArtifacts1(ctx *model.Context, o types.Object, entry *model.XRefTableEntry, resDict types.Dict, pageNr int) (bool, error) { + found := false + switch o := o.(type) { + + case types.StreamDict: + ok, err := removeArtifactsFromPage(ctx, &o, resDict, pageNr) + if err != nil { + return false, err + } + if !found && ok { + found = true + } + entry.Object = o + + case types.Array: + // Get stream dict for first element. + o1 := o[0] + ir, _ := o1.(types.IndirectRef) + objNr := ir.ObjectNumber.Value() + genNr := ir.GenerationNumber.Value() + entry, _ := ctx.FindTableEntry(objNr, genNr) + sd, _ := (entry.Object).(types.StreamDict) + + ok, err := removeArtifactsFromPage(ctx, &sd, resDict, pageNr) + if err != nil { + return false, err + } + if !found && ok { + found = true + entry.Object = sd + } + + if len(o) > 1 { + // Get stream dict for last element. + if len(o) == 0 { + return false, nil + } + o1 := o[len(o)-1] + ir, _ := o1.(types.IndirectRef) + objNr = ir.ObjectNumber.Value() + genNr := ir.GenerationNumber.Value() + entry, _ := ctx.FindTableEntry(objNr, genNr) + sd, _ := (entry.Object).(types.StreamDict) + + ok, err = removeArtifactsFromPage(ctx, &sd, resDict, pageNr) + if err != nil { + return false, err + } + if !found && ok { + found = true + entry.Object = sd + } + } + + } + return found, nil +} + +func removePageWatermark(ctx *model.Context, pageNr int) (bool, error) { + o, pageDictIndRef, resDict, err := locatePageContentAndResourceDict(ctx, pageNr) + if err != nil { + return false, err + } + + var entry *model.XRefTableEntry + + ir, ok := o.(types.IndirectRef) + if ok { + objNr := ir.ObjectNumber.Value() + genNr := ir.GenerationNumber.Value() + entry, _ = ctx.FindTableEntry(objNr, genNr) + o = entry.Object + } + + found, err := removeArtifacts1(ctx, o, entry, resDict, pageNr) + if err != nil { + return false, err + } + + /* + Supposedly the form needs a PieceInfo in order to be recognized by Acrobat like so: + + + + + >>> + >>> + + */ + + if found { + // Remove any associated link annotations. + d, err := ctx.DereferenceDict(*pageDictIndRef) + if err != nil { + return false, err + } + objNr := pageDictIndRef.ObjectNumber.Value() + if _, err = RemoveAnnotationsFromPageDict(ctx, nil, []string{"pdfcpu"}, nil, d, objNr, pageNr, false); err != nil { + return false, err + } + } + + return found, nil +} + +func locateOCGs(ctx *model.Context) (types.Array, error) { + rootDict, err := ctx.Catalog() + if err != nil { + return nil, err + } + + o, ok := rootDict.Find("OCProperties") + if !ok { + return nil, errNoWatermark + } + + d, err := ctx.DereferenceDict(o) + if err != nil { + return nil, err + } + + o, found := d.Find("OCGs") + if !found { + return nil, errNoWatermark + } + + return ctx.DereferenceArray(o) +} + +func detectStampOCG(ctx *model.Context, arr types.Array) error { + for _, o := range arr { + + d, err := ctx.DereferenceDict(o) + if err != nil { + return err + } + + if o == nil { + continue + } + + if *d.Type() != "OCG" { + continue + } + + n := d.StringEntry("Name") + if n == nil { + continue + } + + if *n != "Background" && *n != "Watermark" { + continue + } + + return nil + } + + return errNoWatermark +} + +func removePageWatermarks(ctx *model.Context, selectedPages types.IntSet) error { + var removed bool + + for k, v := range selectedPages { + + if !v { + continue + } + + ok, err := removePageWatermark(ctx, k) + if err != nil { + return err + } + + if ok { + removed = true + } + } + + if !removed { + return errNoWatermark + } + + return nil +} + +// RemoveWatermarks removes watermarks for all pages selected. +func RemoveWatermarks(ctx *model.Context, selectedPages types.IntSet) error { + if log.DebugEnabled() { + log.Debug.Printf("RemoveWatermarks\n") + } + + arr, err := locateOCGs(ctx) + if err != nil { + return err + } + + if err := detectStampOCG(ctx, arr); err != nil { + return err + } + + return removePageWatermarks(ctx, selectedPages) +} + +func detectArtifacts(sd *types.StreamDict) (bool, error) { + if err := sd.Decode(); err != nil { + return false, err + } + // Watermarks may begin or end the content stream. + i := strings.Index(string(sd.Content), "/Artifact <>BDC") + return i >= 0, nil +} + +func findPageWatermarks(ctx *model.Context, pageDictIndRef *types.IndirectRef) (bool, error) { + d, err := ctx.DereferenceDict(*pageDictIndRef) + if err != nil { + return false, err + } + + o, found := d.Find("Contents") + if !found { + return false, model.ErrNoContent + } + + var entry *model.XRefTableEntry + + ir, ok := o.(types.IndirectRef) + if ok { + objNr := ir.ObjectNumber.Value() + genNr := ir.GenerationNumber.Value() + entry, _ = ctx.FindTableEntry(objNr, genNr) + o = entry.Object + } + + switch o := o.(type) { + + case types.StreamDict: + return detectArtifacts(&o) + + case types.Array: + // Get stream dict for first element. + if len(o) == 0 { + return false, nil + } + o1 := o[0] + ir, _ := o1.(types.IndirectRef) + objNr := ir.ObjectNumber.Value() + genNr := ir.GenerationNumber.Value() + entry, _ := ctx.FindTableEntry(objNr, genNr) + sd, _ := (entry.Object).(types.StreamDict) + ok, err := detectArtifacts(&sd) + if err != nil { + return false, err + } + if ok { + return true, nil + } + + if len(o) > 1 { + // Get stream dict for last element. + o1 := o[len(o)-1] + ir, _ := o1.(types.IndirectRef) + objNr = ir.ObjectNumber.Value() + genNr := ir.GenerationNumber.Value() + entry, _ := ctx.FindTableEntry(objNr, genNr) + sd, _ := (entry.Object).(types.StreamDict) + return detectArtifacts(&sd) + } + + } + + return false, nil +} + +func detectPageTreeWatermarks(ctx *model.Context, root *types.IndirectRef) error { + d, err := ctx.DereferenceDict(*root) + if err != nil { + return err + } + + kids := d.ArrayEntry("Kids") + if kids == nil { + return nil + } + + for _, o := range kids { + + if ctx.Watermarked { + return nil + } + + if o == nil { + continue + } + + // Dereference next page node dict. + ir, ok := o.(types.IndirectRef) + if !ok { + return errors.Errorf("pdfcpu: detectPageTreeWatermarks: corrupt page node dict") + } + + pageNodeDict, err := ctx.DereferenceDict(ir) + if err != nil { + return err + } + + switch *pageNodeDict.Type() { + + case "Pages": + // Recurse over sub pagetree. + if err := detectPageTreeWatermarks(ctx, &ir); err != nil { + return err + } + + case "Page": + found, err := findPageWatermarks(ctx, &ir) + if err != nil { + return err + } + if found { + ctx.Watermarked = true + return nil + } + + } + } + + return nil +} + +// DetectPageTreeWatermarks checks xRefTable's page tree for watermarks +// and records the result to xRefTable.Watermarked. +func DetectPageTreeWatermarks(ctx *model.Context) error { + root, err := ctx.Pages() + if err != nil { + return err + } + return detectPageTreeWatermarks(ctx, root) +} + +// DetectWatermarks checks ctx for watermarks +// and records the result to xRefTable.Watermarked. +func DetectWatermarks(ctx *model.Context) error { + a, err := locateOCGs(ctx) + if err != nil { + if err == errNoWatermark { + ctx.Watermarked = false + return nil + } + return err + } + + found := false + + for _, o := range a { + d, err := ctx.DereferenceDict(o) + if err != nil { + return err + } + + if o == nil { + continue + } + + if *d.Type() != "OCG" { + continue + } + + n := d.StringEntry("Name") + if n == nil { + continue + } + + if *n != "Background" && *n != "Watermark" { + continue + } + + found = true + break + } + + if !found { + ctx.Watermarked = false + return nil + } + + return DetectPageTreeWatermarks(ctx) +} diff --git a/pkg/pdfcpu/types/anchor.go b/pkg/pdfcpu/types/anchor.go new file mode 100644 index 0000000000000000000000000000000000000000..24325b595811f103f2be5a7bf55798e792b22537 --- /dev/null +++ b/pkg/pdfcpu/types/anchor.go @@ -0,0 +1,73 @@ +/* +Copyright 2022 The pdfcpu Authors. + +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 + + 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 types + +// Anchor represents symbolic positions within a rectangular region. +type Anchor int + +func (a Anchor) String() string { + + switch a { + + case TopLeft: + return "top left" + + case TopCenter: + return "top center" + + case TopRight: + return "top right" + + case Left: + return "left" + + case Center: + return "center" + + case Right: + return "right" + + case BottomLeft: + return "bottom left" + + case BottomCenter: + return "bottom center" + + case BottomRight: + return "bottom right" + + case Full: + return "full" + + } + + return "" +} + +// These are the defined anchors for relative positioning. +const ( + TopLeft Anchor = iota + TopCenter + TopRight + Left + Center // default + Right + BottomLeft + BottomCenter + BottomRight + Full // special case, no anchor needed, imageSize = pageSize +) diff --git a/pkg/pdfcpu/types/array.go b/pkg/pdfcpu/types/array.go new file mode 100644 index 0000000000000000000000000000000000000000..c5a265aafa865fa36562f7dfc6e7f6a6b216ee38 --- /dev/null +++ b/pkg/pdfcpu/types/array.go @@ -0,0 +1,196 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 types + +import ( + "fmt" + + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/log" +) + +// Array represents a PDF array object. +type Array []Object + +// NewStringLiteralArray returns a PDFArray with StringLiteral entries. +func NewStringLiteralArray(sVars ...string) Array { + + a := Array{} + + for _, s := range sVars { + a = append(a, StringLiteral(s)) + } + + return a +} + +// NewHexLiteralArray returns a PDFArray with HexLiteralLiteral entries. +func NewHexLiteralArray(sVars ...string) Array { + + a := Array{} + + for _, s := range sVars { + a = append(a, NewHexLiteral([]byte(s))) + } + + return a +} + +// NewNameArray returns a PDFArray with Name entries. +func NewNameArray(sVars ...string) Array { + + a := Array{} + + for _, s := range sVars { + a = append(a, Name(s)) + } + + return a +} + +// NewNumberArray returns a PDFArray with Float entries. +func NewNumberArray(fVars ...float64) Array { + + a := Array{} + + for _, f := range fVars { + a = append(a, Float(f)) + } + + return a +} + +// NewIntegerArray returns a PDFArray with Integer entries. +func NewIntegerArray(fVars ...int) Array { + + a := Array{} + + for _, f := range fVars { + a = append(a, Integer(f)) + } + + return a +} + +// Clone returns a clone of a. +func (a Array) Clone() Object { + a1 := Array(make([]Object, len(a))) + for k, v := range a { + if v != nil { + v = v.Clone() + } + a1[k] = v + } + return a1 +} + +func (a Array) indentedString(level int) string { + + logstr := []string{"["} + tabstr := strings.Repeat("\t", level) + first := true + sepstr := "" + + for _, entry := range a { + + if first { + first = false + sepstr = "" + } else { + sepstr = " " + } + + switch entry := entry.(type) { + case Dict: + dictstr := entry.indentedString(level + 1) + logstr = append(logstr, fmt.Sprintf("\n%[1]s%[2]s\n%[1]s", tabstr, dictstr)) + first = true + case Array: + arrstr := entry.indentedString(level + 1) + logstr = append(logstr, fmt.Sprintf("%s%s", sepstr, arrstr)) + default: + v := "null" + if entry != nil { + v = entry.String() + if n, ok := entry.(Name); ok { + v, _ = DecodeName(string(n)) + } + } + + logstr = append(logstr, fmt.Sprintf("%s%v", sepstr, v)) + } + } + + logstr = append(logstr, "]") + + return strings.Join(logstr, "") +} + +func (a Array) String() string { + return a.indentedString(1) +} + +// PDFString returns a string representation as found in and written to a PDF file. +func (a Array) PDFString() string { + + logstr := []string{} + logstr = append(logstr, "[") + first := true + var sepstr string + + for _, entry := range a { + + if first { + first = false + sepstr = "" + } else { + sepstr = " " + } + + switch entry := entry.(type) { + case nil: + logstr = append(logstr, fmt.Sprintf("%snull", sepstr)) + case Dict: + logstr = append(logstr, entry.PDFString()) + case Array: + logstr = append(logstr, entry.PDFString()) + case IndirectRef: + logstr = append(logstr, fmt.Sprintf("%s%s", sepstr, entry.PDFString())) + case Name: + logstr = append(logstr, entry.PDFString()) + case Integer: + logstr = append(logstr, fmt.Sprintf("%s%s", sepstr, entry.PDFString())) + case Float: + logstr = append(logstr, fmt.Sprintf("%s%s", sepstr, entry.PDFString())) + case Boolean: + logstr = append(logstr, fmt.Sprintf("%s%s", sepstr, entry.PDFString())) + case StringLiteral: + logstr = append(logstr, fmt.Sprintf("%s%s", sepstr, entry.PDFString())) + case HexLiteral: + logstr = append(logstr, fmt.Sprintf("%s%s", sepstr, entry.PDFString())) + default: + if log.InfoEnabled() { + log.Info.Fatalf("PDFArray.PDFString(): entry of unknown object type: %[1]T %[1]v\n", entry) + } + } + } + + logstr = append(logstr, "]") + + return strings.Join(logstr, "") +} diff --git a/pkg/pdfcpu/types/date.go b/pkg/pdfcpu/types/date.go new file mode 100644 index 0000000000000000000000000000000000000000..a2f7a214428fa3c9d1f5aba4c74f614beebe4f6c --- /dev/null +++ b/pkg/pdfcpu/types/date.go @@ -0,0 +1,429 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 types + +import ( + "fmt" + "strconv" + "strings" + "time" +) + +// DateString returns a string representation of t. +func DateString(t time.Time) string { + _, tz := t.Zone() + tzm := tz / 60 + sign := "+" + if tzm < 0 { + sign = "-" + tzm = -tzm + } + + return fmt.Sprintf("D:%d%02d%02d%02d%02d%02d%s%02d'%02d'", + t.Year(), t.Month(), t.Day(), + t.Hour(), t.Minute(), t.Second(), + sign, tzm/60, tzm%60) +} + +func prevalidateDate(s string, relaxed bool) (string, bool) { + // utf16 conversion if applicable. + if IsStringUTF16BE(s) { + utf16s, err := DecodeUTF16String(s) + if err != nil { + return "", false + } + s = utf16s + } + + s = strings.TrimPrefix(s, "\xEF\xBB\xBF") + + // Remove trailing 0x00 + s = strings.TrimRight(s, "\x00") + + if relaxed { + // Accept missing "D:" prefix. + // "YYYY" is mandatory + s = strings.TrimPrefix(s, "D:") + s = strings.TrimSpace(s) + s = strings.ReplaceAll(s, ".", "") + s = strings.ReplaceAll(s, "\\", "") + return s, len(s) >= 4 + } + + // "D:YYYY" is mandatory + if len(s) < 6 { + return "", false + } + + return s[2:], strings.HasPrefix(s, "D:") +} + +func parseTimezoneHours(s string, o byte) (int, bool) { + tzh, err := strconv.Atoi(s) + if err != nil { + return 0, false + } + + // Opininated hack. + tzh = tzh % 24 + + if o == 'Z' && tzh != 0 { + return 0, false + } + + return tzh, true +} + +func parseTimezoneMinutes(s string, o byte) (int, bool) { + + tzm, err := strconv.Atoi(s) + if err != nil { + return 0, false + } + + if tzm > 59 { + return 0, false + } + + if o == 'Z' && tzm != 0 { + return 0, false + } + + return tzm, true +} + +func validateTimezoneSeparator(c byte) bool { + return c == '+' || c == '-' || c == 'Z' +} + +func parseTimezone(s string, relaxed bool) (h, m int, ok bool) { + + o := s[14] + + if !validateTimezoneSeparator(o) { + // Ignore timezone on corrupt timezone separator if relaxed. + return 0, 0, relaxed + } + + // local time equal to UT. + // "YYYYMMDDHHmmSSZ" or + // if relaxed + // "YYYYMMDDHHmmSSZ'" + // "YYYYMMDDHHmmSSZ'0" + + if o == 'Z' { + t := s[15:] + if t == "" || relaxed && (t == "'" || t == "'0") { + return 0, 0, true + } + } + + // HH'mm + s = s[15:] + if s[0] == '-' { + s = s[1:] + } + s = strings.ReplaceAll(s, " ", "0") + ss := strings.Split(s, "'") + if len(ss) == 0 { + return 0, 0, false + } + + neg := o == '-' + + tzh, ok := parseTimezoneHours(ss[0], o) + if !ok { + return 0, 0, false + } + + if neg { + tzh *= -1 + } + + if len(ss) == 1 || len(ss) == 2 && len(ss[1]) == 0 { + // Ignore missing timezone minutes. + return tzh, 0, true + } + + tzm, ok := parseTimezoneMinutes(ss[1], o) + if !ok { + return 0, 0, false + } + + return tzh, tzm, true +} + +func parseYear(s string) (y int, finished, ok bool) { + year := s[0:4] + + y, err := strconv.Atoi(year) + if err != nil { + return 0, false, false + } + + // "YYYY" + if len(s) == 4 { + return y, true, true + } + + if len(s) == 5 { + return 0, false, false + } + + return y, false, true +} + +func parseMonth(s string) (m int, finished, ok bool) { + month := s[4:6] + + var err error + m, err = strconv.Atoi(month) + if err != nil { + return 0, false, false + } + + if m < 1 || m > 12 { + return 0, false, false + } + + // "YYYYMM" + if len(s) == 6 { + return m, true, true + } + + if len(s) == 7 { + return 0, false, false + } + + return m, false, true +} + +func parseDay(s string, y, m int) (d int, finished, ok bool) { + day := s[6:8] + + d, err := strconv.Atoi(day) + if err != nil { + return 0, false, false + } + + if d < 1 || d > 31 { + return 0, false, false + } + + // check valid Date(year,month,day) + // The day before the first day of next month: + t := time.Date(y, time.Month(m+1), 0, 0, 0, 0, 0, time.UTC) + if d > t.Day() { + return 0, false, false + } + + // "YYYYMMDD" + if len(s) == 8 { + return d, true, true + } + + if len(s) == 9 { + return 0, false, false + } + + return d, false, true +} + +func parseHour(s string) (h int, finished, ok bool) { + hour := s[8:10] + + h, err := strconv.Atoi(hour) + if err != nil { + return 0, false, false + } + + if h > 23 { + return 0, false, false + } + + // "YYYYMMDDHH" + if len(s) == 10 { + return h, true, true + } + + if len(s) == 11 { + return 0, false, false + } + + return h, false, true +} + +func parseMinute(s string) (min int, finished, ok bool) { + minute := s[10:12] + + min, err := strconv.Atoi(minute) + if err != nil { + return 0, false, false + } + + if min > 59 { + return 0, false, false + } + + // "YYYYMMDDHHmm" + if len(s) == 12 { + return min, true, true + } + + if len(s) == 13 { + return 0, false, false + } + + return min, false, true +} + +func parseSecond(s string) (sec int, finished, ok bool) { + second := s[12:14] + + sec, err := strconv.Atoi(second) + if err != nil { + return 0, false, false + } + + if sec > 59 { + return 0, false, false + } + + // "YYYYMMDDHHmmSS" + if len(s) == 14 { + return sec, true, true + } + + return sec, false, true +} + +func digestPopularOutOfSpecDates(s string) (time.Time, bool) { + + // Mon Jan 2 15:04:05 2006 + // Monday, January 02, 2006 3:04:05 PM + // 1/2/2006 15:04:05 + // Mon, Jan 2, 2006 + + t, err := time.Parse("Mon Jan 2 15:04:05 2006", s) + if err == nil { + return t, true + } + + t, err = time.Parse("Monday, January 02, 2006 3:04:05 PM", s) + if err == nil { + return t, true + } + + t, err = time.Parse("1/2/2006 15:04:05", s) + if err == nil { + return t, true + } + + t, err = time.Parse("Mon, Jan 2, 2006", s) + if err == nil { + return t, true + } + + return t, false +} + +// DateTime decodes s into a time.Time. +func DateTime(s string, relaxed bool) (time.Time, bool) { + // 7.9.4 Dates + // (D:YYYYMMDDHHmmSSOHH'mm) + + var d time.Time + + var ok bool + s, ok = prevalidateDate(s, relaxed) + if !ok { + return d, false + } + + y, finished, ok := parseYear(s) + if !ok { + // Try workaround + return digestPopularOutOfSpecDates(s) + } + + // Construct time for yyyy 01 01 00:00:00 + d = time.Date(y, 1, 1, 0, 0, 0, 0, time.UTC) + if finished { + return d, true + } + + m, finished, ok := parseMonth(s) + if !ok { + return d, false + } + + d = d.AddDate(0, m-1, 0) + if finished { + return d, true + } + + day, finished, ok := parseDay(s, y, m) + if !ok { + return d, false + } + + d = d.AddDate(0, 0, day-1) + if finished { + return d, true + } + + h, finished, ok := parseHour(s) + if !ok { + return d, false + } + + d = d.Add(time.Duration(h) * time.Hour) + if finished { + return d, true + } + + min, finished, ok := parseMinute(s) + if !ok { + return d, false + } + + d = d.Add(time.Duration(min) * time.Minute) + if finished { + return d, true + } + + sec, finished, ok := parseSecond(s) + if !ok { + return d, false + } + + d = d.Add(time.Duration(sec) * time.Second) + if finished { + return d, true + } + + // Process timezone + tzh, tzm, ok := parseTimezone(s, relaxed) + if !ok { + return d, false + } + + loc := time.FixedZone("", tzh*60*60+tzm*60) + d = time.Date(y, time.Month(m), day, h, min, sec, 0, loc) + + return d, true +} diff --git a/pkg/pdfcpu/types/date_test.go b/pkg/pdfcpu/types/date_test.go new file mode 100644 index 0000000000000000000000000000000000000000..ee29aa85e2b0a33944a1deaaf6158d779b632865 --- /dev/null +++ b/pkg/pdfcpu/types/date_test.go @@ -0,0 +1,175 @@ +/* +Copyright 2020 The pdfcpu Authors. + +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 + + 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 types + +import ( + "testing" + "time" +) + +func doParseDateTimeRelaxedOK(s string, t *testing.T) { + t.Helper() + if time, ok := DateTime(s, true); ok { + _ = time + //t.Logf("DateTime(%s) valid => %s\n", s, time) + } else { + t.Errorf("DateTime(%s) invalid => not ok!\n", s) + } + +} + +func doParseDateTimeOK(s string, t *testing.T) { + t.Helper() + if time, ok := DateTime(s, false); ok { + _ = time + t.Logf("DateTime(%s) valid => %s\n", s, time) + } else { + t.Errorf("DateTime(%s) invalid => not ok!\n", s) + } + +} + +func doParseDateTimeFail(s string, t *testing.T) { + t.Helper() + if time, ok := DateTime(s, false); ok { + t.Errorf("DateTime(%s) valid => not ok! %s\n", s, time) + } // else { + //t.Logf("DateTime(%s) invalid => ok\n", s) + //} + +} + +func TestParseDateTime(t *testing.T) { + + // (D:YYYYMMDDHHmmSSOHH'mm) + // O = -,+,Z + + s := "D:2017" + doParseDateTimeOK(s, t) + + //UTF-8 bytes for UTF-16 string "D:2017" + s = "\xfe\xff\x00\x44\x00\x3A\x00\x32\x00\x30\x00\x31\x00\x37" + doParseDateTimeOK(s, t) + + s = "D:201703" + doParseDateTimeOK(s, t) + + s = "D:20170430" + doParseDateTimeOK(s, t) + + s = "D:2017043015" + doParseDateTimeOK(s, t) + + s = "D:201704301559" + doParseDateTimeOK(s, t) + + s = "D:20170430155901Z" + doParseDateTimeOK(s, t) + + s = "D:20170430155901" + doParseDateTimeOK(s, t) + + s = "D:20170430155901+06'59" + doParseDateTimeOK(s, t) + + s = "D:20170430155901Z00" + doParseDateTimeOK(s, t) + + s = "D:20170430155901Z00'00" + doParseDateTimeOK(s, t) + + s = "D:20210602180254-06" + doParseDateTimeOK(s, t) + + s = "D:20170430155901+06'" + doParseDateTimeOK(s, t) + + s = "D:20170430155901+06'59" + doParseDateTimeOK(s, t) + + s = "D:20210515103719-02'00" + doParseDateTimeOK(s, t) + + s = "D:20170430155901+66'A9" + doParseDateTimeFail(s, t) + + s = "D:20201222164228Z'" + doParseDateTimeRelaxedOK(s, t) + + s = "D:20230912144809Z'0" + doParseDateTimeRelaxedOK(s, t) + + s = "20141117162446Z00'00'" + doParseDateTimeRelaxedOK(s, t) + + s = "D: 20210827124448+00'00'" + doParseDateTimeRelaxedOK(s, t) + + s = "D: 20191003062617-07'00'" + doParseDateTimeRelaxedOK(s, t) + + s = "D:20150521.124925823" + doParseDateTimeRelaxedOK(s, t) + + s = "D:20210517043452}" + doParseDateTimeRelaxedOK(s, t) + + s = "D:20210608122455Z00\\'00" + doParseDateTimeRelaxedOK(s, t) + + s = "D:20020301230221- 5' 0'" + doParseDateTimeRelaxedOK(s, t) + + s = "D:20061102145045-05'" + doParseDateTimeRelaxedOK(s, t) + + s = "D:20150312082530-5'00'" + doParseDateTimeRelaxedOK(s, t) + + s = "D:20191009100417-05'00''" + doParseDateTimeRelaxedOK(s, t) + + s = "D:20200429084309+ 0' 0'" + doParseDateTimeRelaxedOK(s, t) + + s = "D:20211028112621--04'00" + doParseDateTimeRelaxedOK(s, t) + + s = "D:20210419150333-04'00'Z" + doParseDateTimeRelaxedOK(s, t) + + s = "\357\273\277D:20160404061414+65'53'" + doParseDateTimeRelaxedOK(s, t) +} + +func TestWriteDateTime(t *testing.T) { + + now := DateString(time.Now()) + doParseDateTimeOK(now, t) + + loc, _ := time.LoadLocation("Europe/Vienna") + now = DateString(time.Now().In(loc)) + doParseDateTimeOK(now, t) + + loc, _ = time.LoadLocation("Pacific/Honolulu") + now = DateString(time.Now().In(loc)) + doParseDateTimeOK(now, t) + + loc, _ = time.LoadLocation("Australia/Sydney") + now = DateString(time.Now().In(loc)) + doParseDateTimeOK(now, t) +} diff --git a/pkg/pdfcpu/types/dict.go b/pkg/pdfcpu/types/dict.go new file mode 100644 index 0000000000000000000000000000000000000000..653d99274648b61b562f4f571857d22961c71c6f --- /dev/null +++ b/pkg/pdfcpu/types/dict.go @@ -0,0 +1,540 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 types + +import ( + "fmt" + "sort" + "strconv" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pkg/errors" +) + +// Dict represents a PDF dict object. +type Dict map[string]Object + +// NewDict returns a new PDFDict object. +func NewDict() Dict { + return map[string]Object{} +} + +// Len returns the length of this PDFDict. +func (d Dict) Len() int { + return len(d) +} + +// Clone returns a clone of d. +func (d Dict) Clone() Object { + d1 := NewDict() + for k, v := range d { + if v != nil { + v = v.Clone() + } + d1.Insert(k, v) + } + return d1 +} + +// Insert adds a new entry to this PDFDict. +func (d Dict) Insert(k string, v Object) bool { + if _, found := d.Find(k); !found { + d[k] = v + return true + } + return false +} + +// InsertBool adds a new bool entry to this PDFDict. +func (d Dict) InsertBool(key string, value bool) { + d.Insert(key, Boolean(value)) +} + +// InsertInt adds a new int entry to this PDFDict. +func (d Dict) InsertInt(key string, value int) { + d.Insert(key, Integer(value)) +} + +// InsertFloat adds a new float entry to this PDFDict. +func (d Dict) InsertFloat(key string, value float32) { + d.Insert(key, Float(value)) +} + +// InsertString adds a new string entry to this PDFDict. +func (d Dict) InsertString(key, value string) { + d.Insert(key, StringLiteral(value)) +} + +// InsertName adds a new name entry to this PDFDict. +func (d Dict) InsertName(key, value string) { + d.Insert(key, Name(value)) +} + +// Update modifies an existing entry of this PDFDict. +func (d Dict) Update(key string, value Object) { + if value != nil { + d[key] = value + } +} + +// Find returns the Object for given key and PDFDict. +func (d Dict) Find(key string) (Object, bool) { + v, found := d[key] + if found { + return v, found + } + for n, v := range d { + k, err := DecodeName(n) + if err != nil { + return nil, false + } + if k == key { + return v, true + } + } + return nil, false +} + +// Delete deletes the Object for given key. +func (d Dict) Delete(key string) (value Object) { + value, found := d.Find(key) + if !found { + return nil + } + // TODO Take encoded names into account. + delete(d, key) + return value +} + +// NewIDForPrefix returns next id with prefix. +func (d Dict) NewIDForPrefix(prefix string, i int) string { + var id string + found := true + for j := i; found; j++ { + id = prefix + strconv.Itoa(j) + _, found = d.Find(id) + } + return id +} + +// Entry returns the value for given key. +func (d Dict) Entry(dictName, key string, required bool) (Object, error) { + obj, found := d.Find(key) + if !found || obj == nil { + if required { + return nil, errors.Errorf("dict=%s required entry=%s missing", dictName, key) + } + //log.Trace.Printf("dict=%s entry %s is nil\n", dictName, key) + return nil, nil + } + return obj, nil +} + +// BooleanEntry expects and returns a BooleanEntry for given key. +func (d Dict) BooleanEntry(key string) *bool { + + value, found := d.Find(key) + if !found { + return nil + } + + bb, ok := value.(Boolean) + if ok { + b := bb.Value() + return &b + } + + return nil +} + +// StringEntry expects and returns a StringLiteral entry for given key. +func (d Dict) StringEntry(key string) *string { + + value, found := d.Find(key) + if !found { + return nil + } + + pdfStr, ok := value.(StringLiteral) + if ok { + s := string(pdfStr) + return &s + } + + return nil +} + +// NameEntry expects and returns a Name entry for given key. +func (d Dict) NameEntry(key string) *string { + + value, found := d.Find(key) + if !found { + return nil + } + + name, ok := value.(Name) + if ok { + s := name.Value() + return &s + } + + return nil +} + +// IntEntry expects and returns a Integer entry for given key. +func (d Dict) IntEntry(key string) *int { + + value, found := d.Find(key) + if !found { + return nil + } + + pdfInt, ok := value.(Integer) + if ok { + i := int(pdfInt) + return &i + } + + return nil +} + +// Int64Entry expects and returns a Integer entry representing an int64 value for given key. +func (d Dict) Int64Entry(key string) *int64 { + + value, found := d.Find(key) + if !found { + return nil + } + + pdfInt, ok := value.(Integer) + if ok { + i := int64(pdfInt) + return &i + } + + return nil +} + +// IndirectRefEntry returns an indirectRefEntry for given key for this dictionary. +func (d Dict) IndirectRefEntry(key string) *IndirectRef { + + value, found := d.Find(key) + if !found { + return nil + } + + pdfIndRef, ok := value.(IndirectRef) + if ok { + return &pdfIndRef + } + + // return err? + return nil +} + +// DictEntry expects and returns a PDFDict entry for given key. +func (d Dict) DictEntry(key string) Dict { + + value, found := d.Find(key) + if !found { + return nil + } + + // TODO resolve indirect ref. + + d, ok := value.(Dict) + if ok { + return d + } + + return nil +} + +// StreamDictEntry expects and returns a StreamDict entry for given key. +// unused. +func (d Dict) StreamDictEntry(key string) *StreamDict { + + value, found := d.Find(key) + if !found { + return nil + } + + sd, ok := value.(StreamDict) + if ok { + return &sd + } + + return nil +} + +// ArrayEntry expects and returns a Array entry for given key. +func (d Dict) ArrayEntry(key string) Array { + + value, found := d.Find(key) + if !found { + return nil + } + + a, ok := value.(Array) + if ok { + return a + } + + return nil +} + +// StringLiteralEntry returns a StringLiteral object for given key. +func (d Dict) StringLiteralEntry(key string) *StringLiteral { + + value, found := d.Find(key) + if !found { + return nil + } + + s, ok := value.(StringLiteral) + if ok { + return &s + } + + return nil +} + +// HexLiteralEntry returns a HexLiteral object for given key. +func (d Dict) HexLiteralEntry(key string) *HexLiteral { + + value, found := d.Find(key) + if !found { + return nil + } + + s, ok := value.(HexLiteral) + if ok { + return &s + } + + return nil +} + +func (d Dict) StringOrHexLiteralEntry(key string) (*string, error) { + if obj, ok := d.Find(key); ok { + return StringOrHexLiteral(obj) + } + return nil, nil +} + +// Length returns a *int64 for entry with key "Length". +// Stream length may be referring to an indirect object. +func (d Dict) Length() (*int64, *int) { + + val := d.Int64Entry("Length") + if val != nil { + return val, nil + } + + indirectRef := d.IndirectRefEntry("Length") + if indirectRef == nil { + return nil, nil + } + + intVal := indirectRef.ObjectNumber.Value() + + return nil, &intVal +} + +// Type returns the value of the name entry for key "Type". +func (d Dict) Type() *string { + return d.NameEntry("Type") +} + +// Subtype returns the value of the name entry for key "Subtype". +func (d Dict) Subtype() *string { + return d.NameEntry("Subtype") +} + +// Size returns the value of the int entry for key "Size" +func (d Dict) Size() *int { + return d.IntEntry("Size") +} + +func (d Dict) IsPage() bool { + return d.Type() != nil && *d.Type() == "Page" +} + +// IsObjStm returns true if given PDFDict is an object stream. +func (d Dict) IsObjStm() bool { + return d.Type() != nil && *d.Type() == "ObjStm" +} + +// W returns a *Array for key "W". +func (d Dict) W() Array { + return d.ArrayEntry("W") +} + +// Prev returns the previous offset. +func (d Dict) Prev() *int64 { + return d.Int64Entry("Prev") +} + +// Index returns a *Array for key "Index". +func (d Dict) Index() Array { + return d.ArrayEntry("Index") +} + +// N returns a *int for key "N". +func (d Dict) N() *int { + return d.IntEntry("N") +} + +// First returns a *int for key "First". +func (d Dict) First() *int { + return d.IntEntry("First") +} + +// IsLinearizationParmDict returns true if this dict has an int entry for key "Linearized". +func (d Dict) IsLinearizationParmDict() bool { + return d.IntEntry("Linearized") != nil +} + +// IncrementBy increments the integer value for given key by i. +func (d *Dict) IncrementBy(key string, i int) error { + v := d.IntEntry(key) + if v == nil { + return errors.Errorf("IncrementBy: unknown key: %s", key) + } + *v += i + d.Update(key, Integer(*v)) + return nil +} + +// Increment increments the integer value for given key. +func (d *Dict) Increment(key string) error { + return d.IncrementBy(key, 1) +} + +func (d Dict) indentedString(level int) string { + + logstr := []string{"<<\n"} + tabstr := strings.Repeat("\t", level) + + var keys []string + for k := range d { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + + v := d[k] + + switch v := v.(type) { + case Dict: + dictStr := v.indentedString(level + 1) + logstr = append(logstr, fmt.Sprintf("%s<%s, %s>\n", tabstr, k, dictStr)) + case Array: + arrStr := v.indentedString(level + 1) + logstr = append(logstr, fmt.Sprintf("%s<%s, %s>\n", tabstr, k, arrStr)) + default: + val := "null" + if v != nil { + val = v.String() + if n, ok := v.(Name); ok { + val, _ = DecodeName(string(n)) + } + } + + logstr = append(logstr, fmt.Sprintf("%s<%s, %v>\n", tabstr, k, val)) + } + } + + logstr = append(logstr, fmt.Sprintf("%s%s", strings.Repeat("\t", level-1), ">>")) + + return strings.Join(logstr, "") +} + +// PDFString returns a string representation as found in and written to a PDF file. +func (d Dict) PDFString() string { + + logstr := []string{} //make([]string, 20) + logstr = append(logstr, "<<") + + var keys []string + for k := range d { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + + v := d[k] + keyName := EncodeName(k) + + switch v := v.(type) { + case nil: + logstr = append(logstr, fmt.Sprintf("/%s null", keyName)) + case Dict: + logstr = append(logstr, fmt.Sprintf("/%s%s", keyName, v.PDFString())) + case Array: + logstr = append(logstr, fmt.Sprintf("/%s%s", keyName, v.PDFString())) + case IndirectRef: + logstr = append(logstr, fmt.Sprintf("/%s %s", keyName, v.PDFString())) + case Name: + logstr = append(logstr, fmt.Sprintf("/%s%s", keyName, v.PDFString())) + case Integer: + logstr = append(logstr, fmt.Sprintf("/%s %s", keyName, v.PDFString())) + case Float: + logstr = append(logstr, fmt.Sprintf("/%s %s", keyName, v.PDFString())) + case Boolean: + logstr = append(logstr, fmt.Sprintf("/%s %s", keyName, v.PDFString())) + case StringLiteral: + logstr = append(logstr, fmt.Sprintf("/%s%s", keyName, v.PDFString())) + case HexLiteral: + logstr = append(logstr, fmt.Sprintf("/%s%s", keyName, v.PDFString())) + default: + if log.InfoEnabled() { + log.Info.Fatalf("PDFDict.PDFString(): entry of unknown object type: %T %[1]v\n", v) + } + } + } + + logstr = append(logstr, ">>") + return strings.Join(logstr, "") +} + +func (d Dict) String() string { + return d.indentedString(1) +} + +// StringEntryBytes returns the byte slice representing the string value for key. +func (d Dict) StringEntryBytes(key string) ([]byte, error) { + + s := d.StringLiteralEntry(key) + if s != nil { + bb, err := Unescape(s.Value()) + if err != nil { + return nil, err + } + return bb, nil + } + + h := d.HexLiteralEntry(key) + if h != nil { + return h.Bytes() + } + + return nil, nil +} diff --git a/pkg/pdfcpu/types/dict_test.go b/pkg/pdfcpu/types/dict_test.go new file mode 100644 index 0000000000000000000000000000000000000000..e28816b4fb629521af35cbaed79cb6ca0f3219f4 --- /dev/null +++ b/pkg/pdfcpu/types/dict_test.go @@ -0,0 +1,32 @@ +/* +Copyright 2024 The pdfcpu Authors. + +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 + + 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 types + +import ( + "testing" +) + +func TestEncodeDict(t *testing.T) { + dict := Dict{ + "A()": Integer(1), + } + expected := `<>` + s := dict.PDFString() + if s != expected { + t.Errorf("expected %s for %+v, got %s", expected, dict, s) + } +} diff --git a/pkg/pdfcpu/types/layout.go b/pkg/pdfcpu/types/layout.go new file mode 100644 index 0000000000000000000000000000000000000000..174a1ed95abe4a9c241a0079c7e8bef3797a8940 --- /dev/null +++ b/pkg/pdfcpu/types/layout.go @@ -0,0 +1,411 @@ +/* +Copyright 2022 The pdfcpu Authors. + +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 + + 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 types + +import ( + "strings" + + "github.com/pkg/errors" +) + +// Corner represents one of four rectangle corners. +type Corner int + +// The four corners of a rectangle. +const ( + LowerLeft Corner = iota + LowerRight + UpperLeft + UpperRight +) + +// HAlignment represents the horizontal alignment of text. +type HAlignment int + +// These are the options for horizontal aligned text. +const ( + AlignLeft HAlignment = iota + AlignCenter + AlignRight + AlignJustify +) + +// VAlignment represents the vertical alignment of text. +type VAlignment int + +// These are the options for vertical aligned text. +const ( + AlignBaseline VAlignment = iota + AlignTop + AlignMiddle + AlignBottom +) + +// LineJoinStyle represents the shape to be used at the corners of paths that are stroked (see 8.4.3.4) +type LineJoinStyle int + +// Render mode +const ( + LJMiter LineJoinStyle = iota + LJRound + LJBevel +) + +func ParseHorAlignment(s string) (HAlignment, error) { + var a HAlignment + switch strings.ToLower(s) { + case "l", "left": + a = AlignLeft + case "r", "right": + a = AlignRight + case "c", "center": + a = AlignCenter + case "j", "justify": + a = AlignJustify + default: + return a, errors.Errorf("pdfcpu: unknown textfield alignment (left, center, right, justify): %s", s) + } + return a, nil +} + +func ParseOrigin(s string) (Corner, error) { + var c Corner + switch strings.ToLower(s) { + case "ll", "lowerleft": + c = LowerLeft + case "lr", "lowerright": + c = LowerRight + case "ul", "upperleft": + c = UpperLeft + case "ur", "upperright": + c = UpperRight + default: + return c, errors.Errorf("pdfcpu: unknown origin (ll, lr, ul, ur): %s", s) + } + return c, nil +} + +func ParseAnchor(s string) (Anchor, error) { + var a Anchor + switch strings.ToLower(s) { + case "tl", "topleft": + a = TopLeft + case "tc", "topcenter": + a = TopCenter + case "tr", "topright": + a = TopRight + case "l", "left": + a = Left + case "c", "center": + a = Center + case "r", "right": + a = Right + case "bl", "bottomleft": + a = BottomLeft + case "bc", "bottomcenter": + a = BottomCenter + case "br", "bottomright": + a = BottomRight + default: + return a, errors.Errorf("pdfcpu: unknown anchor: %s", s) + } + return a, nil +} + +func ParsePositionAnchor(s string) (Anchor, error) { + var a Anchor + switch s { + case "tl", "topleft", "top-left": + a = TopLeft + case "tc", "topcenter", "top-center": + a = TopCenter + case "tr", "topright", "top-right": + a = TopRight + case "l", "left": + a = Left + case "c", "center": + a = Center + case "r", "right": + a = Right + case "bl", "bottomleft", "bottom-left": + a = BottomLeft + case "bc", "bottomcenter", "bottom-center": + a = BottomCenter + case "br", "bottomright", "bottom-right": + a = BottomRight + case "f", "full": + a = Full + default: + return a, errors.Errorf("pdfcpu: unknown position anchor: %s", s) + } + return a, nil +} + +func AnchorPosition(a Anchor, r *Rectangle, w, h float64) (x float64, y float64) { + switch a { + case TopLeft: + x, y = 0, r.Height()-h + case TopCenter: + x, y = r.Width()/2-w/2, r.Height()-h + case TopRight: + x, y = r.Width()-w, r.Height()-h + case Left: + x, y = 0, r.Height()/2-h/2 + case Center: + x, y = r.Width()/2-w/2, r.Height()/2-h/2 + case Right: + x, y = r.Width()-w, r.Height()/2-h/2 + case BottomLeft: + x, y = 0, 0 + case BottomCenter: + x, y = r.Width()/2-w/2, 0 + case BottomRight: + x, y = r.Width()-w, 0 + } + return +} + +// TODO Refactor because of orientation in nup.go +type Orientation int + +const ( + Horizontal Orientation = iota + Vertical +) + +// RelPosition represents the relative position of a text field's label. +type RelPosition int + +// These are the options for relative label positions. +const ( + RelPosLeft RelPosition = iota + RelPosRight + RelPosTop + RelPosBottom +) + +func ParseRelPosition(s string) (RelPosition, error) { + var p RelPosition + switch strings.ToLower(s) { + case "l", "left": + p = RelPosLeft + case "r", "right": + p = RelPosRight + case "t", "top": + p = RelPosTop + case "b", "bottom": + p = RelPosBottom + default: + return p, errors.Errorf("pdfcpu: unknown textfield alignment (left, right, top, bottom): %s", s) + } + return p, nil +} + +// NormalizeCoord transfers P(x,y) from pdfcpu user space into PDF user space, +// which uses a coordinate system with origin in the lower left corner of r. +// +// pdfcpu user space coordinate systems have the origin in one of four corners of r: +// +// LowerLeft corner (default = PDF user space) +// +// x extends to the right, +// y extends upward +// +// LowerRight corner: +// +// x extends to the left, +// y extends upward +// +// UpperLeft corner: +// +// x extends to the right, +// y extends downward +// +// UpperRight corner: +// +// x extends to the left, +// y extends downward +func NormalizeCoord(x, y float64, r *Rectangle, origin Corner, absolute bool) (float64, float64) { + switch origin { + case UpperLeft: + if y >= 0 { + y = r.Height() - y + if y < 0 { + y = 0 + } + } + case LowerRight: + if x >= 0 { + x = r.Width() - x + if x < 0 { + x = 0 + } + } + case UpperRight: + if x >= 0 { + x = r.Width() - x + if x < 0 { + x = 0 + } + } + if y >= 0 { + y = r.Height() - y + if y < 0 { + y = 0 + } + } + } + if absolute { + if x >= 0 { + x += r.LL.X + } + if y >= 0 { + y += r.LL.Y + } + } + return x, y +} + +// Normalize offset transfers x and y into offsets in the PDF user space. +func NormalizeOffset(x, y float64, origin Corner) (float64, float64) { + switch origin { + case UpperLeft: + y = -y + case LowerRight: + x = -x + case UpperRight: + x = -x + y = -y + } + return x, y +} + +func BestFitRectIntoRect(rSrc, rDest *Rectangle, enforceOrient, scaleUp bool) (w, h, dx, dy, rot float64) { + if !scaleUp && rSrc.FitsWithin(rDest) { + // Translate rSrc into center of rDest without scaling. + w = rSrc.Width() + h = rSrc.Height() + dx = rDest.Width()/2 - rSrc.Width()/2 + dy = rDest.Height()/2 - rSrc.Height()/2 + return + } + + if rSrc.Landscape() { + if rDest.Landscape() { + if rSrc.AspectRatio() > rDest.AspectRatio() { + w = rDest.Width() + h = rSrc.ScaledHeight(w) + dy = (rDest.Height() - h) / 2 + } else { + h = rDest.Height() + w = rSrc.ScaledWidth(h) + dx = (rDest.Width() - w) / 2 + } + } else { + if enforceOrient { + rot = 90 + if 1/rSrc.AspectRatio() < rDest.AspectRatio() { + w = rDest.Height() + h = rSrc.ScaledHeight(w) + dx = (rDest.Width() - h) / 2 + } else { + h = rDest.Width() + w = rSrc.ScaledWidth(h) + dy = (rDest.Height() - w) / 2 + } + return + } + w = rDest.Width() + h = rSrc.ScaledHeight(w) + dy = (rDest.Height() - h) / 2 + } + return + } + + if rSrc.Portrait() { + if rDest.Portrait() { + if rSrc.AspectRatio() < rDest.AspectRatio() { + h = rDest.Height() + w = rSrc.ScaledWidth(h) + dx = (rDest.Width() - w) / 2 + } else { + w = rDest.Width() + h = rSrc.ScaledHeight(w) + dy = (rDest.Height() - h) / 2 + } + } else { + if enforceOrient { + rot = 90 + if 1/rSrc.AspectRatio() > rDest.AspectRatio() { + h = rDest.Width() + w = rSrc.ScaledWidth(h) + dy = (rDest.Height() - w) / 2 + } else { + w = rDest.Height() + h = rSrc.ScaledHeight(w) + dx = (rDest.Width() - h) / 2 + } + return + } + h = rDest.Height() + w = rSrc.ScaledWidth(h) + dx = (rDest.Width() - w) / 2 + } + return + } + + if rDest.Portrait() { + w = rDest.Width() + dy = rDest.Height()/2 - rSrc.ScaledHeight(w)/2 + h = w + } else { + h = rDest.Height() + dx = rDest.Width()/2 - rSrc.ScaledWidth(h)/2 + w = h + } + + return +} + +func ParsePageFormat(v string) (*Dim, string, error) { + + // Optional: appended last letter L indicates landscape mode. + // Optional: appended last letter P indicates portrait mode. + // eg. A4L means A4 in landscape mode whereas A4 defaults to A4P + // The default mode is defined implicitly via PaperSize dimensions. + + portrait := true + + if strings.HasSuffix(v, "L") { + v = v[:len(v)-1] + portrait = false + } else { + v = strings.TrimSuffix(v, "P") + } + + d, ok := PaperSize[v] + if !ok { + return nil, v, errors.Errorf("pdfcpu: page format %s is unsupported.\n", v) + } + + dim := Dim{d.Width, d.Height} + if (d.Portrait() && !portrait) || (d.Landscape() && portrait) { + dim.Width, dim.Height = dim.Height, dim.Width + } + + return &dim, v, nil +} diff --git a/pkg/pdfcpu/types/layout_test.go b/pkg/pdfcpu/types/layout_test.go new file mode 100644 index 0000000000000000000000000000000000000000..f50a8cfe1d4adb6f7e7356d1520770f87fceb058 --- /dev/null +++ b/pkg/pdfcpu/types/layout_test.go @@ -0,0 +1,33 @@ +/* +Copyright 2024 The pdfcpu Authors. + +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 + + 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 types + +import "testing" + +func TestParsePageFormat(t *testing.T) { + dim, _, err := ParsePageFormat("A3L") + if err != nil { + t.Error(err) + } + if (dim.Width != 1191) || (dim.Height != 842) { + t.Errorf("expected 1191x842. got %s", dim) + } + // the original dim should be unmodified + dimOrig := PaperSize["A3"] + if (dimOrig.Width != 842) || (dimOrig.Height != 1191) { + t.Errorf("expected origDim=842x1191x842. got %s", dimOrig) + } +} diff --git a/pkg/pdfcpu/types/paperSize.go b/pkg/pdfcpu/types/paperSize.go new file mode 100644 index 0000000000000000000000000000000000000000..93ee9f3b7f095038340de31fca6df95f7d928b6f --- /dev/null +++ b/pkg/pdfcpu/types/paperSize.go @@ -0,0 +1,208 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 types + +// PaperSize is a map of known paper sizes in user units (=72 dpi pixels). +var PaperSize = map[string]*Dim{ + + // ISO 216:1975 A + "4A0": {4768, 6741}, // 66 1/4" x 93 5/8" 1682 x 2378 mm + "2A0": {3370, 4768}, // 46 3/4" x 66 1/4" 1189 x 1682 mm + "A0": {2384, 3370}, // 33" x 46 3/4" 841 x 1189 mm + "A1": {1684, 2384}, // 23 3/8" x 33" 594 x 841 mm + "A2": {1191, 1684}, // 16 1/2" x 23 3/8" 420 x 594 mm + "A3": {842, 1191}, // 11 3/4" x 16 1/2" 297 x 420 mm + "A4": {595, 842}, // 8 1/4" x 11 3/4" 210 x 297 mm + "A5": {420, 595}, // 5 7/8" x 8 1/4" 148 x 210 mm + "A6": {298, 420}, // 4 1/8" x 5 7/8" 105 x 148 mm + "A7": {210, 298}, // 2 7/8" x 4 1/8" 74 x 105 mm + "A8": {147, 210}, // 2" x 2 7/8" 52 x 74 mm + "A9": {105, 147}, // 1 1/2" x 2" 37 x 52 mm + "A10": {74, 105}, // 1" x 1 1/2" 26 x 37 mm + + // ISO 216:1975 B + "B0+": {3170, 4479}, // 44" x 62 1/4" 1118 x 1580 mm + "B0": {2835, 4008}, // 39 3/8" x 55 3/4" 1000 x 1414 mm + "B1+": {2041, 2892}, // 28 3/8" x 40 1/8" 720 x 1020 mm + "B1": {2004, 2835}, // 27 3/4" x 39 3/8" 707 x 1000 mm + "B2+": {1474, 2041}, // 20 1/2" x 28 3/8" 520 x 720 mm + "B2": {1417, 2004}, // 19 3/4" x 27 3/4" 500 x 707 mm + "B3": {1001, 1417}, // 13 7/8" x 19 3/4" 353 x 500 mm + "B4": {709, 1001}, // 9 7/8" x 13 7/8" 250 x 353 mm + "B5": {499, 709}, // 7" x 9 7/8" 176 x 250 mm + "B6": {354, 499}, // 4 7/8" x 7" 125 x 176 mm + "B7": {249, 354}, // 3 1/2" x 4 7/8" 88 x 125 mm + "B8": {176, 249}, // 2 1/2" x 3 1/2" 62 x 88 mm + "B9": {125, 176}, // 1 3/4" x 2 1/2" 44 x 62 mm + "B10": {88, 125}, // 1 1/4" x 1 3/4" 31 x 44 mm + + // ISO 269:1985 envelopes aka ISO C + "C0": {2599, 3677}, // 36" x 51" 917 x 1297 mm + "C1": {1837, 2599}, // 25 1/2" x 36" 648 x 917 mm + "C2": {1298, 1837}, // 18" x 25 1/2" 458 x 648 mm + "C3": {918, 1298}, // 12 3/4" x 18" 324 x 458 mm + "C4": {649, 918}, // 9" x 12 3/4" 229 x 324 mm + "C5": {459, 649}, // 6 3/8" x 9" 162 x 229 mm + "C6": {323, 459}, // 4 1/2" x 6 3/8" 114 x 162 mm + "C7": {230, 323}, // 3 3/16" x 4 1/2" 81 x 114 mm + "C8": {162, 230}, // 2 1/4" x 3 3/16 57 x 81 mm + "C9": {113, 162}, // 1 5/8" x 2 1/4" 40 x 57 mm + "C10": {79, 113}, // 1 1/8" x 1 5/8" 28 x 40 mm + + // ISO 217:2013 untrimmed raw paper + "RA0": {2438, 3458}, // 33.9" x 48.0" 860 x 1220 mm + "RA1": {1729, 2438}, // 24.0" x 33.9" 610 x 860 mm + "RA2": {1219, 1729}, // 16.9" x 24.0" 430 x 610 mm + "RA3": {865, 1219}, // 12.0" x 16.9" 305 x 430 mm + "RA4": {610, 865}, // 8.5" x 12.0" 215 x 305 mm + + "SRA0": {2551, 3628}, // 35.4" x 50.4" 900 x 1280 mm + "SRA1": {1814, 2551}, // 25.2" x 35.4" 640 x 900 mm + "SRA2": {1276, 1814}, // 17.7" x 25.2" 450 x 640 mm + "SRA3": {907, 1276}, // 12.6" x 17.7" 320 x 450 mm + "SRA4": {638, 907}, // 8.9" x 12.6" 225 x 320 mm + + "SRA1+": {2835, 4008}, // 26.0" x 36.2" 660 x 920 mm + "SRA2+": {1361, 1843}, // 18.9" x 25.6" 480 x 650 mm + "SRA3+": {907, 1304}, // 12.6" x 18.1" 320 x 460 mm + "SRA3++": {2835, 4008}, // 12.6" x 18.3" 320 x 464 mm + + // American + "SuperB": {936, 1368}, // 13" x 19" + "B+": {936, 1368}, + + "Tabloid": {792, 1224}, // 11" x 17" ANSIB, DobleCarta + "ExtraTabloid": {864, 1296}, // 12" x 18" ARCHB, Arch2 + "Ledger": {1224, 792}, // 17" x 11" ANSIB + "Legal": {612, 1008}, // 8 1/2" x 14" + + "GovLegal": {612, 936}, // 8 1/2" x 13" + "Oficio": {612, 936}, + "Folio": {612, 936}, + + "Letter": {612, 792}, // 8 1/2" x 11" ANSIA + "Carta": {612, 792}, + "AmericanQuarto": {612, 792}, + + "DobleCarta": {792, 1224}, // 11" x 17" Tabloid, ANSIB + + "GovLetter": {576, 756}, // 8" x 10 1/2" + "Executive": {522, 756}, // 7 1/4" x 10 1/2" + + "HalfLetter": {396, 612}, // 5 1/2" x 8 1/2" + "Memo": {396, 612}, + "Statement": {396, 612}, + "Stationary": {396, 612}, + + "JuniorLegal": {360, 576}, // 5" x 8" + "IndexCard": {360, 576}, + + "Photo": {288, 432}, // 4" x 6" + + // ANSI/ASME Y14.1 + "ANSIA": {612, 792}, // 8 1/2" x 11" Letter, Carta, AmericanQuarto + "ANSIB": {792, 1224}, // 11" x 17" Ledger, Tabloid, DobleCarta + "ANSIC": {1224, 1584}, // 17" x 22" + "ANSID": {1584, 2448}, // 22" x 34" + "ANSIE": {2448, 3168}, // 34" x 44" + "ANSIF": {2016, 2880}, // 28" x 40" + + // ANSI/ASME Y14.1 Architectural series + "ARCHA": {649, 865}, // 9" x 12" Arch 1 + "ARCHB": {865, 1296}, // 12" x 18" Arch 2, ExtraTabloide + "ARCHC": {1296, 1729}, // 18" x 24" Arch 3 + "ARCHD": {1729, 2591}, // 24" x 36" Arch 4 + "ARCHE": {2591, 3456}, // 36" x 48" Arch 6 + "ARCHE1": {2160, 3025}, // 30" x 42" Arch 5 + "ARCHE2": {1871, 2736}, // 26" x 38" + "ARCHE3": {1945, 2809}, // 27" x 39" + + "Arch1": {648, 864}, // 9" x 12" ARCHA + "Arch2": {864, 1296}, // 12" x 18" ARCHB, ExtraTabloide + "Arch3": {1296, 1728}, // 18" x 24" ARCHC + "Arch4": {1728, 2592}, // 24" x 36" ARCHD + "Arch5": {2160, 3024}, // 30" x 42" ARCHE1 + "Arch6": {2592, 3456}, // 36" x 48" ARCHE + + // American Uncut + "Bond": {1584, 1224}, // 22" x 17" + "Book": {2736, 1800}, // 38" x 25" + "Cover": {1872, 1440}, // 26" x 20" + "Index": {2196, 1836}, // 30 1/2" x 25 1/2" + + "Newsprint": {2592, 1728}, // 36" x 24" + "Tissue": {2592, 1728}, + + "Offset": {2736, 1800}, // 38" x 25" + "Text": {2736, 1800}, + + // English Uncut + "Crown": {1170, 1512}, // 16 1/4" x 21" + "DoubleCrown": {1440, 2160}, // 20" x 30" + "Quad": {2160, 2880}, // 30" x 40" + "Demy": {1278, 1620}, // 17 3/4" x 22 1/2" + "DoubleDemy": {1620, 2556}, // 22 1/2" x 35 1/2" + "Medium": {1314, 1656}, // 18 1/4" x 23" + "Royal": {1440, 1804}, // 20" x 25 1/16" + "SuperRoyal": {1512, 1944}, // 21" x 27" + "DoublePott": {1080, 1800}, // 15" x 25" + "DoublePost": {1368, 2196}, // 19" x 30 1/2" + "Foolscap": {972, 1224}, // 13 1/2" x 17" + "DoubleFoolscap": {1224, 1944}, // 17" x 27" + + "F4": {594, 936}, // 8 1/4" x 13" + + // GB/T 148-1997 D Series China + "D0": {2166, 3016}, // 29.9" x 41.9" 764 x 1064 mm + "D1": {1508, 2155}, // 20.9" x 29.9" 532 x 760 mm + "D2": {1077, 1497}, // 15.0" x 20.8" 380 x 528 mm + "D3": {748, 1066}, // 10.4" x 14.8" 264 x 376 mm + "D4": {533, 737}, // 7.4" x 10.2" 188 x 260 mm + "D5": {369, 522}, // 5.1" x 7.2" 130 x 184 mm + "D6": {261, 357}, // 3.6" x 5.0" 92 x 126 mm + + "RD0": {2231, 3096}, // 31.0" x 43.0" 787 x 1092 mm + "RD1": {1548, 2231}, // 21.5" x 31.0" 546 x 787 mm + "RD2": {1114, 1548}, // 15.5" x 21.5" 393 x 546 mm + "RD3": {774, 1114}, // 10.7" x 15.5" 273 x 393 mm + "RD4": {556, 774}, // 7.7" x 10.7" 196 x 273 mm + "RD5": {386, 556}, // 5.4" x 7.7" 136 x 196 mm + "RD6": {278, 386}, // 3.9" x 5.4" 98 x 136 mm + + // Japanese B-series variant + "JIS-B0": {2920, 4127}, // 40.55" x 57.32" 1030 x 1456 mm + "JIS-B1": {2064, 2920}, // 28.66" x 40.55" 728 x 1030 mm + "JIS-B2": {1460, 2064}, // 20.28" x 28.66" 515 x 728 mm + "JIS-B3": {1032, 1460}, // 14.33" x 20.28" 364 x 515 mm + "JIS-B4": {729, 1032}, // 10.12" x 14.33" 257 x 364 mm + "JIS-B5": {516, 729}, // 7.17" x 10.12" 182 x 257 mm + "JIS-B6": {363, 516}, // 5.04" x 7.17" 128 x 182 mm + "JIS-B7": {258, 363}, // 3.58" x 5.04" 91 x 128 mm + "JIS-B8": {181, 258}, // 2.52" x 3.58" 64 x 91 mm + "JIS-B9": {127, 181}, // 1.77" x 2.52" 45 x 64 mm + "JIS-B10": {91, 127}, // 1.26" x 1.77" 32 x 45 mm + "JIS-B11": {63, 91}, // 0.87" x 1.26" 22 x 32 mm + "JIS-B12": {45, 63}, // 0.63" x 0.87" 16 x 22 mm + "Shirokuban4": {748, 1074}, // 10.39" x 14.92" 264 x 379 mm + "Shirokuban5": {536, 742}, // 7.44" x 10.31" 189 x 262 mm + "Shirokuban6": {360, 533}, // 5.00" x 7.40" 127 x 188 mm + "Kiku4": {644, 868}, // 8.94" x 12.05" 227 x 306 mm + "Kiku5": {428, 644}, // 5.95" x 8.94" 151 x 227 mm + "AB": {595, 729}, // 8.27" x 10.12" 210 x 257 mm + "B40": {292, 516}, // 4.06" x 7.17" 103 x 182 mm + "Shikisen": {238, 420}, // 3.31" x 5.83" 84 x 148 mm +} diff --git a/pkg/pdfcpu/types/slice.go b/pkg/pdfcpu/types/slice.go new file mode 100644 index 0000000000000000000000000000000000000000..60d3ae5bb788269c1b446a5d7291ee7c8dfe5e1a --- /dev/null +++ b/pkg/pdfcpu/types/slice.go @@ -0,0 +1,59 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 types + +// MemberOf returns true if list contains s. +func MemberOf(s string, list []string) bool { + for _, v := range list { + if s == v { + return true + } + } + return false +} + +// IntMemberOf returns true if list contains i. +func IntMemberOf(i int, list []int) bool { + for _, v := range list { + if i == v { + return true + } + } + return false +} + +// IntMemberOf returns true if list contains i. +func IndRefMemberOf(i IndirectRef, arr Array) bool { + for _, v := range arr { + if i == v { + return true + } + } + return false +} + +func EqualSlices(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i, v := range a { + if v != b[i] { + return false + } + } + return true +} diff --git a/pkg/pdfcpu/types/streamdict.go b/pkg/pdfcpu/types/streamdict.go new file mode 100644 index 0000000000000000000000000000000000000000..7f4b890ffcccfe0145fb48ddee4d3206752995ae --- /dev/null +++ b/pkg/pdfcpu/types/streamdict.go @@ -0,0 +1,470 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 types + +import ( + "bytes" + "context" + "fmt" + "io" + + "github.com/pdfcpu/pdfcpu/pkg/filter" + "github.com/pdfcpu/pdfcpu/pkg/log" + + "github.com/pkg/errors" +) + +// PDFFilter represents a PDF stream filter object. +type PDFFilter struct { + Name string + DecodeParms Dict +} + +// StreamDict represents a PDF stream dict object. +type StreamDict struct { + Dict + StreamOffset int64 + StreamLength *int64 + StreamLengthObjNr *int + FilterPipeline []PDFFilter + Raw []byte // Encoded + Content []byte // Decoded + //DCTImage image.Image + IsPageContent bool + CSComponents int +} + +// NewStreamDict creates a new PDFStreamDict for given PDFDict, stream offset and length. +func NewStreamDict(d Dict, streamOffset int64, streamLength *int64, streamLengthObjNr *int, filterPipeline []PDFFilter) StreamDict { + return StreamDict{ + d, + streamOffset, + streamLength, + streamLengthObjNr, + filterPipeline, + nil, + nil, + //nil, + false, + 0, + } +} + +// Clone returns a clone of sd. +func (sd StreamDict) Clone() Object { + sd1 := sd + sd1.Dict = sd.Dict.Clone().(Dict) + pl := make([]PDFFilter, len(sd.FilterPipeline)) + for k, v := range sd.FilterPipeline { + f := PDFFilter{} + f.Name = v.Name + if v.DecodeParms != nil { + f.DecodeParms = v.DecodeParms.Clone().(Dict) + } + pl[k] = f + } + sd1.FilterPipeline = pl + return sd1 +} + +// HasSoleFilterNamed returns true if sd has a +// filterPipeline with 1 filter named filterName. +func (sd StreamDict) HasSoleFilterNamed(filterName string) bool { + fpl := sd.FilterPipeline + if fpl == nil || len(fpl) != 1 { + return false + } + return fpl[0].Name == filterName +} + +func (sd StreamDict) Image() bool { + s := sd.Type() + if s == nil || *s != "XObject" { + return false + } + s = sd.Subtype() + if s == nil || *s != "Image" { + return false + } + return true +} + +type DecodeLazyObjectStreamObjectFunc func(c context.Context, s string) (Object, error) + +type LazyObjectStreamObject struct { + osd *ObjectStreamDict + startOffset int + endOffset int + + decodeFunc DecodeLazyObjectStreamObjectFunc + decodedObject Object + decodedError error +} + +func NewLazyObjectStreamObject(osd *ObjectStreamDict, startOffset, endOffset int, decodeFunc DecodeLazyObjectStreamObjectFunc) Object { + return LazyObjectStreamObject{ + osd: osd, + startOffset: startOffset, + endOffset: endOffset, + + decodeFunc: decodeFunc, + } +} + +func (l LazyObjectStreamObject) Clone() Object { + return LazyObjectStreamObject{ + osd: l.osd, + startOffset: l.startOffset, + endOffset: l.endOffset, + + decodeFunc: l.decodeFunc, + decodedObject: l.decodedObject, + decodedError: l.decodedError, + } +} + +func (l LazyObjectStreamObject) PDFString() string { + data, err := l.GetData() + if err != nil { + panic(err) + } + + return string(data) +} + +func (l LazyObjectStreamObject) String() string { + return l.PDFString() +} + +func (l *LazyObjectStreamObject) GetData() ([]byte, error) { + if err := l.osd.Decode(); err != nil { + return nil, err + } + + var data []byte + if l.endOffset == -1 { + data = l.osd.Content[l.startOffset:] + } else { + data = l.osd.Content[l.startOffset:l.endOffset] + } + return data, nil +} + +func (l *LazyObjectStreamObject) DecodedObject(c context.Context) (Object, error) { + if l.decodedObject == nil && l.decodedError == nil { + data, err := l.GetData() + if err != nil { + return nil, err + } + + if log.ReadEnabled() { + log.Read.Printf("parseObjectStream: objString = %s\n", string(data)) + } + + l.decodedObject, l.decodedError = l.decodeFunc(c, string(data)) + if l.decodedError != nil { + return nil, l.decodedError + } + + if log.ReadEnabled() { + //log.Read.Printf("parseObjectStream: [%d] = obj %s:\n%s\n", i/2-1, objs[i-2], o) + } + } + return l.decodedObject, l.decodedError +} + +// ObjectStreamDict represents a object stream dictionary. +type ObjectStreamDict struct { + StreamDict + Prolog []byte + ObjCount int + FirstObjOffset int + ObjArray Array +} + +// NewObjectStreamDict creates a new ObjectStreamDict object. +func NewObjectStreamDict() *ObjectStreamDict { + sd := StreamDict{Dict: NewDict()} + sd.Insert("Type", Name("ObjStm")) + sd.Insert("Filter", Name(filter.Flate)) + sd.FilterPipeline = []PDFFilter{{Name: filter.Flate, DecodeParms: nil}} + return &ObjectStreamDict{StreamDict: sd} +} + +func parmsForFilter(d Dict) map[string]int { + m := map[string]int{} + + if d == nil { + return m + } + + for k, v := range d { + + i, ok := v.(Integer) + if ok { + m[k] = i.Value() + continue + } + + // Encode boolean values: false -> 0, true -> 1 + b, ok := v.(Boolean) + if ok { + m[k] = 0 + if b.Value() { + m[k] = 1 + } + continue + } + + } + + return m +} + +// Encode applies sd's filter pipeline to sd.Content in order to produce sd.Raw. +func (sd *StreamDict) Encode() error { + if sd.Content == nil && sd.Raw != nil { + // Not decoded yet, no need to encode. + return nil + } + + // No filter specified, nothing to encode. + if sd.FilterPipeline == nil { + if log.TraceEnabled() { + log.Trace.Println("encodeStream: returning uncompressed stream.") + } + sd.Raw = sd.Content + streamLength := int64(len(sd.Raw)) + sd.StreamLength = &streamLength + sd.Update("Length", Integer(streamLength)) + return nil + } + + var b, c io.Reader + b = bytes.NewReader(sd.Content) + + // Apply each filter in the pipeline to result of preceding filter. + + for i := len(sd.FilterPipeline) - 1; i >= 0; i-- { + f := sd.FilterPipeline[i] + if log.TraceEnabled() { + if f.DecodeParms != nil { + log.Trace.Printf("encodeStream: encoding filter:%s\ndecodeParms:%s\n", f.Name, f.DecodeParms) + } else { + log.Trace.Printf("encodeStream: encoding filter:%s\n", f.Name) + } + } + + // Make parms map[string]int + parms := parmsForFilter(f.DecodeParms) + + fi, err := filter.NewFilter(f.Name, parms) + if err != nil { + return err + } + + c, err = fi.Encode(b) + if err != nil { + return err + } + + b = c + } + + if bb, ok := c.(*bytes.Buffer); ok { + sd.Raw = bb.Bytes() + } else { + var buf bytes.Buffer + if _, err := io.Copy(&buf, c); err != nil { + return err + } + + sd.Raw = buf.Bytes() + } + + streamLength := int64(len(sd.Raw)) + sd.StreamLength = &streamLength + sd.Update("Length", Integer(streamLength)) + + return nil +} + +func fixParms(f PDFFilter, parms map[string]int, sd *StreamDict) error { + if f.Name == filter.CCITTFax { + // x/image/ccitt needs the optional decode parameter "Rows" + // if not available we supply image "Height". + _, ok := parms["Rows"] + if !ok { + ip := sd.IntEntry("Height") + if ip == nil { + return errors.New("pdfcpu: ccitt: \"Height\" required") + } + parms["Rows"] = *ip + } + } + return nil +} + +// Decode applies sd's filter pipeline to sd.Raw in order to produce sd.Content. +func (sd *StreamDict) Decode() error { + _, err := sd.DecodeLength(-1) + return err +} + +func (sd *StreamDict) decodeLength(maxLen int64) ([]byte, error) { + var b, c io.Reader + b = bytes.NewReader(sd.Raw) + + // Apply each filter in the pipeline to result of preceding filter. + for idx, f := range sd.FilterPipeline { + + if f.Name == filter.JPX { + break + } + + if f.Name == filter.DCT { + if sd.CSComponents != 4 { + break + } + // if sd.CSComponents == 4 { + // // Special case where we have to do real JPG decoding. + // // Another option is using a dctDecode filter using gob - performance hit? + + // im, err := jpeg.Decode(b) + // if err != nil { + // return err + // } + // sd.DCTImage = im // hacky + // return nil + // } + } + + parms := parmsForFilter(f.DecodeParms) + if err := fixParms(f, parms, sd); err != nil { + return nil, err + } + + fi, err := filter.NewFilter(f.Name, parms) + if err != nil { + return nil, err + } + + if maxLen >= 0 && idx == len(sd.FilterPipeline)-1 { + c, err = fi.DecodeLength(b, maxLen) + } else { + c, err = fi.Decode(b) + } + if err != nil { + return nil, err + } + + //fmt.Printf("decodedStream after:%s\n%s\n", f.Name, hex.Dump(c.Bytes())) + b = c + } + + var data []byte + if bb, ok := c.(*bytes.Buffer); ok { + data = bb.Bytes() + } else { + var buf bytes.Buffer + if _, err := io.Copy(&buf, c); err != nil { + return nil, err + } + + data = buf.Bytes() + } + + if maxLen < 0 { + sd.Content = data + return data, nil + } + + return data[:maxLen], nil +} + +func (sd *StreamDict) DecodeLength(maxLen int64) ([]byte, error) { + if sd.Content != nil { + // This stream has already been decoded. + if maxLen < 0 { + return sd.Content, nil + } + + return sd.Content[:maxLen], nil + } + + fpl := sd.FilterPipeline + + // No filter or sole filter DTC && !CMYK or JPX - nothing to decode. + if fpl == nil || len(fpl) == 1 && ((fpl[0].Name == filter.DCT && sd.CSComponents != 4) || fpl[0].Name == filter.JPX) { + sd.Content = sd.Raw + //fmt.Printf("decodedStream returning %d(#%02x)bytes: \n%s\n", len(sd.Content), len(sd.Content), hex.Dump(sd.Content)) + if maxLen < 0 { + return sd.Content, nil + } + + return sd.Content[:maxLen], nil + } + + //fmt.Printf("decodedStream before:\n%s\n", hex.Dump(sd.Raw)) + + return sd.decodeLength(maxLen) +} + +// IndexedObject returns the object at given index from a ObjectStreamDict. +func (osd *ObjectStreamDict) IndexedObject(index int) (Object, error) { + if osd.ObjArray == nil { + return nil, errors.Errorf("IndexedObject(%d): object not available", index) + } + return osd.ObjArray[index], nil +} + +// AddObject adds another object to this object stream. +// Relies on decoded content! +func (osd *ObjectStreamDict) AddObject(objNumber int, pdfString string) error { + offset := len(osd.Content) + s := "" + if osd.ObjCount > 0 { + s = " " + } + s = s + fmt.Sprintf("%d %d", objNumber, offset) + osd.Prolog = append(osd.Prolog, []byte(s)...) + //pdfString := entry.Object.PDFString() + osd.Content = append(osd.Content, []byte(pdfString)...) + osd.ObjCount++ + if log.TraceEnabled() { + log.Trace.Printf("AddObject end : ObjCount:%d prolog = <%s> Content = <%s>\n", osd.ObjCount, osd.Prolog, osd.Content) + } + return nil +} + +// Finalize prepares the final content of the objectstream. +func (osd *ObjectStreamDict) Finalize() { + osd.Content = append(osd.Prolog, osd.Content...) + osd.FirstObjOffset = len(osd.Prolog) + if log.TraceEnabled() { + log.Trace.Printf("Finalize : firstObjOffset:%d Content = <%s>\n", osd.FirstObjOffset, osd.Content) + } +} + +// XRefStreamDict represents a cross reference stream dictionary. +type XRefStreamDict struct { + StreamDict + Size int + Objects []int + W [3]int + PreviousOffset *int64 +} diff --git a/pkg/pdfcpu/types/string.go b/pkg/pdfcpu/types/string.go new file mode 100644 index 0000000000000000000000000000000000000000..9150346ec5654a1f5d07f89068acbb6b59831bd2 --- /dev/null +++ b/pkg/pdfcpu/types/string.go @@ -0,0 +1,306 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 types + +import ( + "bytes" + "encoding/hex" + "strconv" + "strings" + "unicode/utf8" + + "github.com/pkg/errors" + "golang.org/x/text/unicode/norm" +) + +// NewStringSet returns a new StringSet for slice. +func NewStringSet(slice []string) StringSet { + strSet := StringSet{} + if slice == nil { + return strSet + } + for _, s := range slice { + strSet[s] = true + } + return strSet +} + +// Convert a 1,2 or 3 digit unescaped octal string into the corresponding byte value. +func ByteForOctalString(octalBytes string) (b byte) { + i, _ := strconv.ParseInt(octalBytes, 8, 64) + return byte(i) +} + +// Escape applies all defined escape sequences to s. +func Escape(s string) (*string, error) { + + var b bytes.Buffer + + for i := 0; i < len(s); i++ { + + c := s[i] + + switch c { + case 0x0A: + c = 'n' + case 0x0D: + c = 'r' + case 0x09: + c = 't' + case 0x08: + c = 'b' + case 0x0C: + c = 'f' + case '\\', '(', ')': + default: + b.WriteByte(c) + continue + } + + b.WriteByte('\\') + b.WriteByte(c) + } + + s1 := b.String() + + return &s1, nil +} + +func escaped(c byte) (bool, byte) { + + switch c { + case 'n': + c = 0x0A + case 'r': + c = 0x0D + case 't': + c = 0x09 + case 'b': + c = 0x08 + case 'f': + c = 0x0C + case '(', ')': + case '0', '1', '2', '3', '4', '5', '6', '7': + return true, c + } + + return false, c +} + +func regularChar(c byte, esc bool) bool { + return c != 0x5c && !esc +} + +// Unescape resolves all escape sequences of s. +func Unescape(s string) ([]byte, error) { + var esc bool + var longEol bool + var octalCode string + var b bytes.Buffer + + for i := 0; i < len(s); i++ { + + c := s[i] + + if longEol { + esc = false + longEol = false + // c is completing a 0x5C0D0A line break. + if c == 0x0A { + continue + } + } + + if len(octalCode) > 0 { + if strings.ContainsRune("01234567", rune(c)) { + octalCode = octalCode + string(c) + if len(octalCode) == 3 { + b.WriteByte(ByteForOctalString(octalCode)) + octalCode = "" + esc = false + } + continue + } + b.WriteByte(ByteForOctalString(octalCode)) + octalCode = "" + esc = false + } + + if regularChar(c, esc) { + b.WriteByte(c) + continue + } + + if c == 0x5c { // '\' + if !esc { // Start escape sequence. + esc = true + } else { // Escaped \ + if len(octalCode) > 0 { + return nil, errors.Errorf("Unescape: illegal \\ in octal code sequence detected %X", octalCode) + } + b.WriteByte(c) + esc = false + } + continue + } + + // escaped = true && any other than \ + + // Ignore \eol line breaks. + if c == 0x0A { + esc = false + continue + } + + if c == 0x0D { + longEol = true + continue + } + + // Relax for issue 305 and also accept "\ ". + //if !enc && !strings.ContainsRune(" nrtbf()01234567", rune(c)) { + // return nil, errors.Errorf("Unescape: illegal escape sequence \\%c detected: <%s>", c, s) + //} + + var octal bool + octal, c = escaped(c) + if octal { + octalCode += string(c) + continue + } + + b.WriteByte(c) + esc = false + } + + if len(octalCode) > 0 { + b.WriteByte(ByteForOctalString(octalCode)) + } + + return b.Bytes(), nil +} + +// UTF8ToCP1252 converts UTF-8 to CP1252. Unused +func UTF8ToCP1252(s string) string { + bb := []byte{} + for _, r := range s { + bb = append(bb, byte(r)) + } + return string(bb) +} + +// CP1252ToUTF8 converts CP1252 to UTF-8. Unused +func CP1252ToUTF8(s string) string { + utf8Buf := make([]byte, utf8.UTFMax) + bb := []byte{} + for i := 0; i < len(s); i++ { + n := utf8.EncodeRune(utf8Buf, rune(s[i])) + bb = append(bb, utf8Buf[:n]...) + } + return string(bb) +} + +// Reverse reverses the runes within s. +func Reverse(s string) string { + inRunes := []rune(norm.NFC.String(s)) + outRunes := make([]rune, len(inRunes)) + iMax := len(inRunes) - 1 + for i, r := range inRunes { + outRunes[iMax-i] = r + } + return string(outRunes) +} + +// needsHexSequence checks if a given character must be hex-encoded. +// See "7.3.5 Name Objects" for details. +func needsHexSequence(c byte) bool { + switch c { + case '(', ')', '<', '>', '[', ']', '{', '}', '/', '%', '#': + // Delimiter characters (see "7.2.2 Character Set") + return true + } + return c < '!' || c > '~' +} + +// EncodeName applies name encoding according to PDF spec. +func EncodeName(s string) string { + replaced := false + var sb strings.Builder // will be used only if replacements are necessary + for i := 0; i < len(s); i++ { + ch := s[i] + // TODO: Handle invalid character 0x00, 2nd error return value + if needsHexSequence(ch) { + if !replaced { + sb.WriteString(s[:i]) + } + sb.WriteByte('#') + sb.WriteString(hex.EncodeToString([]byte{ch})) + replaced = true + } else if replaced { + sb.WriteByte(ch) + } + } + if !replaced { + return s + } + return sb.String() +} + +// DecodeName applies name decoding according to PDF spec. +func DecodeName(s string) (string, error) { + replaced := false + var sb strings.Builder // will be used only if replacements are necessary + for i := 0; i < len(s); i++ { + c := s[i] + if c == 0 { + return "", errors.New("a name may not contain a null byte") + } else if c != '#' { + if replaced { + sb.WriteByte(c) + } + continue + } + + // # detected, next 2 chars have to exist. + if len(s) < i+3 { + return "", errors.New("not enough characters after #") + } + + s1 := s[i+1 : i+3] + + // And they have to be hex characters. + decoded, err := hex.DecodeString(s1) + if err != nil { + return "", err + } + + if decoded[0] == 0 { + return "", errors.New("a name may not contain a null byte") + } + + if !replaced { + sb.WriteString(s[:i]) + replaced = true + } + sb.Write(decoded) + i += 2 + } + if !replaced { + return s, nil + } + return sb.String(), nil +} diff --git a/pkg/pdfcpu/types/string_test.go b/pkg/pdfcpu/types/string_test.go new file mode 100644 index 0000000000000000000000000000000000000000..2fe19c321a117b48490cd9988f517418ece4f6d3 --- /dev/null +++ b/pkg/pdfcpu/types/string_test.go @@ -0,0 +1,200 @@ +/* +Copyright 2022 The pdfcpu Authors. + +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 + + 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 types + +import ( + "bytes" + "testing" +) + +func TestByteForOctalString(t *testing.T) { + tests := []struct { + input string + expected byte + }{ + { + "001", + 0x1, + }, + { + "01", + 0x1, + }, + { + "1", + 0x1, + }, + { + "010", + 0x8, + }, + { + "020", + 0x10, + }, + { + "377", + 0xff, + }, + } + for _, test := range tests { + t.Run(test.input, func(t *testing.T) { + got := ByteForOctalString(test.input) + if got != test.expected { + t.Errorf("got %x; want %x", got, test.expected) + } + }) + } +} + +func TestUnescapeStringWithOctal(t *testing.T) { + tests := []struct { + input string + expected []byte + }{ + { + "\\5", + []byte{0x05}, + }, + { + "\\5a", + []byte{0x05, 'a'}, + }, + { + "\\5\\5", + []byte{0x05, 0x05}, + }, + { + "\\53", + []byte{'+'}, + }, + { + "\\53a", + []byte{'+', 'a'}, + }, + { + "\\053", + []byte{'+'}, + }, + { + "\\0053", + []byte{0x05, '3'}, + }, + } + for _, test := range tests { + t.Run(test.input, func(t *testing.T) { + got, err := Unescape(test.input) + if err != nil { + t.Fail() + } + if !bytes.Equal(got, test.expected) { + t.Errorf("got %x; want %x", got, test.expected) + } + }) + } +} + +func TestDecodeName(t *testing.T) { + tests := []struct { + input string + expected string + }{ + { + "", + "", + }, + { + "Size", + "Size", + }, + { + "S#69#7a#65", + "Size", + }, + { + "#52#6f#6f#74", + "Root", + }, + { + "#4f#75t#6c#69#6e#65#73", + "Outlines", + }, + { + "C#6fu#6et", + "Count", + }, + { + "K#69#64s", + "Kids", + }, + { + "#50a#72e#6et", + "Parent", + }, + { + "#4d#65di#61#42#6f#78", + "MediaBox", + }, + { + "#46#69#6c#74er", + "Filter", + }, + { + "#46#6ca#74e#44#65c#6fde", + "FlateDecode", + }, + { + "A#53#43#49I#48e#78D#65code", + "ASCIIHexDecode", + }, + } + for _, test := range tests { + t.Run(test.input, func(t *testing.T) { + got, err := DecodeName(test.input) + if err != nil { + t.Fail() + } + if got != test.expected { + t.Errorf("got %x; want %x", got, test.expected) + } + }) + } +} + +func TestEncodeName(t *testing.T) { + testcases := []struct { + Input string + Expected string + }{ + {"Foo", "Foo"}, + {"A#", "A#23"}, + {"F#o", "F#23o"}, + {"A;Name_With-Various***Characters?", "A;Name_With-Various***Characters?"}, + {"1.2", "1.2"}, + {"$$", "$$"}, + {"@pattern", "@pattern"}, + {".notdef", ".notdef"}, + {"Lime Green", "Lime#20Green"}, + {"paired()parentheses", "paired#28#29parentheses"}, + {"The_Key_of_F#_Minor", "The_Key_of_F#23_Minor"}, + } + for _, tc := range testcases { + if encoded := EncodeName(tc.Input); encoded != tc.Expected { + t.Errorf("expected %s for %s, got %s", tc.Expected, tc.Input, encoded) + } + } +} diff --git a/pkg/pdfcpu/types/types.go b/pkg/pdfcpu/types/types.go new file mode 100644 index 0000000000000000000000000000000000000000..bbc0c5c702bca2a88c0a0e40f88723bb2214ed60 --- /dev/null +++ b/pkg/pdfcpu/types/types.go @@ -0,0 +1,624 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 types + +import ( + "encoding/hex" + "fmt" + "strconv" +) + +// Supported line delimiters +const ( + EolLF = "\x0A" + EolCR = "\x0D" + EolCRLF = "\x0D\x0A" +) + +// FreeHeadGeneration is the predefined generation number for the head of the free list. +const FreeHeadGeneration = 65535 + +// ByteSize represents the various terms for storage space. +type ByteSize float64 + +// Storage space terms. +const ( + _ = iota // ignore first value by assigning to blank identifier + KB ByteSize = 1 << (10 * iota) + MB + GB +) + +func (b ByteSize) String() string { + + switch { + case b >= GB: + return fmt.Sprintf("%.2f GB", b/GB) + case b >= MB: + return fmt.Sprintf("%.1f MB", b/MB) + case b >= KB: + return fmt.Sprintf("%.0f KB", b/KB) + } + + return fmt.Sprintf("%.0f", b) +} + +// IntSet is a set of integers. +type IntSet map[int]bool + +// StringSet is a set of strings. +type StringSet map[string]bool + +// Object defines an interface for all Objects. +type Object interface { + fmt.Stringer + Clone() Object + PDFString() string +} + +// Boolean represents a PDF boolean object. +type Boolean bool + +// Clone returns a clone of boolean. +func (boolean Boolean) Clone() Object { + return boolean +} + +func (boolean Boolean) String() string { + return fmt.Sprintf("%v", bool(boolean)) +} + +// PDFString returns a string representation as found in and written to a PDF file. +func (boolean Boolean) PDFString() string { + return boolean.String() +} + +// Value returns a bool value for this PDF object. +func (boolean Boolean) Value() bool { + return bool(boolean) +} + +/////////////////////////////////////////////////////////////////////////////////// + +// Float represents a PDF float object. +type Float float64 + +// Clone returns a clone of f. +func (f Float) Clone() Object { + return f +} + +func (f Float) String() string { + // Use a precision of 2 for logging readability. + return fmt.Sprintf("%.2f", float64(f)) +} + +// PDFString returns a string representation as found in and written to a PDF file. +func (f Float) PDFString() string { + // The max precision encountered so far has been 12 (fontType3 fontmatrix components). + return strconv.FormatFloat(f.Value(), 'f', 12, 64) +} + +// Value returns a float64 value for this PDF object. +func (f Float) Value() float64 { + return float64(f) +} + +/////////////////////////////////////////////////////////////////////////////////// + +// Integer represents a PDF integer object. +type Integer int + +// Clone returns a clone of i. +func (i Integer) Clone() Object { + return i +} + +func (i Integer) String() string { + return strconv.Itoa(int(i)) +} + +// PDFString returns a string representation as found in and written to a PDF file. +func (i Integer) PDFString() string { + return i.String() +} + +// Value returns an int value for this PDF object. +func (i Integer) Value() int { + return int(i) +} + +/////////////////////////////////////////////////////////////////////////////////// + +// Point represents a user space location. +type Point struct { + X float64 `json:"x"` + Y float64 `json:"y"` +} + +func NewPoint(x, y float64) Point { + return Point{X: x, Y: y} +} + +// Translate modifies p's coordinates. +func (p *Point) Translate(dx, dy float64) { + p.X += dx + p.Y += dy +} + +func (p Point) String() string { + return fmt.Sprintf("(%.2f,%.2f)\n", p.X, p.Y) +} + +// Rectangle represents a rectangular region in userspace. +type Rectangle struct { + LL Point `json:"ll"` + UR Point `json:"ur"` +} + +// NewRectangle returns a new rectangle for given corner coordinates. +func NewRectangle(llx, lly, urx, ury float64) *Rectangle { + return &Rectangle{LL: Point{llx, lly}, UR: Point{urx, ury}} +} + +// RectForDim returns a new rectangle for given dimensions. +func RectForDim(width, height float64) *Rectangle { + return NewRectangle(0.0, 0.0, width, height) +} + +// RectForWidthAndHeight returns a new rectangle for given dimensions. +func RectForWidthAndHeight(llx, lly, width, height float64) *Rectangle { + return NewRectangle(llx, lly, llx+width, lly+height) +} + +// RectForFormat returns a new rectangle for given format. +func RectForFormat(f string) *Rectangle { + d := PaperSize[f] + return RectForDim(d.Width, d.Height) +} + +// Width returns the horizontal span of a rectangle in userspace. +func (r Rectangle) Width() float64 { + return r.UR.X - r.LL.X +} + +// Height returns the vertical span of a rectangle in userspace. +func (r Rectangle) Height() float64 { + return r.UR.Y - r.LL.Y +} + +func (r Rectangle) Equals(r2 Rectangle) bool { + return r.LL == r2.LL && r.UR == r2.UR +} + +// FitsWithin returns true if rectangle r fits within rectangle r2. +func (r Rectangle) FitsWithin(r2 *Rectangle) bool { + return r.Width() <= r2.Width() && r.Height() <= r2.Height() +} + +// AspectRatio returns the relation between width and height of a rectangle. +func (r Rectangle) AspectRatio() float64 { + return r.Width() / r.Height() +} + +// Landscape returns true if r is in landscape mode. +func (r Rectangle) Landscape() bool { + return r.AspectRatio() > 1 +} + +// Portrait returns true if r is in portrait mode. +func (r Rectangle) Portrait() bool { + return r.AspectRatio() < 1 +} + +// Contains returns true if rectangle r contains point p. +func (r Rectangle) Contains(p Point) bool { + return p.X >= r.LL.X && p.X <= r.UR.X && p.Y >= r.LL.Y && p.Y <= r.LL.Y +} + +// ScaledWidth returns the width for given height according to r's aspect ratio. +func (r Rectangle) ScaledWidth(h float64) float64 { + return r.AspectRatio() * h +} + +// ScaledHeight returns the height for given width according to r's aspect ratio. +func (r Rectangle) ScaledHeight(w float64) float64 { + return w / r.AspectRatio() +} + +// Dimensions returns r's dimensions. +func (r Rectangle) Dimensions() Dim { + return Dim{r.Width(), r.Height()} +} + +// Translate moves r by dx and dy. +func (r *Rectangle) Translate(dx, dy float64) { + r.LL.Translate(dx, dy) + r.UR.Translate(dx, dy) +} + +// Center returns the center point of a rectangle. +func (r Rectangle) Center() Point { + return Point{(r.UR.X - r.Width()/2), (r.UR.Y - r.Height()/2)} +} + +func (r Rectangle) String() string { + return fmt.Sprintf("(%3.2f, %3.2f, %3.2f, %3.2f) w=%.2f h=%.2f ar=%.2f", r.LL.X, r.LL.Y, r.UR.X, r.UR.Y, r.Width(), r.Height(), r.AspectRatio()) +} + +// ShortString returns a compact string representation for r. +func (r Rectangle) ShortString() string { + return fmt.Sprintf("(%3.0f, %3.0f, %3.0f, %3.0f)", r.LL.X, r.LL.Y, r.UR.X, r.UR.Y) +} + +// Array returns the PDF representation of a rectangle. +func (r Rectangle) Array() Array { + return NewNumberArray(r.LL.X, r.LL.Y, r.UR.X, r.UR.Y) +} + +// Clone returns a clone of r. +func (r Rectangle) Clone() *Rectangle { + return NewRectangle(r.LL.X, r.LL.Y, r.UR.X, r.UR.Y) +} + +// CroppedCopy returns a copy of r with applied margin.. +func (r Rectangle) CroppedCopy(margin float64) *Rectangle { + return NewRectangle(r.LL.X+margin, r.LL.Y+margin, r.UR.X-margin, r.UR.Y-margin) +} + +// ToInches converts r to inches. +func (r Rectangle) ToInches() *Rectangle { + return NewRectangle(r.LL.X*userSpaceToInch, r.LL.Y*userSpaceToInch, r.UR.X*userSpaceToInch, r.UR.Y*userSpaceToInch) +} + +// ToCentimetres converts r to centimetres. +func (r Rectangle) ToCentimetres() *Rectangle { + return NewRectangle(r.LL.X*userSpaceToCm, r.LL.Y*userSpaceToCm, r.UR.X*userSpaceToCm, r.UR.Y*userSpaceToCm) +} + +// ToMillimetres converts r to millimetres. +func (r Rectangle) ToMillimetres() *Rectangle { + return NewRectangle(r.LL.X*userSpaceToMm, r.LL.Y*userSpaceToMm, r.UR.X*userSpaceToMm, r.UR.Y*userSpaceToMm) +} + +// ConvertToUnit converts r to unit. +func (r *Rectangle) ConvertToUnit(unit DisplayUnit) *Rectangle { + switch unit { + case INCHES: + return r.ToInches() + case CENTIMETRES: + return r.ToCentimetres() + case MILLIMETRES: + return r.ToMillimetres() + } + return r +} + +func (r Rectangle) formatToInches() string { + return fmt.Sprintf("(%3.2f, %3.2f, %3.2f, %3.2f) w=%.2f h=%.2f ar=%.2f", + r.LL.X*userSpaceToInch, + r.LL.Y*userSpaceToInch, + r.UR.X*userSpaceToInch, + r.UR.Y*userSpaceToInch, + r.Width()*userSpaceToInch, + r.Height()*userSpaceToInch, + r.AspectRatio()) +} + +func (r Rectangle) formatToCentimetres() string { + return fmt.Sprintf("(%3.2f, %3.2f, %3.2f, %3.2f) w=%.2f h=%.2f ar=%.2f", + r.LL.X*userSpaceToCm, + r.LL.Y*userSpaceToCm, + r.UR.X*userSpaceToCm, + r.UR.Y*userSpaceToCm, + r.Width()*userSpaceToCm, + r.Height()*userSpaceToCm, + r.AspectRatio()) +} + +func (r Rectangle) formatToMillimetres() string { + return fmt.Sprintf("(%3.2f, %3.2f, %3.2f, %3.2f) w=%.2f h=%.2f ar=%.2f", + r.LL.X*userSpaceToMm, + r.LL.Y*userSpaceToMm, + r.UR.X*userSpaceToMm, + r.UR.Y*userSpaceToMm, + r.Width()*userSpaceToMm, + r.Height()*userSpaceToMm, + r.AspectRatio()) +} + +// Format returns r's details converted into unit. +func (r Rectangle) Format(unit DisplayUnit) string { + switch unit { + case INCHES: + return r.formatToInches() + case CENTIMETRES: + return r.formatToCentimetres() + case MILLIMETRES: + return r.formatToMillimetres() + } + return r.String() +} + +/////////////////////////////////////////////////////////////////////////////////// + +// QuadLiteral is a polygon with four edges and four vertices. +// The four vertices are assumed to be specified in counter clockwise order. +type QuadLiteral struct { + P1, P2, P3, P4 Point +} + +func NewQuadLiteralForRect(r *Rectangle) *QuadLiteral { + // p1 := Point{X: r.LL.X, Y: r.LL.Y} + // p2 := Point{X: r.UR.X, Y: r.LL.Y} + // p3 := Point{X: r.UR.X, Y: r.UR.Y} + // p4 := Point{X: r.LL.X, Y: r.UR.Y} + + p3 := Point{X: r.LL.X, Y: r.LL.Y} + p4 := Point{X: r.UR.X, Y: r.LL.Y} + p2 := Point{X: r.UR.X, Y: r.UR.Y} + p1 := Point{X: r.LL.X, Y: r.UR.Y} + + return &QuadLiteral{P1: p1, P2: p2, P3: p3, P4: p4} +} + +// Array returns the PDF representation of ql. +func (ql QuadLiteral) Array() Array { + return NewNumberArray(ql.P1.X, ql.P1.Y, ql.P2.X, ql.P2.Y, ql.P3.X, ql.P3.Y, ql.P4.X, ql.P4.Y) +} + +// EnclosingRectangle calculates the rectangle enclosing ql's vertices at a distance f. +func (ql QuadLiteral) EnclosingRectangle(f float64) *Rectangle { + xmin, xmax := ql.P1.X, ql.P1.X + ymin, ymax := ql.P1.Y, ql.P1.Y + for _, p := range []Point{ql.P2, ql.P3, ql.P4} { + if p.X < xmin { + xmin = p.X + } else if p.X > xmax { + xmax = p.X + } + if p.Y < ymin { + ymin = p.Y + } else if p.Y > ymax { + ymax = p.Y + } + } + return NewRectangle(xmin-f, ymin-f, xmax+f, ymax+f) +} + +// QuadPoints is an array of 8 × n numbers specifying the coordinates of n quadrilaterals in default user space. +type QuadPoints []QuadLiteral + +// AddQuadLiteral adds a quadliteral to qp. +func (qp *QuadPoints) AddQuadLiteral(ql QuadLiteral) { + *qp = append(*qp, ql) +} + +// Array returns the PDF representation of qp. +func (qp *QuadPoints) Array() Array { + a := Array{} + for _, ql := range *qp { + a = append(a, ql.Array()...) + } + return a +} + +/////////////////////////////////////////////////////////////////////////////////// + +// Name represents a PDF name object. +type Name string + +// Clone returns a clone of nameObject. +func (nameObject Name) Clone() Object { + return nameObject +} + +func (nameObject Name) String() string { + return fmt.Sprint(string(nameObject)) +} + +// PDFString returns a string representation as found in and written to a PDF file. +func (nameObject Name) PDFString() string { + s := " " + if len(nameObject) > 0 { + s = EncodeName(string(nameObject)) + } + return fmt.Sprintf("/%s", s) +} + +// Value returns a string value for this PDF object. +func (nameObject Name) Value() string { + return string(nameObject) +} + +/////////////////////////////////////////////////////////////////////////////////// + +// StringLiteral represents a PDF string literal object. +type StringLiteral string + +// Clone returns a clone of stringLiteral. +func (stringliteral StringLiteral) Clone() Object { + return stringliteral +} + +func (stringliteral StringLiteral) String() string { + return fmt.Sprintf("(%s)", string(stringliteral)) +} + +// PDFString returns a string representation as found in and written to a PDF file. +func (stringliteral StringLiteral) PDFString() string { + return stringliteral.String() +} + +// Value returns a string value for this PDF object. +func (stringliteral StringLiteral) Value() string { + return string(stringliteral) +} + +/////////////////////////////////////////////////////////////////////////////////// + +// HexLiteral represents a PDF hex literal object. +type HexLiteral string + +// NewHexLiteral creates a new HexLiteral for b.. +func NewHexLiteral(b []byte) HexLiteral { + return HexLiteral(hex.EncodeToString(b)) +} + +// Clone returns a clone of hexliteral. +func (hexliteral HexLiteral) Clone() Object { + return hexliteral +} +func (hexliteral HexLiteral) String() string { + return fmt.Sprintf("<%s>", string(hexliteral)) +} + +// PDFString returns the string representation as found in and written to a PDF file. +func (hexliteral HexLiteral) PDFString() string { + return hexliteral.String() +} + +// Value returns a string value for this PDF object. +func (hexliteral HexLiteral) Value() string { + return string(hexliteral) +} + +// Bytes returns the byte representation. +func (hexliteral HexLiteral) Bytes() ([]byte, error) { + return hex.DecodeString(hexliteral.Value()) +} + +/////////////////////////////////////////////////////////////////////////////////// + +// IndirectRef represents a PDF indirect object. +type IndirectRef struct { + ObjectNumber Integer + GenerationNumber Integer +} + +// NewIndirectRef returns a new PDFIndirectRef object. +func NewIndirectRef(objectNumber, generationNumber int) *IndirectRef { + return &IndirectRef{ + ObjectNumber: Integer(objectNumber), + GenerationNumber: Integer(generationNumber)} +} + +// Clone returns a clone of ir. +func (ir IndirectRef) Clone() Object { + ir2 := ir + return ir2 +} + +func (ir IndirectRef) String() string { + return fmt.Sprintf("(%s)", ir.PDFString()) +} + +// PDFString returns a string representation as found in and written to a PDF file. +func (ir IndirectRef) PDFString() string { + return fmt.Sprintf("%d %d R", ir.ObjectNumber, ir.GenerationNumber) +} + +///////////////////////////////////////////////////////////////////////////////////// + +// DisplayUnit is the metric unit used to output paper sizes. +type DisplayUnit int + +// Options for display unit in effect. +const ( + POINTS DisplayUnit = iota + INCHES + CENTIMETRES + MILLIMETRES +) + +const ( + userSpaceToInch = float64(1) / 72 + userSpaceToCm = 2.54 / 72 + userSpaceToMm = userSpaceToCm * 10 + + inchToUserSpace = 1 / userSpaceToInch + cmToUserSpace = 1 / userSpaceToCm + mmToUserSpace = 1 / userSpaceToMm +) + +func ToUserSpace(f float64, unit DisplayUnit) float64 { + switch unit { + case INCHES: + return f * inchToUserSpace + case CENTIMETRES: + return f * cmToUserSpace + case MILLIMETRES: + return f * mmToUserSpace + + } + return f +} + +// Dim represents the dimensions of a rectangular view medium +// like a PDF page, a sheet of paper or an image grid +// in user space, inches, centimetres or millimetres. +type Dim struct { + Width float64 `json:"width"` + Height float64 `json:"height"` +} + +// ToInches converts d to inches. +func (d Dim) ToInches() Dim { + return Dim{d.Width * userSpaceToInch, d.Height * userSpaceToInch} +} + +// ToCentimetres converts d to centimetres. +func (d Dim) ToCentimetres() Dim { + return Dim{d.Width * userSpaceToCm, d.Height * userSpaceToCm} +} + +// ToMillimetres converts d to millimetres. +func (d Dim) ToMillimetres() Dim { + return Dim{d.Width * userSpaceToMm, d.Height * userSpaceToMm} +} + +// ConvertToUnit converts d to unit. +func (d Dim) ConvertToUnit(unit DisplayUnit) Dim { + switch unit { + case INCHES: + return d.ToInches() + case CENTIMETRES: + return d.ToCentimetres() + case MILLIMETRES: + return d.ToMillimetres() + } + return d +} + +// AspectRatio returns the relation between width and height. +func (d Dim) AspectRatio() float64 { + return d.Width / d.Height +} + +// Landscape returns true if d is in landscape mode. +func (d Dim) Landscape() bool { + return d.AspectRatio() > 1 +} + +// Portrait returns true if d is in portrait mode. +func (d Dim) Portrait() bool { + return d.AspectRatio() < 1 +} + +func (d Dim) String() string { + return fmt.Sprintf("%fx%f", d.Width, d.Height) +} diff --git a/pkg/pdfcpu/types/utf16.go b/pkg/pdfcpu/types/utf16.go new file mode 100644 index 0000000000000000000000000000000000000000..8aed84c47e806c433f14ec18143d1134be2e0bdf --- /dev/null +++ b/pkg/pdfcpu/types/utf16.go @@ -0,0 +1,176 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 types + +import ( + "bytes" + "encoding/hex" + "fmt" + "strings" + "unicode/utf16" + "unicode/utf8" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pkg/errors" +) + +// ErrInvalidUTF16BE represents an error that gets raised for invalid UTF-16BE byte sequences. +var ErrInvalidUTF16BE = errors.New("pdfcpu: invalid UTF-16BE detected") + +// IsStringUTF16BE checks a string for Big Endian byte order BOM. +func IsStringUTF16BE(s string) bool { + s1 := fmt.Sprint(s) + ok := strings.HasPrefix(s1, "\376\377") // 0xFE 0xFF + return ok +} + +// IsUTF16BE checks for Big Endian byte order mark and valid length. +func IsUTF16BE(b []byte) bool { + if len(b) == 0 || len(b)%2 != 0 { + return false + } + // Check BOM + return b[0] == 0xFE && b[1] == 0xFF +} + +func decodeUTF16String(b []byte) (string, error) { + // Convert UTF-16 to UTF-8 + // We only accept big endian byte order. + if !IsUTF16BE(b) { + if log.DebugEnabled() { + log.Debug.Printf("decodeUTF16String: not UTF16BE: %s\n", hex.Dump(b)) + } + return "", ErrInvalidUTF16BE + } + + // Strip BOM. + b = b[2:] + + // code points + u16 := make([]uint16, 0, len(b)) + + // Collect code points. + for i := 0; i < len(b); { + + val := (uint16(b[i]) << 8) + uint16(b[i+1]) + + if val <= 0xD7FF || val > 0xE000 && val <= 0xFFFF { + // Basic Multilingual Plane + u16 = append(u16, val) + i += 2 + continue + } + + // Ensure bytes needed in order to decode surrogate pair. + if i+2 >= len(b) { + return "", errors.Errorf("decodeUTF16String: corrupt UTF16BE byte length on unicode point 1: %v", b) + } + + // Ensure high surrogate is leading in possible surrogate pair. + if val >= 0xDC00 && val <= 0xDFFF { + return "", errors.Errorf("decodeUTF16String: corrupt UTF16BE on unicode point 1: %v", b) + } + + // Supplementary Planes + u16 = append(u16, val) + val = (uint16(b[i+2]) << 8) + uint16(b[i+3]) + if val < 0xDC00 || val > 0xDFFF { + return "", errors.Errorf("decodeUTF16String: corrupt UTF16BE on unicode point 2: %v", b) + } + + u16 = append(u16, val) + i += 4 + } + + decb := []byte{} + utf8Buf := make([]byte, utf8.UTFMax) + + for _, rune := range utf16.Decode(u16) { + n := utf8.EncodeRune(utf8Buf, rune) + decb = append(decb, utf8Buf[:n]...) + } + + return string(decb), nil +} + +// DecodeUTF16String decodes a UTF16BE string from a hex string. +func DecodeUTF16String(s string) (string, error) { + return decodeUTF16String([]byte(s)) +} + +func EncodeUTF16String(s string) string { + rr := utf16.Encode([]rune(s)) + bb := []byte{0xFE, 0xFF} + for _, r := range rr { + bb = append(bb, byte(r>>8), byte(r&0xFF)) + } + return string(bb) +} + +func EscapeUTF16String(s string) (*string, error) { + return Escape(EncodeUTF16String(s)) +} + +// StringLiteralToString returns the best possible string rep for a string literal. +func StringLiteralToString(sl StringLiteral) (string, error) { + bb, err := Unescape(sl.Value()) + if err != nil { + return "", err + } + if IsUTF16BE(bb) { + return decodeUTF16String(bb) + } + // if no acceptable UTF16 encoding found, ensure utf8 encoding. + bb = bytes.TrimPrefix(bb, []byte{239, 187, 191}) + s := string(bb) + if !utf8.ValidString(s) { + s = CP1252ToUTF8(s) + } + return s, nil +} + +// HexLiteralToString returns a possibly UTF16 encoded string for a hex string. +func HexLiteralToString(hl HexLiteral) (string, error) { + bb, err := hl.Bytes() + if err != nil { + return "", err + } + if IsUTF16BE(bb) { + return decodeUTF16String(bb) + } + + bb, err = Unescape(string(bb)) + if err != nil { + return "", err + } + + bb = bytes.TrimPrefix(bb, []byte{239, 187, 191}) + + return string(bb), nil +} + +func StringOrHexLiteral(obj Object) (*string, error) { + if sl, ok := obj.(StringLiteral); ok { + s, err := StringLiteralToString(sl) + return &s, err + } + if hl, ok := obj.(HexLiteral); ok { + s, err := HexLiteralToString(hl) + return &s, err + } + return nil, errors.New("pdfcpu: expected StringLiteral or HexLiteral") +} diff --git a/pkg/pdfcpu/validate/action.go b/pkg/pdfcpu/validate/action.go new file mode 100644 index 0000000000000000000000000000000000000000..3268f9907b0210b3c70730f76536ec928668df24 --- /dev/null +++ b/pkg/pdfcpu/validate/action.go @@ -0,0 +1,956 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 validate + +import ( + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +func validateGoToActionDict(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.6.4.2 Go-To Actions + required := REQUIRED + if xRefTable.ValidationMode == model.ValidationRelaxed { + required = OPTIONAL + } + + // D, required, name, byte string or array + return validateActionDestinationEntry(xRefTable, d, dictName, "D", required, model.V10) +} + +func validateGoToRActionDict(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.6.4.3 Remote Go-To Actions + + // F, required, file specification + _, err := validateFileSpecEntry(xRefTable, d, dictName, "F", REQUIRED, model.V11) + if err != nil { + return err + } + + // D, required, name, byte string or array + err = validateActionDestinationEntry(xRefTable, d, dictName, "D", REQUIRED, model.V10) + if err != nil { + return err + } + + // NewWindow, optional, boolean, since V1.2 + _, err = validateBooleanEntry(xRefTable, d, dictName, "NewWindow", OPTIONAL, model.V12, nil) + + return err +} + +func validateTargetDictEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) error { + + // table 202 + + d1, err := validateDictEntry(xRefTable, d, dictName, entryName, required, sinceVersion, nil) + if err != nil || d1 == nil { + return err + } + + dictName = "targetDict" + + // R, required, name + _, err = validateNameEntry(xRefTable, d1, dictName, "R", REQUIRED, model.V10, func(s string) bool { return s == "P" || s == "C" }) + if err != nil { + return err + } + + // N, optional, byte string + _, err = validateStringEntry(xRefTable, d1, dictName, "N", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // P, optional, integer or byte string + err = validateIntOrStringEntry(xRefTable, d1, dictName, "P", OPTIONAL, model.V10) + if err != nil { + return err + } + + // A, optional, integer or text string + err = validateIntOrStringEntry(xRefTable, d1, dictName, "A", OPTIONAL, model.V10) + if err != nil { + return err + } + + // T, optional, target dict + return validateTargetDictEntry(xRefTable, d1, dictName, "T", OPTIONAL, model.V10) +} + +func validateGoToEActionDict(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.6.4.4 Embedded Go-To Actions + + // F, optional, file specification + f, err := validateFileSpecEntry(xRefTable, d, dictName, "F", OPTIONAL, model.V11) + if err != nil { + return err + } + + // D, required, name, byte string or array + err = validateActionDestinationEntry(xRefTable, d, dictName, "D", REQUIRED, model.V10) + if err != nil { + return err + } + + // NewWindow, optional, boolean, since V1.2 + _, err = validateBooleanEntry(xRefTable, d, dictName, "NewWindow", OPTIONAL, model.V12, nil) + if err != nil { + return err + } + + // T, required unless entry F is present, target dict + return validateTargetDictEntry(xRefTable, d, dictName, "T", f == nil, model.V10) +} + +func validateWinDict(xRefTable *model.XRefTable, d types.Dict) error { + + // see table 204 + + dictName := "winDict" + + // F, required, byte string + _, err := validateStringEntry(xRefTable, d, dictName, "F", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + // D, optional, byte string + _, err = validateStringEntry(xRefTable, d, dictName, "D", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // O, optional, ASCII string + _, err = validateStringEntry(xRefTable, d, dictName, "O", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // P, optional, byte string + _, err = validateStringEntry(xRefTable, d, dictName, "P", OPTIONAL, model.V10, nil) + + return err +} + +func validateLaunchActionDict(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.6.4.5 + + // F, optional, file specification + _, err := validateFileSpecEntry(xRefTable, d, dictName, "F", OPTIONAL, model.V11) + if err != nil { + return err + } + + // Win, optional, dict + d1, err := validateDictEntry(xRefTable, d, dictName, "Win", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + if d1 != nil { + err = validateWinDict(xRefTable, d1) + } + + // Mac, optional, undefined dict + + // Unix, optional, undefined dict + + return err +} + +func validateDestinationThreadEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) error { + + // The destination thread (table 205) + + o, err := validateEntry(xRefTable, d, dictName, entryName, required, sinceVersion) + if err != nil || o == nil { + return err + } + + switch o.(type) { + + case types.Dict, types.StringLiteral, types.Integer: + // an indRef to a thread dictionary + // or an index of the thread within the roots Threads array + // or the title of the thread as specified in its thread info dict + + default: + return errors.Errorf("validateDestinationThreadEntry: dict=%s entry=%s invalid type", dictName, entryName) + } + + return nil +} + +func validateDestinationBeadEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) error { + + // The bead in the destination thread (table 205) + + o, err := validateEntry(xRefTable, d, dictName, entryName, required, sinceVersion) + if err != nil || o == nil { + return err + } + + switch o.(type) { + + case types.Dict, types.Integer: + // an indRef to a bead dictionary of a thread in the current file + // or an index of the thread within its thread + + default: + return errors.Errorf("validateDestinationBeadEntry: dict=%s entry=%s invalid type", dictName, entryName) + } + + return nil +} + +func validateThreadActionDict(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + //see 12.6.4.6 + + // F, optional, file specification + _, err := validateFileSpecEntry(xRefTable, d, dictName, "F", OPTIONAL, model.V11) + if err != nil { + return err + } + + // D, required, indRef to thread dict, integer or text string. + err = validateDestinationThreadEntry(xRefTable, d, dictName, "D", REQUIRED, model.V10) + if err != nil { + return err + } + + // B, optional, indRef to bead dict or integer. + return validateDestinationBeadEntry(xRefTable, d, dictName, "B", OPTIONAL, model.V10) +} + +func hasURIForChecking(xRefTable *model.XRefTable, s string) bool { + for _, links := range xRefTable.URIs { + for uri := range links { + if uri == s { + return true + } + } + } + return false +} + +func validateURIActionDict(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.6.4.7 + + // URI, required, string + uri, err := validateStringEntry(xRefTable, d, dictName, "URI", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + // Record URIs for link checking. + if xRefTable.ValidateLinks && uri != nil && + strings.HasPrefix(*uri, "http") && !hasURIForChecking(xRefTable, *uri) { + if len(xRefTable.URIs[xRefTable.CurPage]) == 0 { + xRefTable.URIs[xRefTable.CurPage] = map[string]string{} + } + xRefTable.URIs[xRefTable.CurPage][*uri] = "" + } + + // IsMap, optional, boolean + _, err = validateBooleanEntry(xRefTable, d, dictName, "IsMap", OPTIONAL, model.V10, nil) + + return err +} + +func validateSoundDictEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) error { + + sd, err := validateStreamDictEntry(xRefTable, d, dictName, entryName, required, sinceVersion, nil) + if err != nil || sd == nil { + return err + } + + dictName = "soundDict" + + // Type, optional, name + _, err = validateNameEntry(xRefTable, sd.Dict, dictName, "Type", OPTIONAL, model.V10, func(s string) bool { return s == "Sound" }) + if err != nil { + return err + } + + // R, required, number - sampling rate + _, err = validateNumberEntry(xRefTable, sd.Dict, dictName, "R", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // C, required, integer - # of sound channels + _, err = validateIntegerEntry(xRefTable, sd.Dict, dictName, "C", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // B, required, integer - bits per sample value per channel + _, err = validateIntegerEntry(xRefTable, sd.Dict, dictName, "B", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // E, optional, name - encoding format + validateSampleDataEncoding := func(s string) bool { + return types.MemberOf(s, []string{"Raw", "Signed", "muLaw", "ALaw"}) + } + _, err = validateNameEntry(xRefTable, sd.Dict, dictName, "E", OPTIONAL, model.V10, validateSampleDataEncoding) + + return err +} + +func validateSoundActionDict(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.6.4.8 + + // Sound, required, stream dict + err := validateSoundDictEntry(xRefTable, d, dictName, "Sound", REQUIRED, model.V10) + if err != nil { + return err + } + + // Volume, optional, number: -1.0 .. +1.0 + _, err = validateNumberEntry(xRefTable, d, dictName, "Volume", OPTIONAL, model.V10, func(f float64) bool { return -1.0 <= f && f <= 1.0 }) + if err != nil { + return err + } + + // Synchronous, optional, boolean + _, err = validateBooleanEntry(xRefTable, d, dictName, "Synchronous", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // Repeat, optional, boolean + _, err = validateBooleanEntry(xRefTable, d, dictName, "Repeat", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // Mix, optional, boolean + _, err = validateBooleanEntry(xRefTable, d, dictName, "Mix", OPTIONAL, model.V10, nil) + + return err +} + +func validateMovieStartOrDurationEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) error { + + o, err := validateEntry(xRefTable, d, dictName, entryName, required, sinceVersion) + if err != nil || o == nil { + return err + } + + switch o := o.(type) { + + case types.Integer, types.StringLiteral: + // no further processing + + case types.Array: + if len(o) != 2 { + return errors.New("pdfcpu: validateMovieStartOrDurationEntry: array length <> 2") + } + } + + return nil +} + +func validateMovieActivationDict(xRefTable *model.XRefTable, d types.Dict) error { + + dictName := "movieActivationDict" + + // Start, optional + err := validateMovieStartOrDurationEntry(xRefTable, d, dictName, "Start", OPTIONAL, model.V10) + if err != nil { + return err + } + + // Duration, optional + err = validateMovieStartOrDurationEntry(xRefTable, d, dictName, "Duration", OPTIONAL, model.V10) + if err != nil { + return err + } + + // Rate, optional, number + _, err = validateNumberEntry(xRefTable, d, dictName, "Rate", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // Volume, optional, number + _, err = validateNumberEntry(xRefTable, d, dictName, "Volume", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // ShowControls, optional, boolean + _, err = validateBooleanEntry(xRefTable, d, dictName, "ShowControls", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // Mode, optional, name + validatePlayMode := func(s string) bool { + return types.MemberOf(s, []string{"Once", "Open", "Repeat", "Palindrome"}) + } + _, err = validateNameEntry(xRefTable, d, dictName, "Mode", OPTIONAL, model.V10, validatePlayMode) + if err != nil { + return err + } + + // Synchronous, optional, boolean + _, err = validateBooleanEntry(xRefTable, d, dictName, "Synchronous", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // FWScale, optional, array of 2 positive integers + _, err = validateIntegerArrayEntry(xRefTable, d, dictName, "FWScale", OPTIONAL, model.V10, func(a types.Array) bool { return len(a) == 2 }) + if err != nil { + return err + } + + // FWPosition, optional, array of 2 numbers [0.0 .. 1.0] + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "FWPosition", OPTIONAL, model.V10, func(a types.Array) bool { return len(a) == 2 }) + + return err +} + +func validateMovieActionDict(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.6.4.9 + + // is a movie activation dict + err := validateMovieActivationDict(xRefTable, d) + if err != nil { + return err + } + + // Needs either Annotation or T entry but not both. + + // T, text string + _, err = validateStringEntry(xRefTable, d, dictName, "T", OPTIONAL, model.V10, nil) + if err == nil { + return nil + } + + // Annotation, indRef of movie annotation dict + ir, err := validateIndRefEntry(xRefTable, d, dictName, "Annotation", REQUIRED, model.V10) + if err != nil || ir == nil { + return err + } + + d, err = xRefTable.DereferenceDict(*ir) + if err != nil || d == nil { + return errors.New("pdfcpu: validateMovieActionDict: missing required entry \"T\" or \"Annotation\"") + } + + _, err = validateNameEntry(xRefTable, d, "annotDict", "Subtype", REQUIRED, model.V10, func(s string) bool { return s == "Movie" }) + + return err +} + +func validateHideActionDictEntryT(xRefTable *model.XRefTable, o types.Object) error { + + switch o := o.(type) { + + case types.StringLiteral: + // Ensure UTF16 correctness. + _, err := types.StringLiteralToString(o) + if err != nil { + return err + } + + case types.Dict: + // annotDict, Check for required name Subtype + _, err := validateNameEntry(xRefTable, o, "annotDict", "Subtype", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + case types.Array: + // mixed array of annotationDict indRefs and strings + for _, v := range o { + + o, err := xRefTable.Dereference(v) + if err != nil { + return err + } + + if o == nil { + continue + } + + switch o := o.(type) { + + case types.StringLiteral: + // Ensure UTF16 correctness. + _, err = types.StringLiteralToString(o) + if err != nil { + return err + } + + case types.Dict: + // annotDict, Check for required name Subtype + _, err = validateNameEntry(xRefTable, o, "annotDict", "Subtype", REQUIRED, model.V10, nil) + if err != nil { + return err + } + } + } + + default: + return errors.Errorf("validateHideActionDict: invalid entry \"T\"") + + } + + return nil +} + +func validateHideActionDict(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.6.4.10 + + // T, required, dict, text string or array + o, found := d.Find("T") + if !found || o == nil { + return errors.New("pdfcpu: validateHideActionDict: missing required entry \"T\"") + } + + o, err := xRefTable.Dereference(o) + if err != nil || o == nil { + return err + } + + err = validateHideActionDictEntryT(xRefTable, o) + if err != nil { + return err + } + + // H, optional, boolean + _, err = validateBooleanEntry(xRefTable, d, dictName, "H", OPTIONAL, model.V10, nil) + + return err +} + +func validateNamedActionDict(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.6.4.11 + + validate := func(s string) bool { + + if types.MemberOf(s, []string{"NextPage", "PrevPage", "FirstPage", "Lastpage"}) { + return true + } + + // Some known non standard named actions + if types.MemberOf(s, []string{"GoToPage", "GoBack", "GoForward", "Find", "Print", "SaveAs", "Quit", "FullScreen"}) { + return true + } + + return false + } + + _, err := validateNameEntry(xRefTable, d, dictName, "N", REQUIRED, model.V10, validate) + + return err +} + +func validateSubmitFormActionDict(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.7.5.2 + + // F, required, URL specification + _, err := validateURLSpecEntry(xRefTable, d, dictName, "F", REQUIRED, model.V10) + if err != nil { + return err + } + + // Fields, optional, array + // Each element of the array shall be either an indirect reference to a field dictionary + // or (PDF 1.3) a text string representing the fully qualified name of a field. + a, err := validateArrayEntry(xRefTable, d, dictName, "Fields", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + for _, v := range a { + switch v.(type) { + case types.StringLiteral, types.IndirectRef: + // no further processing + + default: + return errors.New("pdfcpu: validateSubmitFormActionDict: unknown Fields entry") + } + } + + // Flags, optional, integer + _, err = validateIntegerEntry(xRefTable, d, dictName, "Flags", OPTIONAL, model.V10, nil) + + return err +} + +func validateResetFormActionDict(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.7.5.3 + + // Fields, optional, array + // Each element of the array shall be either an indirect reference to a field dictionary + // or (PDF 1.3) a text string representing the fully qualified name of a field. + a, err := validateArrayEntry(xRefTable, d, dictName, "Fields", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + for _, v := range a { + switch v.(type) { + case types.StringLiteral, types.IndirectRef: + // no further processing + + default: + return errors.New("pdfcpu: validateResetFormActionDict: unknown Fields entry") + } + } + + // Flags, optional, integer + _, err = validateIntegerEntry(xRefTable, d, dictName, "Flags", OPTIONAL, model.V10, nil) + + return err +} + +func validateImportDataActionDict(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.7.5.4 + + // F, required, file specification + _, err := validateFileSpecEntry(xRefTable, d, dictName, "F", OPTIONAL, model.V11) + + return err +} + +func validateJavaScript(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool) error { + + o, err := validateEntry(xRefTable, d, dictName, entryName, required, model.V13) + if err != nil || o == nil { + return err + } + + switch o := o.(type) { + + case types.StringLiteral: + // Ensure UTF16 correctness. + _, err = types.StringLiteralToString(o) + + case types.HexLiteral: + // Ensure UTF16 correctness. + _, err = types.HexLiteralToString(o) + + case types.StreamDict: + // no further processing + + default: + err = errors.Errorf("validateJavaScript: invalid type\n") + + } + + return err +} + +func validateJavaScriptActionDict(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.6.4.16 + + // JS, required, text string or stream + return validateJavaScript(xRefTable, d, dictName, "JS", REQUIRED) +} + +func validateSetOCGStateActionDict(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.6.4.12 + + // State, required, array + _, err := validateArrayEntry(xRefTable, d, dictName, "State", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + // PreserveRB, optional, boolean + _, err = validateBooleanEntry(xRefTable, d, dictName, "PreserveRB", OPTIONAL, model.V10, nil) + + return err +} + +func validateRenditionActionDict(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.6.4.13 + + // OP or JS need to be present. + + // OP, integer + op, err := validateIntegerEntry(xRefTable, d, dictName, "OP", OPTIONAL, model.V15, func(i int) bool { return 0 <= i && i <= 4 }) + if err != nil { + return err + } + + // JS, text string or stream + err = validateJavaScript(xRefTable, d, dictName, "JS", op == nil) + if err != nil { + return err + } + + // R, required for OP 0 and 4, rendition object dict + required := func(op *types.Integer) bool { + if op == nil { + return false + } + v := op.Value() + return v == 0 || v == 4 + }(op) + + d1, err := validateDictEntry(xRefTable, d, dictName, "R", required, model.V15, nil) + if err != nil { + return err + } + if d1 != nil { + err = validateRenditionDict(xRefTable, d1, model.V15) + if err != nil { + return err + } + } + + // AN, required for any OP 0..4, indRef of screen annotation dict + d1, err = validateDictEntry(xRefTable, d, dictName, "AN", op != nil, model.V10, nil) + if err != nil { + return err + } + if d1 != nil { + _, err = validateNameEntry(xRefTable, d1, dictName, "Subtype", REQUIRED, model.V10, func(s string) bool { return s == "Screen" }) + if err != nil { + return err + } + } + + return nil +} + +func validateTransActionDict(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.6.4.14 + + // Trans, required, transitionDict + d1, err := validateDictEntry(xRefTable, d, dictName, "Trans", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + return validateTransitionDict(xRefTable, d1) +} + +func validateGoTo3DViewActionDict(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.6.4.15 + + // TA, required, target annotation + d1, err := validateDictEntry(xRefTable, d, dictName, "TA", REQUIRED, model.V16, nil) + if err != nil { + return err + } + + _, err = validateAnnotationDict(xRefTable, d1) + if err != nil { + return err + } + + // V, required, the view to use: 3DViewDict or integer or text string or name + // TODO Validation. + _, err = validateEntry(xRefTable, d, dictName, "V", REQUIRED, model.V16) + + return err +} + +func validateActionDictCore(xRefTable *model.XRefTable, n *types.Name, d types.Dict) error { + + for k, v := range map[string]struct { + validate func(xRefTable *model.XRefTable, d types.Dict, dictName string) error + sinceVersion model.Version + }{ + "GoTo": {validateGoToActionDict, model.V10}, + "GoToR": {validateGoToRActionDict, model.V10}, + "GoToE": {validateGoToEActionDict, model.V16}, + "Launch": {validateLaunchActionDict, model.V10}, + "Thread": {validateThreadActionDict, model.V10}, + "URI": {validateURIActionDict, model.V10}, + "Sound": {validateSoundActionDict, model.V12}, + "Movie": {validateMovieActionDict, model.V12}, + "Hide": {validateHideActionDict, model.V12}, + "Named": {validateNamedActionDict, model.V12}, + "SubmitForm": {validateSubmitFormActionDict, model.V10}, + "ResetForm": {validateResetFormActionDict, model.V12}, + "ImportData": {validateImportDataActionDict, model.V12}, + "JavaScript": {validateJavaScriptActionDict, model.V13}, + "SetOCGState": {validateSetOCGStateActionDict, model.V15}, + "Rendition": {validateRenditionActionDict, model.V15}, + "Trans": {validateTransActionDict, model.V15}, + "GoTo3DView": {validateGoTo3DViewActionDict, model.V16}, + } { + if n.Value() == k { + + err := xRefTable.ValidateVersion(k, v.sinceVersion) + if err != nil { + return err + } + + return v.validate(xRefTable, d, k) + } + } + + return errors.Errorf("validateActionDictCore: unsupported action type: %s\n", *n) +} + +func validateActionDict(xRefTable *model.XRefTable, d types.Dict) error { + + dictName := "actionDict" + + // Type, optional, name + allowedTypes := []string{"Action"} + if xRefTable.ValidationMode == model.ValidationRelaxed { + allowedTypes = []string{"A", "Action"} + } + _, err := validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, model.V10, func(s string) bool { return types.MemberOf(s, allowedTypes) }) + if err != nil { + return err + } + + // S, required, name, action Type + s, err := validateNameEntry(xRefTable, d, dictName, "S", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + err = validateActionDictCore(xRefTable, s, d) + if err != nil { + return err + } + + if o, ok := d.Find("Next"); ok { + + // either optional action dict + d, err := xRefTable.DereferenceDict(o) + if err == nil { + return validateActionDict(xRefTable, d) + } + + // or optional array of action dicts + a, err := xRefTable.DereferenceArray(o) + if err != nil { + return err + } + + for _, v := range a { + + d, err := xRefTable.DereferenceDict(v) + if err != nil { + return err + } + + if d == nil { + continue + } + + err = validateActionDict(xRefTable, d) + if err != nil { + return err + } + } + + } + + return nil +} + +func validateRootAdditionalActions(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) error { + + return validateAdditionalActions(xRefTable, rootDict, "rootDict", "AA", required, sinceVersion, "root") +} + +func validateAdditionalActions(xRefTable *model.XRefTable, dict types.Dict, dictName, entryName string, required bool, sinceVersion model.Version, source string) error { + + d, err := validateDictEntry(xRefTable, dict, dictName, entryName, required, sinceVersion, nil) + if err != nil || d == nil { + return err + } + + validateAdditionalAction := func(s, source string) bool { + + switch source { + + case "root": + if types.MemberOf(s, []string{"WC", "WS", "DS", "WP", "DP"}) { + return true + } + + case "page": + if types.MemberOf(s, []string{"O", "C"}) { + return true + } + + case "fieldOrAnnot": + // A terminal form field may be merged with a widget annotation. + fieldOptions := []string{"K", "F", "V", "C"} + annotOptions := []string{"E", "X", "D", "U", "Fo", "Bl", "PO", "PC", "PV", "Pl"} + options := append(fieldOptions, annotOptions...) + if types.MemberOf(s, options) { + return true + } + + } + + return false + } + + for k, v := range d { + + if !validateAdditionalAction(k, source) { + return errors.Errorf("validateAdditionalActions: action %s not allowed for source %s", k, source) + } + + d, err := xRefTable.DereferenceDict(v) + if err != nil { + return err + } + + if d == nil { + continue + } + + err = validateActionDict(xRefTable, d) + if err != nil { + return err + } + + } + + return nil +} diff --git a/pkg/pdfcpu/validate/annotation.go b/pkg/pdfcpu/validate/annotation.go new file mode 100644 index 0000000000000000000000000000000000000000..cedb5760ec9a9eb73d47f391d100138fdb585d49 --- /dev/null +++ b/pkg/pdfcpu/validate/annotation.go @@ -0,0 +1,1881 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 validate + +import ( + "strconv" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +var errInvalidPageAnnotArray = errors.New("pdfcpu: validatePageAnnotations: page annotation array without indirect references.") + +func validateAAPLAKExtrasDictEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) error { + + // No documentation for this PDF-Extension - purely speculative implementation. + + d1, err := validateDictEntry(xRefTable, d, dictName, entryName, required, sinceVersion, nil) + if err != nil || d1 == nil { + return err + } + + dictName = "AAPLAKExtrasDict" + + // AAPL:AKAnnotationObject, string + _, err = validateStringEntry(xRefTable, d1, dictName, "AAPL:AKAnnotationObject", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // AAPL:AKPDFAnnotationDictionary, annotationDict + ad, err := validateDictEntry(xRefTable, d1, dictName, "AAPL:AKPDFAnnotationDictionary", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + _, err = validateAnnotationDict(xRefTable, ad) + if err != nil { + return err + } + + return nil +} + +func validateBorderEffectDictEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) error { + + // see 12.5.4 + + d1, err := validateDictEntry(xRefTable, d, dictName, entryName, required, sinceVersion, nil) + if err != nil || d1 == nil { + return err + } + + dictName = "borderEffectDict" + + // S, optional, name, S or C + _, err = validateNameEntry(xRefTable, d1, dictName, "S", OPTIONAL, model.V10, func(s string) bool { return s == "S" || s == "C" }) + if err != nil { + return err + } + + // I, optional, number in the range 0 to 2 + _, err = validateNumberEntry(xRefTable, d1, dictName, "I", OPTIONAL, model.V10, func(f float64) bool { return 0 <= f && f <= 2 }) // validation missing + if err != nil { + return err + } + + return nil +} + +func validateBorderStyleDict(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) error { + + // see 12.5.4 + + d1, err := validateDictEntry(xRefTable, d, dictName, entryName, required, sinceVersion, nil) + if err != nil || d1 == nil { + return err + } + + dictName = "borderStyleDict" + + // Type, optional, name, "Border" + _, err = validateNameEntry(xRefTable, d1, dictName, "Type", OPTIONAL, model.V10, func(s string) bool { return s == "Border" }) + if err != nil { + return err + } + + // W, optional, number, border width in points + _, err = validateNumberEntry(xRefTable, d1, dictName, "W", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // S, optional, name, border style + validate := func(s string) bool { return types.MemberOf(s, []string{"S", "D", "B", "I", "U", "A"}) } + _, err = validateNameEntry(xRefTable, d1, dictName, "S", OPTIONAL, model.V10, validate) + if err != nil { + return err + } + + // D, optional, dash array + _, err = validateNumberArrayEntry(xRefTable, d1, dictName, "D", OPTIONAL, model.V10, func(a types.Array) bool { return len(a) <= 2 }) + + return err +} + +func validateIconFitDictEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) error { + + // see table 247 + + d1, err := validateDictEntry(xRefTable, d, dictName, entryName, required, sinceVersion, nil) + if err != nil || d1 == nil { + return err + } + + dictName = "iconFitDict" + + // SW, optional, name, A,B,S,N + validate := func(s string) bool { return types.MemberOf(s, []string{"A", "B", "S", "N"}) } + _, err = validateNameEntry(xRefTable, d1, dictName, "SW", OPTIONAL, model.V10, validate) + if err != nil { + return err + } + + // S, optional, name, A,P + _, err = validateNameEntry(xRefTable, d1, dictName, "S", OPTIONAL, model.V10, func(s string) bool { return s == "A" || s == "P" }) + if err != nil { + return err + } + + // A,optional, array of 2 numbers between 0.0 and 1.0 + _, err = validateNumberArrayEntry(xRefTable, d1, dictName, "A", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // FB, optional, bool, since V1.5 + _, err = validateBooleanEntry(xRefTable, d1, dictName, "FB", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + return nil +} + +func validateAppearanceCharacteristicsDictEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) error { + + // see 12.5.6.19 + + d1, err := validateDictEntry(xRefTable, d, dictName, entryName, required, sinceVersion, nil) + if err != nil || d1 == nil { + return err + } + + dictName = "appCharDict" + + // R, optional, integer + _, err = validateIntegerEntry(xRefTable, d1, dictName, "R", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // BC, optional, array of numbers, len=0,1,3,4 + _, err = validateNumberArrayEntry(xRefTable, d1, dictName, "BC", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // BG, optional, array of numbers between 0.0 and 0.1, len=0,1,3,4 + _, err = validateNumberArrayEntry(xRefTable, d1, dictName, "BG", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // CA, optional, text string + _, err = validateStringEntry(xRefTable, d1, dictName, "CA", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // RC, optional, text string + _, err = validateStringEntry(xRefTable, d1, dictName, "RC", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // AC, optional, text string + _, err = validateStringEntry(xRefTable, d1, dictName, "AC", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // I, optional, stream dict + _, err = validateStreamDictEntry(xRefTable, d1, dictName, "I", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // RI, optional, stream dict + _, err = validateStreamDictEntry(xRefTable, d1, dictName, "RI", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // IX, optional, stream dict + _, err = validateStreamDictEntry(xRefTable, d1, dictName, "IX", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // IF, optional, icon fit dict, + err = validateIconFitDictEntry(xRefTable, d1, dictName, "IF", OPTIONAL, model.V10) + if err != nil { + return err + } + + // TP, optional, integer 0..6 + _, err = validateIntegerEntry(xRefTable, d1, dictName, "TP", OPTIONAL, model.V10, func(i int) bool { return 0 <= i && i <= 6 }) + + return err +} + +func validateAnnotationDictText(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.5.6.4 + + // Open, optional, boolean + _, err := validateBooleanEntry(xRefTable, d, dictName, "Open", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // Name, optional, name + _, err = validateNameEntry(xRefTable, d, dictName, "Name", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // State, optional, text string, since V1.5 + state, err := validateStringEntry(xRefTable, d, dictName, "State", OPTIONAL, model.V15, nil) + if err != nil { + return err + } + + // StateModel, text string, since V1.5 + validate := func(s string) bool { return types.MemberOf(s, []string{"Marked", "Review"}) } + stateModel, err := validateStringEntry(xRefTable, d, dictName, "StateModel", state != nil, model.V15, validate) + if err != nil { + return err + } + + if state == nil { + if stateModel != nil { + return errors.Errorf("pdfcpu: validateAnnotationDictText: dict=%s missing state for statemodel=%s", dictName, *stateModel) + } + return nil + } + + // Ensure that the state/model combo is valid. + validStates := []string{"Accepted", "Rejected", "Cancelled", "Completed", "None"} // stateModel "Review" + if *stateModel == "Marked" { + validStates = []string{"Marked", "Unmarked"} + } + if !types.MemberOf(*state, validStates) { + return errors.Errorf("pdfcpu: validateAnnotationDictText: dict=%s invalid state=%s for state model=%s", dictName, *state, *stateModel) + } + + return nil +} + +func validateActionOrDestination(xRefTable *model.XRefTable, d types.Dict, dictName string, sinceVersion model.Version) error { + + // The action that shall be performed when this item is activated. + d1, err := validateDictEntry(xRefTable, d, dictName, "A", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + if d1 != nil { + return validateActionDict(xRefTable, d1) + } + + // A destination that shall be displayed when this item is activated. + obj, err := validateEntry(xRefTable, d, dictName, "Dest", OPTIONAL, sinceVersion) + if err != nil || obj == nil { + return err + } + + name, err := validateDestination(xRefTable, obj, false) + if err != nil { + return err + } + + if len(name) > 0 && xRefTable.IsMerging() { + nm := xRefTable.NameRef("Dests") + nm.Add(name, d) + } + + return nil +} + +func validateURIActionDictEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) error { + + d1, err := validateDictEntry(xRefTable, d, dictName, entryName, required, sinceVersion, nil) + if err != nil || d1 == nil { + return err + } + + dictName = "URIActionDict" + + // Type, optional, name + _, err = validateNameEntry(xRefTable, d1, dictName, "Type", OPTIONAL, model.V10, func(s string) bool { return s == "Action" }) + if err != nil { + return err + } + + // S, required, name, action Type + _, err = validateNameEntry(xRefTable, d1, dictName, "S", REQUIRED, model.V10, func(s string) bool { return s == "URI" }) + if err != nil { + return err + } + + return validateURIActionDict(xRefTable, d1, dictName) +} + +func validateAnnotationDictLink(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.5.6.5 + + // A or Dest, required either or + err := validateActionOrDestination(xRefTable, d, dictName, model.V11) + if err != nil { + return err + } + + // H, optional, name, since V1.2 + _, err = validateNameEntry(xRefTable, d, dictName, "H", OPTIONAL, model.V12, nil) + if err != nil { + return err + } + + // PA, optional, URI action dict, since V1.3 + err = validateURIActionDictEntry(xRefTable, d, dictName, "PA", OPTIONAL, model.V13) + if err != nil { + return err + } + + // QuadPoints, optional, number array, len= a multiple of 8, since V1.6 + sinceVersion := model.V16 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V13 + } + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "QuadPoints", OPTIONAL, sinceVersion, func(a types.Array) bool { return len(a)%8 == 0 }) + if err != nil { + return err + } + + // BS, optional, border style dict, since V1.6 + return validateBorderStyleDict(xRefTable, d, dictName, "BS", OPTIONAL, sinceVersion) +} + +func validateAnnotationDictFreeTextPart1(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // DA, required, string + validate := validateDA + if xRefTable.ValidationMode == model.ValidationRelaxed { + validate = validateDARelaxed + } + da, err := validateStringEntry(xRefTable, d, dictName, "DA", REQUIRED, model.V10, validate) + if err != nil { + return err + } + if xRefTable.ValidationMode == model.ValidationRelaxed && da != nil { + // Repair + d["DA"] = types.StringLiteral(*da) + } + + // Q, optional, integer, since V1.4, 0,1,2 + sinceVersion := model.V14 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V13 + } + _, err = validateIntegerEntry(xRefTable, d, dictName, "Q", OPTIONAL, sinceVersion, func(i int) bool { return 0 <= i && i <= 2 }) + if err != nil { + return err + } + + // RC, optional, text string or text stream, since V1.5 + sinceVersion = model.V15 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V14 + } + err = validateStringOrStreamEntry(xRefTable, d, dictName, "RC", OPTIONAL, sinceVersion) + if err != nil { + return err + } + + // DS, optional, text string, since V1.5 + sinceVersion = model.V15 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V14 + } + _, err = validateStringEntry(xRefTable, d, dictName, "DS", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // CL, optional, number array, since V1.6, len: 4 or 6 + sinceVersion = model.V16 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V14 + } + + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "CL", OPTIONAL, sinceVersion, func(a types.Array) bool { return len(a) == 4 || len(a) == 6 }) + + return err +} + +func validateAnnotationDictFreeTextPart2(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // IT, optional, name, since V1.6 + sinceVersion := model.V16 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V14 + } + validate := func(s string) bool { + return types.MemberOf(s, []string{"FreeText", "FreeTextCallout", "FreeTextTypeWriter", "FreeTextTypewriter"}) + } + _, err := validateNameEntry(xRefTable, d, dictName, "IT", OPTIONAL, sinceVersion, validate) + if err != nil { + return err + } + + // BE, optional, border effect dict, since V1.6 + err = validateBorderEffectDictEntry(xRefTable, d, dictName, "BE", OPTIONAL, model.V15) + if err != nil { + return err + } + + // RD, optional, rectangle, since V1.6 + sinceVersion = model.V16 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V14 + } + _, err = validateRectangleEntry(xRefTable, d, dictName, "RD", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // BS, optional, border style dict, since V1.6 + sinceVersion = model.V16 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V12 + } + err = validateBorderStyleDict(xRefTable, d, dictName, "BS", OPTIONAL, sinceVersion) + if err != nil { + return err + } + + // LE, optional, name, since V1.6 + sinceVersion = model.V16 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V14 + } + _, err = validateNameEntry(xRefTable, d, dictName, "LE", OPTIONAL, sinceVersion, nil) + + return err +} + +func validateAnnotationDictFreeText(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.5.6.6 + + err := validateAnnotationDictFreeTextPart1(xRefTable, d, dictName) + if err != nil { + return err + } + + return validateAnnotationDictFreeTextPart2(xRefTable, d, dictName) +} + +func validateEntryMeasure(xRefTable *model.XRefTable, d types.Dict, dictName string, required bool, sinceVersion model.Version) error { + + d1, err := validateDictEntry(xRefTable, d, dictName, "Measure", required, sinceVersion, nil) + if err != nil { + return err + } + + if d1 != nil { + err = validateMeasureDict(xRefTable, d1, sinceVersion) + } + + return err +} + +func validateCP(s string) bool { return s == "Inline" || s == "Top" } + +func validateAnnotationDictLine(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.5.6.7 + + // L, required, array of numbers, len:4 + _, err := validateNumberArrayEntry(xRefTable, d, dictName, "L", REQUIRED, model.V10, func(a types.Array) bool { return len(a) == 4 }) + if err != nil { + return err + } + + // BS, optional, border style dict + err = validateBorderStyleDict(xRefTable, d, dictName, "BS", OPTIONAL, model.V10) + if err != nil { + return err + } + + // LE, optional, name array, since V1.4, len:2 + sinceVersion := model.V14 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V13 + } + _, err = validateNameArrayEntry(xRefTable, d, dictName, "LE", OPTIONAL, sinceVersion, func(a types.Array) bool { return len(a) == 2 }) + if err != nil { + return err + } + + // IC, optional, number array, since V1.4, len:0,1,3,4 + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "IC", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // LLE, optional, number, since V1.6, >0 + lle, err := validateNumberEntry(xRefTable, d, dictName, "LLE", OPTIONAL, model.V16, func(f float64) bool { return f > 0 }) + if err != nil { + return err + } + + // LL, required if LLE present, number, since V1.6 + _, err = validateNumberEntry(xRefTable, d, dictName, "LL", lle != nil, model.V16, nil) + if err != nil { + return err + } + + // Cap, optional, bool, since V1.6 + _, err = validateBooleanEntry(xRefTable, d, dictName, "Cap", OPTIONAL, model.V16, nil) + if err != nil { + return err + } + + // IT, optional, name, since V1.6 + _, err = validateNameEntry(xRefTable, d, dictName, "IT", OPTIONAL, model.V16, nil) + if err != nil { + return err + } + + // LLO, optionl, number, since V1.7, >0 + _, err = validateNumberEntry(xRefTable, d, dictName, "LLO", OPTIONAL, model.V17, func(f float64) bool { return f > 0 }) + if err != nil { + return err + } + + // CP, optional, name, since V1.7 + _, err = validateNameEntry(xRefTable, d, dictName, "CP", OPTIONAL, model.V17, validateCP) + if err != nil { + return err + } + + // Measure, optional, measure dict, since V1.7 + err = validateEntryMeasure(xRefTable, d, dictName, OPTIONAL, model.V17) + if err != nil { + return err + } + + // CO, optional, number array, since V1.7, len=2 + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "CO", OPTIONAL, model.V17, func(a types.Array) bool { return len(a) == 2 }) + + return err +} + +func validateAnnotationDictCircleOrSquare(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.5.6.8 + + // BS, optional, border style dict + err := validateBorderStyleDict(xRefTable, d, dictName, "BS", OPTIONAL, model.V10) + if err != nil { + return err + } + + // IC, optional, array, since V1.4 + sinceVersion := model.V14 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V13 + } + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "IC", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // BE, optional, border effect dict, since V1.5 + err = validateBorderEffectDictEntry(xRefTable, d, dictName, "BE", OPTIONAL, model.V15) + if err != nil { + return err + } + + // RD, optional, rectangle, since V1.5 + _, err = validateRectangleEntry(xRefTable, d, dictName, "RD", OPTIONAL, model.V15, nil) + + return err +} + +func validateEntryIT(xRefTable *model.XRefTable, d types.Dict, dictName string, required bool, sinceVersion model.Version) error { + + // IT, optional, name, since V1.6 + validateIntent := func(s string) bool { + + if xRefTable.Version() == model.V16 { + return s == "PolygonCloud" + } + + if xRefTable.Version() == model.V17 { + if types.MemberOf(s, []string{"PolygonCloud", "PolyLineDimension", "PolygonDimension"}) { + return true + } + } + + return false + + } + + _, err := validateNameEntry(xRefTable, d, dictName, "IT", required, sinceVersion, validateIntent) + + return err +} + +func validateAnnotationDictPolyLine(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.5.6.9 + + // Vertices, required, array of numbers + _, err := validateNumberArrayEntry(xRefTable, d, dictName, "Vertices", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + // LE, optional, array of 2 names, meaningful only for polyline annotations. + if dictName == "PolyLine" { + _, err = validateNameArrayEntry(xRefTable, d, dictName, "LE", OPTIONAL, model.V10, func(a types.Array) bool { return len(a) == 2 }) + if err != nil { + return err + } + } + + // BS, optional, border style dict + err = validateBorderStyleDict(xRefTable, d, dictName, "BS", OPTIONAL, model.V10) + if err != nil { + return err + } + + // IC, optional, array of numbers [0.0 .. 1.0], len:1,3,4 + ensureArrayLength := func(a types.Array, lengths ...int) bool { + for _, length := range lengths { + if len(a) == length { + return true + } + } + return false + } + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "IC", OPTIONAL, model.V14, func(a types.Array) bool { return ensureArrayLength(a, 1, 3, 4) }) + if err != nil { + return err + } + + // BE, optional, border effect dict, meaningful only for polygon annotations + if dictName == "Polygon" { + err = validateBorderEffectDictEntry(xRefTable, d, dictName, "BE", OPTIONAL, model.V10) + if err != nil { + return err + } + } + + return validateEntryIT(xRefTable, d, dictName, OPTIONAL, model.V16) +} + +func validateTextMarkupAnnotation(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.5.6.10 + + required := REQUIRED + if xRefTable.ValidationMode == model.ValidationRelaxed { + required = OPTIONAL + } + // QuadPoints, required, number array, len: a multiple of 8 + _, err := validateNumberArrayEntry(xRefTable, d, dictName, "QuadPoints", required, model.V10, func(a types.Array) bool { return len(a)%8 == 0 }) + + return err +} + +func validateAnnotationDictStamp(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.5.6.12 + + // Name, optional, name + _, err := validateNameEntry(xRefTable, d, dictName, "Name", OPTIONAL, model.V10, nil) + + return err +} + +func validateAnnotationDictCaret(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.5.6.11 + + // RD, optional, rectangle, since V1.5 + _, err := validateRectangleEntry(xRefTable, d, dictName, "RD", OPTIONAL, model.V15, nil) + if err != nil { + return err + } + + // Sy, optional, name + _, err = validateNameEntry(xRefTable, d, dictName, "Sy", OPTIONAL, model.V10, func(s string) bool { return s == "P" || s == "None" }) + + return err +} + +func validateAnnotationDictInk(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.5.6.13 + + // InkList, required, array of stroked path arrays + _, err := validateArrayArrayEntry(xRefTable, d, dictName, "InkList", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + // BS, optional, border style dict + return validateBorderStyleDict(xRefTable, d, dictName, "BS", OPTIONAL, model.V10) +} + +func validateAnnotationDictPopup(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.5.6.14 + + // Parent, optional, dict indRef + ir, err := validateIndRefEntry(xRefTable, d, dictName, "Parent", OPTIONAL, model.V10) + if err != nil { + return err + } + if ir != nil { + d1, err := xRefTable.DereferenceDict(*ir) + if err != nil || d1 == nil { + return err + } + } + + // Open, optional, boolean + _, err = validateBooleanEntry(xRefTable, d, dictName, "Open", OPTIONAL, model.V10, nil) + + return err +} + +func validateAnnotationDictFileAttachment(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.5.6.15 + + // FS, required, file specification + _, err := validateFileSpecEntry(xRefTable, d, dictName, "FS", REQUIRED, model.V10) + if err != nil { + return err + } + + // Name, optional, name + _, err = validateNameEntry(xRefTable, d, dictName, "Name", OPTIONAL, model.V10, nil) + + return err +} + +func validateAnnotationDictSound(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.5.6.16 + + // Sound, required, stream dict + err := validateSoundDictEntry(xRefTable, d, dictName, "Sound", REQUIRED, model.V10) + if err != nil { + return err + } + + // Name, optional, name + _, err = validateNameEntry(xRefTable, d, dictName, "Name", OPTIONAL, model.V10, nil) + + return err +} + +func validateMovieDict(xRefTable *model.XRefTable, d types.Dict) error { + + dictName := "movieDict" + + // F, required, file specification + _, err := validateFileSpecEntry(xRefTable, d, dictName, "F", REQUIRED, model.V10) + if err != nil { + return err + } + + // Aspect, optional, integer array, length 2 + _, err = validateIntegerArrayEntry(xRefTable, d, dictName, "Ascpect", OPTIONAL, model.V10, func(a types.Array) bool { return len(a) == 2 }) + if err != nil { + return err + } + + // Rotate, optional, integer + _, err = validateIntegerEntry(xRefTable, d, dictName, "Rotate", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // Poster, optional boolean or stream + return validateBooleanOrStreamEntry(xRefTable, d, dictName, "Poster", OPTIONAL, model.V10) +} + +func validateAnnotationDictMovie(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.5.6.17 Movie Annotations + // 13.4 Movies + // The features described in this sub-clause are obsolescent and their use is no longer recommended. + // They are superseded by the general multimedia framework described in 13.2, “Multimedia.” + + // T, optional, text string + _, err := validateStringEntry(xRefTable, d, dictName, "T", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // Movie, required, movie dict + d1, err := validateDictEntry(xRefTable, d, dictName, "Movie", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + err = validateMovieDict(xRefTable, d1) + if err != nil { + return err + } + + // A, optional, boolean or movie activation dict + o, found := d.Find("A") + + if found { + + o, err = xRefTable.Dereference(o) + if err != nil { + return err + } + + if o != nil { + switch o := o.(type) { + case types.Boolean: + // no further processing + + case types.Dict: + err = validateMovieActivationDict(xRefTable, o) + if err != nil { + return err + } + } + } + + } + + return nil +} + +func validateAnnotationDictWidget(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.5.6.19 + + // H, optional, name + validate := func(s string) bool { return types.MemberOf(s, []string{"N", "I", "O", "P", "T", "A"}) } + _, err := validateNameEntry(xRefTable, d, dictName, "H", OPTIONAL, model.V10, validate) + if err != nil { + return err + } + + // MK, optional, dict + // An appearance characteristics dictionary that shall be used in constructing + // a dynamic appearance stream specifying the annotation’s visual presentation on the page.dict + err = validateAppearanceCharacteristicsDictEntry(xRefTable, d, dictName, "MK", OPTIONAL, model.V10) + if err != nil { + return err + } + + // A, optional, dict, since V1.1 + // An action that shall be performed when the annotation is activated. + d1, err := validateDictEntry(xRefTable, d, dictName, "A", OPTIONAL, model.V11, nil) + if err != nil { + return err + } + if d1 != nil { + err = validateActionDict(xRefTable, d1) + if err != nil { + return err + } + } + + // AA, optional, dict, since V1.2 + // An additional-actions dictionary defining the annotation’s behaviour in response to various trigger events. + err = validateAdditionalActions(xRefTable, d, dictName, "AA", OPTIONAL, model.V12, "fieldOrAnnot") + if err != nil { + return err + } + + // BS, optional, border style dict, since V1.2 + // A border style dictionary specifying the width and dash pattern + // that shall be used in drawing the annotation’s border. + validateBorderStyleDict(xRefTable, d, dictName, "BS", OPTIONAL, model.V12) + if err != nil { + return err + } + + // Parent, dict, required if one of multiple children in a field. + // An indirect reference to the widget annotation’s parent field. + _, err = validateIndRefEntry(xRefTable, d, dictName, "Parent", OPTIONAL, model.V10) + + return err +} + +func validateAnnotationDictScreen(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.5.6.18 + + // T, optional, name + _, err := validateNameEntry(xRefTable, d, dictName, "T", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // MK, optional, appearance characteristics dict + err = validateAppearanceCharacteristicsDictEntry(xRefTable, d, dictName, "MK", OPTIONAL, model.V10) + if err != nil { + return err + } + + // A, optional, action dict, since V1.0 + d1, err := validateDictEntry(xRefTable, d, dictName, "A", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + if d1 != nil { + err = validateActionDict(xRefTable, d1) + if err != nil { + return err + } + } + + // AA, optional, additional-actions dict, since V1.2 + return validateAdditionalActions(xRefTable, d, dictName, "AA", OPTIONAL, model.V12, "fieldOrAnnot") +} + +func validateAnnotationDictPrinterMark(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.5.6.20 + + // MN, optional, name + _, err := validateNameEntry(xRefTable, d, dictName, "MN", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // F, required integer, since V1.1, annotation flags + _, err = validateIntegerEntry(xRefTable, d, dictName, "F", REQUIRED, model.V11, nil) + if err != nil { + return err + } + + // AP, required, appearance dict, since V1.2 + return validateAppearDictEntry(xRefTable, d, dictName, REQUIRED, model.V12) +} + +func validateAnnotationDictTrapNet(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.5.6.21 + + // LastModified, optional, date + _, err := validateDateEntry(xRefTable, d, dictName, "LastModified", OPTIONAL, model.V10) + if err != nil { + return err + } + + // Version, optional, array + _, err = validateArrayEntry(xRefTable, d, dictName, "Version", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // AnnotStates, optional, array of names + _, err = validateNameArrayEntry(xRefTable, d, dictName, "AnnotStates", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // FontFauxing, optional, font dict array + validateFontDictArray := func(a types.Array) bool { + + var retValue bool + + for _, v := range a { + + if v == nil { + continue + } + + d, err := xRefTable.DereferenceDict(v) + if err != nil { + return false + } + + if d == nil { + continue + } + + if d.Type() == nil || *d.Type() != "Font" { + return false + } + + retValue = true + + } + + return retValue + } + + _, err = validateArrayEntry(xRefTable, d, dictName, "FontFauxing", OPTIONAL, model.V10, validateFontDictArray) + if err != nil { + return err + } + + _, err = validateIntegerEntry(xRefTable, d, dictName, "F", REQUIRED, model.V11, nil) + + return err +} + +func validateAnnotationDictWatermark(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.5.6.22 + + // FixedPrint, optional, dict + + validateFixedPrintDict := func(d types.Dict) bool { + + dictName := "fixedPrintDict" + + // Type, required, name + _, err := validateNameEntry(xRefTable, d, dictName, "Type", REQUIRED, model.V10, func(s string) bool { return s == "FixedPrint" }) + if err != nil { + return false + } + + // Matrix, optional, integer array, length = 6 + _, err = validateIntegerArrayEntry(xRefTable, d, dictName, "Matrix", OPTIONAL, model.V10, func(a types.Array) bool { return len(a) == 6 }) + if err != nil { + return false + } + + // H, optional, number + _, err = validateNumberEntry(xRefTable, d, dictName, "H", OPTIONAL, model.V10, nil) + if err != nil { + return false + } + + // V, optional, number + _, err = validateNumberEntry(xRefTable, d, dictName, "V", OPTIONAL, model.V10, nil) + return err == nil + } + + _, err := validateDictEntry(xRefTable, d, dictName, "FixedPrint", OPTIONAL, model.V10, validateFixedPrintDict) + + return err +} + +func validateAnnotationDict3D(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 13.6.2 + + // AP with entry N, required + + // 3DD, required, 3D stream or 3D reference dict + err := validateStreamDictOrDictEntry(xRefTable, d, dictName, "3DD", REQUIRED, model.V16) + if err != nil { + return err + } + + // 3DV, optional, various + _, err = validateEntry(xRefTable, d, dictName, "3DV", OPTIONAL, model.V16) + if err != nil { + return err + } + + // 3DA, optional, activation dict + _, err = validateDictEntry(xRefTable, d, dictName, "3DA", OPTIONAL, model.V16, nil) + if err != nil { + return err + } + + // 3DI, optional, boolean + _, err = validateBooleanEntry(xRefTable, d, dictName, "3DI", OPTIONAL, model.V16, nil) + + return err +} + +func validateEntryIC(xRefTable *model.XRefTable, d types.Dict, dictName string, required bool, sinceVersion model.Version) error { + + // IC, optional, number array, length:3 [0.0 .. 1.0] + validateICArray := func(a types.Array) bool { + + if len(a) != 3 { + return false + } + + for _, v := range a { + + o, err := xRefTable.Dereference(v) + if err != nil { + return false + } + + switch o := o.(type) { + case types.Integer: + if o < 0 || o > 1 { + return false + } + + case types.Float: + if o < 0.0 || o > 1.0 { + return false + } + } + } + + return true + } + + _, err := validateNumberArrayEntry(xRefTable, d, dictName, "IC", required, sinceVersion, validateICArray) + + return err +} + +func validateAnnotationDictRedact(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // see 12.5.6.23 + + // QuadPoints, optional, len: a multiple of 8 + _, err := validateNumberArrayEntry(xRefTable, d, dictName, "QuadPoints", OPTIONAL, model.V10, func(a types.Array) bool { return len(a)%8 == 0 }) + if err != nil { + return err + } + + // IC, optional, number array, length:3 [0.0 .. 1.0] + err = validateEntryIC(xRefTable, d, dictName, OPTIONAL, model.V10) + if err != nil { + return err + } + + // RO, optional, stream + _, err = validateStreamDictEntry(xRefTable, d, dictName, "RO", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // OverlayText, optional, text string + _, err = validateStringEntry(xRefTable, d, dictName, "OverlayText", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // Repeat, optional, boolean + _, err = validateBooleanEntry(xRefTable, d, dictName, "Repeat", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // DA, required, byte string + validate := validateDA + if xRefTable.ValidationMode == model.ValidationRelaxed { + validate = validateDARelaxed + } + da, err := validateStringEntry(xRefTable, d, dictName, "DA", REQUIRED, model.V10, validate) + if err != nil { + return err + } + if xRefTable.ValidationMode == model.ValidationRelaxed && da != nil { + // Repair + d["DA"] = types.StringLiteral(*da) + } + + // Q, optional, integer + _, err = validateIntegerEntry(xRefTable, d, dictName, "Q", OPTIONAL, model.V10, nil) + + return err +} + +func validateRichMediaAnnotation(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + // TODO See extension level 3. + return nil +} + +func validateExDataDict(xRefTable *model.XRefTable, d types.Dict) error { + + dictName := "ExData" + + _, err := validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, model.V10, func(s string) bool { return s == "ExData" }) + if err != nil { + return err + } + + _, err = validateNameEntry(xRefTable, d, dictName, "Subtype", REQUIRED, model.V10, func(s string) bool { return s == "Markup3D" }) + + return err +} + +func validatePopupEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) error { + + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V12 + } + d1, err := validateDictEntry(xRefTable, d, dictName, entryName, required, sinceVersion, nil) + if err != nil { + return err + } + + if d1 != nil { + + _, err = validateNameEntry(xRefTable, d1, dictName, "Subtype", REQUIRED, model.V10, func(s string) bool { return s == "Popup" }) + if err != nil { + return err + } + + _, err = validateAnnotationDict(xRefTable, d1) + if err != nil { + return err + } + + } + + return nil +} + +func validateIRTEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) error { + + d1, err := validateDictEntry(xRefTable, d, dictName, entryName, required, sinceVersion, nil) + if err != nil { + return err + } + + if d1 != nil { + _, err = validateAnnotationDict(xRefTable, d1) + if err != nil { + return err + } + } + + return nil +} + +func validateMarkupAnnotationPart1(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // T, optional, text string, since V1.1 + if _, err := validateStringEntry(xRefTable, d, dictName, "T", OPTIONAL, model.V11, nil); err != nil { + return err + } + + // Popup, optional, dict, since V1.3 + if err := validatePopupEntry(xRefTable, d, dictName, "Popup", OPTIONAL, model.V13); err != nil { + return err + } + + // CA, optional, number, since V1.4 + if _, err := validateNumberEntry(xRefTable, d, dictName, "CA", OPTIONAL, model.V14, nil); err != nil { + return err + } + + // RC, optional, text string or stream, since V1.5 + sinceVersion := model.V15 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V14 + } + if err := validateStringOrStreamEntry(xRefTable, d, dictName, "RC", OPTIONAL, sinceVersion); err != nil { + return err + } + + // CreationDate, optional, date, since V1.5 + sinceVersion = model.V15 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V13 + } + if _, err := validateDateEntry(xRefTable, d, dictName, "CreationDate", OPTIONAL, sinceVersion); err != nil { + return err + } + + return nil +} + +func validateMarkupAnnotationPart2(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // IRT, optional, (in reply to) dict, since V1.5 + sinceVersion := model.V15 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V14 + } + if err := validateIRTEntry(xRefTable, d, dictName, "IRT", OPTIONAL, sinceVersion); err != nil { + return err + } + + // Subj, optional, text string, since V1.5 + sinceVersion = model.V15 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V14 + } + if _, err := validateStringEntry(xRefTable, d, dictName, "Subj", OPTIONAL, sinceVersion, nil); err != nil { + return err + } + + // RT, optional, name, since V1.6 + validate := func(s string) bool { return s == "R" || s == "Group" } + if _, err := validateNameEntry(xRefTable, d, dictName, "RT", OPTIONAL, model.V16, validate); err != nil { + return err + } + + // IT, optional, name, since V1.6 + sinceVersion = model.V16 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V14 + } + if _, err := validateNameEntry(xRefTable, d, dictName, "IT", OPTIONAL, sinceVersion, nil); err != nil { + return err + } + + // ExData, optional, dict, since V1.7 + d1, err := validateDictEntry(xRefTable, d, dictName, "ExData", OPTIONAL, model.V17, nil) + if err != nil { + return err + } + if d1 != nil { + if err := validateExDataDict(xRefTable, d1); err != nil { + return err + } + } + + return nil +} + +func validateMarkupAnnotation(xRefTable *model.XRefTable, d types.Dict) error { + + dictName := "markupAnnot" + + if err := validateMarkupAnnotationPart1(xRefTable, d, dictName); err != nil { + return err + } + + if err := validateMarkupAnnotationPart2(xRefTable, d, dictName); err != nil { + return err + } + + return nil +} + +func validateEntryP(xRefTable *model.XRefTable, d types.Dict, dictName string, required bool, sinceVersion model.Version) error { + + ir, err := validateIndRefEntry(xRefTable, d, dictName, "P", required, sinceVersion) + if err != nil || ir == nil { + return err + } + + // check if this indRef points to a pageDict. + + d1, err := xRefTable.DereferenceDict(*ir) + if err != nil { + return err + } + + if d1 == nil { + d.Delete("P") + return nil + } + + _, err = validateNameEntry(xRefTable, d1, "pageDict", "Type", REQUIRED, model.V10, func(s string) bool { return s == "Page" }) + + return err +} + +func validateAppearDictEntry(xRefTable *model.XRefTable, d types.Dict, dictName string, required bool, sinceVersion model.Version) error { + + d1, err := validateDictEntry(xRefTable, d, dictName, "AP", required, sinceVersion, nil) + if err != nil { + return err + } + + if d1 != nil { + err = validateAppearanceDict(xRefTable, d1) + } + + return err +} + +func validateDashPatternArray(xRefTable *model.XRefTable, arr types.Array) bool { + + // len must be 0,1,2,3 numbers (dont'allow only 0s) + + if len(arr) > 3 { + return false + } + + all0 := true + for j := 0; j < len(arr); j++ { + o, err := xRefTable.Dereference(arr[j]) + if err != nil || o == nil { + return false + } + + var f float64 + + switch o := o.(type) { + case types.Integer: + f = float64(o.Value()) + case types.Float: + f = o.Value() + default: + return false + } + + if f < 0 { + return false + } + + if f != 0 { + all0 = false + break + } + + } + + if all0 { + if xRefTable.ValidationMode != model.ValidationRelaxed { + return false + } + if log.ValidateEnabled() { + log.Validate.Println("digesting invalid dash pattern array: %s", arr) + } + } + + return true +} + +func validateBorderArray(xRefTable *model.XRefTable, a types.Array) bool { + if len(a) == 0 { + return true + } + + if xRefTable.Version() == model.V10 { + return len(a) == 3 + } + + if !(len(a) == 3 || len(a) == 4) { + return false + } + + for i := 0; i < len(a); i++ { + + if i == 3 { + // validate dash pattern array + // len must be 0,1,2,3 numbers (dont'allow only 0s) + dpa, ok := a[i].(types.Array) + if !ok { + return xRefTable.ValidationMode == model.ValidationRelaxed + } + + if len(dpa) == 0 { + return true + } + + if !validateDashPatternArray(xRefTable, dpa) { + return false + } + } + + o, err := xRefTable.Dereference(a[i]) + if err != nil || o == nil { + return false + } + + var f float64 + + switch o := o.(type) { + case types.Integer: + f = float64(o.Value()) + case types.Float: + f = o.Value() + default: + return false + } + + if f < 0 { + return false + } + } + + return true +} + +func validateAnnotationDictGeneralPart1(xRefTable *model.XRefTable, d types.Dict, dictName string) (*types.Name, error) { + // Type, optional, name + _, err := validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, model.V10, func(s string) bool { return s == "Annot" }) + if err != nil { + return nil, err + } + + // Subtype, required, name + subtype, err := validateNameEntry(xRefTable, d, dictName, "Subtype", REQUIRED, model.V10, nil) + if err != nil { + return nil, err + } + + // Rect, required, rectangle + _, err = validateRectangleEntry(xRefTable, d, dictName, "Rect", REQUIRED, model.V10, nil) + if err != nil { + return nil, err + } + + // Contents, optional, text string + _, err = validateStringEntry(xRefTable, d, dictName, "Contents", OPTIONAL, model.V10, nil) + if err != nil { + if xRefTable.ValidationMode != model.ValidationRelaxed { + return nil, err + } + i, err := validateIntegerEntry(xRefTable, d, dictName, "Contents", OPTIONAL, model.V10, nil) + if err != nil { + return nil, err + } + if i != nil { + // Repair + s := strconv.Itoa(i.Value()) + d["Contents"] = types.StringLiteral(s) + } + } + + // P, optional, indRef of page dict + err = validateEntryP(xRefTable, d, dictName, OPTIONAL, model.V10) + if err != nil { + return nil, err + } + + // NM, optional, text string, since V1.4 + sinceVersion := model.V14 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V13 + } + _, err = validateStringEntry(xRefTable, d, dictName, "NM", OPTIONAL, sinceVersion, nil) + if err != nil { + return nil, err + } + + return subtype, nil +} + +func validateAnnotationDictGeneralPart2(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + // M, optional, date string in any format, since V1.1 + if _, err := validateStringEntry(xRefTable, d, dictName, "M", OPTIONAL, model.V11, nil); err != nil { + return err + } + + // F, optional integer, since V1.1, annotation flags + if _, err := validateIntegerEntry(xRefTable, d, dictName, "F", OPTIONAL, model.V11, nil); err != nil { + return err + } + + // AP, optional, appearance dict, since V1.2 + if err := validateAppearDictEntry(xRefTable, d, dictName, OPTIONAL, model.V12); err != nil { + return err + } + + // AS, optional, name, since V1.2 + if _, err := validateNameEntry(xRefTable, d, dictName, "AS", OPTIONAL, model.V11, nil); err != nil { + return err + } + + // Border, optional, array of numbers + obj, found := d.Find("BS") + if !found || obj == nil || xRefTable.Version() < model.V12 { + a, err := validateArrayEntry(xRefTable, d, dictName, "Border", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + if !validateBorderArray(xRefTable, a) { + return errors.Errorf("invalid border array: %s", a) + } + } + + // C, optional array, of numbers, since V1.1 + if _, err := validateNumberArrayEntry(xRefTable, d, dictName, "C", OPTIONAL, model.V11, nil); err != nil { + return err + } + + // StructParent, optional, integer, since V1.3 + if _, err := validateIntegerEntry(xRefTable, d, dictName, "StructParent", OPTIONAL, model.V13, nil); err != nil { + return err + } + + return nil +} + +func validateAnnotationDictGeneral(xRefTable *model.XRefTable, d types.Dict, dictName string) (*types.Name, error) { + subType, err := validateAnnotationDictGeneralPart1(xRefTable, d, dictName) + if err != nil { + return nil, err + } + + return subType, validateAnnotationDictGeneralPart2(xRefTable, d, dictName) +} + +func validateAnnotationDictConcrete(xRefTable *model.XRefTable, d types.Dict, dictName string, subtype types.Name) error { + + // OC, optional, content group dict or content membership dict, since V1.5 + // Specifying the optional content properties for the annotation. + sinceVersion := model.V15 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V13 + } + if err := validateOptionalContent(xRefTable, d, dictName, "OC", OPTIONAL, sinceVersion); err != nil { + return err + } + + // see table 169 + + for k, v := range map[string]struct { + validate func(xRefTable *model.XRefTable, d types.Dict, dictName string) error + sinceVersion model.Version + markup bool + }{ + "Text": {validateAnnotationDictText, model.V10, true}, + "Link": {validateAnnotationDictLink, model.V10, false}, + "FreeText": {validateAnnotationDictFreeText, model.V12, true}, // model.V13 + "Line": {validateAnnotationDictLine, model.V13, true}, + "Polygon": {validateAnnotationDictPolyLine, model.V15, true}, + "PolyLine": {validateAnnotationDictPolyLine, model.V15, true}, + "Highlight": {validateTextMarkupAnnotation, model.V13, true}, + "Underline": {validateTextMarkupAnnotation, model.V13, true}, + "Squiggly": {validateTextMarkupAnnotation, model.V14, true}, + "StrikeOut": {validateTextMarkupAnnotation, model.V13, true}, + "Square": {validateAnnotationDictCircleOrSquare, model.V13, true}, + "Circle": {validateAnnotationDictCircleOrSquare, model.V13, true}, + "Stamp": {validateAnnotationDictStamp, model.V13, true}, + "Caret": {validateAnnotationDictCaret, model.V15, true}, + "Ink": {validateAnnotationDictInk, model.V13, true}, + "Popup": {validateAnnotationDictPopup, model.V12, false}, // model.V13 + "FileAttachment": {validateAnnotationDictFileAttachment, model.V13, true}, + "Sound": {validateAnnotationDictSound, model.V12, true}, + "Movie": {validateAnnotationDictMovie, model.V12, false}, + "Widget": {validateAnnotationDictWidget, model.V12, false}, + "Screen": {validateAnnotationDictScreen, model.V15, false}, + "PrinterMark": {validateAnnotationDictPrinterMark, model.V14, false}, + "TrapNet": {validateAnnotationDictTrapNet, model.V13, false}, + "Watermark": {validateAnnotationDictWatermark, model.V16, false}, + "3D": {validateAnnotationDict3D, model.V16, false}, + "Redact": {validateAnnotationDictRedact, model.V17, true}, + "RichMedia": {validateRichMediaAnnotation, model.V17, false}, + "GoldGrid:AddSeal": {validateAnnotationDictTrapNet, model.V13, true}, + "BJCA:Annot": {validateAnnotationDictTrapNet, model.V13, true}, + } { + if subtype.Value() == k { + + err := xRefTable.ValidateVersion(k, v.sinceVersion) + if err != nil { + return err + } + + if v.markup { + err := validateMarkupAnnotation(xRefTable, d) + if err != nil { + return err + } + } + + return v.validate(xRefTable, d, k) + } + } + + return errors.Errorf("validateAnnotationDictConcrete: unsupported annotation subtype:%s\n", subtype) +} + +func validateAnnotationDictSpecial(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // AAPL:AKExtras + // No documentation for this PDF-Extension - this is a speculative implementation. + return validateAAPLAKExtrasDictEntry(xRefTable, d, dictName, "AAPL:AKExtras", OPTIONAL, model.V10) +} + +func validateAnnotationDict(xRefTable *model.XRefTable, d types.Dict) (isTrapNet bool, err error) { + + dictName := "annotDict" + + subtype, err := validateAnnotationDictGeneral(xRefTable, d, dictName) + if err != nil { + return false, err + } + + err = validateAnnotationDictConcrete(xRefTable, d, dictName, *subtype) + if err != nil { + return false, err + } + + err = validateAnnotationDictSpecial(xRefTable, d, dictName) + if err != nil { + return false, err + } + + return *subtype == "TrapNet", nil +} + +func validateAnnotationsArray(xRefTable *model.XRefTable, a types.Array) error { + + // a ... array of indrefs to annotation dicts. + + var annotDict types.Dict + + pgAnnots := model.PgAnnots{} + xRefTable.PageAnnots[xRefTable.CurPage] = pgAnnots + + // an optional TrapNetAnnotation has to be the final entry in this list. + hasTrapNet := false + + for i, v := range a { + + if hasTrapNet { + return errors.New("pdfcpu: validatePageAnnotations: corrupted page annotation list, \"TrapNet\" has to be the last entry") + } + + var ( + ok, hasIndRef bool + indRef types.IndirectRef + err error + ) + + if indRef, ok = v.(types.IndirectRef); ok { + hasIndRef = true + if log.ValidateEnabled() { + log.Validate.Printf("processing annotDict %d\n", indRef.ObjectNumber) + } + annotDict, err = xRefTable.DereferenceDict(indRef) + if err != nil { + return err + } + if len(annotDict) == 0 { + continue + } + } else if xRefTable.ValidationMode != model.ValidationRelaxed { + return errInvalidPageAnnotArray + } else if annotDict, ok = v.(types.Dict); !ok { + return errInvalidPageAnnotArray + } else { + if log.ValidateEnabled() { + log.Validate.Println("digesting page annotation array w/o indirect references") + } + } + + hasTrapNet, err = validateAnnotationDict(xRefTable, annotDict) + if err != nil { + return err + } + + // Collect annotation. + + ann, err := pdfcpu.Annotation(xRefTable, annotDict) + if err != nil { + return err + } + + annots, ok := pgAnnots[ann.Type()] + if !ok { + annots = model.Annot{} + annots.IndRefs = &[]types.IndirectRef{} + annots.Map = model.AnnotMap{} + pgAnnots[ann.Type()] = annots + } + + objNr := -i + if hasIndRef { + objNr = indRef.ObjectNumber.Value() + *(annots.IndRefs) = append(*(annots.IndRefs), indRef) + } + annots.Map[objNr] = ann + } + + return nil +} + +func validatePageAnnotations(xRefTable *model.XRefTable, d types.Dict) error { + a, err := validateArrayEntry(xRefTable, d, "pageDict", "Annots", OPTIONAL, model.V10, nil) + if err != nil || a == nil { + return err + } + + if len(a) == 0 { + return nil + } + + return validateAnnotationsArray(xRefTable, a) +} + +func validatePagesAnnotations(xRefTable *model.XRefTable, d types.Dict, curPage int) (int, error) { + + // Get number of pages of this PDF file. + pageCount := d.IntEntry("Count") + if pageCount == nil { + return curPage, errors.New("pdfcpu: validatePagesAnnotations: missing \"Count\"") + } + + if log.ValidateEnabled() { + log.Validate.Printf("validatePagesAnnotations: This page node has %d pages\n", *pageCount) + } + + // Iterate over page tree. + kidsArray := d.ArrayEntry("Kids") + + for _, v := range kidsArray { + + if v == nil { + if log.ValidateEnabled() { + log.Validate.Println("validatePagesAnnotations: kid is nil") + } + continue + } + + d, err := xRefTable.DereferenceDict(v) + if err != nil { + return curPage, err + } + if d == nil { + return curPage, errors.New("pdfcpu: validatePagesAnnotations: pageNodeDict is null") + } + dictType := d.Type() + if dictType == nil { + return curPage, errors.New("pdfcpu: validatePagesAnnotations: missing pageNodeDict type") + } + + switch *dictType { + + case "Pages": + // Recurse over pagetree + curPage, err = validatePagesAnnotations(xRefTable, d, curPage) + if err != nil { + return curPage, err + } + + case "Page": + curPage++ + xRefTable.CurPage = curPage + err = validatePageAnnotations(xRefTable, d) + if err != nil { + return curPage, err + } + + default: + return curPage, errors.Errorf("validatePagesAnnotations: expected dict type: %s\n", *dictType) + + } + + } + + return curPage, nil +} diff --git a/pkg/pdfcpu/validate/colorspace.go b/pkg/pdfcpu/validate/colorspace.go new file mode 100644 index 0000000000000000000000000000000000000000..71c8c1acc92bb958291b831ebd6b51cfa4937246 --- /dev/null +++ b/pkg/pdfcpu/validate/colorspace.go @@ -0,0 +1,690 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 validate + +import ( + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +func validateDeviceColorSpaceName(s string) bool { + return types.MemberOf(s, []string{model.DeviceGrayCS, model.DeviceRGBCS, model.DeviceCMYKCS}) +} + +func validateAllColorSpaceNamesExceptPattern(s string) bool { + return types.MemberOf(s, []string{model.DeviceGrayCS, model.DeviceRGBCS, model.DeviceCMYKCS, model.CalGrayCS, model.CalRGBCS, model.LabCS, model.ICCBasedCS, model.IndexedCS, model.SeparationCS, model.DeviceNCS}) +} + +func validateCalGrayColorSpace(xRefTable *model.XRefTable, a types.Array, sinceVersion model.Version) error { + + dictName := "calGrayCSDict" + + // Version check + err := xRefTable.ValidateVersion(dictName, sinceVersion) + if err != nil { + return err + } + + if len(a) != 2 { + return errors.Errorf("validateCalGrayColorSpace: invalid array length %d (expected 2) \n.", len(a)) + } + + d, err := xRefTable.DereferenceDict(a[1]) + if err != nil || d == nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "WhitePoint", REQUIRED, sinceVersion, func(a types.Array) bool { return len(a) == 3 }) + if err != nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "BlackPoint", OPTIONAL, sinceVersion, func(a types.Array) bool { return len(a) == 3 }) + if err != nil { + return err + } + + _, err = validateNumberEntry(xRefTable, d, dictName, "Gamma", OPTIONAL, sinceVersion, nil) + + return err +} + +func validateCalRGBColorSpace(xRefTable *model.XRefTable, a types.Array, sinceVersion model.Version) error { + + dictName := "calRGBCSDict" + + err := xRefTable.ValidateVersion(dictName, sinceVersion) + if err != nil { + return err + } + + if len(a) != 2 { + return errors.Errorf("validateCalRGBColorSpace: invalid array length %d (expected 2) \n.", len(a)) + } + + d, err := xRefTable.DereferenceDict(a[1]) + if err != nil || d == nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "WhitePoint", REQUIRED, sinceVersion, func(a types.Array) bool { return len(a) == 3 }) + if err != nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "BlackPoint", OPTIONAL, sinceVersion, func(a types.Array) bool { return len(a) == 3 }) + if err != nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "Gamma", OPTIONAL, sinceVersion, func(a types.Array) bool { return len(a) == 3 }) + if err != nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "Matrix", OPTIONAL, sinceVersion, func(a types.Array) bool { return len(a) == 9 }) + + return err +} + +func validateLabColorSpace(xRefTable *model.XRefTable, a types.Array, sinceVersion model.Version) error { + + dictName := "labCSDict" + + err := xRefTable.ValidateVersion(dictName, sinceVersion) + if err != nil { + return err + } + + if len(a) != 2 { + return errors.Errorf("validateLabColorSpace: invalid array length %d (expected 2) \n.", len(a)) + } + + d, err := xRefTable.DereferenceDict(a[1]) + if err != nil || d == nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "WhitePoint", REQUIRED, sinceVersion, func(a types.Array) bool { return len(a) == 3 }) + if err != nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "BlackPoint", OPTIONAL, sinceVersion, func(a types.Array) bool { return len(a) == 3 }) + if err != nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "Range", OPTIONAL, sinceVersion, func(a types.Array) bool { return len(a) == 4 }) + + return err +} + +func validateAlternateColorSpaceEntryForICC(xRefTable *model.XRefTable, d types.Dict, dictName string, entryName string, required bool, excludePatternCS bool) error { + + o, err := validateEntry(xRefTable, d, dictName, entryName, required, model.V10) + if err != nil || o == nil { + return err + } + + switch o := o.(type) { + + case types.Name: + if ok := validateAllColorSpaceNamesExceptPattern(o.Value()); !ok { + err = errors.Errorf("pdfcpu: validateAlternateColorSpaceEntryForICC: invalid Name:%s\n", o.Value()) + } + + case types.Array: + err = validateColorSpaceArray(xRefTable, o, excludePatternCS) + + default: + err = errors.Errorf("pdfcpu: validateAlternateColorSpaceEntryForICC: dict=%s corrupt entry \"%s\"\n", dictName, entryName) + + } + + return err +} + +func validateICCBasedColorSpace(xRefTable *model.XRefTable, a types.Array, sinceVersion model.Version) error { + + // see 8.6.5.5 + + dictName := "ICCBasedColorSpace" + + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V12 + } + err := xRefTable.ValidateVersion(dictName, sinceVersion) + if err != nil { + return err + } + + if len(a) != 2 { + return errors.Errorf("validateICCBasedColorSpace: invalid array length %d (expected 2) \n.", len(a)) + } + + valid, err := xRefTable.IsValid(a[1].(types.IndirectRef)) + if err != nil { + return err + } + if valid { + return nil + } + + sd, err := validateStreamDict(xRefTable, a[1]) + if err != nil || sd == nil { + return err + } + if err := xRefTable.SetValid(a[1].(types.IndirectRef)); err != nil { + return err + } + + validate := func(i int) bool { return types.IntMemberOf(i, []int{1, 3, 4}) } + N, err := validateIntegerEntry(xRefTable, sd.Dict, dictName, "N", REQUIRED, sinceVersion, validate) + if err != nil { + return err + } + + err = validateAlternateColorSpaceEntryForICC(xRefTable, sd.Dict, dictName, "Alternate", OPTIONAL, ExcludePatternCS) + if err != nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, sd.Dict, dictName, "Range", OPTIONAL, sinceVersion, func(a types.Array) bool { return len(a) == 2*N.Value() }) + if err != nil { + return err + } + + // Metadata, stream, optional since V1.4 + return validateMetadata(xRefTable, sd.Dict, OPTIONAL, model.V14) +} + +func validateIndexedColorSpaceLookuptable(xRefTable *model.XRefTable, o types.Object, sinceVersion model.Version) error { + + o, err := xRefTable.Dereference(o) + if err != nil || o == nil { + return err + } + + switch o.(type) { + + case types.StringLiteral, types.HexLiteral: + err = xRefTable.ValidateVersion("IndexedColorSpaceLookuptable", model.V12) + + case types.StreamDict: + err = xRefTable.ValidateVersion("IndexedColorSpaceLookuptable", sinceVersion) + + default: + err = errors.Errorf("validateIndexedColorSpaceLookuptable: invalid type\n") + + } + + return err +} + +func validateIndexedColorSpace(xRefTable *model.XRefTable, a types.Array, sinceVersion model.Version) error { + + // see 8.6.6.3 + + err := xRefTable.ValidateVersion("IndexedColorSpace", sinceVersion) + if err != nil { + return err + } + + if len(a) != 4 { + return errors.Errorf("validateIndexedColorSpace: invalid array length %d (expected 4) \n.", len(a)) + } + + // arr[1] base: base colorspace + err = validateColorSpace(xRefTable, a[1], ExcludePatternCS) + if err != nil { + return err + } + + // arr[2] hival: 0 <= int <= 255 + _, err = validateInteger(xRefTable, a[2], func(i int) bool { return i >= 0 && i <= 255 }) + if err != nil { + return err + } + + // arr[3] lookup: stream since V1.2 or byte string + return validateIndexedColorSpaceLookuptable(xRefTable, a[3], sinceVersion) +} + +func validatePatternColorSpace(xRefTable *model.XRefTable, a types.Array, sinceVersion model.Version) error { + + err := xRefTable.ValidateVersion("PatternColorSpace", sinceVersion) + if err != nil { + return err + } + + if len(a) < 1 || len(a) > 2 { + return errors.Errorf("validatePatternColorSpace: invalid array length %d (expected 1 or 2) \n.", len(a)) + } + + // 8.7.3.3: arr[1]: name of underlying color space, any cs except PatternCS + if len(a) == 2 { + err := validateColorSpace(xRefTable, a[1], ExcludePatternCS) + if err != nil { + return err + } + } + + return nil +} + +func validateSeparationColorSpace(xRefTable *model.XRefTable, a types.Array, sinceVersion model.Version) error { + + // see 8.6.6.4 + + err := xRefTable.ValidateVersion("SeparationColorSpace", sinceVersion) + if err != nil { + return err + } + + if len(a) != 4 { + return errors.Errorf("validateSeparationColorSpace: invalid array length %d (expected 4) \n.", len(a)) + } + + // arr[1]: colorant name, arbitrary + _, err = validateName(xRefTable, a[1], nil) + if err != nil { + return err + } + + // arr[2]: alternate space + err = validateColorSpace(xRefTable, a[2], ExcludePatternCS) + if err != nil { + return err + } + + // arr[3]: tintTransform, function + return validateFunction(xRefTable, a[3]) +} + +func validateDeviceNColorSpaceColorantsDict(xRefTable *model.XRefTable, d types.Dict) error { + + for _, obj := range d { + + a, err := xRefTable.DereferenceArray(obj) + if err != nil { + return err + } + + if a != nil { + err = validateSeparationColorSpace(xRefTable, a, model.V12) + if err != nil { + return err + } + } + + } + + return nil +} + +func validateDeviceNColorSpaceProcessDict(xRefTable *model.XRefTable, d types.Dict) error { + + dictName := "DeviceNCSProcessDict" + + err := validateColorSpaceEntry(xRefTable, d, dictName, "ColorSpace", REQUIRED, true) + if err != nil { + return err + } + + _, err = validateNameArrayEntry(xRefTable, d, dictName, "Components", REQUIRED, model.V10, nil) + + return err +} + +func validateDeviceNColorSpaceSoliditiesDict(xRefTable *model.XRefTable, d types.Dict) error { + + for _, obj := range d { + _, err := validateFloat(xRefTable, obj, func(f float64) bool { return f >= 0.0 && f <= 1.0 }) + if err != nil { + return err + } + } + + return nil +} + +func validateDeviceNColorSpaceDotGainDict(xRefTable *model.XRefTable, d types.Dict) error { + + for _, obj := range d { + err := validateFunction(xRefTable, obj) + if err != nil { + return err + } + } + + return nil +} + +func validateDeviceNColorSpaceMixingHintsDict(xRefTable *model.XRefTable, d types.Dict) error { + + dictName := "deviceNCSMixingHintsDict" + + d1, err := validateDictEntry(xRefTable, d, dictName, "Solidities", OPTIONAL, model.V11, nil) + if err != nil { + return err + } + if d1 != nil { + err = validateDeviceNColorSpaceSoliditiesDict(xRefTable, d1) + if err != nil { + return err + } + } + + _, err = validateNameArrayEntry(xRefTable, d, dictName, "PrintingOrder", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + d1, err = validateDictEntry(xRefTable, d, dictName, "DotGain", OPTIONAL, model.V11, nil) + if err != nil { + return err + } + + if d1 != nil { + err = validateDeviceNColorSpaceDotGainDict(xRefTable, d1) + } + + return err +} + +func validateDeviceNColorSpaceAttributesDict(xRefTable *model.XRefTable, o types.Object) error { + + d, err := xRefTable.DereferenceDict(o) + if err != nil || d == nil { + return err + } + + dictName := "deviceNCSAttributesDict" + + sinceVersion := model.V16 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V13 + } + + _, err = validateNameEntry(xRefTable, d, dictName, "Subtype", OPTIONAL, sinceVersion, func(s string) bool { return s == "DeviceN" || s == "NChannel" }) + if err != nil { + return err + } + + d1, err := validateDictEntry(xRefTable, d, dictName, "Colorants", OPTIONAL, model.V11, nil) + if err != nil { + return err + } + + if d1 != nil { + err = validateDeviceNColorSpaceColorantsDict(xRefTable, d1) + if err != nil { + return err + } + } + + d1, err = validateDictEntry(xRefTable, d, dictName, "Process", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + if d1 != nil { + err = validateDeviceNColorSpaceProcessDict(xRefTable, d1) + if err != nil { + return err + } + } + + d1, err = validateDictEntry(xRefTable, d, dictName, "MixingHints", OPTIONAL, model.V16, nil) + if err != nil { + return err + } + + if d1 != nil { + err = validateDeviceNColorSpaceMixingHintsDict(xRefTable, d1) + } + + return err +} + +func validateDeviceNColorSpace(xRefTable *model.XRefTable, a types.Array, sinceVersion model.Version) error { + + // see 8.6.6.5 + + err := xRefTable.ValidateVersion("DeviceNColorSpace", sinceVersion) + if err != nil { + return err + } + + if len(a) < 4 || len(a) > 5 { + return errors.Errorf("writeDeviceNColorSpace: invalid array length %d (expected 4 or 5) \n.", len(a)) + } + + // arr[1]: array of names specifying the individual color components + // length subject to implementation limit. + _, err = validateNameArray(xRefTable, a[1]) + if err != nil { + return err + } + + // arr[2]: alternate space + err = validateColorSpace(xRefTable, a[2], ExcludePatternCS) + if err != nil { + return err + } + + // arr[3]: tintTransform, function + err = validateFunction(xRefTable, a[3]) + if err != nil { + return err + } + + // arr[4]: color space attributes dict, optional + if len(a) == 5 { + err = validateDeviceNColorSpaceAttributesDict(xRefTable, a[4]) + } + + return err +} + +func validateCSArray(xRefTable *model.XRefTable, a types.Array, csName string) error { + + // see 8.6 Color Spaces + + switch csName { + + // CIE-based + case model.CalGrayCS: + return validateCalGrayColorSpace(xRefTable, a, model.V11) + + case model.CalRGBCS: + return validateCalRGBColorSpace(xRefTable, a, model.V11) + + case model.LabCS: + return validateLabColorSpace(xRefTable, a, model.V11) + + case model.ICCBasedCS: + return validateICCBasedColorSpace(xRefTable, a, model.V13) + + // Special + case model.IndexedCS: + return validateIndexedColorSpace(xRefTable, a, model.V11) + + case model.PatternCS: + return validatePatternColorSpace(xRefTable, a, model.V12) + + case model.SeparationCS: + return validateSeparationColorSpace(xRefTable, a, model.V12) + + case model.DeviceNCS: + return validateDeviceNColorSpace(xRefTable, a, model.V13) + + default: + return errors.Errorf("validateColorSpaceArray: undefined color space: %s\n", csName) + } + +} + +func validateColorSpaceArraySubset(xRefTable *model.XRefTable, a types.Array, cs []string) error { + + csName, ok := a[0].(types.Name) + if !ok { + return errors.New("pdfcpu: validateColorSpaceArraySubset: corrupt Colorspace array") + } + + for _, v := range cs { + if csName.Value() == v { + return validateCSArray(xRefTable, a, v) + } + } + + return errors.Errorf("pdfcpu: validateColorSpaceArraySubset: invalid color space: %s\n", csName) +} + +func validateColorSpaceArray(xRefTable *model.XRefTable, a types.Array, excludePatternCS bool) (err error) { + + // see 8.6 Color Spaces + + name, ok := a[0].(types.Name) + if !ok { + return errors.New("pdfcpu: validateColorSpaceArray: corrupt Colorspace array") + } + + switch name { + + // CIE-based + case model.CalGrayCS: + err = validateCalGrayColorSpace(xRefTable, a, model.V11) + + case model.CalRGBCS: + err = validateCalRGBColorSpace(xRefTable, a, model.V11) + + case model.LabCS: + err = validateLabColorSpace(xRefTable, a, model.V11) + + case model.ICCBasedCS: + err = validateICCBasedColorSpace(xRefTable, a, model.V13) + + // Special + case model.IndexedCS: + err = validateIndexedColorSpace(xRefTable, a, model.V11) + + case model.PatternCS: + if excludePatternCS { + return errors.New("pdfcpu: validateColorSpaceArray: Pattern color space not allowed") + } + err = validatePatternColorSpace(xRefTable, a, model.V12) + + case model.SeparationCS: + err = validateSeparationColorSpace(xRefTable, a, model.V12) + + case model.DeviceNCS: + err = validateDeviceNColorSpace(xRefTable, a, model.V13) + + case model.DeviceGrayCS, model.DeviceRGBCS, model.DeviceCMYKCS: + if xRefTable.ValidationMode != model.ValidationRelaxed { + err = errors.Errorf("pdfcpu: validateColorSpaceArray: undefined color space: %s\n", name) + } + + default: + err = errors.Errorf("pdfcpu: validateColorSpaceArray: undefined color space: %s\n", name) + } + + return err +} + +func validateColorSpace(xRefTable *model.XRefTable, o types.Object, excludePatternCS bool) error { + + o, err := xRefTable.Dereference(o) + if err != nil || o == nil { + return err + } + + switch o := o.(type) { + + case types.Name: + validateSpecialColorSpaceName := func(s string) bool { return types.MemberOf(s, []string{"Pattern"}) } + if ok := validateDeviceColorSpaceName(o.Value()) || validateSpecialColorSpaceName(o.Value()); !ok { + err = errors.Errorf("validateColorSpace: invalid device color space name: %v\n", o) + } + + case types.Array: + err = validateColorSpaceArray(xRefTable, o, excludePatternCS) + + default: + err = errors.New("pdfcpu: validateColorSpace: corrupt obj typ, must be Name or Array") + + } + + return err +} + +func validateColorSpaceEntry(xRefTable *model.XRefTable, d types.Dict, dictName string, entryName string, required bool, excludePatternCS bool) error { + + o, err := validateEntry(xRefTable, d, dictName, entryName, required, model.V10) + if err != nil || o == nil { + return err + } + + switch o := o.(type) { + + case types.Name: + if ok := validateDeviceColorSpaceName(o.Value()); !ok { + err = errors.Errorf("pdfcpu: validateColorSpaceEntry: Name:%s\n", o.Value()) + } + + case types.Array: + err = validateColorSpaceArray(xRefTable, o, excludePatternCS) + + default: + err = errors.Errorf("pdfcpu: validateColorSpaceEntry: dict=%s corrupt entry \"%s\"\n", dictName, entryName) + + } + + return err +} + +func validateColorSpaceResourceDict(xRefTable *model.XRefTable, o types.Object, sinceVersion model.Version) error { + + // see 8.6 Color Spaces + + // Version check + err := xRefTable.ValidateVersion("ColorSpaceResourceDict", sinceVersion) + if err != nil { + return err + } + + d, err := xRefTable.DereferenceDict(o) + if err != nil || d == nil { + return err + } + + // Iterate over colorspace resource dictionary + for _, o := range d { + + // Process colorspace + err = validateColorSpace(xRefTable, o, IncludePatternCS) + if err != nil { + return err + } + + } + + return nil +} diff --git a/pkg/pdfcpu/validate/destination.go b/pkg/pdfcpu/validate/destination.go new file mode 100644 index 0000000000000000000000000000000000000000..64966851de201fe38bfd002c87528cd991475734 --- /dev/null +++ b/pkg/pdfcpu/validate/destination.go @@ -0,0 +1,183 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 validate + +import ( + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +func validateDestinationArrayFirstElement(xRefTable *model.XRefTable, a types.Array) (types.Object, error) { + + o, err := xRefTable.Dereference(a[0]) + if err != nil || o == nil { + return nil, err + } + + if o == nil { + return nil, errors.Errorf("destination array invalid: %s", a) + } + + switch o := o.(type) { + + case types.Integer, types.Name: // no further processing + + case types.Dict: + if o.Type() == nil || (o.Type() != nil && (*o.Type() != "Page" && *o.Type() != "Pages")) { + err = errors.Errorf("pdfcpu: validateDestinationArrayFirstElement: first element must be a pageDict indRef or an integer: %v (%T)", o, o) + } + + default: + err = errors.Errorf("pdfcpu: validateDestinationArrayFirstElement: first element must be a pageDict indRef or an integer: %v (%T)", o, o) + } + + return o, err +} + +func validateDestinationArrayLength(a types.Array) bool { + l := len(a) + return l == 2 || l == 3 || l == 5 || l == 6 || l == 4 // 4 = hack! see below +} + +func validateDestinationArray(xRefTable *model.XRefTable, a types.Array) error { + + // Validate first element: indRef of page dict or pageNumber(int) of remote doc for remote Go-to Action or nil. + + o, err := validateDestinationArrayFirstElement(xRefTable, a) + if err != nil || o == nil { + return err + } + + if !validateDestinationArrayLength(a) { + return errors.Errorf("pdfcpu: validateDestinationArray: invalid length: %d", len(a)) + } + + // NOTE if len == 4 we possible have a missing first element, which should be an indRef to the dest page. + // TODO Investigate. + i := 1 + if len(a) == 4 { + i = 0 + } + + // Validate rest of array elements. + + name, ok := a[i].(types.Name) + if !ok { + return errors.Errorf("pdfcpu: validateDestinationArray: second element must be a name %v (%d)", a[i], i) + } + + var nameErr bool + + switch len(a) { + + case 2: + nameErr = !types.MemberOf(name.Value(), []string{"Fit", "FitB", "FitH", "FitV", "FitBH", "FitBV"}) + + case 3: + nameErr = !types.MemberOf(name.Value(), []string{"FitH", "FitV", "FitBH", "FitBV"}) + + case 4: + // TODO Cleanup + // hack for #381 - possibly zoom == null or 0 + // eg. [(886 0 R) XYZ 53 303] + nameErr = name.Value() != "XYZ" + + case 5: + nameErr = name.Value() != "XYZ" + + case 6: + nameErr = name.Value() != "FitR" + + default: + return errors.Errorf("validateDestinationArray: array length %d not allowed: %v", len(a), a) + } + + if nameErr { + return errors.New("pdfcpu: validateDestinationArray: arr[1] corrupt") + } + + return nil +} + +func validateDestinationDict(xRefTable *model.XRefTable, d types.Dict) error { + + // D, required, array + a, err := validateArrayEntry(xRefTable, d, "DestinationDict", "D", REQUIRED, model.V10, nil) + if err != nil || a == nil { + return err + } + + return validateDestinationArray(xRefTable, a) +} + +func validateDestination(xRefTable *model.XRefTable, o types.Object, forAction bool) (string, error) { + + o, err := xRefTable.Dereference(o) + if err != nil || o == nil { + return "", err + } + + switch o := o.(type) { + + case types.Name: + return o.Value(), nil + + case types.StringLiteral: + return types.StringLiteralToString(o) + + case types.HexLiteral: + return types.HexLiteralToString(o) + + case types.Dict: + if forAction { + return "", errors.New("pdfcpu: validateDestination: unsupported PDF object") + } + err = validateDestinationDict(xRefTable, o) + + case types.Array: + err = validateDestinationArray(xRefTable, o) + + default: + err = errors.New("pdfcpu: validateDestination: unsupported PDF object") + + } + + return "", err +} + +func validateActionDestinationEntry(xRefTable *model.XRefTable, d types.Dict, dictName string, entryName string, required bool, sinceVersion model.Version) error { + + // see 12.3.2 + + o, err := validateEntry(xRefTable, d, dictName, entryName, required, sinceVersion) + if err != nil { + return err + } + + name, err := validateDestination(xRefTable, o, true) + if err != nil { + return err + } + + if len(name) > 0 && xRefTable.IsMerging() { + nm := xRefTable.NameRef("Dests") + nm.Add(name, d) + } + + return nil +} diff --git a/pkg/pdfcpu/validate/extGState.go b/pkg/pdfcpu/validate/extGState.go new file mode 100644 index 0000000000000000000000000000000000000000..bc25788f3823ed4e1162d287306470ef81db34c1 --- /dev/null +++ b/pkg/pdfcpu/validate/extGState.go @@ -0,0 +1,1017 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 validate + +import ( + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// see 8.4.5 Graphics State Parameter Dictionaries + +func validateBlendMode(s string) bool { + + // see 11.3.5; table 136 + + return types.MemberOf(s, []string{"None", "Normal", "Compatible", "Multiply", "Screen", "Overlay", "Darken", "Lighten", + "ColorDodge", "ColorBurn", "HardLight", "SoftLight", "Difference", "Exclusion", + "Hue", "Saturation", "Color", "Luminosity"}) +} + +func validateLineDashPatternEntry(xRefTable *model.XRefTable, d types.Dict, dictName string, entryName string, required bool, sinceVersion model.Version) error { + + a, err := validateArrayEntry(xRefTable, d, dictName, entryName, required, sinceVersion, func(a types.Array) bool { return len(a) == 2 }) + if err != nil || a == nil { + return err + } + + _, err = validateIntegerArray(xRefTable, a[0]) + if err != nil { + return err + } + + _, err = validateInteger(xRefTable, a[1], nil) + + return err +} + +func validateBGEntry(xRefTable *model.XRefTable, d types.Dict, dictName string, entryName string, required bool, sinceVersion model.Version) error { + + o, err := validateEntry(xRefTable, d, dictName, entryName, required, sinceVersion) + if err != nil || o == nil { + return err + } + + switch o := o.(type) { + + case types.Name: + if xRefTable.ValidationMode == model.ValidationStrict { + err = errors.Errorf("pdfcpu: validateBGEntry: dict=%s corrupt entry \"%s\"\n", dictName, entryName) + break + } + s := o.Value() + if s != "Identity" { + err = errors.New("pdfcpu: validateBGEntry: corrupt name") + } + + case types.Dict: + err = processFunction(xRefTable, o) + + case types.StreamDict: + err = processFunction(xRefTable, o) + + default: + err = errors.Errorf("pdfcpu: validateBGEntry: dict=%s corrupt entry \"%s\"\n", dictName, entryName) + + } + + return err +} + +func validateBG2Entry(xRefTable *model.XRefTable, d types.Dict, dictName string, entryName string, required bool, sinceVersion model.Version) error { + + o, err := validateEntry(xRefTable, d, dictName, entryName, required, sinceVersion) + if err != nil || o == nil { + return err + } + + switch o := o.(type) { + + case types.Name: + s := o.Value() + if s != "Default" { + err = errors.New("pdfcpu: validateBG2Entry: corrupt name") + } + + case types.Dict: + err = processFunction(xRefTable, o) + + case types.StreamDict: + err = processFunction(xRefTable, o) + + default: + err = errors.Errorf("pdfcpu: validateBG2Entry: dict=%s corrupt entry \"%s\"\n", dictName, entryName) + + } + + return err +} + +func validateUCREntry(xRefTable *model.XRefTable, d types.Dict, dictName string, entryName string, required bool, sinceVersion model.Version) error { + + o, err := validateEntry(xRefTable, d, dictName, entryName, required, sinceVersion) + if err != nil || o == nil { + return err + } + + switch o := o.(type) { + + case types.Name: + if xRefTable.ValidationMode == model.ValidationStrict { + err = errors.Errorf("pdfcpu: validateUCREntry: dict=%s corrupt entry \"%s\"\n", dictName, entryName) + break + } + s := o.Value() + if s != "Identity" { + err = errors.New("pdfcpu: writeUCREntry: corrupt name") + } + + case types.Dict: + err = processFunction(xRefTable, o) + + case types.StreamDict: + err = processFunction(xRefTable, o) + + default: + err = errors.Errorf("pdfcpu: validateUCREntry: dict=%s corrupt entry \"%s\"\n", dictName, entryName) + + } + + return err +} + +func validateUCR2Entry(xRefTable *model.XRefTable, d types.Dict, dictName string, entryName string, required bool, sinceVersion model.Version) error { + + o, err := validateEntry(xRefTable, d, dictName, entryName, required, sinceVersion) + if err != nil || o == nil { + return err + } + + switch o := o.(type) { + + case types.Name: + s := o.Value() + if s != "Default" { + err = errors.New("pdfcpu: writeUCR2Entry: corrupt name") + } + + case types.Dict: + err = processFunction(xRefTable, o) + + case types.StreamDict: + err = processFunction(xRefTable, o) + + default: + err = errors.Errorf("pdfcpu: validateUCR2Entry: dict=%s corrupt entry \"%s\"\n", dictName, entryName) + + } + + return err +} + +func validateTransferFunction(xRefTable *model.XRefTable, o types.Object) (err error) { + + switch o := o.(type) { + + case types.Name: + s := o.Value() + if s != "Identity" { + return errors.New("pdfcpu: validateTransferFunction: corrupt name") + } + + case types.Array: + + if len(o) != 4 { + return errors.New("pdfcpu: validateTransferFunction: corrupt function array") + } + + for _, o := range o { + + o, err := xRefTable.Dereference(o) + if err != nil { + return err + } + if o == nil { + continue + } + + err = processFunction(xRefTable, o) + if err != nil { + return err + } + + } + + case types.Dict: + err = processFunction(xRefTable, o) + + case types.StreamDict: + err = processFunction(xRefTable, o) + + default: + return errors.Errorf("validateTransferFunction: corrupt entry: %v\n", o) + + } + + return err +} + +func validateTransferFunctionEntry(xRefTable *model.XRefTable, d types.Dict, dictName string, entryName string, required bool, sinceVersion model.Version) error { + + o, err := validateEntry(xRefTable, d, dictName, entryName, required, sinceVersion) + if err != nil || o == nil { + return err + } + + return validateTransferFunction(xRefTable, o) +} + +func validateTR(xRefTable *model.XRefTable, o types.Object) (err error) { + + switch o := o.(type) { + + case types.Name: + s := o.Value() + if s != "Identity" { + return errors.Errorf("pdfcpu: validateTR: corrupt name\n") + } + + case types.Array: + + if len(o) != 4 { + return errors.New("pdfcpu: validateTR: corrupt function array") + } + + for _, o := range o { + + o, err = xRefTable.Dereference(o) + if err != nil { + return + } + + if o == nil { + continue + } + + if o, ok := o.(types.Name); ok { + s := o.Value() + if s != "Identity" { + return errors.Errorf("pdfcpu: validateTR: corrupt name\n") + } + continue + } + + err = processFunction(xRefTable, o) + if err != nil { + return + } + + } + + case types.Dict: + err = processFunction(xRefTable, o) + + case types.StreamDict: + err = processFunction(xRefTable, o) + + default: + return errors.Errorf("validateTR: corrupt entry %v\n", o) + + } + + return err +} + +func validateTREntry(xRefTable *model.XRefTable, d types.Dict, dictName string, entryName string, required bool, sinceVersion model.Version) error { + + o, err := validateEntry(xRefTable, d, dictName, entryName, required, sinceVersion) + if err != nil || o == nil { + return err + } + + return validateTR(xRefTable, o) +} + +func validateTR2Name(xRefTable *model.XRefTable, name types.Name) error { + s := name.Value() + if s != "Identity" && s != "Default" { + return errors.Errorf("pdfcpu: validateTR2: corrupt name\n") + } + return nil +} + +func validateTR2(xRefTable *model.XRefTable, o types.Object) (err error) { + + switch o := o.(type) { + + case types.Name: + if err = validateTR2Name(xRefTable, o); err != nil { + return err + } + + case types.Array: + + if len(o) != 4 { + return errors.New("pdfcpu: validateTR2: corrupt function array") + } + + for _, o := range o { + + o, err = xRefTable.Dereference(o) + if err != nil { + return + } + + if o == nil { + continue + } + + if o, ok := o.(types.Name); ok { + if err = validateTR2Name(xRefTable, o); err != nil { + return err + } + continue + } + + err = processFunction(xRefTable, o) + if err != nil { + return + } + + } + + case types.Dict: + err = processFunction(xRefTable, o) + + case types.StreamDict: + err = processFunction(xRefTable, o) + + default: + return errors.Errorf("validateTR2: corrupt entry %v\n", o) + + } + + return err +} + +func validateTR2Entry(xRefTable *model.XRefTable, d types.Dict, dictName string, entryName string, required bool, sinceVersion model.Version) error { + + o, err := validateEntry(xRefTable, d, dictName, entryName, required, sinceVersion) + if err != nil || o == nil { + return err + } + + return validateTR2(xRefTable, o) +} + +func validateSpotFunctionEntry(xRefTable *model.XRefTable, d types.Dict, dictName string, entryName string, required bool, sinceVersion model.Version) error { + + o, err := validateEntry(xRefTable, d, dictName, entryName, required, sinceVersion) + if err != nil || o == nil { + return err + } + + switch o := o.(type) { + + case types.Name: + validateSpotFunctionName := func(s string) bool { + return types.MemberOf(s, []string{ + "SimpleDot", "InvertedSimpleDot", "DoubleDot", "InvertedDoubleDot", "CosineDot", + "Double", "InvertedDouble", "Line", "LineX", "LineY", "Round", "Ellipse", "EllipseA", + "InvertedEllipseA", "EllipseB", "EllipseC", "InvertedEllipseC", "Square", "Cross", "Rhomboid"}) + } + s := o.Value() + if !validateSpotFunctionName(s) { + return errors.Errorf("validateSpotFunctionEntry: corrupt name\n") + } + + case types.Dict: + err = processFunction(xRefTable, o) + + case types.StreamDict: + err = processFunction(xRefTable, o) + + default: + return errors.Errorf("validateSpotFunctionEntry: dict=%s corrupt entry \"%s\"\n", dictName, entryName) + + } + + return err +} + +func validateType1HalftoneDict(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + + dictName := "type1HalftoneDict" + + // HalftoneName, optional, string + _, err := validateStringEntry(xRefTable, d, dictName, "HalftoneName", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // Frequency, required, number + _, err = validateNumberEntry(xRefTable, d, dictName, "Frequency", REQUIRED, sinceVersion, nil) + if err != nil { + return err + } + + // Angle, required, number + _, err = validateNumberEntry(xRefTable, d, dictName, "Angle", REQUIRED, sinceVersion, nil) + if err != nil { + return err + } + + // SpotFunction, required, function or name + err = validateSpotFunctionEntry(xRefTable, d, dictName, "SpotFunction", REQUIRED, sinceVersion) + if err != nil { + return err + } + + // TransferFunction, optional, function + err = validateTransferFunctionEntry(xRefTable, d, dictName, "TransferFunction", OPTIONAL, sinceVersion) + if err != nil { + return err + } + + _, err = validateBooleanEntry(xRefTable, d, dictName, "AccurateScreens", OPTIONAL, sinceVersion, nil) + + return err +} + +func validateType5HalftoneDict(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + + dictName := "type5HalftoneDict" + + _, err := validateStringEntry(xRefTable, d, dictName, "HalftoneName", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + for _, c := range []string{"Gray", "Red", "Green", "Blue", "Cyan", "Magenta", "Yellow", "Black"} { + err = validateHalfToneEntry(xRefTable, d, dictName, c, OPTIONAL, sinceVersion) + if err != nil { + return err + } + } + + return validateHalfToneEntry(xRefTable, d, dictName, "Default", REQUIRED, sinceVersion) +} + +func validateType6HalftoneStreamDict(xRefTable *model.XRefTable, sd *types.StreamDict, sinceVersion model.Version) error { + + dictName := "type6HalftoneDict" + + _, err := validateStringEntry(xRefTable, sd.Dict, dictName, "HalftoneName", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + _, err = validateIntegerEntry(xRefTable, sd.Dict, dictName, "Width", REQUIRED, sinceVersion, nil) + if err != nil { + return err + } + + _, err = validateIntegerEntry(xRefTable, sd.Dict, dictName, "Height", REQUIRED, sinceVersion, nil) + if err != nil { + return err + } + + return validateTransferFunctionEntry(xRefTable, sd.Dict, dictName, "TransferFunction", OPTIONAL, sinceVersion) +} + +func validateType10HalftoneStreamDict(xRefTable *model.XRefTable, sd *types.StreamDict, sinceVersion model.Version) error { + + dictName := "type10HalftoneDict" + + _, err := validateStringEntry(xRefTable, sd.Dict, dictName, "HalftoneName", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + _, err = validateIntegerEntry(xRefTable, sd.Dict, dictName, "Xsquare", REQUIRED, sinceVersion, nil) + if err != nil { + return err + } + + _, err = validateIntegerEntry(xRefTable, sd.Dict, dictName, "Ysquare", REQUIRED, sinceVersion, nil) + if err != nil { + return err + } + + return validateTransferFunctionEntry(xRefTable, sd.Dict, dictName, "TransferFunction", OPTIONAL, sinceVersion) +} + +func validateType16HalftoneStreamDict(xRefTable *model.XRefTable, sd *types.StreamDict, sinceVersion model.Version) error { + + dictName := "type16HalftoneDict" + + _, err := validateStringEntry(xRefTable, sd.Dict, dictName, "HalftoneName", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + _, err = validateIntegerEntry(xRefTable, sd.Dict, dictName, "Width", REQUIRED, sinceVersion, nil) + if err != nil { + return err + } + + _, err = validateIntegerEntry(xRefTable, sd.Dict, dictName, "Height", REQUIRED, sinceVersion, nil) + if err != nil { + return err + } + + _, err = validateIntegerEntry(xRefTable, sd.Dict, dictName, "Width2", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + _, err = validateIntegerEntry(xRefTable, sd.Dict, dictName, "Height2", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + return validateTransferFunctionEntry(xRefTable, sd.Dict, dictName, "TransferFunction", OPTIONAL, sinceVersion) +} + +func validateHalfToneDict(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + + dictName := "halfToneDict" + + // Type, optional, name + _, err := validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, sinceVersion, func(s string) bool { return s == "Halftone" }) + if err != nil { + return err + } + + // HalftoneType, required, integer + halftoneType, err := validateIntegerEntry(xRefTable, d, dictName, "HalftoneType", REQUIRED, sinceVersion, nil) + if err != nil { + return err + } + + switch *halftoneType { + + case 1: + err = validateType1HalftoneDict(xRefTable, d, sinceVersion) + + case 5: + err = validateType5HalftoneDict(xRefTable, d, sinceVersion) + + default: + err = errors.Errorf("validateHalfToneDict: unknown halftoneTyp: %d\n", *halftoneType) + + } + + return err +} + +func validateHalfToneStreamDict(xRefTable *model.XRefTable, sd *types.StreamDict, sinceVersion model.Version) error { + + dictName := "writeHalfToneStreamDict" + + // Type, name, optional + _, err := validateNameEntry(xRefTable, sd.Dict, dictName, "Type", OPTIONAL, sinceVersion, func(s string) bool { return s == "Halftone" }) + if err != nil { + return err + } + + // HalftoneType, required, integer + halftoneType, err := validateIntegerEntry(xRefTable, sd.Dict, dictName, "HalftoneType", REQUIRED, sinceVersion, nil) + if err != nil || halftoneType == nil { + return err + } + + switch *halftoneType { + + case 6: + err = validateType6HalftoneStreamDict(xRefTable, sd, sinceVersion) + + case 10: + err = validateType10HalftoneStreamDict(xRefTable, sd, sinceVersion) + + case 16: + err = validateType16HalftoneStreamDict(xRefTable, sd, sinceVersion) + + default: + err = errors.Errorf("validateHalfToneStreamDict: unknown halftoneTyp: %d\n", *halftoneType) + + } + + return err +} + +func validateHalfToneEntry(xRefTable *model.XRefTable, d types.Dict, dictName string, entryName string, required bool, sinceVersion model.Version) (err error) { + + // See 10.5 + + o, err := validateEntry(xRefTable, d, dictName, entryName, required, sinceVersion) + if err != nil || o == nil { + return err + } + + switch o := o.(type) { + + case types.Name: + if o.Value() != "Default" { + return errors.Errorf("pdfcpu: validateHalfToneEntry: undefined name: %s\n", o) + } + + case types.Dict: + err = validateHalfToneDict(xRefTable, o, sinceVersion) + + case types.StreamDict: + err = validateHalfToneStreamDict(xRefTable, &o, sinceVersion) + + default: + err = errors.New("pdfcpu: validateHalfToneEntry: corrupt (stream)dict") + } + + return err +} + +func validateBlendModeEntry(xRefTable *model.XRefTable, d types.Dict, dictName string, entryName string, required bool, sinceVersion model.Version) error { + + o, err := validateEntry(xRefTable, d, dictName, entryName, required, sinceVersion) + if err != nil || o == nil { + return err + } + + switch o := o.(type) { + + case types.Name: + _, err = xRefTable.DereferenceName(o, sinceVersion, validateBlendMode) + if err != nil { + return err + } + + case types.Array: + for _, o := range o { + _, err = xRefTable.DereferenceName(o, sinceVersion, validateBlendMode) + if err != nil { + return err + } + } + + default: + return errors.Errorf("validateBlendModeEntry: dict=%s corrupt entry \"%s\"\n", dictName, entryName) + + } + + return nil +} + +func validateSoftMaskTransferFunctionEntry(xRefTable *model.XRefTable, d types.Dict, dictName string, entryName string, required bool, sinceVersion model.Version) error { + + o, err := validateEntry(xRefTable, d, dictName, entryName, required, sinceVersion) + if err != nil || o == nil { + return err + } + + switch o := o.(type) { + + case types.Name: + s := o.Value() + if s != "Identity" { + return errors.New("pdfcpu: validateSoftMaskTransferFunctionEntry: corrupt name") + } + + case types.Dict: + err = processFunction(xRefTable, o) + + case types.StreamDict: + err = processFunction(xRefTable, o) + + default: + return errors.Errorf("pdfcpu: validateSoftMaskTransferFunctionEntry: dict=%s corrupt entry \"%s\"\n", dictName, entryName) + + } + + return err +} + +func validateSoftMaskDict(xRefTable *model.XRefTable, d types.Dict) error { + + // see 11.6.5.2 + + dictName := "softMaskDict" + + // Type, name, optional + _, err := validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, model.V10, func(s string) bool { return s == "Mask" }) + if err != nil { + return err + } + + // S, name, required + _, err = validateNameEntry(xRefTable, d, dictName, "S", REQUIRED, model.V10, func(s string) bool { return s == "Alpha" || s == "Luminosity" }) + if err != nil { + return err + } + + // G, stream, required + // A transparency group XObject (see “Transparency Group XObjects”) + // to be used as the source of alpha or colour values for deriving the mask. + sd, err := validateStreamDictEntry(xRefTable, d, dictName, "G", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + if sd != nil { + err = validateXObjectStreamDict(xRefTable, *sd) + if err != nil { + return err + } + } + + // TR (Optional) function or name + // A function object (see “Functions”) specifying the transfer function + // to be used in deriving the mask values. + err = validateSoftMaskTransferFunctionEntry(xRefTable, d, dictName, "TR", OPTIONAL, model.V10) + if err != nil { + return err + } + + // BC, number array, optional + // Array of component values specifying the colour to be used + // as the backdrop against which to composite the transparency group XObject G. + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "BC", OPTIONAL, model.V10, nil) + + return err +} + +func validateSoftMaskEntry(xRefTable *model.XRefTable, d types.Dict, dictName string, entryName string, required bool, sinceVersion model.Version) error { + + // see 11.3.7.2 Source Shape and Opacity + // see 11.6.4.3 Mask Shape and Opacity + + o, err := validateEntry(xRefTable, d, dictName, entryName, required, sinceVersion) + if err != nil || o == nil { + return err + } + + switch o := o.(type) { + + case types.Name: + s := o.Value() + if !validateBlendMode(s) { + return errors.Errorf("pdfcpu: validateSoftMaskEntry: invalid soft mask: %s\n", s) + } + + case types.Dict: + err = validateSoftMaskDict(xRefTable, o) + + default: + err = errors.Errorf("pdfcpu: validateSoftMaskEntry: dict=%s corrupt entry \"%s\"\n", dictName, entryName) + + } + + return err +} + +func validateExtGStateDictPart1(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // LW, number, optional, since V1.3 + _, err := validateNumberEntry(xRefTable, d, dictName, "LW", OPTIONAL, model.V13, nil) + if err != nil { + return err + } + + // LC, integer, optional, since V1.3 + _, err = validateIntegerEntry(xRefTable, d, dictName, "LC", OPTIONAL, model.V13, nil) + if err != nil { + return err + } + + // LJ, integer, optional, since V1.3 + _, err = validateIntegerEntry(xRefTable, d, dictName, "LJ", OPTIONAL, model.V13, nil) + if err != nil { + return err + } + + // ML, number, optional, since V1.3 + _, err = validateNumberEntry(xRefTable, d, dictName, "ML", OPTIONAL, model.V13, nil) + if err != nil { + return err + } + + // D, array, optional, since V1.3, [dashArray dashPhase(integer)] + err = validateLineDashPatternEntry(xRefTable, d, dictName, "D", OPTIONAL, model.V13) + if err != nil { + return err + } + + // RI, name, optional, since V1.3 + _, err = validateNameEntry(xRefTable, d, dictName, "RI", OPTIONAL, model.V13, nil) + if err != nil { + return err + } + + // OP, boolean, optional, + _, err = validateBooleanEntry(xRefTable, d, dictName, "OP", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // op, boolean, optional, since V1.3 + _, err = validateBooleanEntry(xRefTable, d, dictName, "op", OPTIONAL, model.V13, nil) + if err != nil { + return err + } + + // OPM, integer, optional, since V1.3 + _, err = validateIntegerEntry(xRefTable, d, dictName, "OPM", OPTIONAL, model.V13, nil) + if err != nil { + return err + } + + // Font, array, optional, since V1.3 + _, err = validateArrayEntry(xRefTable, d, dictName, "Font", OPTIONAL, model.V13, nil) + + return err +} + +func validateExtGStateDictPart2(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // BG, function, optional, black-generation function, see 10.3.4 + err := validateBGEntry(xRefTable, d, dictName, "BG", OPTIONAL, model.V10) + if err != nil { + return err + } + + // BG2, function or name(/Default), optional, since V1.3 + err = validateBG2Entry(xRefTable, d, dictName, "BG2", OPTIONAL, model.V10) + if err != nil { + return err + } + + // UCR, function, optional, undercolor-removal function, see 10.3.4 + err = validateUCREntry(xRefTable, d, dictName, "UCR", OPTIONAL, model.V10) + if err != nil { + return err + } + + // UCR2, function or name(/Default), optional, since V1.3 + err = validateUCR2Entry(xRefTable, d, dictName, "UCR2", OPTIONAL, model.V10) + if err != nil { + return err + } + + // TR, function, array of 4 functions or name(/Identity), optional, see 10.4 transfer functions + err = validateTREntry(xRefTable, d, dictName, "TR", OPTIONAL, model.V10) + if err != nil { + return err + } + + // TR2, function, array of 4 functions or name(/Identity,/Default), optional, since V1.3 + err = validateTR2Entry(xRefTable, d, dictName, "TR2", OPTIONAL, model.V10) + if err != nil { + return err + } + + // HT, dict, stream or name, optional + // half tone dictionary or stream or /Default, see 10.5 + err = validateHalfToneEntry(xRefTable, d, dictName, "HT", OPTIONAL, model.V12) + if err != nil { + return err + } + + // FL, number, optional, since V1.3, flatness tolerance, see 10.6.2 + _, err = validateNumberEntry(xRefTable, d, dictName, "FL", OPTIONAL, model.V13, nil) + if err != nil { + return err + } + + // SM, number, optional, since V1.3, smoothness tolerance + _, err = validateNumberEntry(xRefTable, d, dictName, "SM", OPTIONAL, model.V13, nil) + if err != nil { + return err + } + + // SA, boolean, optional, see 10.6.5 Automatic Stroke Adjustment + _, err = validateBooleanEntry(xRefTable, d, dictName, "SA", OPTIONAL, model.V10, nil) + + return err +} + +func validateExtGStateDictPart3(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // BM, name or array, optional, since V1.4 + sinceVersion := model.V14 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V13 + } + err := validateBlendModeEntry(xRefTable, d, dictName, "BM", OPTIONAL, sinceVersion) + if err != nil { + return err + } + + // SMask, dict or name, optional, since V1.4 + sinceVersion = model.V14 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V13 + } + err = validateSoftMaskEntry(xRefTable, d, dictName, "SMask", OPTIONAL, sinceVersion) + if err != nil { + return err + } + + // CA, number, optional, since V1.4, current stroking alpha constant, see 11.3.7.2 and 11.6.4.4 + sinceVersion = model.V14 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V13 + } + _, err = validateNumberEntry(xRefTable, d, dictName, "CA", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // ca, number, optional, since V1.4, same as CA but for nonstroking operations. + sinceVersion = model.V14 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V13 + } + _, err = validateNumberEntry(xRefTable, d, dictName, "ca", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // AIS, alpha source flag "alpha is shape", boolean, optional, since V1.4 + sinceVersion = model.V14 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V13 + } + _, err = validateBooleanEntry(xRefTable, d, dictName, "AIS", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // TK, boolean, optional, since V1.4, text knockout flag. + sinceVersion = model.V14 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V13 + } + _, err = validateBooleanEntry(xRefTable, d, dictName, "TK", OPTIONAL, sinceVersion, nil) + + return err +} + +func validateExtGStateDict(xRefTable *model.XRefTable, o types.Object) error { + + // 8.4.5 Graphics State Parameter Dictionaries + + d, err := xRefTable.DereferenceDict(o) + if err != nil || d == nil { + return err + } + + dictName := "extGStateDict" + + // Type, name, optional + _, err = validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, model.V10, func(s string) bool { return s == "ExtGState" }) + if err != nil { + return err + } + + err = validateExtGStateDictPart1(xRefTable, d, dictName) + if err != nil { + return err + } + + err = validateExtGStateDictPart2(xRefTable, d, dictName) + if err != nil { + return err + } + + return validateExtGStateDictPart3(xRefTable, d, dictName) +} + +func validateExtGStateResourceDict(xRefTable *model.XRefTable, o types.Object, sinceVersion model.Version) error { + + d, err := xRefTable.DereferenceDict(o) + if err != nil || d == nil { + return err + } + + // Version check + err = xRefTable.ValidateVersion("ExtGStateResourceDict", sinceVersion) + if err != nil { + return err + } + + // Iterate over extGState resource dictionary + for _, o := range d { + + // Process extGStateDict + err = validateExtGStateDict(xRefTable, o) + if err != nil { + return err + } + + } + + return nil +} diff --git a/pkg/pdfcpu/validate/fileSpec.go b/pkg/pdfcpu/validate/fileSpec.go new file mode 100644 index 0000000000000000000000000000000000000000..9afb1d353f5c18cdb06675c99e3339655f061f6c --- /dev/null +++ b/pkg/pdfcpu/validate/fileSpec.go @@ -0,0 +1,498 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 validate + +import ( + "net/url" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// See 7.11.4 + +func validateFileSpecString(s string) bool { + + // see 7.11.2 + // The standard format for representing a simple file specification in string form divides the string into component substrings + // separated by the SOLIDUS character (2Fh) (/). The SOLIDUS is a generic component separator that shall be mapped to the appropriate + // platform-specific separator when generating a platform-dependent file name. Any of the components may be empty. + // If a component contains one or more literal SOLIDI, each shall be preceded by a REVERSE SOLIDUS (5Ch) (\), which in turn shall be + // preceded by another REVERSE SOLIDUS to indicate that it is part of the string and not an escape character. + // + // EXAMPLE ( in\\/out ) + // represents the file name in/out + + // I have not seen an instance of a single file spec string that actually complies with this definition and uses + // the double reverse solidi in front of the solidus, because of that we simply + return true +} + +func validateURLString(s string) bool { + + // RFC1738 compliant URL, see 7.11.5 + + _, err := url.ParseRequestURI(s) + + return err == nil +} + +func validateEmbeddedFileStreamMacParameterDict(xRefTable *model.XRefTable, d types.Dict) error { + + dictName := "embeddedFileStreamMacParameterDict" + + // Subtype, optional integer + // The embedded file's file type integer encoded according to Mac OS conventions. + _, err := validateIntegerEntry(xRefTable, d, dictName, "Subtype", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // Creator, optional integer + // The embedded file's creator signature integer encoded according to Mac OS conventions. + _, err = validateIntegerEntry(xRefTable, d, dictName, "Creator", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // ResFork, optional stream dict + // The binary contents of the embedded file's resource fork. + _, err = validateStreamDictEntry(xRefTable, d, dictName, "ResFork", OPTIONAL, model.V10, nil) + + return err +} + +func validateEmbeddedFileStreamParameterDict(xRefTable *model.XRefTable, o types.Object) error { + + d, err := xRefTable.DereferenceDict(o) + if err != nil || d == nil { + return err + } + + dictName := "embeddedFileStreamParmDict" + + // Size, optional integer + _, err = validateIntegerEntry(xRefTable, d, dictName, "Size", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // CreationDate, optional date + _, err = validateDateEntry(xRefTable, d, dictName, "CreationDate", OPTIONAL, model.V10) + if err != nil { + return err + } + + // ModDate, optional date + _, err = validateDateEntry(xRefTable, d, dictName, "ModDate", OPTIONAL, model.V10) + if err != nil { + return err + } + + // Mac, optional dict + macDict, err := validateDictEntry(xRefTable, d, dictName, "Mac", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + if macDict != nil { + err = validateEmbeddedFileStreamMacParameterDict(xRefTable, macDict) + if err != nil { + return err + } + } + + // CheckSum, optional string + _, err = validateStringEntry(xRefTable, d, dictName, "CheckSum", OPTIONAL, model.V10, nil) + + return err +} + +func validateEmbeddedFileStreamDict(xRefTable *model.XRefTable, sd *types.StreamDict) error { + + dictName := "embeddedFileStreamDict" + + // Type, optional, name + _, err := validateNameEntry(xRefTable, sd.Dict, dictName, "Type", OPTIONAL, model.V10, func(s string) bool { return s == "EmbeddedFile" }) + if err != nil { + return err + } + + // Subtype, optional, name + _, err = validateNameEntry(xRefTable, sd.Dict, dictName, "Subtype", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // Params, optional, dict + // parameter dict containing additional file-specific information. + if o, found := sd.Dict.Find("Params"); found && o != nil { + err = validateEmbeddedFileStreamParameterDict(xRefTable, o) + } + + return err +} + +func validateFileSpecDictEntriesEFAndRFKeys(k string) bool { + return k == "F" || k == "UF" || k == "DOS" || k == "Mac" || k == "Unix" +} + +func validateFileSpecDictEntryEFDict(xRefTable *model.XRefTable, d types.Dict) error { + + for k, obj := range d { + + if !validateFileSpecDictEntriesEFAndRFKeys(k) { + return errors.Errorf("validateFileSpecEntriesEFAndRF: invalid key: %s", k) + } + + // value must be embedded file stream dict + // see 7.11.4 + sd, err := validateStreamDict(xRefTable, obj) + if err != nil { + return err + } + if sd == nil { + continue + } + + err = validateEmbeddedFileStreamDict(xRefTable, sd) + if err != nil { + return err + } + + } + + return nil +} + +func validateRFDictFilesArray(xRefTable *model.XRefTable, a types.Array) error { + + if len(a)%2 > 0 { + return errors.New("pdfcpu: validateRFDictFilesArray: rfDict array corrupt") + } + + for k, v := range a { + + if v == nil { + return errors.New("pdfcpu: validateRFDictFilesArray: rfDict, array entry nil") + } + + o, err := xRefTable.Dereference(v) + if err != nil { + return err + } + + if o == nil { + return errors.New("pdfcpu: validateRFDictFilesArray: rfDict, array entry nil") + } + + if k%2 > 0 { + + _, ok := o.(types.StringLiteral) + if !ok { + return errors.New("pdfcpu: validateRFDictFilesArray: rfDict, array entry corrupt") + } + + } else { + + // value must be embedded file stream dict + // see 7.11.4 + sd, err := validateStreamDict(xRefTable, o) + if err != nil { + return err + } + + err = validateEmbeddedFileStreamDict(xRefTable, sd) + if err != nil { + return err + } + + } + } + + return nil +} + +func validateFileSpecDictEntriesEFAndRF(xRefTable *model.XRefTable, efDict, rfDict types.Dict) error { + + // EF only or EF and RF + + if efDict == nil { + return errors.Errorf("pdfcpu: validateFileSpecEntriesEFAndRF: missing required efDict.") + } + + err := validateFileSpecDictEntryEFDict(xRefTable, efDict) + if err != nil { + return err + } + + for k, val := range rfDict { + + if _, ok := efDict.Find(k); !ok { + return errors.Errorf("pdfcpu: validateFileSpecEntriesEFAndRF: rfDict entry=%s missing corresponding efDict entry\n", k) + } + + // value must be related files array. + // see 7.11.4.2 + a, err := xRefTable.DereferenceArray(val) + if err != nil { + return err + } + + if a == nil { + continue + } + + err = validateRFDictFilesArray(xRefTable, a) + if err != nil { + return err + } + + } + + return nil +} + +func validateFileSpecDictType(xRefTable *model.XRefTable, d types.Dict) error { + + if d.Type() == nil || (*d.Type() != "Filespec" && (xRefTable.ValidationMode == model.ValidationRelaxed && *d.Type() != "F")) { + return errors.New("pdfcpu: validateFileSpecDictType: missing type: FileSpec") + } + + return nil +} + +func requiredF(dosFound, macFound, unixFound bool) bool { + return !dosFound && !macFound && !unixFound +} + +func validateFileSpecDictEFAndRF(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // RF, optional, dict of related files arrays, since V1.3 + rfDict, err := validateDictEntry(xRefTable, d, dictName, "RF", OPTIONAL, model.V13, nil) + if err != nil { + return err + } + + // EF, required if RF present, dict of embedded file streams, since 1.3 + efDict, err := validateDictEntry(xRefTable, d, dictName, "EF", rfDict != nil, model.V13, nil) + if err != nil { + return err + } + + // Type, required if EF present, name + validate := func(s string) bool { + return s == "Filespec" || (xRefTable.ValidationMode == model.ValidationRelaxed && s == "F") + } + _, err = validateNameEntry(xRefTable, d, dictName, "Type", efDict != nil, model.V10, validate) + if err != nil { + return err + } + + // if EF present, Type "FileSpec" is required + if efDict != nil { + + err = validateFileSpecDictType(xRefTable, d) + if err != nil { + return err + } + + err = validateFileSpecDictEntriesEFAndRF(xRefTable, efDict, rfDict) + + } + + return err +} + +func validateFileSpecDict(xRefTable *model.XRefTable, d types.Dict) error { + + dictName := "fileSpecDict" + + // FS, optional, name + fsName, err := validateNameEntry(xRefTable, d, dictName, "FS", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // DOS, byte string, optional, obsolescent. + _, dosFound := d.Find("DOS") + + // Mac, byte string, optional, obsolescent. + _, macFound := d.Find("Mac") + + // Unix, byte string, optional, obsolescent. + _, unixFound := d.Find("Unix") + + // F, file spec string + validate := validateFileSpecString + if fsName != nil && fsName.Value() == "URL" { + validate = validateURLString + } + + _, err = validateStringEntry(xRefTable, d, dictName, "F", requiredF(dosFound, macFound, unixFound), model.V10, validate) + if err != nil { + return err + } + + // UF, optional, text string + sinceVersion := model.V17 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V13 + } + _, err = validateStringEntry(xRefTable, d, dictName, "UF", OPTIONAL, sinceVersion, validateFileSpecString) + if err != nil { + return err + } + + // ID, optional, array of strings + _, err = validateStringArrayEntry(xRefTable, d, dictName, "ID", OPTIONAL, model.V11, func(a types.Array) bool { return len(a) == 2 }) + if err != nil { + return err + } + + // V, optional, boolean, since V1.2 + _, err = validateBooleanEntry(xRefTable, d, dictName, "V", OPTIONAL, model.V12, nil) + if err != nil { + return err + } + + err = validateFileSpecDictEFAndRF(xRefTable, d, dictName) + if err != nil { + return err + } + + // Desc, optional, text string, since V1.6 + sinceVersion = model.V16 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V10 + } + _, err = validateStringEntry(xRefTable, d, dictName, "Desc", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // CI, optional, collection item dict, since V1.7 + _, err = validateDictEntry(xRefTable, d, dictName, "CI", OPTIONAL, model.V17, nil) + + return err +} + +func validateFileSpecification(xRefTable *model.XRefTable, o types.Object) (types.Object, error) { + + // See 7.11.4 + + o, err := xRefTable.Dereference(o) + if err != nil { + return nil, err + } + + switch o := o.(type) { + + case types.StringLiteral: + s := o.Value() + if !validateFileSpecString(s) { + return nil, errors.Errorf("pdfcpu: validateFileSpecification: invalid file spec string: %s", s) + } + + case types.HexLiteral: + s := o.Value() + if !validateFileSpecString(s) { + return nil, errors.Errorf("pdfcpu: validateFileSpecification: invalid file spec string: %s", s) + } + + case types.Dict: + err = validateFileSpecDict(xRefTable, o) + if err != nil { + return nil, err + } + + default: + return nil, errors.Errorf("pdfcpu: validateFileSpecification: invalid type") + + } + + return o, nil +} + +func validateURLSpecification(xRefTable *model.XRefTable, o types.Object) (types.Object, error) { + + // See 7.11.4 + + d, err := xRefTable.DereferenceDict(o) + if err != nil { + return nil, err + } + + if d == nil { + return nil, errors.New("pdfcpu: validateURLSpecification: missing dict") + } + + dictName := "urlSpec" + + // FS, required, name + _, err = validateNameEntry(xRefTable, d, dictName, "FS", REQUIRED, model.V10, func(s string) bool { return s == "URL" }) + if err != nil { + return nil, err + } + + // F, required, string, URL (Internet RFC 1738) + _, err = validateStringEntry(xRefTable, d, dictName, "F", REQUIRED, model.V10, validateURLString) + + return o, err +} + +func validateFileSpecEntry(xRefTable *model.XRefTable, d types.Dict, dictName string, entryName string, required bool, sinceVersion model.Version) (types.Object, error) { + + o, err := validateEntry(xRefTable, d, dictName, entryName, required, sinceVersion) + if err != nil || o == nil { + return nil, err + } + + err = xRefTable.ValidateVersion("fileSpec", sinceVersion) + if err != nil { + return nil, err + } + + return validateFileSpecification(xRefTable, o) +} + +func validateURLSpecEntry(xRefTable *model.XRefTable, d types.Dict, dictName string, entryName string, required bool, sinceVersion model.Version) (types.Object, error) { + + o, err := validateEntry(xRefTable, d, dictName, entryName, required, sinceVersion) + if err != nil || o == nil { + return nil, err + } + + err = xRefTable.ValidateVersion("URLSpec", sinceVersion) + if err != nil { + return nil, err + } + + return validateURLSpecification(xRefTable, o) +} + +func validateFileSpecificationOrFormObject(xRefTable *model.XRefTable, obj types.Object) error { + + sd, ok := obj.(types.StreamDict) + if ok { + return validateFormStreamDict(xRefTable, &sd) + } + + _, err := validateFileSpecification(xRefTable, obj) + + return err +} diff --git a/pkg/pdfcpu/validate/font.go b/pkg/pdfcpu/validate/font.go new file mode 100644 index 0000000000000000000000000000000000000000..c19a3bfc9fbac321a00322155affaf5da8ebc05b --- /dev/null +++ b/pkg/pdfcpu/validate/font.go @@ -0,0 +1,1034 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 validate + +import ( + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +func validateStandardType1Font(s string) bool { + + return types.MemberOf(s, []string{"Times-Roman", "Times-Bold", "Times-Italic", "Times-BoldItalic", + "Helvetica", "Helvetica-Bold", "Helvetica-Oblique", "Helvetica-BoldOblique", + "Courier", "Courier-Bold", "Courier-Oblique", "Courier-BoldOblique", + "Symbol", "ZapfDingbats"}) +} + +func validateFontFile3SubType(sd *types.StreamDict, fontType string) error { + + // Hint about used font program. + dictSubType := sd.Subtype() + + if dictSubType == nil { + return errors.New("pdfcpu: validateFontFile3SubType: missing Subtype") + } + + switch fontType { + case "Type1": + if *dictSubType != "Type1C" && *dictSubType != "OpenType" { + return errors.Errorf("pdfcpu: validateFontFile3SubType: Type1: unexpected Subtype %s", *dictSubType) + } + + case "MMType1": + if *dictSubType != "Type1C" { + return errors.Errorf("pdfcpu: validateFontFile3SubType: MMType1: unexpected Subtype %s", *dictSubType) + } + + case "CIDFontType0": + if *dictSubType != "CIDFontType0C" && *dictSubType != "OpenType" { + return errors.Errorf("pdfcpu: validateFontFile3SubType: CIDFontType0: unexpected Subtype %s", *dictSubType) + } + + case "CIDFontType2", "TrueType": + if *dictSubType != "OpenType" { + return errors.Errorf("pdfcpu: validateFontFile3SubType: %s: unexpected Subtype %s", fontType, *dictSubType) + } + } + + return nil +} + +func validateFontFile(xRefTable *model.XRefTable, d types.Dict, dictName string, entryName string, fontType string, required bool, sinceVersion model.Version) error { + + sd, err := validateStreamDictEntry(xRefTable, d, dictName, entryName, required, sinceVersion, nil) + if err != nil || sd == nil { + return err + } + + // Process font file stream dict entries. + + // SubType + if entryName == "FontFile3" { + err = validateFontFile3SubType(sd, fontType) + if err != nil { + return err + } + + } + + dName := "fontFileStreamDict" + compactFontFormat := entryName == "FontFile3" + + _, err = validateIntegerEntry(xRefTable, sd.Dict, dName, "Length1", (fontType == "Type1" || fontType == "TrueType") && !compactFontFormat, model.V10, nil) + if err != nil { + return err + } + + _, err = validateIntegerEntry(xRefTable, sd.Dict, dName, "Length2", fontType == "Type1" && !compactFontFormat, model.V10, nil) + if err != nil { + return err + } + + _, err = validateIntegerEntry(xRefTable, sd.Dict, dName, "Length3", fontType == "Type1" && !compactFontFormat, model.V10, nil) + if err != nil { + return err + } + + // Metadata, stream, optional, since 1.4 + return validateMetadata(xRefTable, sd.Dict, OPTIONAL, model.V14) +} + +func validateFontDescriptorType(xRefTable *model.XRefTable, d types.Dict) (err error) { + + dictType := d.Type() + + if dictType == nil { + + if xRefTable.ValidationMode == model.ValidationRelaxed { + if log.ValidateEnabled() { + log.Validate.Println("validateFontDescriptor: missing entry \"Type\"") + } + } else { + return errors.New("pdfcpu: validateFontDescriptor: missing entry \"Type\"") + } + + } + + if dictType != nil && *dictType != "FontDescriptor" && *dictType != "Font" { + return errors.New("pdfcpu: validateFontDescriptor: corrupt font descriptor dict") + } + + return nil +} + +func validateFontDescriptorPart1(xRefTable *model.XRefTable, d types.Dict, dictName, fontDictType string) error { + + err := validateFontDescriptorType(xRefTable, d) + if err != nil { + return err + } + + required := true + if xRefTable.ValidationMode == model.ValidationRelaxed { + required = false + } + _, err = validateNameEntry(xRefTable, d, dictName, "FontName", required, model.V10, nil) + if err != nil { + _, err = validateStringEntry(xRefTable, d, dictName, "FontName", required, model.V10, nil) + if err != nil { + if xRefTable.ValidationMode != model.ValidationRelaxed { + return err + } + model.ReportSpecViolation(xRefTable, err) + } + } + + sinceVersion := model.V15 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V13 + } + _, err = validateStringEntry(xRefTable, d, dictName, "FontFamily", OPTIONAL, sinceVersion, nil) + if err != nil { + // Repair + _, err = validateNameEntry(xRefTable, d, dictName, "FontFamily", OPTIONAL, sinceVersion, nil) + return err + } + + sinceVersion = model.V15 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V13 + } + _, err = validateNameEntry(xRefTable, d, dictName, "FontStretch", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + sinceVersion = model.V15 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V13 + } + _, err = validateNumberEntry(xRefTable, d, dictName, "FontWeight", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + _, err = validateIntegerEntry(xRefTable, d, dictName, "Flags", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + _, err = validateRectangleEntry(xRefTable, d, dictName, "FontBBox", fontDictType != "Type3", model.V10, nil) + if err != nil { + return err + } + + _, err = validateNumberEntry(xRefTable, d, dictName, "ItalicAngle", REQUIRED, model.V10, nil) + + return err +} + +func validateFontDescriptorPart2(xRefTable *model.XRefTable, d types.Dict, dictName, fontDictType string) error { + + _, err := validateNumberEntry(xRefTable, d, dictName, "Ascent", fontDictType != "Type3", model.V10, nil) + if err != nil { + return err + } + + _, err = validateNumberEntry(xRefTable, d, dictName, "Descent", fontDictType != "Type3", model.V10, nil) + if err != nil { + return err + } + + _, err = validateNumberEntry(xRefTable, d, dictName, "Leading", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + _, err = validateNumberEntry(xRefTable, d, dictName, "CapHeight", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + _, err = validateNumberEntry(xRefTable, d, dictName, "XHeight", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + required := fontDictType != "Type3" + if xRefTable.ValidationMode == model.ValidationRelaxed { + required = false + } + _, err = validateNumberEntry(xRefTable, d, dictName, "StemV", required, model.V10, nil) + if err != nil { + return err + } + + _, err = validateNumberEntry(xRefTable, d, dictName, "StemH", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + _, err = validateNumberEntry(xRefTable, d, dictName, "AvgWidth", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + _, err = validateNumberEntry(xRefTable, d, dictName, "MaxWidth", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + _, err = validateNumberEntry(xRefTable, d, dictName, "MissingWidth", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + err = validateFontDescriptorFontFile(xRefTable, d, dictName, fontDictType) + if err != nil { + return err + } + + _, err = validateStringEntry(xRefTable, d, dictName, "CharSet", OPTIONAL, model.V11, nil) + + return err +} + +func validateFontDescriptorFontFile(xRefTable *model.XRefTable, d types.Dict, dictName, fontDictType string) (err error) { + + switch fontDictType { + + case "Type1", "MMType1": + + err = validateFontFile(xRefTable, d, dictName, "FontFile", fontDictType, OPTIONAL, model.V10) + if err != nil { + return err + } + + err = validateFontFile(xRefTable, d, dictName, "FontFile3", fontDictType, OPTIONAL, model.V12) + + case "TrueType", "CIDFontType2": + err = validateFontFile(xRefTable, d, dictName, "FontFile2", fontDictType, OPTIONAL, model.V11) + + case "CIDFontType0": + err = validateFontFile(xRefTable, d, dictName, "FontFile3", fontDictType, OPTIONAL, model.V13) + + case "Type3": // No fontfile. + + default: + return errors.Errorf("pdfcpu: unknown fontDictType: %s\n", fontDictType) + + } + + return err +} + +func validateFontDescriptor(xRefTable *model.XRefTable, d types.Dict, fontDictName string, fontDictType string, required bool, sinceVersion model.Version) error { + + d1, err := validateDictEntry(xRefTable, d, fontDictName, "FontDescriptor", required, sinceVersion, nil) + if err != nil || d1 == nil { + return err + } + + dictName := "fdDict" + + // Process font descriptor dict + + err = validateFontDescriptorPart1(xRefTable, d1, dictName, fontDictType) + if err != nil { + return err + } + + err = validateFontDescriptorPart2(xRefTable, d1, dictName, fontDictType) + if err != nil { + return err + } + + if fontDictType == "CIDFontType0" || fontDictType == "CIDFontType2" { + + validateStyleDict := func(d types.Dict) bool { + + // see 9.8.3.2 + + if d.Len() != 1 { + return false + } + + _, found := d.Find("Panose") + + return found + } + + // Style, optional, dict + _, err = validateDictEntry(xRefTable, d1, dictName, "Style", OPTIONAL, model.V10, validateStyleDict) + if err != nil { + return err + } + + // Lang, optional, name + sinceVersion := model.V15 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V13 + } + _, err = validateNameEntry(xRefTable, d1, dictName, "Lang", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // FD, optional, dict + _, err = validateDictEntry(xRefTable, d1, dictName, "FD", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // CIDSet, optional, stream + _, err = validateStreamDictEntry(xRefTable, d1, dictName, "CIDSet", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + } + + return nil +} + +func validateFontEncoding(xRefTable *model.XRefTable, d types.Dict, dictName string, required bool) error { + + entryName := "Encoding" + + o, err := validateEntry(xRefTable, d, dictName, entryName, required, model.V10) + if err != nil || o == nil { + return err + } + + encodings := []string{"MacRomanEncoding", "MacExpertEncoding", "WinAnsiEncoding"} + if xRefTable.ValidationMode == model.ValidationRelaxed { + encodings = append(encodings, "StandardEncoding", "SymbolSetEncoding") + } + + switch o := o.(type) { + + case types.Name: + s := o.Value() + validateFontEncodingName := func(s string) bool { + return types.MemberOf(s, encodings) + } + if !validateFontEncodingName(s) { + return errors.Errorf("validateFontEncoding: invalid Encoding name: %s\n", s) + } + + case types.Dict: + // no further processing + + default: + return errors.Errorf("validateFontEncoding: dict=%s corrupt entry \"%s\"\n", dictName, entryName) + + } + + return nil +} + +func validateTrueTypeFontDict(xRefTable *model.XRefTable, d types.Dict) error { + + // see 9.6.3 + dictName := "trueTypeFontDict" + + // Name, name, obsolet and should not be used. + + // BaseFont, required, name + _, err := validateNameEntry(xRefTable, d, dictName, "BaseFont", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + // FirstChar, required, integer + required := REQUIRED + if xRefTable.ValidationMode == model.ValidationRelaxed { + required = OPTIONAL + } + _, err = validateIntegerEntry(xRefTable, d, dictName, "FirstChar", required, model.V10, nil) + if err != nil { + return err + } + + // LastChar, required, integer + required = REQUIRED + if xRefTable.ValidationMode == model.ValidationRelaxed { + required = OPTIONAL + } + _, err = validateIntegerEntry(xRefTable, d, dictName, "LastChar", required, model.V10, nil) + if err != nil { + return err + } + + // Widths, array of numbers. + required = REQUIRED + if xRefTable.ValidationMode == model.ValidationRelaxed { + required = OPTIONAL + } + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "Widths", required, model.V10, nil) + if err != nil { + return err + } + + // FontDescriptor, required, dictionary + required = REQUIRED + if xRefTable.ValidationMode == model.ValidationRelaxed { + required = OPTIONAL + } + err = validateFontDescriptor(xRefTable, d, dictName, "TrueType", required, model.V10) + if err != nil { + return err + } + + // Encoding, optional, name or dict + err = validateFontEncoding(xRefTable, d, dictName, OPTIONAL) + if err != nil { + return err + } + + // ToUnicode, optional, stream + _, err = validateStreamDictEntry(xRefTable, d, dictName, "ToUnicode", OPTIONAL, model.V12, nil) + + return err +} + +func validateCIDToGIDMap(xRefTable *model.XRefTable, o types.Object) error { + + o, err := xRefTable.Dereference(o) + if err != nil || o == nil { + return err + } + + switch o := o.(type) { + + case types.Name: + s := o.Value() + if s != "Identity" { + return errors.Errorf("pdfcpu: validateCIDToGIDMap: invalid name: %s - must be \"Identity\"\n", s) + } + + case types.StreamDict: + // no further processing + + default: + return errors.New("pdfcpu: validateCIDToGIDMap: corrupt entry") + + } + + return nil +} + +func validateCIDFontGlyphWidths(xRefTable *model.XRefTable, d types.Dict, dictName string, entryName string, required bool, sinceVersion model.Version) error { + + a, err := validateArrayEntry(xRefTable, d, dictName, entryName, required, sinceVersion, nil) + if err != nil || a == nil { + return err + } + + for i, o := range a { + + o, err := xRefTable.Dereference(o) + if err != nil || o == nil { + return err + } + + switch o.(type) { + + case types.Integer: + // no further processing. + + case types.Float: + // no further processing + + case types.Array: + _, err = validateNumberArray(xRefTable, o) + if err != nil { + return err + } + + default: + return errors.Errorf("validateCIDFontGlyphWidths: dict=%s entry=%s invalid type at index %d\n", dictName, entryName, i) + } + + } + + return nil +} + +func validateCIDFontDictEntryCIDSystemInfo(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + d1, err := validateDictEntry(xRefTable, d, dictName, "CIDSystemInfo", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + if d1 != nil { + err = validateCIDSystemInfoDict(xRefTable, d1) + + } + + return err +} + +func validateCIDFontDictEntryCIDToGIDMap(xRefTable *model.XRefTable, d types.Dict, isCIDFontType2 bool) error { + + if o, found := d.Find("CIDToGIDMap"); found { + + if xRefTable.ValidationMode == model.ValidationStrict && !isCIDFontType2 { + return errors.New("pdfcpu: validateCIDFontDict: entry CIDToGIDMap not allowed - must be CIDFontType2") + } + + err := validateCIDToGIDMap(xRefTable, o) + if err != nil { + return err + } + + } + + return nil +} + +func validateCIDFontDict(xRefTable *model.XRefTable, d types.Dict) error { + + // see 9.7.4 + + dictName := "CIDFontDict" + + // Type, required, name + _, err := validateNameEntry(xRefTable, d, dictName, "Type", REQUIRED, model.V10, func(s string) bool { return s == "Font" }) + if err != nil { + return err + } + + var isCIDFontType2 bool + var fontType string + + // Subtype, required, name + subType, err := validateNameEntry(xRefTable, d, dictName, "Subtype", REQUIRED, model.V10, func(s string) bool { return s == "CIDFontType0" || s == "CIDFontType2" }) + if err != nil { + return err + } + + isCIDFontType2 = *subType == "CIDFontType2" + fontType = subType.Value() + + // BaseFont, required, name + _, err = validateNameEntry(xRefTable, d, dictName, "BaseFont", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + // CIDSystemInfo, required, dict + err = validateCIDFontDictEntryCIDSystemInfo(xRefTable, d, "CIDFontDict") + if err != nil { + return err + } + + // FontDescriptor, required, dict + err = validateFontDescriptor(xRefTable, d, dictName, fontType, REQUIRED, model.V10) + if err != nil { + return err + } + + // DW, optional, integer + _, err = validateIntegerEntry(xRefTable, d, dictName, "DW", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // W, optional, array + err = validateCIDFontGlyphWidths(xRefTable, d, dictName, "W", OPTIONAL, model.V10) + if err != nil { + return err + } + + // DW2, optional, array + // An array of two numbers specifying the default metrics for vertical writing. + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "DW2", OPTIONAL, model.V10, func(a types.Array) bool { return len(a) == 2 }) + if err != nil { + return err + } + + // W2, optional, array + err = validateCIDFontGlyphWidths(xRefTable, d, dictName, "W2", OPTIONAL, model.V10) + if err != nil { + return err + } + + // CIDToGIDMap, stream or (name /Identity) + // optional, Type 2 CIDFonts with embedded associated TrueType font program only. + return validateCIDFontDictEntryCIDToGIDMap(xRefTable, d, isCIDFontType2) +} + +func validateDescendantFonts(xRefTable *model.XRefTable, d types.Dict, fontDictName string, required bool) error { + + // A one-element array holding a CID font dictionary. + + a, err := validateArrayEntry(xRefTable, d, fontDictName, "DescendantFonts", required, model.V10, func(a types.Array) bool { return len(a) == 1 }) + if err != nil || a == nil { + return err + } + + d1, err := xRefTable.DereferenceDict(a[0]) + if err != nil { + return err + } + + if d1 == nil { + if required { + return errors.Errorf("validateDescendantFonts: dict=%s required descendant font dict missing.\n", fontDictName) + } + return nil + } + + return validateCIDFontDict(xRefTable, d1) +} + +func validateType0FontDict(xRefTable *model.XRefTable, d types.Dict) error { + + dictName := "type0FontDict" + + // BaseFont, required, name + _, err := validateNameEntry(xRefTable, d, dictName, "BaseFont", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + // Encoding, required, name or CMap stream dict + err = validateType0FontEncoding(xRefTable, d, dictName, REQUIRED) + if err != nil { + return err + } + + // DescendantFonts: one-element array specifying the CIDFont dictionary that is the descendant of this Type 0 font, required. + err = validateDescendantFonts(xRefTable, d, dictName, REQUIRED) + if err != nil { + return err + } + + // ToUnicode, optional, CMap stream dict + _, err = validateStreamDictEntry(xRefTable, d, dictName, "ToUnicode", OPTIONAL, model.V12, nil) + if err != nil && xRefTable.ValidationMode == model.ValidationRelaxed { + _, err = validateNameEntry(xRefTable, d, dictName, "ToUnicode", REQUIRED, model.V12, func(s string) bool { return s == "Identity-H" }) + } + + return err +} + +func validateType1FontDict(xRefTable *model.XRefTable, d types.Dict) error { + + // see 9.6.2 + + dictName := "type1FontDict" + + // Name, name, obsolet and should not be used. + + // BaseFont, required, name + fontName, err := validateNameEntry(xRefTable, d, dictName, "BaseFont", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + fn := (*fontName).Value() + required := xRefTable.Version() >= model.V15 || !validateStandardType1Font(fn) + if xRefTable.ValidationMode == model.ValidationRelaxed { + required = false + } + // FirstChar, required except for standard 14 fonts. since 1.5 always required, integer + fc, err := validateIntegerEntry(xRefTable, d, dictName, "FirstChar", required, model.V10, nil) + if err != nil { + return err + } + + if !required && fc != nil { + // For the standard 14 fonts, the entries FirstChar, LastChar, Widths and FontDescriptor shall either all be present or all be absent. + if xRefTable.ValidationMode == model.ValidationStrict { + required = true + } + } + + // LastChar, required except for standard 14 fonts. since 1.5 always required, integer + _, err = validateIntegerEntry(xRefTable, d, dictName, "LastChar", required, model.V10, nil) + if err != nil { + return err + } + + // Widths, required except for standard 14 fonts. since 1.5 always required, array of numbers + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "Widths", required, model.V10, nil) + if err != nil { + return err + } + + // FontDescriptor, required since version 1.5; required unless standard font for version < 1.5, dict + err = validateFontDescriptor(xRefTable, d, dictName, "Type1", required, model.V10) + if err != nil { + return err + } + + // Encoding, optional, name or dict + err = validateFontEncoding(xRefTable, d, dictName, OPTIONAL) + if err != nil { + return err + } + + // ToUnicode, optional, stream + _, err = validateStreamDictEntry(xRefTable, d, dictName, "ToUnicode", OPTIONAL, model.V12, nil) + + return err +} + +func validateCharProcsDict(xRefTable *model.XRefTable, d types.Dict, dictName string, required bool, sinceVersion model.Version) error { + + d1, err := validateDictEntry(xRefTable, d, dictName, "CharProcs", required, sinceVersion, nil) + if err != nil || d1 == nil { + return err + } + + for _, v := range d1 { + + _, _, err = xRefTable.DereferenceStreamDict(v) + if err != nil { + return err + } + + } + + return nil +} + +func validateUseCMapEntry(xRefTable *model.XRefTable, d types.Dict, dictName string, required bool, sinceVersion model.Version) error { + + entryName := "UseCMap" + + o, err := validateEntry(xRefTable, d, dictName, entryName, required, sinceVersion) + if err != nil || o == nil { + return err + } + + switch o := o.(type) { + + case types.Name: + // no further processing + + case types.StreamDict: + err = validateCMapStreamDict(xRefTable, &o) + if err != nil { + return err + } + + default: + return errors.Errorf("validateUseCMapEntry: dict=%s corrupt entry \"%s\"\n", dictName, entryName) + + } + + return nil +} + +func validateCIDSystemInfoDict(xRefTable *model.XRefTable, d types.Dict) error { + + dictName := "CIDSystemInfoDict" + + // Registry, required, ASCII string + _, err := validateStringEntry(xRefTable, d, dictName, "Registry", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + // Ordering, required, ASCII string + _, err = validateStringEntry(xRefTable, d, dictName, "Ordering", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + // Supplement, required, integer + _, err = validateIntegerEntry(xRefTable, d, dictName, "Supplement", REQUIRED, model.V10, nil) + + return err +} + +func validateCMapStreamDict(xRefTable *model.XRefTable, sd *types.StreamDict) error { + + // See table 120 + + dictName := "CMapStreamDict" + + // Type, optional, name + _, err := validateNameEntry(xRefTable, sd.Dict, dictName, "Type", OPTIONAL, model.V10, func(s string) bool { return s == "CMap" }) + if err != nil { + return err + } + + // CMapName, required, name + _, err = validateNameEntry(xRefTable, sd.Dict, dictName, "CMapName", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + // CIDFontType0SystemInfo, required, dict + d, err := validateDictEntry(xRefTable, sd.Dict, dictName, "CIDSystemInfo", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + if d != nil { + err = validateCIDSystemInfoDict(xRefTable, d) + if err != nil { + return err + } + } + + // WMode, optional, integer, 0 or 1 + _, err = validateIntegerEntry(xRefTable, sd.Dict, dictName, "WMode", OPTIONAL, model.V10, func(i int) bool { return i == 0 || i == 1 }) + if err != nil { + return err + } + + // UseCMap, name or cmap stream dict, optional. + // If present, the referencing CMap shall specify only + // the character mappings that differ from the referenced CMap. + return validateUseCMapEntry(xRefTable, sd.Dict, dictName, OPTIONAL, model.V10) +} + +func validateType0FontEncoding(xRefTable *model.XRefTable, d types.Dict, dictName string, required bool) error { + + entryName := "Encoding" + + o, err := validateEntry(xRefTable, d, dictName, entryName, required, model.V10) + if err != nil || o == nil { + return err + } + + switch o := o.(type) { + + case types.Name: + // no further processing + + case types.StreamDict: + err = validateCMapStreamDict(xRefTable, &o) + + default: + err = errors.Errorf("validateType0FontEncoding: dict=%s corrupt entry \"Encoding\"\n", dictName) + + } + + return err +} + +func validateType3FontDict(xRefTable *model.XRefTable, d types.Dict) error { + + // see 9.6.5 + + dictName := "type3FontDict" + + // Name, name, obsolet and should not be used. + + // FontBBox, required, rectangle + _, err := validateRectangleEntry(xRefTable, d, dictName, "FontBBox", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + // FontMatrix, required, number array + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "FontMatrix", REQUIRED, model.V10, func(a types.Array) bool { return len(a) == 6 }) + if err != nil { + return err + } + + // CharProcs, required, dict + err = validateCharProcsDict(xRefTable, d, dictName, REQUIRED, model.V10) + if err != nil { + return err + } + + // Encoding, required, name or dict + err = validateFontEncoding(xRefTable, d, dictName, REQUIRED) + if err != nil { + return err + } + + // FirstChar, required, integer + _, err = validateIntegerEntry(xRefTable, d, dictName, "FirstChar", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + // LastChar, required, integer + _, err = validateIntegerEntry(xRefTable, d, dictName, "LastChar", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + // Widths, required, array of number + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "Widths", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + // FontDescriptor, required since version 1.5 for tagged PDF documents, dict + sinceVersion := model.V15 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V13 + } + err = validateFontDescriptor(xRefTable, d, dictName, "Type3", xRefTable.Tagged, sinceVersion) + if err != nil { + return err + } + + // Resources, optional, dict, since V1.2 + d1, err := validateDictEntry(xRefTable, d, dictName, "Resources", OPTIONAL, model.V12, nil) + if err != nil { + return err + } + if d1 != nil { + _, err := validateResourceDict(xRefTable, d1) + if err != nil { + return err + } + } + + // ToUnicode, optional, stream + _, err = validateStreamDictEntry(xRefTable, d, dictName, "ToUnicode", OPTIONAL, model.V12, nil) + + return err +} + +func validateFontDict(xRefTable *model.XRefTable, o types.Object) (err error) { + + d, err := xRefTable.DereferenceDict(o) + if err != nil || d == nil { + return err + } + + if xRefTable.ValidationMode == model.ValidationRelaxed { + if len(d) == 0 { + return nil + } + } + + if d.Type() == nil || *d.Type() != "Font" { + return errors.New("pdfcpu: validateFontDict: corrupt font dict") + } + + subtype := d.Subtype() + if subtype == nil { + return errors.New("pdfcpu: validateFontDict: missing Subtype") + } + + switch *subtype { + + case "TrueType": + err = validateTrueTypeFontDict(xRefTable, d) + + case "Type0": + err = validateType0FontDict(xRefTable, d) + + case "Type1": + err = validateType1FontDict(xRefTable, d) + + case "MMType1": + err = validateType1FontDict(xRefTable, d) + + case "Type3": + err = validateType3FontDict(xRefTable, d) + + default: + return errors.Errorf("pdfcpu: validateFontDict: unknown Subtype: %s\n", *subtype) + + } + + return err +} + +func validateFontResourceDict(xRefTable *model.XRefTable, o types.Object, sinceVersion model.Version) error { + + // Version check + err := xRefTable.ValidateVersion("fontResourceDict", sinceVersion) + if err != nil { + return err + } + + d, err := xRefTable.DereferenceDict(o) + if err != nil || d == nil { + return err + } + + // Iterate over font resource dict + for _, obj := range d { + + // Process fontDict + err = validateFontDict(xRefTable, obj) + if err != nil { + return err + } + + } + + return nil +} diff --git a/pkg/pdfcpu/validate/form.go b/pkg/pdfcpu/validate/form.go new file mode 100644 index 0000000000000000000000000000000000000000..a6d8a2dfe2a79816c8e1fd098a96fa18046b4292 --- /dev/null +++ b/pkg/pdfcpu/validate/form.go @@ -0,0 +1,623 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 validate + +import ( + "strconv" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// func validateSignatureDict(xRefTable *model.XRefTable, o pdf.Object) error { +// +// d, err := xRefTable.DereferenceDict(o) +// if err != nil || d == nil { +// return err +// } +// +// // Type, optional, name +// _, err = validateNameEntry(xRefTable, d, "signatureDict", "Type", OPTIONAL, model.V10, func(s string) bool { return s == "Sig" }) +// +// // process signature dict fields. +// +// return err +// } + +func validateAppearanceSubDict(xRefTable *model.XRefTable, d types.Dict) error { + + // dict of xobjects + for _, o := range d { + + if xRefTable.ValidationMode == model.ValidationRelaxed { + if d, ok := o.(types.Dict); ok && len(d) == 0 { + continue + } + } + + err := validateXObjectStreamDict(xRefTable, o) + if err != nil { + return err + } + + } + + return nil +} + +func validateAppearanceDictEntry(xRefTable *model.XRefTable, o types.Object) error { + + // stream or dict + // single appearance stream or subdict + + o, err := xRefTable.Dereference(o) + if err != nil || o == nil { + return err + } + + switch o := o.(type) { + + case types.Dict: + err = validateAppearanceSubDict(xRefTable, o) + + case types.StreamDict: + err = validateXObjectStreamDict(xRefTable, o) + + default: + err = errors.New("pdfcpu: validateAppearanceDictEntry: unsupported PDF object") + + } + + return err +} + +func validateAppearanceDict(xRefTable *model.XRefTable, o types.Object) error { + + // see 12.5.5 Appearance Streams + + d, err := xRefTable.DereferenceDict(o) + if err != nil || d == nil { + return err + } + + // Normal Appearance + o, ok := d.Find("N") + if !ok { + if xRefTable.ValidationMode == model.ValidationStrict { + return errors.New("pdfcpu: validateAppearanceDict: missing required entry \"N\"") + } + } else { + err = validateAppearanceDictEntry(xRefTable, o) + if err != nil { + return err + } + } + + // Rollover Appearance + if o, ok = d.Find("R"); ok { + err = validateAppearanceDictEntry(xRefTable, o) + if err != nil { + return err + } + } + + // Down Appearance + if o, ok = d.Find("D"); ok { + err = validateAppearanceDictEntry(xRefTable, o) + if err != nil { + return err + } + } + + return nil +} + +func validateDA(s string) bool { + // A sequence of valid page-content graphics or text state operators. + // At a minimum, the string shall include a Tf (text font) operator along with its two operands, font and size. + da := strings.Fields(s) + for i := 0; i < len(da); i++ { + if da[i] == "Tf" { + if i < 2 { + return false + } + if da[i-2][0] != '/' { + return false + } + fontID := da[i-2][1:] + if len(fontID) == 0 { + return false + } + if _, err := strconv.ParseFloat(da[i-1], 64); err != nil { + return false + } + continue + } + if da[i] == "rg" { + if i < 3 { + return false + } + if _, err := strconv.ParseFloat(da[i-3], 32); err != nil { + return false + } + if _, err := strconv.ParseFloat(da[i-2], 32); err != nil { + return false + } + if _, err := strconv.ParseFloat(da[i-1], 32); err != nil { + return false + } + } + if da[i] == "g" { + if i < 1 { + return false + } + if _, err := strconv.ParseFloat(da[i-1], 32); err != nil { + return false + } + } + } + + return true +} + +func validateDARelaxed(s string) bool { + // A sequence of valid page-content graphics or text state operators. + // At a minimum, the string shall include a Tf (text font) operator along with its two operands, font and size. + da := strings.Fields(s) + for i := 0; i < len(da); i++ { + if da[i] == "Tf" { + if i < 2 { + return false + } + if da[i-2][0] != '/' { + return false + } + //fontID := da[i-2][1:] + // if len(fontID) == 0 { + // return false + // } + if _, err := strconv.ParseFloat(da[i-1], 64); err != nil { + return false + } + continue + } + if da[i] == "rg" { + if i < 3 { + return false + } + if _, err := strconv.ParseFloat(da[i-3], 32); err != nil { + return false + } + if _, err := strconv.ParseFloat(da[i-2], 32); err != nil { + return false + } + if _, err := strconv.ParseFloat(da[i-1], 32); err != nil { + return false + } + } + if da[i] == "g" { + if i < 1 { + return false + } + if _, err := strconv.ParseFloat(da[i-1], 32); err != nil { + return false + } + } + } + + return true +} + +func validateFormFieldDA(xRefTable *model.XRefTable, d types.Dict, dictName string, terminalNode bool, outFieldType *types.Name, requiresDA bool) (bool, error) { + validate := validateDA + if xRefTable.ValidationMode == model.ValidationRelaxed { + validate = validateDARelaxed + } + if terminalNode && (*outFieldType).Value() == "Tx" { + da, err := validateStringEntry(xRefTable, d, dictName, "DA", terminalNode && requiresDA, model.V10, validate) + if err != nil { + return false, err + } + if xRefTable.ValidationMode == model.ValidationRelaxed && da != nil { + // Repair + d["DA"] = types.StringLiteral(*da) + } + + return da != nil && *da != "", nil + } + + return false, nil +} + +func validateFormFieldDictEntries(xRefTable *model.XRefTable, d types.Dict, terminalNode bool, inFieldType *types.Name, requiresDA bool) (outFieldType *types.Name, hasDA bool, err error) { + + dictName := "formFieldDict" + + // FT: name, Btn,Tx,Ch,Sig + validate := func(s string) bool { return types.MemberOf(s, []string{"Btn", "Tx", "Ch", "Sig"}) } + fieldType, err := validateNameEntry(xRefTable, d, dictName, "FT", terminalNode && inFieldType == nil, model.V10, validate) + if err != nil { + return nil, false, err + } + + outFieldType = inFieldType + if fieldType != nil { + outFieldType = fieldType + } + + // Parent, required if this is a child in the field hierarchy. + _, err = validateIndRefEntry(xRefTable, d, dictName, "Parent", OPTIONAL, model.V10) + if err != nil { + return nil, false, err + } + + // T, optional, text string + _, err = validateStringEntry(xRefTable, d, dictName, "T", OPTIONAL, model.V10, nil) + if err != nil { + return nil, false, err + } + + // TU, optional, text string, since V1.3 + _, err = validateStringEntry(xRefTable, d, dictName, "TU", OPTIONAL, model.V13, nil) + if err != nil { + return nil, false, err + } + + // TM, optional, text string, since V1.3 + _, err = validateStringEntry(xRefTable, d, dictName, "TM", OPTIONAL, model.V13, nil) + if err != nil { + return nil, false, err + } + + // Ff, optional, integer + _, err = validateIntegerEntry(xRefTable, d, dictName, "Ff", OPTIONAL, model.V10, nil) + if err != nil { + return nil, false, err + } + + // V, optional, various + _, err = validateEntry(xRefTable, d, dictName, "V", OPTIONAL, model.V10) + if err != nil { + return nil, false, err + } + + // DV, optional, various + _, err = validateEntry(xRefTable, d, dictName, "DV", OPTIONAL, model.V10) + if err != nil { + return nil, false, err + } + + // AA, optional, dict, since V1.2 + err = validateAdditionalActions(xRefTable, d, dictName, "AA", OPTIONAL, model.V12, "fieldOrAnnot") + if err != nil { + return nil, false, err + } + + // DA, required for text fields, since ? + // The default appearance string containing a sequence of valid page-content graphics or text state operators that define such properties as the field’s text size and colour. + hasDA, err = validateFormFieldDA(xRefTable, d, dictName, terminalNode, outFieldType, requiresDA) + + return outFieldType, hasDA, err +} + +func validateFormFieldParts(xRefTable *model.XRefTable, d types.Dict, inFieldType *types.Name, requiresDA bool) error { + // dict represents a terminal field and must have Subtype "Widget" + if _, err := validateNameEntry(xRefTable, d, "formFieldDict", "Subtype", REQUIRED, model.V10, func(s string) bool { return s == "Widget" }); err != nil { + d["Subtype"] = types.Name("Widget") + } + + // Validate field dict entries. + if _, _, err := validateFormFieldDictEntries(xRefTable, d, true, inFieldType, requiresDA); err != nil { + return err + } + + // Validate widget annotation - Validation of AA redundant because of merged acrofield with widget annotation. + _, err := validateAnnotationDict(xRefTable, d) + return err +} + +func validateFormFieldKids(xRefTable *model.XRefTable, d types.Dict, o types.Object, inFieldType *types.Name, requiresDA bool) error { + var err error + // dict represents a non terminal field. + if d.Subtype() != nil && *d.Subtype() == "Widget" { + return errors.New("pdfcpu: validateFormFieldKids: non terminal field can not be widget annotation") + } + + // Validate field entries. + var xInFieldType *types.Name + var hasDA bool + if xInFieldType, hasDA, err = validateFormFieldDictEntries(xRefTable, d, false, inFieldType, requiresDA); err != nil { + return err + } + if requiresDA && hasDA { + requiresDA = false + } + + // Recurse over kids. + a, err := xRefTable.DereferenceArray(o) + if err != nil || a == nil { + return err + } + + for _, value := range a { + ir, ok := value.(types.IndirectRef) + if !ok { + return errors.New("pdfcpu: validateFormFieldKids: corrupt kids array: entries must be indirect reference") + } + valid, err := xRefTable.IsValid(ir) + if err != nil { + return err + } + + if !valid { + if err = validateFormFieldDict(xRefTable, ir, xInFieldType, requiresDA); err != nil { + return err + } + } + } + + return nil +} + +func validateFormFieldDict(xRefTable *model.XRefTable, ir types.IndirectRef, inFieldType *types.Name, requiresDA bool) error { + d, err := xRefTable.DereferenceDict(ir) + if err != nil || d == nil { + return err + } + + if xRefTable.ValidationMode == model.ValidationRelaxed { + if len(d) == 0 { + return nil + } + } + + if err := xRefTable.SetValid(ir); err != nil { + return err + } + + if o, ok := d.Find("Kids"); ok { + return validateFormFieldKids(xRefTable, d, o, inFieldType, requiresDA) + } + + return validateFormFieldParts(xRefTable, d, inFieldType, requiresDA) +} + +func validateFormFields(xRefTable *model.XRefTable, o types.Object, requiresDA bool) error { + + a, err := xRefTable.DereferenceArray(o) + if err != nil || len(a) == 0 { + return err + } + + for _, value := range a { + + ir, ok := value.(types.IndirectRef) + if !ok { + return errors.New("pdfcpu: validateFormFields: corrupt form field array entry") + } + + valid, err := xRefTable.IsValid(ir) + if err != nil { + return err + } + + if !valid { + if err = validateFormFieldDict(xRefTable, ir, nil, requiresDA); err != nil { + return err + } + } + + } + + return nil +} + +func validateFormCO(xRefTable *model.XRefTable, o types.Object, sinceVersion model.Version, requiresDA bool) error { + + // see 12.6.3 Trigger Events + // Array of indRefs to field dicts with calculation actions, since V1.3 + + // Version check + err := xRefTable.ValidateVersion("AcroFormCO", sinceVersion) + if err != nil { + return err + } + + return validateFormFields(xRefTable, o, requiresDA) +} + +func validateFormXFA(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + + // see 12.7.8 + + o, ok := d.Find("XFA") + if !ok { + return nil + } + + // streamDict or array of text,streamDict pairs + + o, err := xRefTable.Dereference(o) + if err != nil || o == nil { + return err + } + + switch o := o.(type) { + + case types.StreamDict: + // no further processing + + case types.Array: + + i := 0 + + for _, v := range o { + + if v == nil { + return errors.New("pdfcpu: validateFormXFA: array entry is nil") + } + + o, err := xRefTable.Dereference(v) + if err != nil { + return err + } + + if i%2 == 0 { + + _, ok := o.(types.StringLiteral) + if !ok { + return errors.New("pdfcpu: validateFormXFA: even array must be a string") + } + + } else { + + _, ok := o.(types.StreamDict) + if !ok { + return errors.New("pdfcpu: validateFormXFA: odd array entry must be a streamDict") + } + + } + + i++ + } + + default: + return errors.New("pdfcpu: validateFormXFA: needs to be streamDict or array") + } + + return xRefTable.ValidateVersion("AcroFormXFA", sinceVersion) +} + +func validateQ(i int) bool { return i >= 0 && i <= 2 } + +func validateFormEntryCO(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version, requiresDA bool) error { + + o, ok := d.Find("CO") + if !ok { + return nil + } + + return validateFormCO(xRefTable, o, sinceVersion, requiresDA) +} + +func validateFormEntryDR(xRefTable *model.XRefTable, d types.Dict) error { + + o, ok := d.Find("DR") + if !ok { + return nil + } + + _, err := validateResourceDict(xRefTable, o) + + return err +} + +func validateFormEntries(xRefTable *model.XRefTable, d types.Dict, dictName string, requiresDA bool, sinceVersion model.Version) error { + // NeedAppearances: optional, boolean + _, err := validateBooleanEntry(xRefTable, d, dictName, "NeedAppearances", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // SigFlags: optional, since 1.3, integer + sinceV := model.V13 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceV = model.V12 + } + sf, err := validateIntegerEntry(xRefTable, d, dictName, "SigFlags", OPTIONAL, sinceV, nil) + if err != nil { + return err + } + if sf != nil { + i := sf.Value() + xRefTable.SignatureExist = i&1 > 0 + xRefTable.AppendOnly = i&2 > 0 + } + + // CO: arra + err = validateFormEntryCO(xRefTable, d, model.V13, requiresDA) + if err != nil { + return err + } + + // DR, optional, resource dict + err = validateFormEntryDR(xRefTable, d) + if err != nil { + return err + } + + // Q: optional, integer + _, err = validateIntegerEntry(xRefTable, d, dictName, "Q", OPTIONAL, model.V10, validateQ) + if err != nil { + return err + } + + // XFA: optional, since 1.5, stream or array + return validateFormXFA(xRefTable, d, sinceVersion) +} + +func validateForm(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) error { + + // => 12.7.2 Interactive Form Dictionary + + d, err := validateDictEntry(xRefTable, rootDict, "rootDict", "AcroForm", OPTIONAL, sinceVersion, nil) + if err != nil || d == nil { + return err + } + + // Version check + if err = xRefTable.ValidateVersion("AcroForm", sinceVersion); err != nil { + return err + } + + // Fields, required, array of indirect references + o, ok := d.Find("Fields") + if !ok { + // Fix empty AcroForm dict. + rootDict.Delete("AcroForm") + return nil + } + + xRefTable.Form = d + + dictName := "acroFormDict" + + // DA: optional, string + validate := validateDA + if xRefTable.ValidationMode == model.ValidationRelaxed { + validate = validateDARelaxed + } + da, err := validateStringEntry(xRefTable, d, dictName, "DA", OPTIONAL, model.V10, validate) + if err != nil { + return err + } + if xRefTable.ValidationMode == model.ValidationRelaxed && da != nil { + // Repair + d["DA"] = types.StringLiteral(*da) + } + + requiresDA := da == nil || len(*da) == 0 + + err = validateFormFields(xRefTable, o, requiresDA) + if err != nil { + return err + } + + return validateFormEntries(xRefTable, d, dictName, requiresDA, sinceVersion) +} diff --git a/pkg/pdfcpu/validate/function.go b/pkg/pdfcpu/validate/function.go new file mode 100644 index 0000000000000000000000000000000000000000..216470271adb4926a0889d7fff9a87f523b54ff0 --- /dev/null +++ b/pkg/pdfcpu/validate/function.go @@ -0,0 +1,240 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 validate + +import ( + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// see 7.10 Functions + +func validateExponentialInterpolationFunctionDict(xRefTable *model.XRefTable, d types.Dict) error { + + dictName := "exponentialInterpolationFunctionDict" + + // Version check + err := xRefTable.ValidateVersion(dictName, model.V13) + if err != nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "Domain", REQUIRED, model.V13, nil) + if err != nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "Range", OPTIONAL, model.V13, nil) + if err != nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "C0", OPTIONAL, model.V13, nil) + if err != nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "C1", OPTIONAL, model.V13, nil) + if err != nil { + return err + } + + _, err = validateNumberEntry(xRefTable, d, dictName, "N", REQUIRED, model.V13, nil) + + return err +} + +func validateStitchingFunctionDict(xRefTable *model.XRefTable, d types.Dict) error { + + dictName := "stitchingFunctionDict" + + // Version check + err := xRefTable.ValidateVersion(dictName, model.V13) + if err != nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "Domain", REQUIRED, model.V13, nil) + if err != nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "Range", OPTIONAL, model.V13, nil) + if err != nil { + return err + } + + _, err = validateFunctionArrayEntry(xRefTable, d, dictName, "Functions", REQUIRED, model.V13, nil) + if err != nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "Bounds", REQUIRED, model.V13, nil) + if err != nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "Encode", REQUIRED, model.V13, nil) + + return err +} + +func validateSampledFunctionStreamDict(xRefTable *model.XRefTable, sd *types.StreamDict) error { + + dictName := "sampledFunctionStreamDict" + + // Version check + err := xRefTable.ValidateVersion(dictName, model.V12) + if err != nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, sd.Dict, dictName, "Domain", REQUIRED, model.V12, nil) + if err != nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, sd.Dict, dictName, "Range", REQUIRED, model.V12, nil) + if err != nil { + return err + } + + _, err = validateIntegerArrayEntry(xRefTable, sd.Dict, dictName, "Size", REQUIRED, model.V12, nil) + if err != nil { + return err + } + + validate := func(i int) bool { return types.IntMemberOf(i, []int{1, 2, 4, 8, 12, 16, 24, 32}) } + _, err = validateIntegerEntry(xRefTable, sd.Dict, dictName, "BitsPerSample", REQUIRED, model.V12, validate) + if err != nil { + return err + } + + _, err = validateIntegerEntry(xRefTable, sd.Dict, dictName, "Order", OPTIONAL, model.V12, func(i int) bool { return i == 1 || i == 3 }) + if err != nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, sd.Dict, dictName, "Encode", OPTIONAL, model.V12, nil) + if err != nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, sd.Dict, dictName, "Decode", OPTIONAL, model.V12, nil) + + return err +} + +func validatePostScriptCalculatorFunctionStreamDict(xRefTable *model.XRefTable, sd *types.StreamDict) error { + + dictName := "postScriptCalculatorFunctionStreamDict" + + // Version check + err := xRefTable.ValidateVersion(dictName, model.V13) + if err != nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, sd.Dict, dictName, "Domain", REQUIRED, model.V13, nil) + if err != nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, sd.Dict, dictName, "Range", REQUIRED, model.V13, nil) + + return err +} + +func processFunctionDict(xRefTable *model.XRefTable, d types.Dict) error { + + funcType, err := validateIntegerEntry(xRefTable, d, "functionDict", "FunctionType", REQUIRED, model.V10, func(i int) bool { return i == 2 || i == 3 }) + if err != nil { + return err + } + + switch *funcType { + + case 2: + err = validateExponentialInterpolationFunctionDict(xRefTable, d) + + case 3: + err = validateStitchingFunctionDict(xRefTable, d) + + } + + return err +} + +func processFunctionStreamDict(xRefTable *model.XRefTable, sd *types.StreamDict) error { + + funcType, err := validateIntegerEntry(xRefTable, sd.Dict, "functionDict", "FunctionType", REQUIRED, model.V10, func(i int) bool { return i == 0 || i == 4 }) + if err != nil { + return err + } + + switch *funcType { + case 0: + err = validateSampledFunctionStreamDict(xRefTable, sd) + + case 4: + err = validatePostScriptCalculatorFunctionStreamDict(xRefTable, sd) + + } + + return err +} + +func processFunction(xRefTable *model.XRefTable, o types.Object) (err error) { + + // Function dict: dict or stream dict with required entry "FunctionType" (integer): + // 0: Sampled function (stream dict) + // 2: Exponential interpolation function (dict) + // 3: Stitching function (dict) + // 4: PostScript calculator function (stream dict), since V1.3 + + switch o := o.(type) { + + case types.Dict: + + // process function 2,3 + err = processFunctionDict(xRefTable, o) + + case types.StreamDict: + + // process function 0,4 + err = processFunctionStreamDict(xRefTable, &o) + + default: + return errors.New("pdfcpu: processFunction: obj must be dict or stream dict") + } + + return err +} + +func validateFunction(xRefTable *model.XRefTable, o types.Object) error { + + o, err := xRefTable.Dereference(o) + if err != nil { + return err + } + if o == nil { + return errors.New("pdfcpu: validateFunction: missing object") + } + + return processFunction(xRefTable, o) +} diff --git a/pkg/pdfcpu/validate/info.go b/pkg/pdfcpu/validate/info.go new file mode 100644 index 0000000000000000000000000000000000000000..1a2a32cded40af33a1aafe42b3527c6826ab36ff --- /dev/null +++ b/pkg/pdfcpu/validate/info.go @@ -0,0 +1,225 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 validate + +import ( + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// DocumentProperty ensures a property name that may be modified. +func DocumentProperty(s string) bool { + return !types.MemberOf(s, []string{"Keywords", "Producer", "CreationDate", "ModDate", "Trapped"}) +} + +func validateInfoDictDate(xRefTable *model.XRefTable, o types.Object) (s string, err error) { + return validateDateObject(xRefTable, o, model.V10) +} + +func validateInfoDictTrapped(xRefTable *model.XRefTable, o types.Object) error { + sinceVersion := model.V13 + + validate := func(s string) bool { return types.MemberOf(s, []string{"True", "False", "Unknown"}) } + + if xRefTable.ValidationMode == model.ValidationRelaxed { + validate = func(s string) bool { + return types.MemberOf(s, []string{"True", "False", "Unknown", "true", "false", "unknown"}) + } + } + + _, err := xRefTable.DereferenceName(o, sinceVersion, validate) + if err == nil { + return nil + } + + if xRefTable.ValidationMode == model.ValidationRelaxed { + _, err = xRefTable.DereferenceBoolean(o, sinceVersion) + } + + return err +} + +func handleProperties(xRefTable *model.XRefTable, key string, val types.Object) error { + v, err := xRefTable.DereferenceStringOrHexLiteral(val, model.V10, nil) + if err != nil { + if xRefTable.ValidationMode == model.ValidationStrict { + return err + } + _, err = xRefTable.Dereference(val) + return err + } + + if v != "" { + + k, err := types.DecodeName(key) + if err != nil { + return err + } + + xRefTable.Properties[k] = v + } + + return nil +} + +func validateKeywords(xRefTable *model.XRefTable, v types.Object) (err error) { + xRefTable.Keywords, err = xRefTable.DereferenceStringOrHexLiteral(v, model.V11, nil) + if err != nil { + return err + } + + ss := strings.FieldsFunc(xRefTable.Keywords, func(c rune) bool { return c == ',' || c == ';' || c == '\r' }) + for _, s := range ss { + keyword := strings.TrimSpace(s) + xRefTable.KeywordList[keyword] = true + } + + return nil +} + +func validateDocInfoDictEntry(xRefTable *model.XRefTable, k string, v types.Object) (bool, error) { + var ( + err error + hasModDate bool + ) + + switch k { + + // text string, opt, since V1.1 + case "Title": + xRefTable.Title, err = xRefTable.DereferenceStringOrHexLiteral(v, model.V11, nil) + + // text string, optional + case "Author": + xRefTable.Author, err = xRefTable.DereferenceStringOrHexLiteral(v, model.V10, nil) + + // text string, optional, since V1.1 + case "Subject": + xRefTable.Subject, err = xRefTable.DereferenceStringOrHexLiteral(v, model.V11, nil) + + // text string, optional, since V1.1 + case "Keywords": + if err := validateKeywords(xRefTable, v); err != nil { + return hasModDate, err + } + + // text string, optional + case "Creator": + xRefTable.Creator, err = xRefTable.DereferenceStringOrHexLiteral(v, model.V10, nil) + + // text string, optional + case "Producer": + xRefTable.Producer, err = xRefTable.DereferenceStringOrHexLiteral(v, model.V10, nil) + + // date, optional + case "CreationDate": + xRefTable.CreationDate, err = validateInfoDictDate(xRefTable, v) + if err != nil && xRefTable.ValidationMode == model.ValidationRelaxed { + err = nil + } + + // date, required if PieceInfo is present in document catalog. + case "ModDate": + hasModDate = true + xRefTable.ModDate, err = validateInfoDictDate(xRefTable, v) + + // name, optional, since V1.3 + case "Trapped": + err = validateInfoDictTrapped(xRefTable, v) + + // text string, optional + default: + err = handleProperties(xRefTable, k, v) + } + + return hasModDate, err +} + +func validateDocumentInfoDict(xRefTable *model.XRefTable, obj types.Object) (bool, error) { + // Document info object is optional. + d, err := xRefTable.DereferenceDict(obj) + if err != nil || d == nil { + return false, err + } + + hasModDate := false + + for k, v := range d { + + hmd, err := validateDocInfoDictEntry(xRefTable, k, v) + + if err == types.ErrInvalidUTF16BE { + // Fix for #264: + err = nil + } + + if err != nil { + return false, err + } + + if !hasModDate && hmd { + hasModDate = true + } + } + + return hasModDate, nil +} + +func validateDocumentInfoObject(xRefTable *model.XRefTable) error { + // Document info object is optional. + if xRefTable.Info == nil { + return nil + } + + if log.ValidateEnabled() { + log.Validate.Println("*** validateDocumentInfoObject begin ***") + } + + hasModDate, err := validateDocumentInfoDict(xRefTable, *xRefTable.Info) + if err != nil { + return err + } + + hasPieceInfo, err := xRefTable.CatalogHasPieceInfo() + if err != nil { + return err + } + + if hasPieceInfo && !hasModDate { + return errors.Errorf("validateDocumentInfoObject: missing required entry \"ModDate\"") + } + + if log.ValidateEnabled() { + log.Validate.Println("*** validateDocumentInfoObject end ***") + } + + return nil +} + +// DocumentPageLayout returns true for valid page layout values. +func DocumentPageLayout(s string) bool { + return types.MemberOf(strings.ToLower(s), []string{"singlepage", "twocolumnleft", "twocolumnright", "twopageleft", "twopageright"}) +} + +// DocumentPageMode returns true for valid page mode values. +func DocumentPageMode(s string) bool { + return types.MemberOf(strings.ToLower(s), []string{"usenone", "useoutlines", "usethumbs", "fullscreen", "useoc", "useattachments"}) +} diff --git a/pkg/pdfcpu/validate/media.go b/pkg/pdfcpu/validate/media.go new file mode 100644 index 0000000000000000000000000000000000000000..07393bc95302880c25e350d900aed7bb6d9221fb --- /dev/null +++ b/pkg/pdfcpu/validate/media.go @@ -0,0 +1,1042 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 validate + +import ( + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +func validateMinimumBitDepthDict(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + + // see table 269 + + dictName := "minBitDepthDict" + + // Type, optional, name + _, err := validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, sinceVersion, func(s string) bool { return s == "MinBitDepth" }) + if err != nil { + return err + } + + // V, required, integer + _, err = validateIntegerEntry(xRefTable, d, dictName, "V", REQUIRED, sinceVersion, func(i int) bool { return i >= 0 }) + if err != nil { + return err + } + + // M, optional, integer + _, err = validateIntegerEntry(xRefTable, d, dictName, "M", OPTIONAL, sinceVersion, nil) + + return err +} + +func validateMinimumScreenSizeDict(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + + // see table 269 + + dictName := "minBitDepthDict" + + // Type, optional, name + _, err := validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, sinceVersion, func(s string) bool { return s == "MinScreenSize" }) + if err != nil { + return err + } + + // V, required, integer array, length 2 + _, err = validateIntegerArrayEntry(xRefTable, d, dictName, "V", REQUIRED, sinceVersion, func(a types.Array) bool { return len(a) == 2 }) + if err != nil { + return err + } + + // M, optional, integer + _, err = validateIntegerEntry(xRefTable, d, dictName, "M", OPTIONAL, sinceVersion, nil) + + return err +} + +func validateSoftwareIdentifierDict(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + + // see table 292 + + dictName := "swIdDict" + + // Type, optional, name + _, err := validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, sinceVersion, func(s string) bool { return s == "SoftwareIdentifier" }) + if err != nil { + return err + } + + // U, required, ASCII string + _, err = validateStringEntry(xRefTable, d, dictName, "U", REQUIRED, sinceVersion, nil) + if err != nil { + return err + } + + // L, optional, array + _, err = validateArrayEntry(xRefTable, d, dictName, "L", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // LI, optional, boolean + _, err = validateBooleanEntry(xRefTable, d, dictName, "LI", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // H, optional, array + _, err = validateArrayEntry(xRefTable, d, dictName, "H", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // HI, optional, boolean + _, err = validateBooleanEntry(xRefTable, d, dictName, "HI", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // OS, optional, array + _, err = validateStringArrayEntry(xRefTable, d, dictName, "OS", OPTIONAL, sinceVersion, nil) + + return err +} + +func validateMediaCriteriaDictEntryD(xRefTable *model.XRefTable, d types.Dict, dictName string, required bool, sinceVersion model.Version) error { + + d1, err := validateDictEntry(xRefTable, d, dictName, "D", required, sinceVersion, nil) + if err != nil { + return err + } + + if d1 != nil { + err = validateMinimumBitDepthDict(xRefTable, d1, sinceVersion) + } + + return err +} + +func validateMediaCriteriaDictEntryZ(xRefTable *model.XRefTable, d types.Dict, dictName string, required bool, sinceVersion model.Version) error { + + d1, err := validateDictEntry(xRefTable, d, dictName, "Z", required, sinceVersion, nil) + if err != nil { + return err + } + + if d1 != nil { + err = validateMinimumScreenSizeDict(xRefTable, d1, sinceVersion) + } + + return err +} + +func validateMediaCriteriaDictEntryV(xRefTable *model.XRefTable, d types.Dict, dictName string, required bool, sinceVersion model.Version) error { + + a, err := validateArrayEntry(xRefTable, d, dictName, "V", required, sinceVersion, nil) + if err != nil { + return err + } + + for _, v := range a { + + if v == nil { + continue + } + + d, err := xRefTable.DereferenceDict(v) + if err != nil { + return err + } + + if d != nil { + err = validateSoftwareIdentifierDict(xRefTable, d, sinceVersion) + if err != nil { + return err + } + } + + } + + return nil +} + +func validateMediaCriteriaDict(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + + // see table 268 + + dictName := "mediaCritDict" + + // Type, optional, name + _, err := validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, sinceVersion, func(s string) bool { return s == "MediaCriteria" }) + if err != nil { + return err + } + + // A, optional, boolean + _, err = validateBooleanEntry(xRefTable, d, dictName, "A", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // C, optional, boolean + _, err = validateBooleanEntry(xRefTable, d, dictName, "C", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // O, optional, boolean + _, err = validateBooleanEntry(xRefTable, d, dictName, "O", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // S, optional, boolean + _, err = validateBooleanEntry(xRefTable, d, dictName, "S", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // R, optional, integer + _, err = validateIntegerEntry(xRefTable, d, dictName, "R", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // D, optional, dict + err = validateMediaCriteriaDictEntryD(xRefTable, d, dictName, OPTIONAL, sinceVersion) + if err != nil { + return err + } + + // Z, optional, dict + err = validateMediaCriteriaDictEntryZ(xRefTable, d, dictName, OPTIONAL, sinceVersion) + if err != nil { + return err + } + + // V, optional, array + err = validateMediaCriteriaDictEntryV(xRefTable, d, dictName, OPTIONAL, sinceVersion) + if err != nil { + return err + } + + // P, optional, array + _, err = validateNameArrayEntry(xRefTable, d, dictName, "P", OPTIONAL, sinceVersion, func(a types.Array) bool { return len(a) == 1 || len(a) == 2 }) + if err != nil { + return err + } + + // L, optional, array + _, err = validateStringArrayEntry(xRefTable, d, dictName, "L", OPTIONAL, sinceVersion, nil) + + return err +} + +func validateMediaPermissionsDict(xRefTable *model.XRefTable, d types.Dict, dictName string, sinceVersion model.Version) error { + + // see table 275 + d1, err := validateDictEntry(xRefTable, d, dictName, "P", OPTIONAL, sinceVersion, nil) + if err != nil || d1 == nil { + return err + } + + dictName = "mediaPermissionDict" + + // Type, optional, name + _, err = validateNameEntry(xRefTable, d1, dictName, "Type", OPTIONAL, sinceVersion, func(s string) bool { return s == "MediaPermissions" }) + if err != nil { + return err + } + + // TF, optional, ASCII string + validateTempFilePolicy := func(s string) bool { + return types.MemberOf(s, []string{"TEMPNEVER", "TEMPEXTRACT", "TEMPACCESS", "TEMPALWAYS"}) + } + _, err = validateStringEntry(xRefTable, d1, dictName, "TF", OPTIONAL, sinceVersion, validateTempFilePolicy) + + return err +} + +func validateMediaPlayerInfoDict(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + + // see table 291 + + dictName := "mediaPlayerInfoDict" + + // Type, optional, name + _, err := validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, sinceVersion, func(s string) bool { return s == "MediaPlayerInfo" }) + if err != nil { + return err + } + + // PID, required, software identifier dict + d1, err := validateDictEntry(xRefTable, d, dictName, "PID", REQUIRED, sinceVersion, nil) + if err != nil { + return err + } + err = validateSoftwareIdentifierDict(xRefTable, d1, sinceVersion) + if err != nil { + return err + } + + // MH, optional, dict + _, err = validateDictEntry(xRefTable, d, dictName, "MH", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // BE, optional, dict + _, err = validateDictEntry(xRefTable, d, dictName, "BE", OPTIONAL, sinceVersion, nil) + + return err +} + +func validateMediaPlayersDict(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + + // see 13.2.7.2 + + dictName := "mediaPlayersDict" + + // Type, optional, name + _, err := validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, sinceVersion, func(s string) bool { return s == "MediaPlayers" }) + if err != nil { + return err + } + + // MU, optional, array of media player info dicts + a, err := validateArrayEntry(xRefTable, d, dictName, "MU", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + for _, v := range a { + + if v == nil { + continue + } + + d, err := xRefTable.DereferenceDict(v) + if err != nil { + return err + } + + if d == nil { + continue + } + + err = validateMediaPlayerInfoDict(xRefTable, d, sinceVersion) + if err != nil { + return err + } + + } + + return nil + +} + +func validateFileSpecOrFormXObjectEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) error { + + o, err := validateEntry(xRefTable, d, dictName, entryName, required, sinceVersion) + if err != nil || o == nil { + return err + } + + return validateFileSpecificationOrFormObject(xRefTable, o) +} + +func validateMediaClipDataDict(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + + // see 13.2.4.2 + + dictName := "mediaClipDataDict" + + // D, required, file specification or stream + err := validateFileSpecOrFormXObjectEntry(xRefTable, d, dictName, "D", REQUIRED, sinceVersion) + if err != nil { + return err + } + + // CT, optional, ASCII string + _, err = validateStringEntry(xRefTable, d, dictName, "CT", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // P, optional, media permissions dict + err = validateMediaPermissionsDict(xRefTable, d, dictName, sinceVersion) + if err != nil { + return err + } + + // Alt, optional, string array + _, err = validateStringArrayEntry(xRefTable, d, dictName, "Alt", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // PL, optional, media players dict + d1, err := validateDictEntry(xRefTable, d, dictName, "PL", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + if d1 != nil { + err = validateMediaPlayersDict(xRefTable, d1, sinceVersion) + if err != nil { + return err + } + } + + // MH, optional, dict + d1, err = validateDictEntry(xRefTable, d, dictName, "MH", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + if d1 != nil { + // BU, optional, ASCII string + _, err = validateStringEntry(xRefTable, d1, "", "BU", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + } + + // BE. optional, dict + d1, err = validateDictEntry(xRefTable, d, dictName, "BE", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + if d1 != nil { + // BU, optional, ASCII string + _, err = validateStringEntry(xRefTable, d1, "", "BU", OPTIONAL, sinceVersion, nil) + } + + return err +} + +func validateTimespanDict(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + + dictName := "timespanDict" + + // Type, optional, name + _, err := validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, sinceVersion, func(s string) bool { return s == "Timespan" }) + if err != nil { + return err + } + + // S, required, name + _, err = validateNameEntry(xRefTable, d, dictName, "S", REQUIRED, sinceVersion, func(s string) bool { return s == "S" }) + if err != nil { + return err + } + + // V, required, number + _, err = validateNumberEntry(xRefTable, d, dictName, "V", REQUIRED, sinceVersion, nil) + + return err +} + +func validateMediaOffsetDict(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + + // see 13.2.6.2 + + dictName := "mediaOffsetDict" + + // Type, optional, name + _, err := validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, sinceVersion, func(s string) bool { return s == "MediaOffset" }) + if err != nil { + return err + } + + // S, required, name + subType, err := validateNameEntry(xRefTable, d, dictName, "S", REQUIRED, sinceVersion, func(s string) bool { return types.MemberOf(s, []string{"T", "F", "M"}) }) + if err != nil { + return err + } + + switch *subType { + + case "T": + d1, err := validateDictEntry(xRefTable, d, dictName, "T", REQUIRED, sinceVersion, nil) + if err != nil { + return err + } + err = validateTimespanDict(xRefTable, d1, sinceVersion) + if err != nil { + return err + } + + case "F": + _, err = validateIntegerEntry(xRefTable, d, dictName, "F", REQUIRED, sinceVersion, func(i int) bool { return i >= 0 }) + if err != nil { + return err + } + + case "M": + _, err = validateStringEntry(xRefTable, d, dictName, "M", REQUIRED, sinceVersion, nil) + if err != nil { + return err + } + + } + + return nil +} + +func validateMediaClipSectionDictMHBE(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + + dictName := "mediaClipSectionMHBE" + + d1, err := validateDictEntry(xRefTable, d, dictName, "B", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + if d1 != nil { + err = validateMediaOffsetDict(xRefTable, d1, sinceVersion) + if err != nil { + return err + } + } + + d1, err = validateDictEntry(xRefTable, d, dictName, "E", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + if d1 != nil { + err = validateMediaOffsetDict(xRefTable, d1, sinceVersion) + } + + return err +} + +func validateMediaClipSectionDict(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + + // see 13.2.4.3 + + dictName := "mediaClipSectionDict" + + // D, required, media clip dict + d1, err := validateDictEntry(xRefTable, d, dictName, "D", REQUIRED, sinceVersion, nil) + if err != nil { + return err + } + err = validateMediaClipDict(xRefTable, d1, sinceVersion) + if err != nil { + return err + } + + // Alt, optional, string array + _, err = validateStringArrayEntry(xRefTable, d, dictName, "Alt", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // MH, optional, dict + d1, err = validateDictEntry(xRefTable, d, dictName, "MH", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + if d1 != nil { + err = validateMediaClipSectionDictMHBE(xRefTable, d1, sinceVersion) + if err != nil { + return err + } + } + + // BE, optional, dict + d1, err = validateDictEntry(xRefTable, d, dictName, "BE", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + if d1 != nil { + err = validateMediaClipSectionDictMHBE(xRefTable, d1, sinceVersion) + } + + return err +} + +func validateMediaClipDict(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + + // see 13.2.4 + + dictName := "mediaClipDict" + + // Type, optional, name + _, err := validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, sinceVersion, func(s string) bool { return s == "MediaClip" }) + if err != nil { + return err + } + + // S, required, name + subType, err := validateNameEntry(xRefTable, d, dictName, "S", REQUIRED, sinceVersion, func(s string) bool { return s == "MCD" || s == "MCS" }) + if err != nil { + return err + } + + // N, optional, text string + _, err = validateStringEntry(xRefTable, d, dictName, "N", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + if *subType == "MCD" { + err = validateMediaClipDataDict(xRefTable, d, sinceVersion) + if err != nil { + return err + } + } + + if *subType == "MCS" { + err = validateMediaClipSectionDict(xRefTable, d, sinceVersion) + } + + return err +} + +func validateMediaDurationDict(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + + dictName := "mediaDurationDict" + + // Type, optional, name + _, err := validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, sinceVersion, func(s string) bool { return s == "MediaDuration" }) + if err != nil { + return err + } + + // S, required, name + validate := func(s string) bool { return types.MemberOf(s, []string{"I", "F", "T"}) } + s, err := validateNameEntry(xRefTable, d, dictName, "S", REQUIRED, sinceVersion, validate) + if err != nil { + return err + } + + // T, required if S == "T", timespann dict + d1, err := validateDictEntry(xRefTable, d, dictName, "T", *s == "T", sinceVersion, nil) + if err != nil { + return err + } + if d1 != nil { + err = validateTimespanDict(xRefTable, d1, sinceVersion) + } + + return err +} + +func validateMediaPlayParamsMHBEDict(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + + dictName := "mediaPlayParamsMHBEDict" + + // V, optional, integer + _, err := validateIntegerEntry(xRefTable, d, dictName, "V", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // C, optional, boolean + _, err = validateBooleanEntry(xRefTable, d, dictName, "C", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // F, optional, integer + _, err = validateIntegerEntry(xRefTable, d, dictName, "RT", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // D, optional, media duration dict + d1, err := validateDictEntry(xRefTable, d, dictName, "D", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + if d1 != nil { + err = validateMediaDurationDict(xRefTable, d1, sinceVersion) + if err != nil { + return err + } + } + + // A, optional, boolean + _, err = validateBooleanEntry(xRefTable, d, dictName, "A", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // RC, optional, number + _, err = validateNumberEntry(xRefTable, d, dictName, "RC", OPTIONAL, sinceVersion, nil) + + return err +} + +func validateMediaPlayParamsDict(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + + // see 13.2.5 + + dictName := "mediaPlayParamsDict" + + // Type, optional, name + _, err := validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, sinceVersion, func(s string) bool { return s == "MediaPlayParams" }) + if err != nil { + return err + } + + // PL, optional, media players dict + d1, err := validateDictEntry(xRefTable, d, dictName, "PL", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + if d1 != nil { + err = validateMediaPlayersDict(xRefTable, d1, sinceVersion) + if err != nil { + return err + } + } + + // MH, optional, dict + d1, err = validateDictEntry(xRefTable, d, dictName, "MH", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + if d1 != nil { + err = validateMediaPlayParamsMHBEDict(xRefTable, d1, sinceVersion) + if err != nil { + return err + } + } + + // BE, optional, dict + d1, err = validateDictEntry(xRefTable, d, dictName, "BE", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + if d1 != nil { + err = validateMediaPlayParamsMHBEDict(xRefTable, d1, sinceVersion) + } + + return err +} + +func validateFloatingWindowsParameterDict(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + + // see table 284 + + dictName := "floatWinParamsDict" + + // Type, optional, name + _, err := validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, sinceVersion, func(s string) bool { return s == "FWParams" }) + if err != nil { + return err + } + + // D, required, array of integers + _, err = validateIntegerArrayEntry(xRefTable, d, dictName, "D", REQUIRED, sinceVersion, func(a types.Array) bool { return len(a) == 2 }) + if err != nil { + return err + } + + // RT, optional, integer + _, err = validateIntegerEntry(xRefTable, d, dictName, "RT", OPTIONAL, sinceVersion, func(i int) bool { return types.IntMemberOf(i, []int{0, 1, 2, 3}) }) + if err != nil { + return err + } + + // P, optional, integer + _, err = validateIntegerEntry(xRefTable, d, dictName, "P", OPTIONAL, sinceVersion, func(i int) bool { return types.IntMemberOf(i, []int{0, 1, 2, 3, 4, 5, 6, 7, 8}) }) + if err != nil { + return err + } + + // O, optional, integer + _, err = validateIntegerEntry(xRefTable, d, dictName, "O", OPTIONAL, sinceVersion, func(i int) bool { return types.IntMemberOf(i, []int{0, 1, 2}) }) + if err != nil { + return err + } + + // T, optional, boolean + _, err = validateBooleanEntry(xRefTable, d, dictName, "T", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // UC, optional, boolean + _, err = validateBooleanEntry(xRefTable, d, dictName, "UC", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // R, optional, integer + _, err = validateIntegerEntry(xRefTable, d, dictName, "R", OPTIONAL, sinceVersion, func(i int) bool { return types.IntMemberOf(i, []int{0, 1, 2}) }) + if err != nil { + return err + } + + // TT, optional, string array + _, err = validateStringArrayEntry(xRefTable, d, dictName, "TT", OPTIONAL, sinceVersion, nil) + + return err +} + +func validateScreenParametersMHBEDict(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + + dictName := "screenParmsMHBEDict" + + w := 3 + + // W, optional, integer + i, err := validateIntegerEntry(xRefTable, d, dictName, "W", OPTIONAL, sinceVersion, func(i int) bool { return types.IntMemberOf(i, []int{0, 1, 2, 3}) }) + if err != nil { + return err + } + if i != nil { + w = (*i).Value() + } + + // B, optional, array of 3 numbers + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "B", OPTIONAL, sinceVersion, func(a types.Array) bool { return len(a) == 3 }) + if err != nil { + return err + } + + // O, optional, number + _, err = validateNumberEntry(xRefTable, d, dictName, "O", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // M, optional, integer + _, err = validateIntegerEntry(xRefTable, d, dictName, "M", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // F, required if W == 0, floating windows parameter dict + d1, err := validateDictEntry(xRefTable, d, dictName, "F", w == 0, sinceVersion, nil) + if err != nil { + return err + } + if d1 != nil { + err = validateFloatingWindowsParameterDict(xRefTable, d1, sinceVersion) + } + + return err +} + +func validateScreenParametersDict(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + + // see 13.2. + + dictName := "screenParmsDict" + + // Type, optional, name + _, err := validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, sinceVersion, func(s string) bool { return s == "MediaScreenParams" }) + if err != nil { + return err + } + + // MH, optional, dict + d1, err := validateDictEntry(xRefTable, d, dictName, "MH", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + if d1 != nil { + err = validateScreenParametersMHBEDict(xRefTable, d1, sinceVersion) + if err != nil { + return err + } + } + + // BE. optional. dict + d1, err = validateDictEntry(xRefTable, d, dictName, "BE", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + if d1 != nil { + err = validateScreenParametersMHBEDict(xRefTable, d1, sinceVersion) + } + + return err +} + +func validateMediaRenditionDict(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + + // table 271 + + dictName := "mediaRendDict" + + // C, optional, dict + d1, err := validateDictEntry(xRefTable, d, dictName, "C", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + if d1 != nil { + err = validateMediaClipDict(xRefTable, d1, sinceVersion) + if err != nil { + return err + } + } + + // P, required if C not present, dict + d1, err = validateDictEntry(xRefTable, d, dictName, "P", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + if d1 != nil { + err = validateMediaPlayParamsDict(xRefTable, d1, sinceVersion) + if err != nil { + return err + } + } + + // SP, optional, dict + d1, err = validateDictEntry(xRefTable, d, dictName, "SP", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + if d1 != nil { + err = validateScreenParametersDict(xRefTable, d1, sinceVersion) + } + + return err +} + +func validateSelectorRenditionDict(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + + // table 272 + + dictName := "selectorRendDict" + + a, err := validateArrayEntry(xRefTable, d, dictName, "R", REQUIRED, sinceVersion, nil) + if err != nil { + return err + } + + for _, v := range a { + + if v == nil { + continue + } + + d, err := xRefTable.DereferenceDict(v) + if err != nil { + return err + } + + if d == nil { + continue + } + + err = validateRenditionDict(xRefTable, d, sinceVersion) + if err != nil { + return err + } + + } + + return nil +} + +func validateRenditionDictEntryMH(xRefTable *model.XRefTable, d types.Dict, dictName string, sinceVersion model.Version) error { + + d1, err := validateDictEntry(xRefTable, d, dictName, "MH", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + if d1 != nil { + + d2, err := validateDictEntry(xRefTable, d1, "MHDict", "C", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + if d2 != nil { + return validateMediaCriteriaDict(xRefTable, d2, sinceVersion) + } + + } + + return nil +} + +func validateRenditionDictEntryBE(xRefTable *model.XRefTable, d types.Dict, dictName string, sinceVersion model.Version) (err error) { + + d1, err := validateDictEntry(xRefTable, d, dictName, "BE", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + if d1 != nil { + + d2, err := validateDictEntry(xRefTable, d1, "BEDict", "C", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + return validateMediaCriteriaDict(xRefTable, d2, sinceVersion) + + } + + return nil +} + +func validateRenditionDict(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) (err error) { + + dictName := "renditionDict" + + // Type, optional, name + _, err = validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, sinceVersion, func(s string) bool { return s == "Rendition" }) + if err != nil { + return err + } + + // S, required, name + renditionType, err := validateNameEntry(xRefTable, d, dictName, "S", REQUIRED, sinceVersion, func(s string) bool { return s == "MR" || s == "SR" }) + if err != nil { + return + } + + // N, optional, text string + _, err = validateStringEntry(xRefTable, d, dictName, "N", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // MH, optional, dict + err = validateRenditionDictEntryMH(xRefTable, d, dictName, sinceVersion) + if err != nil { + return err + } + + // BE, optional, dict + err = validateRenditionDictEntryBE(xRefTable, d, dictName, sinceVersion) + if err != nil { + return err + } + + if *renditionType == "MR" { + err = validateMediaRenditionDict(xRefTable, d, sinceVersion) + if err != nil { + return err + } + } + + if *renditionType == "SR" { + err = validateSelectorRenditionDict(xRefTable, d, sinceVersion) + } + + return err +} diff --git a/pkg/pdfcpu/validate/metaData.go b/pkg/pdfcpu/validate/metaData.go new file mode 100644 index 0000000000000000000000000000000000000000..9e5dab6c1c5b6db15472bea1a37e2cb227c046e3 --- /dev/null +++ b/pkg/pdfcpu/validate/metaData.go @@ -0,0 +1,131 @@ +/* +Copyright 2023 The pdfcpu Authors. + +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 + + 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 validate + +import ( + "encoding/xml" + "strings" + "time" + + "github.com/pdfcpu/pdfcpu/pkg/filter" + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +func validateMetadataStream(xRefTable *model.XRefTable, d types.Dict, required bool, sinceVersion model.Version) (*types.StreamDict, error) { + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V13 + } + + sd, err := validateStreamDictEntry(xRefTable, d, "dict", "Metadata", required, sinceVersion, nil) + if err != nil || sd == nil { + return nil, err + } + + dictName := "metaDataDict" + + if _, err = validateNameEntry(xRefTable, sd.Dict, dictName, "Type", OPTIONAL, sinceVersion, func(s string) bool { return s == "Metadata" }); err != nil { + return nil, err + } + + if _, err = validateNameEntry(xRefTable, sd.Dict, dictName, "Subtype", OPTIONAL, sinceVersion, func(s string) bool { return s == "XML" }); err != nil { + return nil, err + } + + return sd, nil +} + +func validateMetadata(xRefTable *model.XRefTable, d types.Dict, required bool, sinceVersion model.Version) error { + // => 14.3 Metadata + // In general, any PDF stream or dictionary may have metadata attached to it + // as long as the stream or dictionary represents an actual information resource, + // as opposed to serving as an implementation artifact. + // Some PDF constructs are considered implementational, and hence may not have associated metadata. + + _, err := validateMetadataStream(xRefTable, d, required, sinceVersion) + return err +} + +func catalogMetaData(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) (*model.XMPMeta, error) { + sd, err := validateMetadataStream(xRefTable, rootDict, required, sinceVersion) + if err != nil || sd == nil { + return nil, err + } + + // if xRefTable.Version() < model.V20 { + // return nil + // } + + // Decode streamDict for supported filters only. + err = sd.Decode() + if err == filter.ErrUnsupportedFilter { + return nil, nil + } + if err != nil { + return nil, err + } + + x := model.XMPMeta{} + + if err = xml.Unmarshal(sd.Content, &x); err != nil { + if xRefTable.ValidationMode == model.ValidationStrict { + return nil, err + } + log.CLI.Println("ignoring metadata parse error") + return nil, nil + } + + return &x, nil +} + +func validateRootMetadata(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) error { + + if xRefTable.CatalogXMPMeta == nil { + return nil + } + + x := xRefTable.CatalogXMPMeta + + // fmt.Printf(" Title: %v\n", x.RDF.Description.Title.Alt.Entries) + // fmt.Printf(" Author: %v\n", x.RDF.Description.Author.Seq.Entries) + // fmt.Printf(" Subject: %v\n", x.RDF.Description.Subject.Alt.Entries) + // fmt.Printf(" Creator: %s\n", x.RDF.Description.Creator) + // fmt.Printf("CreationDate: %v\n", time.Time(x.RDF.Description.CreationDate).Format(time.RFC3339Nano)) + // fmt.Printf(" ModDate: %v\n", time.Time(x.RDF.Description.ModDate).Format(time.RFC3339Nano)) + // fmt.Printf(" Producer: %s\n", x.RDF.Description.Producer) + // fmt.Printf(" Trapped: %t\n", x.RDF.Description.Trapped) + // fmt.Printf(" Keywords: %s\n", x.RDF.Description.Keywords) + + d := x.RDF.Description + xRefTable.Title = strings.Join(d.Title.Alt.Entries, ", ") + xRefTable.Author = strings.Join(d.Author.Seq.Entries, ", ") + xRefTable.Subject = strings.Join(d.Subject.Alt.Entries, ", ") + xRefTable.Creator = d.Creator + xRefTable.CreationDate = time.Time(d.CreationDate).Format(time.RFC3339Nano) + xRefTable.ModDate = time.Time(d.ModDate).Format(time.RFC3339Nano) + xRefTable.Producer = d.Producer + //xRefTable.Trapped = d.Trapped + + ss := strings.FieldsFunc(d.Keywords, func(c rune) bool { return c == ',' || c == ';' || c == '\r' }) + for _, s := range ss { + keyword := strings.TrimSpace(s) + xRefTable.KeywordList[keyword] = true + } + + return nil +} diff --git a/pkg/pdfcpu/validate/nameTree.go b/pkg/pdfcpu/validate/nameTree.go new file mode 100644 index 0000000000000000000000000000000000000000..cd8619999f8922f7223dc95313338ff777ab7910 --- /dev/null +++ b/pkg/pdfcpu/validate/nameTree.go @@ -0,0 +1,760 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 validate + +import ( + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +func validateDestsNameTreeValue(xRefTable *model.XRefTable, o types.Object, sinceVersion model.Version) error { + + // Version check + err := xRefTable.ValidateVersion("DestsNameTreeValue", sinceVersion) + if err != nil { + return err + } + + _, err = validateDestination(xRefTable, o, false) + return err +} + +func validateAPNameTreeValue(xRefTable *model.XRefTable, o types.Object, sinceVersion model.Version) error { + + // Version check + err := xRefTable.ValidateVersion("APNameTreeValue", sinceVersion) + if err != nil { + return err + } + + return validateXObjectStreamDict(xRefTable, o) +} + +func validateJavaScriptNameTreeValue(xRefTable *model.XRefTable, o types.Object, sinceVersion model.Version) error { + + // Version check + err := xRefTable.ValidateVersion("JavaScriptNameTreeValue", sinceVersion) + if err != nil { + return err + } + + d, err := xRefTable.DereferenceDict(o) + if err != nil { + return err + } + + // Javascript Action: + return validateJavaScriptActionDict(xRefTable, d, "JavaScript") +} + +func validatePagesNameTreeValue(xRefTable *model.XRefTable, o types.Object, sinceVersion model.Version) error { + + // see 12.7.6 + + // Version check + err := xRefTable.ValidateVersion("PagesNameTreeValue", sinceVersion) + if err != nil { + return err + } + + // Value is a page dict. + + d, err := xRefTable.DereferenceDict(o) + if err != nil { + return err + } + + if d == nil { + return errors.New("pdfcpu: validatePagesNameTreeValue: value is nil") + } + + _, err = validateNameEntry(xRefTable, d, "pageDict", "Type", REQUIRED, model.V10, func(s string) bool { return s == "Page" }) + + return err +} + +func validateTemplatesNameTreeValue(xRefTable *model.XRefTable, o types.Object, sinceVersion model.Version) error { + + // see 12.7.6 + + // Version check + err := xRefTable.ValidateVersion("TemplatesNameTreeValue", sinceVersion) + if err != nil { + return err + } + + // Value is a template dict. + + d, err := xRefTable.DereferenceDict(o) + if err != nil { + return err + } + if d == nil { + return errors.New("pdfcpu: validatePagesNameTreeValue: value is nil") + } + + _, err = validateNameEntry(xRefTable, d, "templateDict", "Type", REQUIRED, model.V10, func(s string) bool { return s == "Template" }) + + return err +} + +func validateURLAliasDict(xRefTable *model.XRefTable, d types.Dict) error { + + dictName := "urlAliasDict" + + // U, required, ASCII string + _, err := validateStringEntry(xRefTable, d, dictName, "U", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + // C, optional, array of strings + _, err = validateStringArrayEntry(xRefTable, d, dictName, "C", OPTIONAL, model.V10, nil) + + return err +} + +func validateCommandSettingsDict(xRefTable *model.XRefTable, d types.Dict) error { + + // see 14.10.5.4 + + dictName := "cmdSettingsDict" + + // G, optional, dict + _, err := validateDictEntry(xRefTable, d, dictName, "G", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // C, optional, dict + _, err = validateDictEntry(xRefTable, d, dictName, "C", OPTIONAL, model.V10, nil) + + return err +} + +func validateCaptureCommandDict(xRefTable *model.XRefTable, d types.Dict) error { + + dictName := "captureCommandDict" + + // URL, required, string + _, err := validateStringEntry(xRefTable, d, dictName, "URL", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + // L, optional, integer + _, err = validateIntegerEntry(xRefTable, d, dictName, "L", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // F, optional, integer + _, err = validateIntegerEntry(xRefTable, d, dictName, "F", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // P, optional, string or stream + err = validateStringOrStreamEntry(xRefTable, d, dictName, "P", OPTIONAL, model.V10) + if err != nil { + return err + } + + // CT, optional, ASCII string + _, err = validateStringEntry(xRefTable, d, dictName, "CT", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // H, optional, string + _, err = validateStringEntry(xRefTable, d, dictName, "H", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // S, optional, command settings dict + d1, err := validateDictEntry(xRefTable, d, dictName, "S", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + if d1 != nil { + err = validateCommandSettingsDict(xRefTable, d1) + } + + return err +} + +func validateSourceInfoDictEntryAU(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) error { + + o, err := validateEntry(xRefTable, d, dictName, entryName, required, sinceVersion) + if err != nil || o == nil { + return err + } + + switch o := o.(type) { + + case types.StringLiteral, types.HexLiteral: + // no further processing + + case types.Dict: + err = validateURLAliasDict(xRefTable, o) + if err != nil { + return err + } + + default: + return errors.New("pdfcpu: validateSourceInfoDict: entry \"AU\" must be string or dict") + + } + + return nil +} + +func validateSourceInfoDict(xRefTable *model.XRefTable, d types.Dict) error { + + dictName := "sourceInfoDict" + + // AU, required, ASCII string or dict + err := validateSourceInfoDictEntryAU(xRefTable, d, dictName, "AU", REQUIRED, model.V10) + if err != nil { + return err + } + + // E, optional, date + _, err = validateDateEntry(xRefTable, d, dictName, "E", OPTIONAL, model.V10) + if err != nil { + return err + } + + // S, optional, integer + _, err = validateIntegerEntry(xRefTable, d, dictName, "S", OPTIONAL, model.V10, func(i int) bool { return 0 <= i && i <= 2 }) + if err != nil { + return err + } + + // C, optional, indRef of command dict + ir, err := validateIndRefEntry(xRefTable, d, dictName, "C", OPTIONAL, model.V10) + if err != nil { + return err + } + + if ir != nil { + + d1, err := xRefTable.DereferenceDict(*ir) + if err != nil { + return err + } + + return validateCaptureCommandDict(xRefTable, d1) + + } + + return nil +} + +func validateEntrySI(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) error { + + // see 14.10.5, table 355, source information dictionary + + o, err := validateEntry(xRefTable, d, dictName, entryName, required, sinceVersion) + if err != nil || o == nil { + return err + } + + switch o := o.(type) { + + case types.Dict: + err = validateSourceInfoDict(xRefTable, o) + if err != nil { + return err + } + + case types.Array: + + for _, v := range o { + + if v == nil { + continue + } + + d1, err := xRefTable.DereferenceDict(v) + if err != nil { + return err + } + + err = validateSourceInfoDict(xRefTable, d1) + if err != nil { + return err + } + + } + + } + + return nil +} + +func validateWebCaptureContentSetDict(XRefTable *model.XRefTable, d types.Dict) error { + + // see 14.10.4 + + dictName := "webCaptureContentSetDict" + + // Type, optional, name + _, err := validateNameEntry(XRefTable, d, dictName, "Type", OPTIONAL, model.V10, func(s string) bool { return s == "SpiderContentSet" }) + if err != nil { + return err + } + + // S, required, name + s, err := validateNameEntry(XRefTable, d, dictName, "S", REQUIRED, model.V10, func(s string) bool { return s == "SPS" || s == "SIS" }) + if err != nil { + return err + } + + // ID, required, byte string + _, err = validateStringEntry(XRefTable, d, dictName, "ID", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + // O, required, array of indirect references. + _, err = validateIndRefArrayEntry(XRefTable, d, dictName, "O", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + // SI, required, source info dict or array of source info dicts + err = validateEntrySI(XRefTable, d, dictName, "SI", REQUIRED, model.V10) + if err != nil { + return err + } + + // CT, optional, string + _, err = validateStringEntry(XRefTable, d, dictName, "CT", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // TS, optional, date + _, err = validateDateEntry(XRefTable, d, dictName, "TS", OPTIONAL, model.V10) + if err != nil { + return err + } + + // spider page set + if *s == "SPS" { + + // T, optional, string + _, err = validateStringEntry(XRefTable, d, dictName, "T", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // TID, optional, byte string + _, err = validateStringEntry(XRefTable, d, dictName, "TID", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + } + + // spider image set + if *s == "SIS" { + + // R, required, integer or array of integers + err = validateIntegerOrArrayOfIntegerEntry(XRefTable, d, dictName, "R", REQUIRED, model.V10) + + } + + return err +} + +func validateIDSNameTreeValue(xRefTable *model.XRefTable, o types.Object, sinceVersion model.Version) error { + + // see 14.10.4 + + // Version check + err := xRefTable.ValidateVersion("IDSNameTreeValue", sinceVersion) + if err != nil { + return err + } + + // Value is a web capture content set. + d, err := xRefTable.DereferenceDict(o) + if err != nil || d == nil { + return err + } + + return validateWebCaptureContentSetDict(xRefTable, d) +} + +func validateURLSNameTreeValue(xRefTable *model.XRefTable, o types.Object, sinceVersion model.Version) error { + + // see 14.10.4 + + // Version check + err := xRefTable.ValidateVersion("URLSNameTreeValue", sinceVersion) + if err != nil { + return err + } + + // Value is a web capture content set. + d, err := xRefTable.DereferenceDict(o) + if err != nil || d == nil { + return err + } + + return validateWebCaptureContentSetDict(xRefTable, d) +} + +func validateEmbeddedFilesNameTreeValue(xRefTable *model.XRefTable, o types.Object, sinceVersion model.Version) error { + + // see 7.11.4 + + // Value is a file specification for an embedded file stream. + + // Version check + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V13 + } + err := xRefTable.ValidateVersion("EmbeddedFilesNameTreeValue", sinceVersion) + if err != nil { + return err + } + + if o == nil { + return nil + } + + _, err = validateFileSpecification(xRefTable, o) + + return err +} + +func validateSlideShowDict(XRefTable *model.XRefTable, d types.Dict) error { + + // see 13.5, table 297 + + dictName := "slideShowDict" + + // Type, required, name, since V1.4 + _, err := validateNameEntry(XRefTable, d, dictName, "Type", REQUIRED, model.V14, func(s string) bool { return s == "SlideShow" }) + if err != nil { + return err + } + + // Subtype, required, name, since V1.4 + _, err = validateNameEntry(XRefTable, d, dictName, "Subtype", REQUIRED, model.V14, func(s string) bool { return s == "Embedded" }) + if err != nil { + return err + } + + // Resources, required, name tree, since V1.4 + // Note: This is really an array of (string,indRef) pairs. + _, err = validateArrayEntry(XRefTable, d, dictName, "Resources", REQUIRED, model.V14, nil) + if err != nil { + return err + } + + // StartResource, required, byte string, since V1.4 + _, err = validateStringEntry(XRefTable, d, dictName, "StartResource", REQUIRED, model.V14, nil) + + return err +} + +func validateAlternatePresentationsNameTreeValue(xRefTable *model.XRefTable, o types.Object, sinceVersion model.Version) error { + + // see 13.5 + + // Value is a slide show dict. + + // Version check + err := xRefTable.ValidateVersion("AlternatePresentationsNameTreeValue", sinceVersion) + if err != nil { + return err + } + + d, err := xRefTable.DereferenceDict(o) + if err != nil { + return err + } + + if d != nil { + err = validateSlideShowDict(xRefTable, d) + } + + return err +} + +func validateRenditionsNameTreeValue(xRefTable *model.XRefTable, o types.Object, sinceVersion model.Version) error { + + // see 13.2.3 + + // Value is a rendition object. + + // Version check + err := xRefTable.ValidateVersion("RenditionsNameTreeValue", sinceVersion) + if err != nil { + return err + } + + d, err := xRefTable.DereferenceDict(o) + if err != nil { + return err + } + + if d != nil { + err = validateRenditionDict(xRefTable, d, sinceVersion) + } + + return err +} + +func validateIDTreeValue(xRefTable *model.XRefTable, o types.Object, sinceVersion model.Version) error { + + // Version check + err := xRefTable.ValidateVersion("IDTreeValue", sinceVersion) + if err != nil { + return err + } + + d, err := xRefTable.DereferenceDict(o) + if err != nil || d == nil { + return err + } + + dictType := d.Type() + if dictType == nil || *dictType == "StructElem" { + err = validateStructElementDict(xRefTable, d) + if err != nil { + return err + } + } else { + return errors.Errorf("pdfcpu: validateIDTreeValue: invalid dictType %s (should be \"StructElem\")\n", *dictType) + } + + return nil +} + +func validateNameTreeValue(name string, xRefTable *model.XRefTable, o types.Object) (err error) { + + // The values associated with the keys may be objects of any type. + // Stream objects shall be specified by indirect object references. + // Dictionary, array, and string objects should be specified by indirect object references. + // Other PDF objects (nulls, numbers, booleans, and names) should be specified as direct objects. + + for k, v := range map[string]struct { + validate func(xRefTable *model.XRefTable, o types.Object, sinceVersion model.Version) error + sinceVersion model.Version + }{ + "Dests": {validateDestsNameTreeValue, model.V12}, + "AP": {validateAPNameTreeValue, model.V13}, + "JavaScript": {validateJavaScriptNameTreeValue, model.V13}, + "Pages": {validatePagesNameTreeValue, model.V13}, + "Templates": {validateTemplatesNameTreeValue, model.V13}, + "IDS": {validateIDSNameTreeValue, model.V13}, + "URLS": {validateURLSNameTreeValue, model.V13}, + "EmbeddedFiles": {validateEmbeddedFilesNameTreeValue, model.V14}, + "AlternatePresentations": {validateAlternatePresentationsNameTreeValue, model.V14}, + "Renditions": {validateRenditionsNameTreeValue, model.V15}, + "IDTree": {validateIDTreeValue, model.V13}, + } { + if name == k { + return v.validate(xRefTable, o, v.sinceVersion) + } + } + + return errors.Errorf("pdfcpu: validateNameTreeDictNamesEntry: unknown dict name: %s", name) +} + +func validateNameTreeDictNamesEntry(xRefTable *model.XRefTable, d types.Dict, name string, node *model.Node) (string, string, error) { + + //fmt.Printf("validateNameTreeDictNamesEntry begin %s\n", d) + + // Names: array of the form [key1 value1 key2 value2 ... key n value n] + o, found := d.Find("Names") + if !found { + return "", "", errors.Errorf("pdfcpu: validateNameTreeDictNamesEntry: missing \"Kids\" or \"Names\" entry.") + } + + a, err := xRefTable.DereferenceArray(o) + if err != nil { + return "", "", err + } + if a == nil { + return "", "", errors.Errorf("pdfcpu: validateNameTreeDictNamesEntry: missing \"Names\" array.") + } + + // arr length needs to be even because of contained key value pairs. + if len(a)%2 == 1 { + return "", "", errors.Errorf("pdfcpu: validateNameTreeDictNamesEntry: Names array entry length needs to be even, length=%d\n", len(a)) + } + + var key, firstKey, lastKey string + + for i := 0; i < len(a); i++ { + o := a[i] + + if i%2 == 0 { + + // TODO Do we really need to process indRefs here? + o, err = xRefTable.Dereference(o) + if err != nil { + return "", "", err + } + + k, err := types.StringOrHexLiteral(o) + if err != nil { + return "", "", err + } + + key = *k + + if firstKey == "" { + firstKey = key + } + + lastKey = key + + continue + } + + err = validateNameTreeValue(name, xRefTable, o) + if err != nil { + return "", "", err + } + + node.AppendToNames(key, o) + + } + + return firstKey, lastKey, nil +} + +func validateNameTreeDictLimitsEntry(xRefTable *model.XRefTable, d types.Dict, firstKey, lastKey string) error { + + a, err := validateStringArrayEntry(xRefTable, d, "nameTreeDict", "Limits", REQUIRED, model.V10, func(a types.Array) bool { return len(a) == 2 }) + if err != nil { + return err + } + + var fkv, lkv string + + o, err := xRefTable.Dereference(a[0]) + if err != nil { + return err + } + + s, err := types.StringOrHexLiteral(o) + if err != nil { + return err + } + fkv = *s + + if o, err = xRefTable.Dereference(a[1]); err != nil { + return err + } + + s, err = types.StringOrHexLiteral(o) + if err != nil { + return err + } + lkv = *s + + if firstKey < fkv || lastKey > lkv { + return errors.Errorf("pdfcpu: validateNameTreeDictLimitsEntry: leaf node corrupted (firstKey: %s vs %s) (lastKey: %s vs %s)\n", firstKey, fkv, lastKey, lkv) + } + + return nil +} + +func validateNameTree(xRefTable *model.XRefTable, name string, d types.Dict, root bool) (string, string, *model.Node, error) { + + //fmt.Printf("validateNameTree begin %s\n", d) + + // see 7.7.4 + + // A node has "Kids" or "Names" entry. + + //fmt.Printf("validateNameTree %s\n", name) + + node := &model.Node{D: d} + var kmin, kmax string + var err error + + // Kids: array of indirect references to the immediate children of this node. + // if Kids present then recurse + if o, found := d.Find("Kids"); found { + + // Intermediate node + + a, err := xRefTable.DereferenceArray(o) + if err != nil { + return "", "", nil, err + } + + if a == nil { + return "", "", nil, errors.New("pdfcpu: validateNameTree: missing \"Kids\" array") + } + + for _, o := range a { + + d, err := xRefTable.DereferenceDict(o) + if err != nil { + return "", "", nil, err + } + + var kminKid string + var kidNode *model.Node + kminKid, kmax, kidNode, err = validateNameTree(xRefTable, name, d, false) + if err != nil { + return "", "", nil, err + } + if kmin == "" { + kmin = kminKid + } + + node.Kids = append(node.Kids, kidNode) + } + + } else { + + // Leaf node + kmin, kmax, err = validateNameTreeDictNamesEntry(xRefTable, d, name, node) + if err != nil { + return "", "", nil, err + } + } + + if !root { + + // Verify calculated key range. + err = validateNameTreeDictLimitsEntry(xRefTable, d, kmin, kmax) + if err != nil { + return "", "", nil, err + } + } + + // We track limits for all nodes internally. + node.Kmin = kmin + node.Kmax = kmax + + //fmt.Println("validateNameTree end") + + return kmin, kmax, node, nil +} diff --git a/pkg/pdfcpu/validate/numberTree.go b/pkg/pdfcpu/validate/numberTree.go new file mode 100644 index 0000000000000000000000000000000000000000..9caf0da606495896bde8ef0aa95bb483249e640b --- /dev/null +++ b/pkg/pdfcpu/validate/numberTree.go @@ -0,0 +1,203 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 validate + +import ( + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +func validatePageLabelDict(xRefTable *model.XRefTable, o types.Object) error { + + // see 12.4.2 Page Labels + + d, err := xRefTable.DereferenceDict(o) + if err != nil || d == nil { + return err + } + + dictName := "pageLabelDict" + + // Type, optional, name + _, err = validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, model.V10, func(s string) bool { return s == "PageLabel" }) + if err != nil { + return err + } + + // Optional name entry S + // The numbering style that shall be used for the numeric portion of each page label. + validate := func(s string) bool { return types.MemberOf(s, []string{"D", "R", "r", "A", "a"}) } + _, err = validateNameEntry(xRefTable, d, dictName, "S", OPTIONAL, model.V10, validate) + if err != nil { + return err + } + + // Optional string entry P + // Label prefix for page labels in this range. + _, err = validateStringEntry(xRefTable, d, dictName, "P", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // Optional integer entry St + // The value of the numeric portion for the first page label in the range. + _, err = validateIntegerEntry(xRefTable, d, dictName, "St", OPTIONAL, model.V10, func(i int) bool { return i >= 1 }) + + return err +} + +func validateNumberTreeDictNumsEntry(xRefTable *model.XRefTable, d types.Dict, name string) (firstKey, lastKey int, err error) { + + // Nums: array of the form [key1 value1 key2 value2 ... key n value n] + o, found := d.Find("Nums") + if !found { + return 0, 0, errors.New("pdfcpu: validateNumberTreeDictNumsEntry: missing \"Kids\" or \"Nums\" entry") + } + + a, err := xRefTable.DereferenceArray(o) + if err != nil { + return 0, 0, err + } + if a == nil { + return 0, 0, errors.New("pdfcpu: validateNumberTreeDictNumsEntry: missing \"Nums\" array") + } + + // arr length needs to be even because of contained key value pairs. + if len(a)%2 == 1 { + return 0, 0, errors.Errorf("pdfcpu: validateNumberTreeDictNumsEntry: Nums array entry length needs to be even, length=%d\n", len(a)) + } + + // every other entry is a value + // value = indRef to an array of indRefs of structElemDicts + // or + // value = indRef of structElementDict. + + for i, o := range a { + + if i%2 == 0 { + + o, err = xRefTable.Dereference(o) + if err != nil { + return 0, 0, err + } + + i, ok := o.(types.Integer) + if !ok { + return 0, 0, errors.Errorf("pdfcpu: validateNumberTreeDictNumsEntry: corrupt key <%v>\n", o) + } + + if firstKey == 0 { + firstKey = i.Value() + } + + lastKey = i.Value() + + continue + } + + switch name { + + case "PageLabel": + err = validatePageLabelDict(xRefTable, o) + if err != nil { + return 0, 0, err + } + + case "StructTree": + err = validateStructTreeRootDictEntryK(xRefTable, o) + if err != nil { + return 0, 0, err + } + } + + } + + return firstKey, lastKey, nil +} + +func validateNumberTreeDictLimitsEntry(xRefTable *model.XRefTable, d types.Dict, firstKey, lastKey int) error { + + a, err := validateIntegerArrayEntry(xRefTable, d, "numberTreeDict", "Limits", REQUIRED, model.V10, func(a types.Array) bool { return len(a) == 2 }) + if err != nil { + return err + } + + fk, _ := a[0].(types.Integer) + lk, _ := a[1].(types.Integer) + + if firstKey < fk.Value() || lastKey > lk.Value() { + return errors.Errorf("pdfcpu: validateNumberTreeDictLimitsEntry: leaf node corrupted: firstKey(%d vs. %d) lastKey(%d vs. %d)\n", firstKey, fk.Value(), lastKey, lk.Value()) + } + + return nil +} + +func validateNumberTree(xRefTable *model.XRefTable, name string, d types.Dict, root bool) (firstKey, lastKey int, err error) { + + // A node has "Kids" or "Nums" entry. + + // Kids: array of indirect references to the immediate children of this node. + // if Kids present then recurse + if o, found := d.Find("Kids"); found { + + a, err := xRefTable.DereferenceArray(o) + if err != nil { + return 0, 0, err + } + if a == nil { + return 0, 0, errors.New("pdfcpu: validateNumberTree: missing \"Kids\" array") + } + + for _, o := range a { + + d1, err := xRefTable.DereferenceDict(o) + if err != nil { + return 0, 0, err + } + + var fk int + fk, lastKey, err = validateNumberTree(xRefTable, name, d1, false) + if err != nil { + return 0, 0, err + } + if firstKey == 0 { + firstKey = fk + } + } + + } else { + + // Leaf node + firstKey, lastKey, err = validateNumberTreeDictNumsEntry(xRefTable, d, name) + if err != nil { + return 0, 0, err + } + } + + if !root { + + // Verify calculated key range. + err = validateNumberTreeDictLimitsEntry(xRefTable, d, firstKey, lastKey) + if err != nil { + return 0, 0, err + } + + } + + return firstKey, lastKey, nil +} diff --git a/pkg/pdfcpu/validate/object.go b/pkg/pdfcpu/validate/object.go new file mode 100644 index 0000000000000000000000000000000000000000..48d68c4aa2c5a04955e93044ccf258bcf5fa91a8 --- /dev/null +++ b/pkg/pdfcpu/validate/object.go @@ -0,0 +1,1628 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 validate + +import ( + "fmt" + "strings" + "time" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +const ( + + // REQUIRED is used for required dict entries. + REQUIRED = true + + // OPTIONAL is used for optional dict entries. + OPTIONAL = false +) + +func validateEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) (types.Object, error) { + o, found := d.Find(entryName) + if !found || o == nil { + if required { + return nil, errors.Errorf("dict=%s required entry=%s missing (obj#%d).", dictName, entryName, xRefTable.CurObj) + } + return nil, nil + } + + o, err := xRefTable.Dereference(o) + if err != nil { + return nil, err + } + + if o == nil { + if required { + return nil, errors.Errorf("dict=%s required entry=%s missing (obj#%d).", dictName, entryName, xRefTable.CurObj) + } + return nil, nil + } + + // Version check + if err = xRefTable.ValidateVersion(fmt.Sprintf("dict=%s entry=%s (obj#%d)", dictName, entryName, xRefTable.CurObj), sinceVersion); err != nil { + return nil, err + } + + return o, nil +} + +func validateArrayEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version, validate func(types.Array) bool) (types.Array, error) { + if log.ValidateEnabled() { + log.Validate.Printf("validateArrayEntry begin: entry=%s\n", entryName) + } + + o, err := d.Entry(dictName, entryName, required) + if err != nil || o == nil { + return nil, err + } + + if o, err = xRefTable.Dereference(o); err != nil { + return nil, err + } + + if o == nil { + if required { + return nil, errors.Errorf("validateArrayEntry: dict=%s required entry=%s is nil", dictName, entryName) + } + if log.ValidateEnabled() { + log.Validate.Printf("validateArrayEntry end: optional entry %s is nil\n", entryName) + } + return nil, nil + } + + // Version check + if err = xRefTable.ValidateVersion("dict="+dictName+" entry="+entryName, sinceVersion); err != nil { + return nil, err + } + + a, ok := o.(types.Array) + if !ok { + return nil, errors.Errorf("validateArrayEntry: dict=%s entry=%s invalid type %T", dictName, entryName, o) + } + + // Validation + if validate != nil && !validate(a) { + return nil, errors.Errorf("validateArrayEntry: dict=%s entry=%s invalid dict entry", dictName, entryName) + } + + if log.ValidateEnabled() { + log.Validate.Printf("validateArrayEntry end: entry=%s\n", entryName) + } + + return a, nil +} + +func validateBooleanEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version, validate func(bool) bool) (*bool, error) { + if log.ValidateEnabled() { + log.Validate.Printf("validateBooleanEntry begin: entry=%s\n", entryName) + } + + o, err := d.Entry(dictName, entryName, required) + if err != nil || o == nil { + return nil, err + } + + if o, err = xRefTable.Dereference(o); err != nil { + return nil, err + } + + if o == nil { + if required { + return nil, errors.Errorf("validateBooleanEntry: dict=%s required entry=%s missing", dictName, entryName) + } + if log.ValidateEnabled() { + log.Validate.Printf("validateBooleanEntry end: entry %s is nil\n", entryName) + } + return nil, nil + } + + // Version check + if err = xRefTable.ValidateVersion("dict="+dictName+" entry="+entryName, sinceVersion); err != nil { + return nil, err + } + + b, ok := o.(types.Boolean) + if !ok { + return nil, errors.Errorf("validateBooleanEntry: dict=%s entry=%s invalid type", dictName, entryName) + } + + // Validation + if validate != nil && !validate(b.Value()) { + return nil, errors.Errorf("validateBooleanEntry: dict=%s entry=%s invalid name dict entry", dictName, entryName) + } + + if log.ValidateEnabled() { + log.Validate.Printf("validateBooleanEntry end: entry=%s\n", entryName) + } + + flag := b.Value() + return &flag, nil +} + +func validateFlexBooleanEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) (*bool, error) { + flag, err := validateBooleanEntry(xRefTable, d, dictName, entryName, required, sinceVersion, nil) + if err == nil { + return flag, nil + } + if xRefTable.ValidationMode != model.ValidationRelaxed { + return nil, err + } + n, err := validateNameEntry(xRefTable, d, dictName, entryName, required, sinceVersion, + func(s string) bool { + return types.MemberOf(strings.ToLower(s), []string{"false", "true"}) + }, + ) + if err != nil || n == nil { + return nil, err + } + + *flag = strings.ToLower(n.Value()) == "true" + + return flag, nil +} + +func validateBooleanArrayEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version, validate func(types.Array) bool) (types.Array, error) { + if log.ValidateEnabled() { + log.Validate.Printf("validateBooleanArrayEntry begin: entry=%s\n", entryName) + } + + a, err := validateArrayEntry(xRefTable, d, dictName, entryName, required, sinceVersion, validate) + if err != nil || a == nil { + return nil, err + } + + for i, o := range a { + + o, err := xRefTable.Dereference(o) + if err != nil { + return nil, err + } + if o == nil { + continue + } + + if _, ok := o.(types.Boolean); !ok { + return nil, errors.Errorf("validateBooleanArrayEntry: dict=%s entry=%s invalid type at index %d\n", dictName, entryName, i) + } + + } + + if log.ValidateEnabled() { + log.Validate.Printf("validateBooleanArrayEntry end: entry=%s\n", entryName) + } + + return a, nil +} + +func timeOfDateObject(xRefTable *model.XRefTable, o types.Object, sinceVersion model.Version) (*time.Time, error) { + s, err := xRefTable.DereferenceStringOrHexLiteral(o, sinceVersion, nil) + if err != nil { + return nil, err + } + + if s == "" { + return nil, nil + } + + t, ok := types.DateTime(s, xRefTable.ValidationMode == model.ValidationRelaxed) + if !ok { + return nil, errors.Errorf("pdfcpu: validateDateObject: <%s> invalid date", s) + } + + return &t, nil +} + +func validateDateObject(xRefTable *model.XRefTable, o types.Object, sinceVersion model.Version) (string, error) { + s, err := xRefTable.DereferenceStringOrHexLiteral(o, sinceVersion, nil) + if err != nil { + return "", err + } + + if s == "" { + return s, nil + } + + t, ok := types.DateTime(s, xRefTable.ValidationMode == model.ValidationRelaxed) + if !ok { + return "", errors.Errorf("pdfcpu: validateDateObject: <%s> invalid date", s) + } + + return types.DateString(t), nil +} + +func validateDateEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) (*time.Time, error) { + if log.ValidateEnabled() { + log.Validate.Printf("validateDateEntry begin: entry=%s\n", entryName) + } + + o, err := d.Entry(dictName, entryName, required) + if err != nil || o == nil { + return nil, err + } + + s, err := xRefTable.DereferenceStringOrHexLiteral(o, sinceVersion, nil) + if err != nil { + return nil, err + } + + if s == "" { + if required { + return nil, errors.Errorf("validateDateEntry: dict=%s required entry=%s is nil", dictName, entryName) + } + if log.ValidateEnabled() { + log.Validate.Printf("validateDateEntry end: optional entry %s is nil\n", entryName) + } + return nil, nil + } + + time, ok := types.DateTime(s, xRefTable.ValidationMode == model.ValidationRelaxed) + if !ok { + return nil, errors.Errorf("pdfcpu: validateDateEntry: <%s> invalid date", s) + } + + if log.ValidateEnabled() { + log.Validate.Printf("validateDateEntry end: entry=%s\n", entryName) + } + + return &time, nil +} + +func validateDictEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version, validate func(types.Dict) bool) (types.Dict, error) { + if log.ValidateEnabled() { + log.Validate.Printf("validateDictEntry begin: entry=%s\n", entryName) + } + + o, err := d.Entry(dictName, entryName, required) + if err != nil || o == nil { + return nil, err + } + + if o, err = xRefTable.Dereference(o); err != nil { + return nil, err + } + + if o == nil { + if required { + return nil, errors.Errorf("validateDictEntry: dict=%s required entry=%s is nil", dictName, entryName) + } + if log.ValidateEnabled() { + log.Validate.Printf("validateDictEntry end: optional entry %s is nil\n", entryName) + } + return nil, nil + } + + // Version check + if err = xRefTable.ValidateVersion("dict="+dictName+" entry="+entryName, sinceVersion); err != nil { + return nil, err + } + + d, ok := o.(types.Dict) + if !ok { + return nil, errors.Errorf("validateDictEntry: dict=%s entry=%s invalid type", dictName, entryName) + } + + // Validation + if validate != nil && !validate(d) { + return nil, errors.Errorf("validateDictEntry: dict=%s entry=%s invalid dict entry", dictName, entryName) + } + + if log.ValidateEnabled() { + log.Validate.Printf("validateDictEntry end: entry=%s\n", entryName) + } + + return d, nil +} + +func validateFloat(xRefTable *model.XRefTable, o types.Object, validate func(float64) bool) (*types.Float, error) { + if log.ValidateEnabled() { + log.Validate.Println("validateFloat begin") + } + + o, err := xRefTable.Dereference(o) + if err != nil { + return nil, err + } + + if o == nil { + return nil, errors.New("pdfcpu: validateFloat: missing object") + } + + f, ok := o.(types.Float) + if !ok { + return nil, errors.New("pdfcpu: validateFloat: invalid type") + } + + // Validation + if validate != nil && !validate(f.Value()) { + return nil, errors.Errorf("pdfcpu: validateFloat: invalid float: %s\n", f) + } + + if log.ValidateEnabled() { + log.Validate.Println("validateFloat end") + } + + return &f, nil +} + +func validateFunctionArrayEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version, validate func(types.Array) bool) (types.Array, error) { + if log.ValidateEnabled() { + log.Validate.Printf("validateFunctionArrayEntry begin: entry=%s\n", entryName) + } + + a, err := validateArrayEntry(xRefTable, d, dictName, entryName, required, sinceVersion, validate) + if err != nil || a == nil { + return nil, err + } + + for _, o := range a { + if err = validateFunction(xRefTable, o); err != nil { + return nil, err + } + } + + if log.ValidateEnabled() { + log.Validate.Printf("validateFunctionArrayEntry end: entry=%s\n", entryName) + } + + return a, nil +} + +func validateFunctionOrArrayOfFunctionsEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) error { + if log.ValidateEnabled() { + log.Validate.Printf("validateFunctionOrArrayOfFunctionsEntry begin: entry=%s\n", entryName) + } + + o, err := d.Entry(dictName, entryName, required) + if err != nil || o == nil { + return err + } + + if o, err = xRefTable.Dereference(o); err != nil { + return err + } + + if o == nil { + if required { + return errors.Errorf("pdfcpu: validateFunctionOrArrayOfFunctionsEntry: dict=%s required entry=%s is nil", dictName, entryName) + } + if log.ValidateEnabled() { + log.Validate.Printf("validateFunctionOrArrayOfFunctionsEntry end: optional entry %s is nil\n", entryName) + } + return nil + } + + switch o := o.(type) { + + case types.Array: + + for _, o := range o { + + if o == nil { + continue + } + + if err = validateFunction(xRefTable, o); err != nil { + return err + } + + } + + default: + if err = validateFunction(xRefTable, o); err != nil { + return err + } + + } + + if err = xRefTable.ValidateVersion("dict="+dictName+" entry="+entryName, sinceVersion); err != nil { + return err + } + + if log.ValidateEnabled() { + log.Validate.Printf("validateFunctionOrArrayOfFunctionsEntry end: entry=%s\n", entryName) + } + + return nil +} + +func validateIndRefEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) (*types.IndirectRef, error) { + if log.ValidateEnabled() { + log.Validate.Printf("validateIndRefEntry begin: entry=%s\n", entryName) + } + + o, err := d.Entry(dictName, entryName, required) + if err != nil || o == nil { + return nil, err + } + + ir, ok := o.(types.IndirectRef) + if !ok { + return nil, errors.Errorf("pdfcpu: validateIndRefEntry: dict=%s entry=%s invalid type", dictName, entryName) + } + + // Version check + if err = xRefTable.ValidateVersion("dict="+dictName+" entry="+entryName, sinceVersion); err != nil { + return nil, err + } + + if log.ValidateEnabled() { + log.Validate.Printf("validateIndRefEntry end: entry=%s\n", entryName) + } + + return &ir, nil +} + +func validateIndRefArrayEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version, validate func(types.Array) bool) (types.Array, error) { + if log.ValidateEnabled() { + log.Validate.Printf("validateIndRefArrayEntry begin: entry=%s\n", entryName) + } + + a, err := validateArrayEntry(xRefTable, d, dictName, entryName, required, sinceVersion, validate) + if err != nil || a == nil { + return nil, err + } + + for i, o := range a { + if _, ok := o.(types.IndirectRef); !ok { + return nil, errors.Errorf("pdfcpu: validateIndRefArrayEntry: invalid type at index %d\n", i) + } + } + + if log.ValidateEnabled() { + log.Validate.Printf("validateIndRefArrayEntry end: entry=%s \n", entryName) + } + + return a, nil +} + +func validateInteger(xRefTable *model.XRefTable, o types.Object, validate func(int) bool) (*types.Integer, error) { + if log.ValidateEnabled() { + log.Validate.Println("validateInteger begin") + } + + o, err := xRefTable.Dereference(o) + if err != nil { + return nil, err + } + + if o == nil { + return nil, errors.New("pdfcpu: validateInteger: missing object") + } + + i, ok := o.(types.Integer) + if !ok { + return nil, errors.New("pdfcpu: validateInteger: invalid type") + } + + // Validation + if validate != nil && !validate(i.Value()) { + return nil, errors.Errorf("pdfcpu: validateInteger: invalid integer: %s\n", i) + } + + if log.ValidateEnabled() { + log.Validate.Println("validateInteger end") + } + + return &i, nil +} + +func validateIntegerEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version, validate func(int) bool) (*types.Integer, error) { + if log.ValidateEnabled() { + log.Validate.Printf("validateIntegerEntry begin: entry=%s\n", entryName) + } + + o, err := d.Entry(dictName, entryName, required) + if err != nil || o == nil { + return nil, err + } + + if o, err = xRefTable.Dereference(o); err != nil { + return nil, err + } + + if o == nil { + if required { + return nil, errors.Errorf("pdfcpu: validateIntegerEntry: dict=%s required entry=%s is nil", dictName, entryName) + } + if log.ValidateEnabled() { + log.Validate.Printf("validateIntegerEntry end: optional entry %s is nil\n", entryName) + } + return nil, nil + } + + // Version check + if err = xRefTable.ValidateVersion("dict="+dictName+" entry="+entryName, sinceVersion); err != nil { + return nil, err + } + + i, ok := o.(types.Integer) + if !ok { + return nil, errors.Errorf("pdfcpu: validateIntegerEntry: dict=%s entry=%s invalid type", dictName, entryName) + } + + // Validation + if validate != nil && !validate(i.Value()) { + return nil, errors.Errorf("pdfcpu: validateIntegerEntry: dict=%s entry=%s invalid dict entry", dictName, entryName) + } + + if log.ValidateEnabled() { + log.Validate.Printf("validateIntegerEntry end: entry=%s\n", entryName) + } + + return &i, nil +} + +func validateIntegerArray(xRefTable *model.XRefTable, o types.Object) (types.Array, error) { + if log.ValidateEnabled() { + log.Validate.Println("validateIntegerArray begin") + } + + a, err := xRefTable.DereferenceArray(o) + if err != nil || a == nil { + return nil, err + } + + for i, o := range a { + + o, err := xRefTable.Dereference(o) + if err != nil { + return nil, err + } + + if o == nil { + continue + } + + switch o.(type) { + + case types.Integer: + // no further processing. + + default: + return nil, errors.Errorf("pdfcpu: validateIntegerArray: invalid type at index %d\n", i) + } + + } + + if log.ValidateEnabled() { + log.Validate.Println("validateIntegerArray end") + } + + return a, nil +} + +func validateIntegerArrayEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version, validate func(types.Array) bool) (types.Array, error) { + if log.ValidateEnabled() { + log.Validate.Printf("validateIntegerArrayEntry begin: entry=%s\n", entryName) + } + + a, err := validateArrayEntry(xRefTable, d, dictName, entryName, required, sinceVersion, validate) + if err != nil || a == nil { + return nil, err + } + + for i, o := range a { + + o, err := xRefTable.Dereference(o) + if err != nil { + return nil, err + } + + if o == nil { + continue + } + + if _, ok := o.(types.Integer); !ok { + return nil, errors.Errorf("pdfcpu: validateIntegerArrayEntry: dict=%s entry=%s invalid type at index %d\n", dictName, entryName, i) + } + + } + + if log.ValidateEnabled() { + log.Validate.Printf("validateIntegerArrayEntry end: entry=%s\n", entryName) + } + + return a, nil +} + +func validateName(xRefTable *model.XRefTable, o types.Object, validate func(string) bool) (*types.Name, error) { + if log.ValidateEnabled() { + log.Validate.Println("validateName begin") + } + + o, err := xRefTable.Dereference(o) + if err != nil { + return nil, err + } + + if o == nil { + return nil, errors.New("pdfcpu: validateName: missing object") + } + + name, ok := o.(types.Name) + if !ok { + return nil, errors.New("pdfcpu: validateName: invalid type") + } + + // Validation + if validate != nil && !validate(name.Value()) { + return nil, errors.Errorf("pdfcpu: validateName: invalid name: %s\n", name) + } + + if log.ValidateEnabled() { + log.Validate.Println("validateName end") + } + + return &name, nil +} + +func validateNameEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version, validate func(string) bool) (*types.Name, error) { + if log.ValidateEnabled() { + log.Validate.Printf("validateNameEntry begin: entry=%s\n", entryName) + } + + o, err := d.Entry(dictName, entryName, required) + if err != nil || o == nil { + return nil, err + } + + if o, err = xRefTable.Dereference(o); err != nil { + return nil, err + } + + if o == nil { + if required { + return nil, errors.Errorf("pdfcpu: validateNameEntry: dict=%s required entry=%s is nil", dictName, entryName) + } + if log.ValidateEnabled() { + log.Validate.Printf("validateNameEntry end: optional entry %s is nil\n", entryName) + } + return nil, nil + } + + // Version check + if err = xRefTable.ValidateVersion("dict="+dictName+" entry="+entryName, sinceVersion); err != nil { + return nil, err + } + + name, ok := o.(types.Name) + if !ok { + return nil, errors.Errorf("pdfcpu: validateNameEntry: dict=%s entry=%s invalid type %T", dictName, entryName, o) + } + + // Validation + v := name.Value() + if validate != nil && (required || len(v) > 0) && !validate(v) { + return nil, errors.Errorf("pdfcpu: validateNameEntry: dict=%s entry=%s invalid dict entry: %s", dictName, entryName, v) + } + + if log.ValidateEnabled() { + log.Validate.Printf("validateNameEntry end: entry=%s\n", entryName) + } + + return &name, nil +} + +func validateNameArray(xRefTable *model.XRefTable, o types.Object) (types.Array, error) { + if log.ValidateEnabled() { + log.Validate.Println("validateNameArray begin") + } + + a, err := xRefTable.DereferenceArray(o) + if err != nil || a == nil { + return nil, err + } + + for i, o := range a { + + o, err := xRefTable.Dereference(o) + if err != nil { + return nil, err + } + + if o == nil { + continue + } + + if _, ok := o.(types.Name); !ok { + return nil, errors.Errorf("pdfcpu: validateNameArray: invalid type at index %d\n", i) + } + + } + + if log.ValidateEnabled() { + log.Validate.Println("validateNameArray end") + } + + return a, nil +} + +func validateNameArrayEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version, validate func(a types.Array) bool) (types.Array, error) { + if log.ValidateEnabled() { + log.Validate.Printf("validateNameArrayEntry begin: entry=%s\n", entryName) + } + + a, err := validateArrayEntry(xRefTable, d, dictName, entryName, required, sinceVersion, validate) + if err != nil || a == nil { + return nil, err + } + + for i, o := range a { + + o, err := xRefTable.Dereference(o) + if err != nil { + return nil, err + } + + if o == nil { + continue + } + + if _, ok := o.(types.Name); !ok { + return nil, errors.Errorf("pdfcpu: validateNameArrayEntry: dict=%s entry=%s invalid type at index %d\n", dictName, entryName, i) + } + + } + + if log.ValidateEnabled() { + log.Validate.Printf("validateNameArrayEntry end: entry=%s\n", entryName) + } + + return a, nil +} + +func validateNumber(xRefTable *model.XRefTable, o types.Object) (types.Object, error) { + if log.ValidateEnabled() { + log.Validate.Println("validateNumber begin") + } + + o, err := xRefTable.Dereference(o) + if err != nil { + return nil, err + } + + if o == nil { + return nil, errors.New("pdfcpu: validateNumber: missing object") + } + + switch o.(type) { + + case types.Integer: + // no further processing. + + case types.Float: + // no further processing. + + default: + return nil, errors.New("pdfcpu: validateNumber: invalid type") + + } + + if log.ValidateEnabled() { + log.Validate.Println("validateNumber end ") + } + + return o, nil +} + +func validateNumberEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version, validate func(f float64) bool) (types.Object, error) { + if log.ValidateEnabled() { + log.Validate.Printf("validateNumberEntry begin: entry=%s\n", entryName) + } + + o, err := d.Entry(dictName, entryName, required) + if err != nil || o == nil { + return nil, err + } + + // Version check + if err = xRefTable.ValidateVersion("dict="+dictName+" entry="+entryName, sinceVersion); err != nil { + return nil, err + } + + if o, err = validateNumber(xRefTable, o); err != nil { + return nil, err + } + + var f float64 + + // Validation + switch o := o.(type) { + + case types.Integer: + f = float64(o.Value()) + + case types.Float: + f = o.Value() + } + + if validate != nil && !validate(f) { + return nil, errors.Errorf("pdfcpu: validateFloatEntry: dict=%s entry=%s invalid dict entry", dictName, entryName) + } + + if log.ValidateEnabled() { + log.Validate.Printf("validateNumberEntry end: entry=%s\n", entryName) + } + + return o, nil +} + +func validateNumberArray(xRefTable *model.XRefTable, o types.Object) (types.Array, error) { + if log.ValidateEnabled() { + log.Validate.Println("validateNumberArray begin") + } + + a, err := xRefTable.DereferenceArray(o) + if err != nil || a == nil { + return nil, err + } + + for i, o := range a { + + o, err := xRefTable.Dereference(o) + if err != nil { + return nil, err + } + + if o == nil { + continue + } + + switch o.(type) { + + case types.Integer: + // no further processing. + + case types.Float: + // no further processing. + + default: + return nil, errors.Errorf("pdfcpu: validateNumberArray: invalid type at index %d\n", i) + } + + } + + if log.ValidateEnabled() { + log.Validate.Println("validateNumberArray end") + } + + return a, err +} + +func validateNumberArrayEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version, validate func(types.Array) bool) (types.Array, error) { + if log.ValidateEnabled() { + log.Validate.Printf("validateNumberArrayEntry begin: entry=%s\n", entryName) + } + + a, err := validateArrayEntry(xRefTable, d, dictName, entryName, required, sinceVersion, validate) + if err != nil || a == nil { + return nil, err + } + + for i, o := range a { + + o, err := xRefTable.Dereference(o) + if err != nil { + return nil, err + } + + if o == nil { + continue + } + + switch o.(type) { + + case types.Integer: + // no further processing. + + case types.Float: + // no further processing. + + default: + return nil, errors.Errorf("pdfcpu: validateNumberArrayEntry: invalid type at index %d\n", i) + } + + } + + if log.ValidateEnabled() { + log.Validate.Printf("validateNumberArrayEntry end: entry=%s\n", entryName) + } + + return a, nil +} + +func validateRectangleEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version, validate func(types.Array) bool) (types.Array, error) { + if log.ValidateEnabled() { + log.Validate.Printf("validateRectangleEntry begin: entry=%s\n", entryName) + } + + a, err := validateNumberArrayEntry(xRefTable, d, dictName, entryName, required, sinceVersion, func(a types.Array) bool { return len(a) == 4 }) + if err != nil || a == nil { + return nil, err + } + + if validate != nil && !validate(a) { + return nil, errors.Errorf("pdfcpu: validateRectangleEntry: dict=%s entry=%s invalid rectangle entry", dictName, entryName) + } + + if log.ValidateEnabled() { + log.Validate.Printf("validateRectangleEntry end: entry=%s\n", entryName) + } + + return a, nil +} + +func validateStreamDict(xRefTable *model.XRefTable, o types.Object) (*types.StreamDict, error) { + if log.ValidateEnabled() { + log.Validate.Println("validateStreamDict begin") + } + + o, err := xRefTable.Dereference(o) + if err != nil { + return nil, err + } + + if o == nil { + return nil, errors.New("pdfcpu: validateStreamDict: missing object") + } + + sd, ok := o.(types.StreamDict) + if !ok { + return nil, errors.New("pdfcpu: validateStreamDict: invalid type") + } + + if log.ValidateEnabled() { + log.Validate.Println("validateStreamDict endobj") + } + + return &sd, nil +} + +func validateStreamDictEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version, validate func(types.StreamDict) bool) (*types.StreamDict, error) { + if log.ValidateEnabled() { + log.Validate.Printf("validateStreamDictEntry begin: entry=%s\n", entryName) + } + + o, err := d.Entry(dictName, entryName, required) + if err != nil || o == nil { + return nil, err + } + + sd, valid, err := xRefTable.DereferenceStreamDict(o) + if valid { + return nil, nil + } + + if err != nil { + return nil, err + } + + if sd == nil { + if required { + return nil, errors.Errorf("pdfcpu: validateStreamDictEntry: dict=%s required entry=%s is nil", dictName, entryName) + } + if log.ValidateEnabled() { + log.Validate.Printf("validateStreamDictEntry end: optional entry %s is nil\n", entryName) + } + return nil, nil + } + + // Version check + if err = xRefTable.ValidateVersion(fmt.Sprintf("dict=%s entry=%s", dictName, entryName), sinceVersion); err != nil { + return nil, err + } + + // Validation + if validate != nil && !validate(*sd) { + return nil, errors.Errorf("pdfcpu: validateStreamDictEntry: dict=%s entry=%s invalid dict entry", dictName, entryName) + } + + if log.ValidateEnabled() { + log.Validate.Printf("validateStreamDictEntry end: entry=%s\n", entryName) + } + + return sd, nil +} + +func decodeString(o types.Object, dictName, entryName string) (s string, err error) { + switch o := o.(type) { + case types.StringLiteral: + s, err = types.StringLiteralToString(o) + case types.HexLiteral: + s, err = types.HexLiteralToString(o) + default: + err = errors.Errorf("pdfcpu: decodeString: dict=%s entry=%s invalid type %T", dictName, entryName, o) + } + return s, err +} + +func validateStringEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version, validate func(string) bool) (*string, error) { + if log.ValidateEnabled() { + log.Validate.Printf("validateStringEntry begin: entry=%s\n", entryName) + } + + o, err := d.Entry(dictName, entryName, required) + if err != nil || o == nil { + return nil, err + } + + if o, err = xRefTable.Dereference(o); err != nil { + return nil, err + } + + if o == nil { + if required { + return nil, errors.Errorf("pdfcpu: validateStringEntry: dict=%s required entry=%s is nil", dictName, entryName) + } + if log.ValidateEnabled() { + log.Validate.Printf("validateStringEntry end: optional entry %s is nil\n", entryName) + } + return nil, nil + } + + // Version check + if err = xRefTable.ValidateVersion(fmt.Sprintf("dict=%s entry=%s", dictName, entryName), sinceVersion); err != nil { + return nil, err + } + + s, err := decodeString(o, dictName, entryName) + if err != nil { + return nil, err + } + + // Validation + if validate != nil && (required || len(s) > 0) && !validate(s) { + return nil, errors.Errorf("pdfcpu: validateStringEntry: dict=%s entry=%s invalid dict entry", dictName, entryName) + } + + if log.ValidateEnabled() { + log.Validate.Printf("validateStringEntry end: entry=%s\n", entryName) + } + + return &s, nil +} + +func validateStringArrayEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version, validate func(types.Array) bool) (types.Array, error) { + if log.ValidateEnabled() { + log.Validate.Printf("validateStringArrayEntry begin: entry=%s\n", entryName) + } + + a, err := validateArrayEntry(xRefTable, d, dictName, entryName, required, sinceVersion, validate) + if err != nil || a == nil { + return nil, err + } + + for i, o := range a { + + o, err := xRefTable.Dereference(o) + if err != nil { + return nil, err + } + + if o == nil { + continue + } + + switch o.(type) { + + case types.StringLiteral: + // no further processing. + + case types.HexLiteral: + // no further processing + + default: + return nil, errors.Errorf("pdfcpu: validateStringArrayEntry: invalid type at index %d\n", i) + } + + } + + if log.ValidateEnabled() { + log.Validate.Printf("validateStringArrayEntry end: entry=%s\n", entryName) + } + + return a, nil +} + +func validateArrayArrayEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version, validate func(types.Array) bool) (types.Array, error) { + if log.ValidateEnabled() { + log.Validate.Printf("validateArrayArrayEntry begin: entry=%s\n", entryName) + } + + a, err := validateArrayEntry(xRefTable, d, dictName, entryName, required, sinceVersion, validate) + if err != nil || a == nil { + return nil, err + } + + for i, o := range a { + + o, err := xRefTable.Dereference(o) + if err != nil { + return nil, err + } + + if o == nil { + continue + } + + switch o.(type) { + + case types.Array: + // no further processing. + + default: + return nil, errors.Errorf("pdfcpu: validateArrayArrayEntry: invalid type at index %d\n", i) + } + + } + + if log.ValidateEnabled() { + log.Validate.Printf("validateArrayArrayEntry end: entry=%s\n", entryName) + } + + return a, nil +} + +func validateStringOrStreamEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) error { + if log.ValidateEnabled() { + log.Validate.Printf("validateStringOrStreamEntry begin: entry=%s\n", entryName) + } + + o, err := d.Entry(dictName, entryName, required) + if err != nil || o == nil { + return err + } + + if o, err = xRefTable.Dereference(o); err != nil { + return err + } + + if o == nil { + if required { + return errors.Errorf("pdfcpu: validateStringOrStreamEntry: dict=%s required entry=%s is nil", dictName, entryName) + } + if log.ValidateEnabled() { + log.Validate.Printf("validateStringOrStreamEntry end: optional entry %s is nil\n", entryName) + } + return nil + } + + // Version check + if err = xRefTable.ValidateVersion("dict="+dictName+" entry="+entryName, sinceVersion); err != nil { + return err + } + + switch o.(type) { + + case types.StringLiteral, types.HexLiteral, types.StreamDict: + // no further processing + + default: + return errors.Errorf("pdfcpu: validateStringOrStreamEntry: dict=%s entry=%s invalid type", dictName, entryName) + } + + if log.ValidateEnabled() { + log.Validate.Printf("validateStringOrStreamEntry end: entry=%s\n", entryName) + } + + return nil +} + +func validateNameOrStringEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) error { + if log.ValidateEnabled() { + log.Validate.Printf("validateNameOrStringEntry begin: entry=%s\n", entryName) + } + + o, err := d.Entry(dictName, entryName, required) + if err != nil || o == nil { + return err + } + + if o, err = xRefTable.Dereference(o); err != nil { + return err + } + + if o == nil { + if required { + return errors.Errorf("pdfcpu: validateNameOrStringEntry: dict=%s required entry=%s is nil", dictName, entryName) + } + if log.ValidateEnabled() { + log.Validate.Printf("validateNameOrStringEntry end: optional entry %s is nil\n", entryName) + } + return nil + } + + // Version check + if err = xRefTable.ValidateVersion("dict="+dictName+" entry="+entryName, sinceVersion); err != nil { + return err + } + + switch o.(type) { + + case types.StringLiteral, types.Name: + // no further processing + + default: + return errors.Errorf("pdfcpu: validateNameOrStringEntry: dict=%s entry=%s invalid type", dictName, entryName) + } + + if log.ValidateEnabled() { + log.Validate.Printf("validateNameOrStringEntry end: entry=%s\n", entryName) + } + + return nil +} + +func validateIntOrStringEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) error { + if log.ValidateEnabled() { + log.Validate.Printf("validateIntOrStringEntry begin: entry=%s\n", entryName) + } + + o, err := d.Entry(dictName, entryName, required) + if err != nil || o == nil { + return err + } + + if o, err = xRefTable.Dereference(o); err != nil { + return err + } + + if o == nil { + if required { + return errors.Errorf("pdfcpu: validateIntOrStringEntry: dict=%s required entry=%s is nil", dictName, entryName) + } + if log.ValidateEnabled() { + log.Validate.Printf("validateIntOrStringEntry end: optional entry %s is nil\n", entryName) + } + return nil + } + + // Version check + if err = xRefTable.ValidateVersion("dict="+dictName+" entry="+entryName, sinceVersion); err != nil { + return err + } + + switch o.(type) { + + case types.StringLiteral, types.HexLiteral, types.Integer: + // no further processing + + default: + return errors.Errorf("pdfcpu: validateIntOrStringEntry: dict=%s entry=%s invalid type", dictName, entryName) + } + + if log.ValidateEnabled() { + log.Validate.Printf("validateIntOrStringEntry end: entry=%s\n", entryName) + } + + return nil +} + +func validateBooleanOrStreamEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) error { + if log.ValidateEnabled() { + log.Validate.Printf("validateBooleanOrStreamEntry begin: entry=%s\n", entryName) + } + + o, err := d.Entry(dictName, entryName, required) + if err != nil || o == nil { + return err + } + + if o, err = xRefTable.Dereference(o); err != nil { + return err + } + + if o == nil { + if required { + return errors.Errorf("pdfcpu: validateBooleanOrStreamEntry: dict=%s required entry=%s is nil", dictName, entryName) + } + if log.ValidateEnabled() { + log.Validate.Printf("validateBooleanOrStreamEntry end: optional entry %s is nil\n", entryName) + } + return nil + } + + // Version check + if err = xRefTable.ValidateVersion("dict="+dictName+" entry="+entryName, sinceVersion); err != nil { + return err + } + + switch o.(type) { + + case types.Boolean, types.StreamDict: + // no further processing + + default: + return errors.Errorf("pdfcpu: validateBooleanOrStreamEntry: dict=%s entry=%s invalid type", dictName, entryName) + } + + if log.ValidateEnabled() { + log.Validate.Printf("validateBooleanOrStreamEntry end: entry=%s\n", entryName) + } + + return nil +} + +func validateStreamDictOrDictEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) error { + if log.ValidateEnabled() { + log.Validate.Printf("validateStreamDictOrDictEntry begin: entry=%s\n", entryName) + } + + o, err := d.Entry(dictName, entryName, required) + if err != nil || o == nil { + return err + } + + if o, err = xRefTable.Dereference(o); err != nil { + return err + } + + if o == nil { + if required { + return errors.Errorf("pdfcpu: validateStreamDictOrDictEntry: dict=%s required entry=%s is nil", dictName, entryName) + } + if log.ValidateEnabled() { + log.Validate.Printf("validateStreamDictOrDictEntry end: optional entry %s is nil\n", entryName) + } + return nil + } + + // Version check + if err = xRefTable.ValidateVersion("dict="+dictName+" entry="+entryName, sinceVersion); err != nil { + return err + } + + switch o.(type) { + + case types.StreamDict: + // TODO validate 3D stream dict + + case types.Dict: + // TODO validate 3D reference dict + + default: + return errors.Errorf("pdfcpu: validateStreamDictOrDictEntry: dict=%s entry=%s invalid type", dictName, entryName) + } + + if log.ValidateEnabled() { + log.Validate.Printf("validateStreamDictOrDictEntry end: entry=%s\n", entryName) + } + + return nil +} + +func validateIntegerOrArrayOfInteger(xRefTable *model.XRefTable, o types.Object, dictName, entryName string) error { + switch o := o.(type) { + + case types.Integer: + // no further processing + + case types.Array: + + for i, o := range o { + + o, err := xRefTable.Dereference(o) + if err != nil { + return err + } + + if o == nil { + continue + } + + if _, ok := o.(types.Integer); !ok { + return errors.Errorf("pdfcpu: validateIntegerOrArrayOfInteger: dict=%s entry=%s invalid type at index %d\n", dictName, entryName, i) + } + + } + + default: + return errors.Errorf("pdfcpu: validateIntegerOrArrayOfInteger: dict=%s entry=%s invalid type", dictName, entryName) + } + + return nil +} + +func validateIntegerOrArrayOfIntegerEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) error { + if log.ValidateEnabled() { + log.Validate.Printf("validateIntegerOrArrayOfIntegerEntry begin: entry=%s\n", entryName) + } + + o, err := d.Entry(dictName, entryName, required) + if err != nil || o == nil { + return err + } + + if o, err = xRefTable.Dereference(o); err != nil { + return err + } + + if o == nil { + if required { + return errors.Errorf("pdfcpu: validateIntegerOrArrayOfIntegerEntry: dict=%s required entry=%s is nil", dictName, entryName) + } + if log.ValidateEnabled() { + log.Validate.Printf("validateIntegerOrArrayOfIntegerEntry end: optional entry %s is nil\n", entryName) + } + return nil + } + + // Version check + if err = xRefTable.ValidateVersion("dict="+dictName+" entry="+entryName, sinceVersion); err != nil { + return err + } + + if err := validateIntegerOrArrayOfInteger(xRefTable, o, dictName, entryName); err != nil { + return err + } + + if log.ValidateEnabled() { + log.Validate.Printf("validateIntegerOrArrayOfIntegerEntry end: entry=%s\n", entryName) + } + + return nil +} + +func validateNameOrArrayOfName(xRefTable *model.XRefTable, o types.Object, dictName, entryName string) error { + switch o := o.(type) { + + case types.Name: + // no further processing + + case types.Array: + + for i, o := range o { + + o, err := xRefTable.Dereference(o) + if err != nil { + return err + } + + if o == nil { + continue + } + + if _, ok := o.(types.Name); !ok { + err = errors.Errorf("pdfcpu: validateNameOrArrayOfName: dict=%s entry=%s invalid type at index %d\n", dictName, entryName, i) + return err + } + + } + + default: + return errors.Errorf("pdfcpu: validateNameOrArrayOfName: dict=%s entry=%s invalid type", dictName, entryName) + } + + return nil +} + +func validateNameOrArrayOfNameEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) error { + if log.ValidateEnabled() { + log.Validate.Printf("validateNameOrArrayOfNameEntry begin: entry=%s\n", entryName) + } + + o, err := d.Entry(dictName, entryName, required) + if err != nil || o == nil { + return err + } + + if o, err = xRefTable.Dereference(o); err != nil { + return err + } + + if o == nil { + if required { + return errors.Errorf("pdfcpu: validateNameOrArrayOfNameEntry: dict=%s required entry=%s is nil", dictName, entryName) + } + if log.ValidateEnabled() { + log.Validate.Printf("validateNameOrArrayOfNameEntry end: optional entry %s is nil\n", entryName) + } + return nil + } + + // Version check + if err = xRefTable.ValidateVersion("dict="+dictName+" entry="+entryName, sinceVersion); err != nil { + return err + } + + if err := validateNameOrArrayOfName(xRefTable, o, dictName, entryName); err != nil { + return err + } + + if log.ValidateEnabled() { + log.Validate.Printf("validateNameOrArrayOfNameEntry end: entry=%s\n", entryName) + } + + return nil +} + +func validateBooleanOrArrayOfBoolean(xRefTable *model.XRefTable, o types.Object, dictName, entryName string) error { + switch o := o.(type) { + + case types.Boolean: + // no further processing + + case types.Array: + + for i, o := range o { + + o, err := xRefTable.Dereference(o) + if err != nil { + return err + } + + if o == nil { + continue + } + + if _, ok := o.(types.Boolean); !ok { + return errors.Errorf("pdfcpu: validateBooleanOrArrayOfBoolean: dict=%s entry=%s invalid type at index %d\n", dictName, entryName, i) + } + + } + + default: + return errors.Errorf("pdfcpu: validateBooleanOrArrayOfBoolean: dict=%s entry=%s invalid type", dictName, entryName) + } + + return nil +} + +func validateBooleanOrArrayOfBooleanEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) error { + if log.ValidateEnabled() { + log.Validate.Printf("validateBooleanOrArrayOfBooleanEntry begin: entry=%s\n", entryName) + } + + o, err := d.Entry(dictName, entryName, required) + if err != nil || o == nil { + return err + } + + if o, err = xRefTable.Dereference(o); err != nil { + return err + } + + if o == nil { + if required { + return errors.Errorf("pdfcpu: validateBooleanOrArrayOfBooleanEntry: dict=%s required entry=%s is nil", dictName, entryName) + } + if log.ValidateEnabled() { + log.Validate.Printf("validateBooleanOrArrayOfBooleanEntry end: optional entry %s is nil\n", entryName) + } + return nil + } + + // Version check + if err = xRefTable.ValidateVersion("dict="+dictName+" entry="+entryName, sinceVersion); err != nil { + return err + } + + if err := validateBooleanOrArrayOfBoolean(xRefTable, o, dictName, entryName); err != nil { + return err + } + + if log.ValidateEnabled() { + log.Validate.Printf("validateBooleanOrArrayOfBooleanEntry end: entry=%s\n", entryName) + } + + return nil +} diff --git a/pkg/pdfcpu/validate/optionalContent.go b/pkg/pdfcpu/validate/optionalContent.go new file mode 100644 index 0000000000000000000000000000000000000000..51658fb1a0335d402e91b34901b0d705fc06f1ef --- /dev/null +++ b/pkg/pdfcpu/validate/optionalContent.go @@ -0,0 +1,455 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 validate + +import ( + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +func validateOptionalContentGroupIntent(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) error { + + // see 8.11.2.1 + + o, err := validateEntry(xRefTable, d, dictName, entryName, required, sinceVersion) + if err != nil || o == nil { + return err + } + + validate := func(s string) bool { + return s == "View" || s == "Design" || s == "All" + } + + switch o := o.(type) { + + case types.Name: + if !validate(o.Value()) { + return errors.Errorf("validateOptionalContentGroupIntent: invalid intent: %s", o.Value()) + } + + case types.Array: + + for i, v := range o { + + if v == nil { + continue + } + + n, ok := v.(types.Name) + if !ok { + return errors.Errorf("pdfcpu: validateOptionalContentGroupIntent: invalid type at index %d\n", i) + } + + if !validate(n.Value()) { + return errors.Errorf("pdfcpu: validateOptionalContentGroupIntent: invalid intent: %s", n.Value()) + } + } + + default: + return errors.New("pdfcpu: validateOptionalContentGroupIntent: invalid type") + } + + return nil +} + +func validateOptionalContentGroupUsageDict(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) error { + + // see 8.11.4.4 + + d1, err := validateDictEntry(xRefTable, d, dictName, entryName, required, sinceVersion, nil) + if err != nil || d1 == nil { + return err + } + + dictName = "OCUsageDict" + + // CreatorInfo, optional, dict + _, err = validateDictEntry(xRefTable, d1, dictName, "CreatorInfo", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // Language, optional, dict + _, err = validateDictEntry(xRefTable, d1, dictName, "Language", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // Export, optional, dict + _, err = validateDictEntry(xRefTable, d1, dictName, "Export", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // Zoom, optional, dict + _, err = validateDictEntry(xRefTable, d1, dictName, "Zoom", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // Print, optional, dict + _, err = validateDictEntry(xRefTable, d1, dictName, "Print", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // View, optional, dict + _, err = validateDictEntry(xRefTable, d1, dictName, "View", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // User, optional, dict + _, err = validateDictEntry(xRefTable, d1, dictName, "User", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // PageElement, optional, dict + _, err = validateDictEntry(xRefTable, d1, dictName, "PageElement", OPTIONAL, sinceVersion, nil) + + return err +} + +func validateOptionalContentGroupDict(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + + // see 8.11 Optional Content + + dictName := "optionalContentGroupDict" + + // Type, required, name, OCG + _, err := validateNameEntry(xRefTable, d, dictName, "Type", REQUIRED, sinceVersion, func(s string) bool { return s == "OCG" }) + if err != nil { + return err + } + + // Name, required, text string + _, err = validateStringEntry(xRefTable, d, dictName, "Name", REQUIRED, sinceVersion, nil) + if err != nil { + return err + } + + // Intent, optional, name or array + err = validateOptionalContentGroupIntent(xRefTable, d, dictName, "Intent", OPTIONAL, sinceVersion) + if err != nil { + return err + } + + // Usage, optional, usage dict + return validateOptionalContentGroupUsageDict(xRefTable, d, dictName, "Usage", OPTIONAL, sinceVersion) +} + +func validateOptionalContentGroupArray(xRefTable *model.XRefTable, d types.Dict, dictName, dictEntry string, sinceVersion model.Version) error { + + a, err := validateArrayEntry(xRefTable, d, dictName, dictEntry, OPTIONAL, sinceVersion, nil) + if err != nil || a == nil { + return err + } + + for _, v := range a { + + if v == nil { + continue + } + + d, err := xRefTable.DereferenceDict(v) + if err != nil { + return err + } + + if d == nil { + continue + } + + err = validateOptionalContentGroupDict(xRefTable, d, sinceVersion) + if err != nil { + return err + } + + } + + return nil +} + +func validateOCGs(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, sinceVersion model.Version) error { + + // see 8.11.2.2 + + o, err := d.Entry(dictName, entryName, OPTIONAL) + if err != nil || o == nil { + return err + } + + // Version check + err = xRefTable.ValidateVersion("OCGs", sinceVersion) + if err != nil { + return err + } + + o, err = xRefTable.Dereference(o) + if err != nil || o == nil { + return err + } + + d1, ok := o.(types.Dict) + if ok { + return validateOptionalContentGroupDict(xRefTable, d1, sinceVersion) + } + + return validateOptionalContentGroupArray(xRefTable, d, dictName, entryName, sinceVersion) +} + +func validateOptionalContentMembershipDict(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + + // see 8.11.2.2 + + dictName := "OCMDict" + + // OCGs, optional, dict or array + err := validateOCGs(xRefTable, d, dictName, "OCGs", sinceVersion) + if err != nil { + return err + } + + // P, optional, name + validate := func(s string) bool { return types.MemberOf(s, []string{"AllOn", "AnyOn", "AnyOff", "AllOff"}) } + _, err = validateNameEntry(xRefTable, d, dictName, "P", OPTIONAL, sinceVersion, validate) + if err != nil { + return err + } + + // VE, optional, array, since V1.6 + _, err = validateArrayEntry(xRefTable, d, dictName, "VE", OPTIONAL, model.V16, nil) + + return err +} + +func validateOptionalContent(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) error { + + d1, err := validateDictEntry(xRefTable, d, dictName, entryName, required, sinceVersion, nil) + if err != nil || d1 == nil { + return err + } + + validate := func(s string) bool { return s == "OCG" || s == "OCMD" } + t, err := validateNameEntry(xRefTable, d1, "optionalContent", "Type", REQUIRED, sinceVersion, validate) + if err != nil { + return err + } + + if *t == "OCG" { + return validateOptionalContentGroupDict(xRefTable, d1, sinceVersion) + } + + return validateOptionalContentMembershipDict(xRefTable, d1, sinceVersion) +} + +func validateUsageApplicationDict(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + + dictName := "usageAppDict" + + // Event, required, name + _, err := validateNameEntry(xRefTable, d, dictName, "Event", REQUIRED, sinceVersion, func(s string) bool { return s == "View" || s == "Print" || s == "Export" }) + if err != nil { + return err + } + + // OCGs, optional, array of content groups + err = validateOptionalContentGroupArray(xRefTable, d, dictName, "OCGs", sinceVersion) + if err != nil { + return err + } + + // Category, required, array of names + _, err = validateNameArrayEntry(xRefTable, d, dictName, "Category", REQUIRED, sinceVersion, nil) + + return err +} + +func validateUsageApplicationDictArray(xRefTable *model.XRefTable, d types.Dict, dictName, dictEntry string, required bool, sinceVersion model.Version) error { + + a, err := validateArrayEntry(xRefTable, d, dictName, dictEntry, required, sinceVersion, nil) + if err != nil || a == nil { + return err + } + + for _, v := range a { + + if v == nil { + continue + } + + d, err := xRefTable.DereferenceDict(v) + if err != nil { + return err + } + + if d == nil { + continue + } + + err = validateUsageApplicationDict(xRefTable, d, sinceVersion) + if err != nil { + return err + } + + } + + return nil +} + +func validateOptionalContentConfigurationDict(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + + dictName := "optContentConfigDict" + + // Name, optional, string + _, err := validateStringEntry(xRefTable, d, dictName, "Name", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // Creator, optional, string + _, err = validateStringEntry(xRefTable, d, dictName, "Creator", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // BaseState, optional, name + validate := func(s string) bool { return types.MemberOf(s, []string{"ON", "OFF", "UNCHANGED"}) } + baseState, err := validateNameEntry(xRefTable, d, dictName, "BaseState", OPTIONAL, sinceVersion, validate) + if err != nil { + return err + } + + if baseState != nil { + + if baseState.Value() != "ON" { + // ON, optional, content group array + err = validateOptionalContentGroupArray(xRefTable, d, dictName, "ON", sinceVersion) + if err != nil { + return err + } + } + + if baseState.Value() != "OFF" { + // OFF, optional, content group array + err = validateOptionalContentGroupArray(xRefTable, d, dictName, "OFF", sinceVersion) + if err != nil { + return err + } + } + + } + + // Intent, optional, name or array + err = validateOptionalContentGroupIntent(xRefTable, d, dictName, "Intent", OPTIONAL, sinceVersion) + if err != nil { + return err + } + + // AS, optional, usage application dicts array + err = validateUsageApplicationDictArray(xRefTable, d, dictName, "AS", OPTIONAL, sinceVersion) + if err != nil { + return err + } + + // Order, optional, array + _, err = validateArrayEntry(xRefTable, d, dictName, "Order", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // ListMode, optional, name + validate = func(s string) bool { return types.MemberOf(s, []string{"AllPages", "VisiblePages"}) } + _, err = validateNameEntry(xRefTable, d, dictName, "ListMode", OPTIONAL, sinceVersion, validate) + if err != nil { + return err + } + + // RBGroups, optional, array + _, err = validateArrayEntry(xRefTable, d, dictName, "RBGroups", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // Locked, optional, array + return validateOptionalContentGroupArray(xRefTable, d, dictName, "Locked", model.V16) +} + +func validateOCProperties(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) error { + + // aka optional content properties dict. + + // => 8.11.4 Configuring Optional Content + + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V14 + } + + d, err := validateDictEntry(xRefTable, rootDict, "rootDict", "OCProperties", required, sinceVersion, nil) + if err != nil || len(d) == 0 { + return err + } + + dictName := "optContentPropertiesDict" + + // "OCGs" required array of already written indRefs + r := true + if xRefTable.ValidationMode == model.ValidationRelaxed { + r = false + } + _, err = validateIndRefArrayEntry(xRefTable, d, dictName, "OCGs", r, sinceVersion, nil) + if err != nil { + return err + } + + // "D" required dict, default viewing optional content configuration dict. + d1, err := validateDictEntry(xRefTable, d, dictName, "D", REQUIRED, sinceVersion, nil) + if err != nil { + return err + } + err = validateOptionalContentConfigurationDict(xRefTable, d1, sinceVersion) + if err != nil { + return err + } + + // "Configs" optional array of alternate optional content configuration dicts. + a, err := validateArrayEntry(xRefTable, d, dictName, "Configs", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + for _, o := range a { + + d, err := xRefTable.DereferenceDict(o) + if err != nil { + return err + } + + if d == nil { + continue + } + + err = validateOptionalContentConfigurationDict(xRefTable, d, sinceVersion) + if err != nil { + return err + } + + } + + return nil +} diff --git a/pkg/pdfcpu/validate/outlineTree.go b/pkg/pdfcpu/validate/outlineTree.go new file mode 100644 index 0000000000000000000000000000000000000000..9940c921fe803ed94758f392d60f21995562b391 --- /dev/null +++ b/pkg/pdfcpu/validate/outlineTree.go @@ -0,0 +1,309 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 validate + +import ( + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +func validateOutlineItemDict(xRefTable *model.XRefTable, d types.Dict) error { + dictName := "outlineItemDict" + + // Title, required, text string + _, err := validateStringEntry(xRefTable, d, dictName, "Title", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + // fmt.Printf("Title: %s\n", *title) + + // Parent, required, dict indRef + ir, err := validateIndRefEntry(xRefTable, d, dictName, "Parent", REQUIRED, model.V10) + if err != nil { + return err + } + _, err = xRefTable.DereferenceDict(*ir) + if err != nil { + return err + } + + // // Count, optional, int + // _, err = validateIntegerEntry(xRefTable, d, dictName, "Count", OPTIONAL, model.V10, nil) + // if err != nil { + // return err + // } + + // SE, optional, dict indRef, since V1.3 + ir, err = validateIndRefEntry(xRefTable, d, dictName, "SE", OPTIONAL, model.V13) + if err != nil { + return err + } + if ir != nil { + _, err = xRefTable.DereferenceDict(*ir) + if err != nil { + return err + } + } + + // C, optional, array of 3 numbers, since V1.4 + version := model.V14 + if xRefTable.ValidationMode == model.ValidationRelaxed { + version = model.V13 + } + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "C", OPTIONAL, version, func(a types.Array) bool { return len(a) == 3 }) + if err != nil { + return err + } + + // F, optional integer, since V1.4 + _, err = validateIntegerEntry(xRefTable, d, dictName, "F", OPTIONAL, model.V14, nil) + if err != nil { + return err + } + + // Optional A or Dest, since V1.1 + return validateActionOrDestination(xRefTable, d, dictName, model.V11) +} + +func handleOutlineItemDict(xRefTable *model.XRefTable, ir types.IndirectRef, objNumber int) (types.Dict, error) { + d, err := xRefTable.DereferenceDict(ir) + if err != nil { + return nil, err + } + if d == nil { + return nil, errors.Errorf("validateOutlineTree: object #%d is nil.", objNumber) + } + + if err = validateOutlineItemDict(xRefTable, d); err != nil { + return nil, err + } + + return d, nil +} + +func leaf(firstChild, lastChild *types.IndirectRef, objNumber, validationMode int) (bool, error) { + if firstChild == nil { + if lastChild == nil { + // Leaf + return true, nil + } + if validationMode == model.ValidationStrict { + return false, errors.Errorf("pdfcpu: validateOutlineTree: missing \"First\" at obj#%d", objNumber) + } + } + if lastChild == nil && validationMode == model.ValidationStrict { + return false, errors.Errorf("pdfcpu: validateOutlineTree: missing \"Last\" at obj#%d", objNumber) + } + if firstChild != nil && firstChild.ObjectNumber.Value() == objNumber && + lastChild != nil && lastChild.ObjectNumber.Value() == objNumber { + // Degenerated leaf = node pointing to itself. + if validationMode == model.ValidationStrict { + return false, errors.Errorf("pdfcpu: validateOutlineTree: corrupted at obj#%d", objNumber) + } + return true, nil + } + return false, nil +} + +func evalOutlineCount(xRefTable *model.XRefTable, c, visc int, count int, total, visible *int) error { + if visc == 0 { + if count == 0 { + if xRefTable.ValidationMode == model.ValidationStrict { + return errors.New("pdfcpu: validateOutlineTree: non-empty outline item dict needs \"Count\" <> 0") + } + count = c + } + if count != c && count != -c { + if xRefTable.ValidationMode == model.ValidationStrict { + return errors.Errorf("pdfcpu: validateOutlineTree: non-empty outline item dict got \"Count\" %d, want %d or %d", count, c, -c) + } + count = c + } + if count == c { + *total += c + } + } + + if visc > 0 { + if count != c+visc { + return errors.Errorf("pdfcpu: validateOutlineTree: non-empty outline item dict got \"Count\" %d, want %d", count, c+visc) + } + *total += c + *visible += visc + } + + return nil +} + +func validateOutlineTree(xRefTable *model.XRefTable, first, last *types.IndirectRef) (int, int, error) { + var ( + d types.Dict + objNr int + total int + visible int + err error + ) + + m := map[int]bool{} + + // Process linked list of outline items. + for ir := first; ir != nil; ir = d.IndirectRefEntry("Next") { + + objNr = ir.ObjectNumber.Value() + if m[objNr] { + return 0, 0, errors.New("pdfcpu: validateOutlineTree: circular outline items") + } + m[objNr] = true + + total++ + + d, err = handleOutlineItemDict(xRefTable, *ir, objNr) + if err != nil { + return 0, 0, err + } + + var count int + if c := d.IntEntry("Count"); c != nil { + count = *c + } + + firstChild := d.IndirectRefEntry("First") + lastChild := d.IndirectRefEntry("Last") + + ok, err := leaf(firstChild, lastChild, objNr, xRefTable.ValidationMode) + if err != nil { + return 0, 0, err + } + if ok { + if count != 0 { + return 0, 0, errors.New("pdfcpu: validateOutlineTree: empty outline item dict \"Count\" must be 0") + } + continue + } + + c, visc, err := validateOutlineTree(xRefTable, firstChild, lastChild) + if err != nil { + return 0, 0, err + } + + if err := evalOutlineCount(xRefTable, c, visc, count, &total, &visible); err != nil { + return 0, 0, err + } + + } + + if xRefTable.ValidationMode == model.ValidationStrict && objNr != last.ObjectNumber.Value() { + return 0, 0, errors.Errorf("pdfcpu: validateOutlineTree: corrupted child list %d <> %d\n", objNr, last.ObjectNumber) + } + + return total, visible, nil +} + +func validateVisibleOutlineCount(xRefTable *model.XRefTable, total, visible int, count *int) error { + if count == nil { + return errors.Errorf("pdfcpu: validateOutlines: corrupted, root \"Count\" is nil, expected to be %d", total+visible) + } + if xRefTable.ValidationMode == model.ValidationStrict && *count != total+visible { + return errors.Errorf("pdfcpu: validateOutlines: corrupted, root \"Count\" = %d, expected to be %d", *count, total+visible) + } + if xRefTable.ValidationMode == model.ValidationRelaxed && *count != total+visible && *count != -total-visible { + return errors.Errorf("pdfcpu: validateOutlines: corrupted, root \"Count\" = %d, expected to be %d", *count, total+visible) + } + + return nil +} + +func validateInvisibleOutlineCount(xRefTable *model.XRefTable, total, visible int, count *int) error { + if count != nil { + if xRefTable.ValidationMode == model.ValidationStrict && *count == 0 { + return errors.New("pdfcpu: validateOutlines: corrupted, root \"Count\" shall be omitted if there are no open outline items") + } + if xRefTable.ValidationMode == model.ValidationStrict && *count != total && *count != -total { + return errors.Errorf("pdfcpu: validateOutlines: corrupted, root \"Count\" = %d, expected to be %d", *count, total) + } + } + + return nil +} + +func validateOutlineCount(xRefTable *model.XRefTable, total, visible int, count *int) error { + if visible == 0 { + return validateInvisibleOutlineCount(xRefTable, total, visible, count) + } + + if visible > 0 { + return validateVisibleOutlineCount(xRefTable, total, visible, count) + } + + return nil +} + +func validateOutlines(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) error { + // => 12.3.3 Document Outline + + ir, err := validateIndRefEntry(xRefTable, rootDict, "rootDict", "Outlines", required, sinceVersion) + if err != nil || ir == nil { + return err + } + + d, err := xRefTable.DereferenceDict(*ir) + if err != nil || d == nil { + return err + } + + xRefTable.Outlines = d + + // Type, optional, name + _, err = validateNameEntry(xRefTable, d, "outlineDict", "Type", OPTIONAL, model.V10, func(s string) bool { return s == "Outlines" || s == "Outline" }) + if err != nil { + return err + } + + first := d.IndirectRefEntry("First") + last := d.IndirectRefEntry("Last") + + if first == nil { + if last != nil { + return errors.New("pdfcpu: validateOutlines: corrupted, root missing \"First\"") + } + // empty outlines + xRefTable.Outlines = nil + rootDict.Delete("Outlines") + return nil + } + if last == nil { + return errors.New("pdfcpu: validateOutlines: corrupted, root missing \"Last\"") + } + + count := d.IntEntry("Count") + if xRefTable.ValidationMode == model.ValidationStrict && count != nil && *count < 0 { + return errors.New("pdfcpu: validateOutlines: corrupted, root \"Count\" can't be negativ") + } + + total, visible, err := validateOutlineTree(xRefTable, first, last) + if err != nil { + return err + } + + if err := validateOutlineCount(xRefTable, total, visible, count); err != nil { + return err + } + + return nil +} diff --git a/pkg/pdfcpu/validate/page.go b/pkg/pdfcpu/validate/page.go new file mode 100644 index 0000000000000000000000000000000000000000..969c1e12d5f14d9beaeeffe25a70b787a4720a11 --- /dev/null +++ b/pkg/pdfcpu/validate/page.go @@ -0,0 +1,1203 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 validate + +import ( + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +func validateResourceDict(xRefTable *model.XRefTable, o types.Object) (hasResources bool, err error) { + + d, err := xRefTable.DereferenceDict(o) + if err != nil || d == nil { + return false, err + } + + for k, v := range map[string]struct { + validate func(xRefTable *model.XRefTable, o types.Object, sinceVersion model.Version) error + sinceVersion model.Version + }{ + "ExtGState": {validateExtGStateResourceDict, model.V10}, + "Font": {validateFontResourceDict, model.V10}, + "XObject": {validateXObjectResourceDict, model.V10}, + "Properties": {validatePropertiesResourceDict, model.V10}, + "ColorSpace": {validateColorSpaceResourceDict, model.V10}, + "Pattern": {validatePatternResourceDict, model.V10}, + "Shading": {validateShadingResourceDict, model.V13}, + } { + if o, ok := d.Find(k); ok { + err = v.validate(xRefTable, o, v.sinceVersion) + if err != nil { + return false, err + } + } + } + + allowedResDictKeys := []string{"ExtGState", "Font", "XObject", "Properties", "ColorSpace", "Pattern", "ProcSet", "Shading"} + if xRefTable.ValidationMode == model.ValidationRelaxed { + allowedResDictKeys = append(allowedResDictKeys, "Encoding") + allowedResDictKeys = append(allowedResDictKeys, "ProcSets") + } + + // Note: Beginning with PDF V1.4 the "ProcSet" feature is considered to be obsolete! + + for k := range d { + if !types.MemberOf(k, allowedResDictKeys) { + d.Delete(k) + } + } + + return true, nil +} + +func validatePageContents(xRefTable *model.XRefTable, d types.Dict) (hasContents bool, err error) { + + o, found := d.Find("Contents") + if !found { + return false, err + } + + o, err = xRefTable.Dereference(o) + if err != nil || o == nil { + return false, err + } + + switch o := o.(type) { + + case types.StreamDict: + // no further processing. + hasContents = true + + case types.Array: + // process array of content stream dicts. + + for _, o := range o { + o1, _, err := xRefTable.DereferenceStreamDict(o) + if err != nil { + return false, err + } + + if o1 == nil { + continue + } + + hasContents = true + + } + + if hasContents { + break + } + + if xRefTable.ValidationMode == model.ValidationStrict { + return false, errors.Errorf("validatePageContents: empty page content array detected") + } + + // Digest empty array. + d["Contents"] = nil + model.ShowRepaired("corrupt page dict \"Contents\"") + + case types.StringLiteral: + + s := strings.TrimSpace(o.Value()) + + if len(s) > 0 || xRefTable.ValidationMode == model.ValidationStrict { + return false, errors.Errorf("validatePageContents: page content must be stream dict or array, got: %T", o) + } + + // Digest empty string literal. + d["Contents"] = nil + model.ShowRepaired("corrupt page dict \"Contents\"") + + default: + return false, errors.Errorf("validatePageContents: page content must be stream dict or array, got: %T", o) + } + + return hasContents, nil +} + +func validatePageResources(xRefTable *model.XRefTable, d types.Dict, hasResources, hasContents bool) error { + + if o, found := d.Find("Resources"); found { + _, err := validateResourceDict(xRefTable, o) + return err + } + + // TODO Check if contents need resources (#169) + // if !hasResources && hasContents { + // return errors.New("pdfcpu: validatePageResources: missing required entry \"Resources\" - should be inherited") + // } + + return nil +} + +func validatePageEntryMediaBox(xRefTable *model.XRefTable, d types.Dict, required bool, sinceVersion model.Version) (hasMediaBox bool, err error) { + + o, err := validateRectangleEntry(xRefTable, d, "pageDict", "MediaBox", required, sinceVersion, nil) + if err != nil { + return false, err + } + if o != nil { + hasMediaBox = true + } + + return hasMediaBox, nil +} + +func validatePageEntryCropBox(xRefTable *model.XRefTable, d types.Dict, required bool, sinceVersion model.Version) error { + + _, err := validateRectangleEntry(xRefTable, d, "pagesDict", "CropBox", required, sinceVersion, nil) + + return err +} + +func validatePageEntryBleedBox(xRefTable *model.XRefTable, d types.Dict, required bool, sinceVersion model.Version) error { + + _, err := validateRectangleEntry(xRefTable, d, "pagesDict", "BleedBox", required, sinceVersion, nil) + + return err +} + +func validatePageEntryTrimBox(xRefTable *model.XRefTable, d types.Dict, required bool, sinceVersion model.Version) error { + + _, err := validateRectangleEntry(xRefTable, d, "pagesDict", "TrimBox", required, sinceVersion, nil) + + return err +} + +func validatePageEntryArtBox(xRefTable *model.XRefTable, d types.Dict, required bool, sinceVersion model.Version) error { + + _, err := validateRectangleEntry(xRefTable, d, "pagesDict", "ArtBox", required, sinceVersion, nil) + + return err +} + +func validateBoxStyleDictEntry(xRefTable *model.XRefTable, d types.Dict, dictName string, entryName string, required bool, sinceVersion model.Version) error { + + d1, err := validateDictEntry(xRefTable, d, dictName, entryName, required, sinceVersion, nil) + if err != nil || d1 == nil { + return err + } + + dictName = "boxStyleDict" + + // C, number array with 3 elements, optional + _, err = validateNumberArrayEntry(xRefTable, d1, dictName, "C", OPTIONAL, sinceVersion, func(a types.Array) bool { return len(a) == 3 }) + if err != nil { + return err + } + + // W, number, optional + _, err = validateNumberEntry(xRefTable, d1, dictName, "W", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // S, name, optional + validate := func(s string) bool { return types.MemberOf(s, []string{"S", "D"}) } + _, err = validateNameEntry(xRefTable, d1, dictName, "S", OPTIONAL, sinceVersion, validate) + if err != nil { + return err + } + + // D, array, optional, since V1.3, dashArray + _, err = validateIntegerArrayEntry(xRefTable, d1, dictName, "D", OPTIONAL, sinceVersion, nil) + + return err +} + +func validatePageBoxColorInfo(xRefTable *model.XRefTable, pageDict types.Dict, required bool, sinceVersion model.Version) error { + + // box color information dict + // see 14.11.2.2 + + dictName := "pageDict" + + d, err := validateDictEntry(xRefTable, pageDict, dictName, "BoxColorInfo", required, sinceVersion, nil) + if err != nil || d == nil { + return err + } + + dictName = "boxColorInfoDict" + + err = validateBoxStyleDictEntry(xRefTable, d, dictName, "CropBox", OPTIONAL, sinceVersion) + if err != nil { + return err + } + + err = validateBoxStyleDictEntry(xRefTable, d, dictName, "BleedBox", OPTIONAL, sinceVersion) + if err != nil { + return err + } + + err = validateBoxStyleDictEntry(xRefTable, d, dictName, "TrimBox", OPTIONAL, sinceVersion) + if err != nil { + return err + } + + return validateBoxStyleDictEntry(xRefTable, d, dictName, "ArtBox", OPTIONAL, sinceVersion) +} + +func validatePageEntryRotate(xRefTable *model.XRefTable, d types.Dict, required bool, sinceVersion model.Version) error { + + validate := func(i int) bool { return i%90 == 0 } + _, err := validateIntegerEntry(xRefTable, d, "pagesDict", "Rotate", required, sinceVersion, validate) + + return err +} + +func validatePageEntryGroup(xRefTable *model.XRefTable, d types.Dict, required bool, sinceVersion model.Version) error { + + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V13 + } + + d1, err := validateDictEntry(xRefTable, d, "pageDict", "Group", required, sinceVersion, nil) + if err != nil { + return err + } + + if d1 != nil { + err = validateGroupAttributesDict(xRefTable, d1) + } + + return err +} + +func validatePageEntryThumb(xRefTable *model.XRefTable, d types.Dict, required bool, sinceVersion model.Version) error { + + sd, err := validateStreamDictEntry(xRefTable, d, "pagesDict", "Thumb", required, sinceVersion, nil) + if err != nil || sd == nil { + return err + } + + if err := validateXObjectStreamDict(xRefTable, *sd); err != nil { + return err + } + + indRef := d.IndirectRefEntry("Thumb") + xRefTable.PageThumbs[xRefTable.CurPage] = *indRef + //fmt.Printf("adding thumb page:%d obj#:%d\n", xRefTable.CurPage, indRef.ObjectNumber.Value()) + + return nil +} + +func validatePageEntryB(xRefTable *model.XRefTable, d types.Dict, required bool, sinceVersion model.Version) error { + + // Note: Only makes sense if "Threads" entry in document root and bead dicts present. + + _, err := validateIndRefArrayEntry(xRefTable, d, "pagesDict", "B", required, sinceVersion, nil) + + return err +} + +func validatePageEntryDur(xRefTable *model.XRefTable, d types.Dict, required bool, sinceVersion model.Version) error { + + _, err := validateNumberEntry(xRefTable, d, "pagesDict", "Dur", required, sinceVersion, nil) + + return err +} + +func validateTransitionDictEntryDi(xRefTable *model.XRefTable, d types.Dict) error { + + o, found := d.Find("Di") + if !found { + return nil + } + + switch o := o.(type) { + + case types.Integer: + validate := func(i int) bool { return types.IntMemberOf(i, []int{0, 90, 180, 270, 315}) } + if !validate(o.Value()) { + return errors.New("pdfcpu: validateTransitionDict: entry Di int value undefined") + } + + case types.Name: + if o.Value() != "None" { + return errors.New("pdfcpu: validateTransitionDict: entry Di name value undefined") + } + } + + return nil +} + +func validateTransitionDictEntryM(xRefTable *model.XRefTable, d types.Dict, dictName string, transStyle *types.Name) error { + + // see 12.4.4 + validateTransitionDirectionOfMotion := func(s string) bool { return types.MemberOf(s, []string{"I", "O"}) } + + validateM := func(s string) bool { + return validateTransitionDirectionOfMotion(s) && + (transStyle != nil && (*transStyle == "Split" || *transStyle == "Box" || *transStyle == "Fly")) + } + + _, err := validateNameEntry(xRefTable, d, dictName, "M", OPTIONAL, model.V10, validateM) + + return err +} + +func validateTransitionDict(xRefTable *model.XRefTable, d types.Dict) error { + + dictName := "transitionDict" + + // S, name, optional + + validateTransitionStyle := func(s string) bool { + return types.MemberOf(s, []string{"Split", "Blinds", "Box", "Wipe", "Dissolve", "Glitter", "R"}) + } + + validate := validateTransitionStyle + + if xRefTable.Version() >= model.V15 { + validate = func(s string) bool { + + if validateTransitionStyle(s) { + return true + } + + return types.MemberOf(s, []string{"Fly", "Push", "Cover", "Uncover", "Fade"}) + } + } + transStyle, err := validateNameEntry(xRefTable, d, dictName, "S", OPTIONAL, model.V10, validate) + if err != nil { + return err + } + + // D, optional, number > 0 + _, err = validateNumberEntry(xRefTable, d, dictName, "D", OPTIONAL, model.V10, func(f float64) bool { return f > 0 }) + if err != nil { + return err + } + + // Dm, optional, name, see 12.4.4 + validateTransitionDimension := func(s string) bool { return types.MemberOf(s, []string{"H", "V"}) } + + validateDm := func(s string) bool { + return validateTransitionDimension(s) && (transStyle != nil && (*transStyle == "Split" || *transStyle == "Blinds")) + } + _, err = validateNameEntry(xRefTable, d, dictName, "Dm", OPTIONAL, model.V10, validateDm) + if err != nil { + return err + } + + // M, optional, name + err = validateTransitionDictEntryM(xRefTable, d, dictName, transStyle) + if err != nil { + return err + } + + // Di, optional, number or name + err = validateTransitionDictEntryDi(xRefTable, d) + if err != nil { + return err + } + + // SS, optional, number, since V1.5 + if transStyle != nil && *transStyle == "Fly" { + _, err = validateNumberEntry(xRefTable, d, dictName, "SS", OPTIONAL, model.V15, nil) + if err != nil { + return err + } + } + + // B, optional, boolean, since V1.5 + validateB := func(b bool) bool { return transStyle != nil && *transStyle == "Fly" } + _, err = validateBooleanEntry(xRefTable, d, dictName, "B", OPTIONAL, model.V15, validateB) + + return err +} + +func validatePageEntryTrans(xRefTable *model.XRefTable, pageDict types.Dict, required bool, sinceVersion model.Version) error { + + d, err := validateDictEntry(xRefTable, pageDict, "pagesDict", "Trans", required, sinceVersion, nil) + if err != nil || d == nil { + return err + } + + return validateTransitionDict(xRefTable, d) +} + +func validatePageEntryStructParents(xRefTable *model.XRefTable, d types.Dict, required bool, sinceVersion model.Version) error { + + _, err := validateIntegerEntry(xRefTable, d, "pagesDict", "StructParents", required, sinceVersion, nil) + + return err +} + +func validatePageEntryID(xRefTable *model.XRefTable, d types.Dict, required bool, sinceVersion model.Version) error { + + _, err := validateStringEntry(xRefTable, d, "pagesDict", "ID", required, sinceVersion, nil) + + return err +} + +func validatePageEntryPZ(xRefTable *model.XRefTable, d types.Dict, required bool, sinceVersion model.Version) error { + + // Preferred zoom factor, number + + _, err := validateNumberEntry(xRefTable, d, "pagesDict", "PZ", required, sinceVersion, nil) + + return err +} + +func validatePageEntrySeparationInfo(xRefTable *model.XRefTable, pagesDict types.Dict, required bool, sinceVersion model.Version) error { + + // see 14.11.4 + + d, err := validateDictEntry(xRefTable, pagesDict, "pagesDict", "SeparationInfo", required, sinceVersion, nil) + if err != nil || d == nil { + return err + } + + dictName := "separationDict" + + _, err = validateIndRefArrayEntry(xRefTable, d, dictName, "Pages", REQUIRED, sinceVersion, nil) + if err != nil { + return err + } + + err = validateNameOrStringEntry(xRefTable, d, dictName, "DeviceColorant", required, sinceVersion) + if err != nil { + return err + } + + a, err := validateArrayEntry(xRefTable, d, dictName, "ColorSpace", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + if a != nil { + err = validateColorSpaceArraySubset(xRefTable, a, []string{"Separation", "DeviceN"}) + } + + return err +} + +func validatePageEntryTabs(xRefTable *model.XRefTable, d types.Dict, required bool, sinceVersion model.Version) error { + + validateTabs := func(s string) bool { return types.MemberOf(s, []string{"R", "C", "S", "A", "W"}) } + + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V13 + } + _, err := validateNameEntry(xRefTable, d, "pagesDict", "Tabs", required, sinceVersion, validateTabs) + + if err != nil && xRefTable.ValidationMode == model.ValidationRelaxed { + _, err = validateStringEntry(xRefTable, d, "pagesDict", "Tabs", required, sinceVersion, validateTabs) + } + + return err +} + +func validatePageEntryTemplateInstantiated(xRefTable *model.XRefTable, d types.Dict, required bool, sinceVersion model.Version) error { + + // see 12.7.6 + + _, err := validateNameEntry(xRefTable, d, "pagesDict", "TemplateInstantiated", required, sinceVersion, nil) + + return err +} + +// TODO implement +func validatePageEntryPresSteps(xRefTable *model.XRefTable, d types.Dict, required bool, sinceVersion model.Version) error { + + // see 12.4.4.2 + + d1, err := validateDictEntry(xRefTable, d, "pagesDict", "PresSteps", required, sinceVersion, nil) + if err != nil || d1 == nil { + return err + } + + return errors.New("pdfcpu: validatePageEntryPresSteps: not supported") +} + +func validatePageEntryUserUnit(xRefTable *model.XRefTable, d types.Dict, required bool, sinceVersion model.Version) error { + + // UserUnit, optional, positive number, since V1.6 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V13 + } + _, err := validateNumberEntry(xRefTable, d, "pagesDict", "UserUnit", required, sinceVersion, func(f float64) bool { return f > 0 }) + + return err +} + +func validateNumberFormatDict(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + + dictName := "numberFormatDict" + + // Type, name, optional + _, err := validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, sinceVersion, func(s string) bool { return s == "NumberFormat" }) + if err != nil { + return err + } + + // U, text string, required + _, err = validateStringEntry(xRefTable, d, dictName, "U", REQUIRED, sinceVersion, nil) + if err != nil { + return err + } + + // C, number, required + _, err = validateNumberEntry(xRefTable, d, dictName, "C", REQUIRED, sinceVersion, nil) + if err != nil { + return err + } + + // F, name, optional + _, err = validateNameEntry(xRefTable, d, dictName, "F", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // D, integer, optional + _, err = validateIntegerEntry(xRefTable, d, dictName, "D", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // FD, bool, optional + _, err = validateBooleanEntry(xRefTable, d, dictName, "FD", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // RT, text string, optional + _, err = validateStringEntry(xRefTable, d, dictName, "RT", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // RD, text string, optional + _, err = validateStringEntry(xRefTable, d, dictName, "RD", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // PS, text string, optional + _, err = validateStringEntry(xRefTable, d, dictName, "PS", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // SS, text string, optional + _, err = validateStringEntry(xRefTable, d, dictName, "SS", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // O, name, optional + _, err = validateNameEntry(xRefTable, d, dictName, "O", OPTIONAL, sinceVersion, nil) + + return err +} + +func validateNumberFormatArrayEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) error { + + a, err := validateArrayEntry(xRefTable, d, dictName, entryName, required, sinceVersion, nil) + if err != nil || a == nil { + return err + } + + for _, v := range a { + + d, err := xRefTable.DereferenceDict(v) + if err != nil { + return err + } + + if d == nil { + continue + } + + err = validateNumberFormatDict(xRefTable, d, sinceVersion) + if err != nil { + return err + } + + } + + return nil +} + +func validateMeasureDict(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + + dictName := "measureDict" + + _, err := validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, sinceVersion, func(s string) bool { return s == "Measure" }) + if err != nil { + return err + } + + // PDF 1.6 defines only a single type of coordinate system, a rectilinear coordinate system, + // that shall be specified by the value RL for the Subtype entry. + coordSys, err := validateNameEntry(xRefTable, d, dictName, "Subtype", OPTIONAL, sinceVersion, nil) + if err != nil || coordSys == nil { + return err + } + + if *coordSys != "RL" { + if xRefTable.Version() > sinceVersion { + // unknown coord system + return nil + } + return errors.Errorf("validateMeasureDict dict=%s entry=%s invalid dict entry: %s", dictName, "Subtype", coordSys.Value()) + } + + // R, text string, required, scale ratio + _, err = validateStringEntry(xRefTable, d, dictName, "R", REQUIRED, sinceVersion, nil) + if err != nil { + return err + } + + // X, number format array, required, for measurement of change along the x axis and, if Y is not present, along the y axis as well. + err = validateNumberFormatArrayEntry(xRefTable, d, dictName, "X", REQUIRED, sinceVersion) + if err != nil { + return err + } + + // Y, number format array, required when the x and y scales have different units or conversion factors. + err = validateNumberFormatArrayEntry(xRefTable, d, dictName, "Y", OPTIONAL, sinceVersion) + if err != nil { + return err + } + + // D, number format array, required, for measurement of distance in any direction. + err = validateNumberFormatArrayEntry(xRefTable, d, dictName, "D", REQUIRED, sinceVersion) + if err != nil { + return err + } + + // A, number format array, required, for measurement of area. + err = validateNumberFormatArrayEntry(xRefTable, d, dictName, "A", REQUIRED, sinceVersion) + if err != nil { + return err + } + + // T, number format array, optional, for measurement of angles. + err = validateNumberFormatArrayEntry(xRefTable, d, dictName, "T", OPTIONAL, sinceVersion) + if err != nil { + return err + } + + // S, number format array, optional, for fmeasurement of the slope of a line. + err = validateNumberFormatArrayEntry(xRefTable, d, dictName, "S", OPTIONAL, sinceVersion) + if err != nil { + return err + } + + // O, number array, optional, array of two numbers that shall specify the origin of the measurement coordinate system in default user space coordinates. + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "O", OPTIONAL, sinceVersion, func(a types.Array) bool { return len(a) == 2 }) + if err != nil { + return err + } + + // CYX, number, optional, a factor that shall be used to convert the largest units along the y axis to the largest units along the x axis. + _, err = validateNumberEntry(xRefTable, d, dictName, "CYX", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + return nil +} + +func validateViewportDict(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + + dictName := "viewportDict" + + _, err := validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, sinceVersion, func(s string) bool { return s == "Viewport" }) + if err != nil { + return err + } + + _, err = validateRectangleEntry(xRefTable, d, dictName, "BBox", REQUIRED, sinceVersion, nil) + if err != nil { + return err + } + + _, err = validateStringEntry(xRefTable, d, dictName, "Name", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // Measure, optional, dict + d1, err := validateDictEntry(xRefTable, d, dictName, "Measure", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + if d1 != nil { + err = validateMeasureDict(xRefTable, d1, sinceVersion) + } + + return err +} + +func validatePageEntryVP(xRefTable *model.XRefTable, d types.Dict, required bool, sinceVersion model.Version) error { + + // see table 260 + + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V15 + } + a, err := validateArrayEntry(xRefTable, d, "pagesDict", "VP", required, sinceVersion, nil) + if err != nil || a == nil { + return err + } + + for _, v := range a { + + if v == nil { + continue + } + + d, err := xRefTable.DereferenceDict(v) + if err != nil { + return err + } + + if d == nil { + continue + } + + err = validateViewportDict(xRefTable, d, sinceVersion) + if err != nil { + return err + } + + } + + return nil +} + +func validatePageDict(xRefTable *model.XRefTable, d types.Dict, objNumber int, hasResources, hasMediaBox bool) error { + + dictName := "pageDict" + + if ir := d.IndirectRefEntry("Parent"); ir == nil { + return errors.New("pdfcpu: validatePageDict: missing parent") + } + + // Contents + hasContents, err := validatePageContents(xRefTable, d) + if err != nil { + return err + } + + // Resources + err = validatePageResources(xRefTable, d, hasResources, hasContents) + if err != nil { + return err + } + + // MediaBox + _, err = validatePageEntryMediaBox(xRefTable, d, !hasMediaBox, model.V10) + if err != nil { + return err + } + + // PieceInfo + if xRefTable.ValidationMode != model.ValidationRelaxed { + sinceVersion := model.V13 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V10 + } + + hasPieceInfo, err := validatePieceInfo(xRefTable, d, dictName, "PieceInfo", OPTIONAL, sinceVersion) + if err != nil { + return err + } + + // LastModified + lm, err := validateDateEntry(xRefTable, d, dictName, "LastModified", OPTIONAL, model.V13) + if err != nil { + return err + } + + if hasPieceInfo && lm == nil && xRefTable.ValidationMode == model.ValidationStrict { + return errors.New("pdfcpu: validatePageDict: missing \"LastModified\" (required by \"PieceInfo\")") + } + } + + // AA + err = validateAdditionalActions(xRefTable, d, dictName, "AA", OPTIONAL, model.V14, "page") + if err != nil { + return err + } + + type v struct { + validate func(xRefTable *model.XRefTable, d types.Dict, required bool, sinceVersion model.Version) (err error) + required bool + sinceVersion model.Version + } + + for _, f := range []v{ + {validatePageEntryCropBox, OPTIONAL, model.V10}, + {validatePageEntryBleedBox, OPTIONAL, model.V13}, + {validatePageEntryTrimBox, OPTIONAL, model.V13}, + {validatePageEntryArtBox, OPTIONAL, model.V13}, + {validatePageBoxColorInfo, OPTIONAL, model.V14}, + {validatePageEntryRotate, OPTIONAL, model.V10}, + {validatePageEntryGroup, OPTIONAL, model.V14}, + {validatePageEntryThumb, OPTIONAL, model.V10}, + {validatePageEntryB, OPTIONAL, model.V11}, + {validatePageEntryDur, OPTIONAL, model.V11}, + {validatePageEntryTrans, OPTIONAL, model.V11}, + {validateMetadata, OPTIONAL, model.V14}, + {validatePageEntryStructParents, OPTIONAL, model.V10}, + {validatePageEntryID, OPTIONAL, model.V13}, + {validatePageEntryPZ, OPTIONAL, model.V13}, + {validatePageEntrySeparationInfo, OPTIONAL, model.V13}, + {validatePageEntryTabs, OPTIONAL, model.V15}, + {validatePageEntryTemplateInstantiated, OPTIONAL, model.V15}, + {validatePageEntryPresSteps, OPTIONAL, model.V15}, + {validatePageEntryUserUnit, OPTIONAL, model.V16}, + {validatePageEntryVP, OPTIONAL, model.V16}, + } { + err = f.validate(xRefTable, d, f.required, f.sinceVersion) + if err != nil { + return err + } + } + + return nil +} + +func validatePagesDictGeneralEntries(xRefTable *model.XRefTable, d types.Dict) (pageCount int, hasResources, hasMediaBox bool, err error) { + + // PageCount of this sub page tree + i := d.IntEntry("Count") + if i == nil { + return 0, false, false, errors.New("pdfcpu: validatePagesDictGeneralEntries: missing \"Count\" in page tree") + } + pageCount = *i + + hasResources, err = validateResources(xRefTable, d) + if err != nil { + return 0, false, false, err + } + + // MediaBox: optional, rectangle + hasMediaBox, err = validatePageEntryMediaBox(xRefTable, d, OPTIONAL, model.V10) + if err != nil { + return 0, false, false, err + } + + // CropBox: optional, rectangle + err = validatePageEntryCropBox(xRefTable, d, OPTIONAL, model.V10) + if err != nil { + return 0, false, false, err + } + + // Rotate: optional, integer + err = validatePageEntryRotate(xRefTable, d, OPTIONAL, model.V10) + if err != nil { + return 0, false, false, err + } + + return pageCount, hasResources, hasMediaBox, nil +} + +func dictTypeForPageNodeDict(d types.Dict) (string, error) { + + if d == nil { + return "", errors.New("pdfcpu: dictTypeForPageNodeDict: pageNodeDict is null") + } + + dictType := d.Type() + if dictType == nil { + return "", errors.New("pdfcpu: dictTypeForPageNodeDict: missing pageNodeDict type") + } + + return *dictType, nil +} + +func validateResources(xRefTable *model.XRefTable, d types.Dict) (hasResources bool, err error) { + + // Get number of pages of this PDF file. + pageCount := d.IntEntry("Count") + if pageCount == nil { + return false, errors.New("pdfcpu: validateResources: missing \"Count\"") + } + + // TODO not ideal - overall pageCount is only set during validation! + if xRefTable.PageCount == 0 { + xRefTable.PageCount = *pageCount + } + + if log.ValidateEnabled() { + log.Validate.Printf("validateResources: This page node has %d pages\n", *pageCount) + } + + // Resources: optional, dict + o, ok := d.Find("Resources") + if !ok { + return false, nil + } + + return validateResourceDict(xRefTable, o) +} + +func pagesDictKids(xRefTable *model.XRefTable, d types.Dict) types.Array { + if xRefTable.ValidationMode != model.ValidationRelaxed { + return d.ArrayEntry("Kids") + } + o, found := d.Find("Kids") + if !found { + return nil + } + kids, err := xRefTable.DereferenceArray(o) + if err != nil { + return nil + } + return kids +} + +func validateParent(pageNodeDict types.Dict, objNr int) error { + parentIndRef := pageNodeDict.IndirectRefEntry("Parent") + if parentIndRef == nil { + return errors.New("pdfcpu: validatePagesDict: missing parent node") + } + if parentIndRef.ObjectNumber.Value() != objNr { + return errors.New("pdfcpu: validatePagesDict: corrupt parent node") + } + return nil +} + +func processPagesKids(xRefTable *model.XRefTable, kids types.Array, objNr int, hasResources, hasMediaBox bool, curPage *int) (types.Array, error) { + var a types.Array + + for _, o := range kids { + + if o == nil { + continue + } + + ir, ok := o.(types.IndirectRef) + if !ok { + return nil, errors.New("pdfcpu: validatePagesDict: missing indirect reference for kid") + } + + if log.ValidateEnabled() { + log.Validate.Printf("validatePagesDict: PageNode: %s\n", ir) + } + + objNumber := ir.ObjectNumber.Value() + if objNumber == 0 { + continue + } + + a = append(a, ir) + + pageNodeDict, err := xRefTable.DereferenceDict(ir) + if err != nil { + return nil, err + } + if pageNodeDict == nil { + return nil, errors.New("pdfcpu: validatePagesDict: corrupt page node") + } + + if err := validateParent(pageNodeDict, objNr); err != nil { + return nil, err + } + + dictType, err := dictTypeForPageNodeDict(pageNodeDict) + if err != nil { + return nil, err + } + + switch dictType { + + case "Pages": + if err = validatePagesDict(xRefTable, pageNodeDict, objNumber, hasResources, hasMediaBox, curPage); err != nil { + return nil, err + } + + case "Page": + *curPage++ + xRefTable.CurPage = *curPage + if err = validatePageDict(xRefTable, pageNodeDict, objNumber, hasResources, hasMediaBox); err != nil { + return nil, err + } + if err := xRefTable.SetValid(ir); err != nil { + return nil, err + } + + default: + return nil, errors.Errorf("pdfcpu: validatePagesDict: Unexpected dict type: %s", dictType) + } + + } + + return a, nil +} + +func validatePagesDict(xRefTable *model.XRefTable, d types.Dict, objNr int, hasResources, hasMediaBox bool, curPage *int) error { + pageCount, dHasResources, dHasMediaBox, err := validatePagesDictGeneralEntries(xRefTable, d) + if err != nil { + return err + } + + if pageCount == 0 { + return nil + } + + if dHasResources { + hasResources = true + } + + if dHasMediaBox { + hasMediaBox = true + } + + kids := pagesDictKids(xRefTable, d) + if kids == nil { + return errors.New("pdfcpu: validatePagesDict: corrupt \"Kids\" entry") + } + + d["Kids"], err = processPagesKids(xRefTable, kids, objNr, hasResources, hasMediaBox, curPage) + + return err +} + +func repairPagesDict(xRefTable *model.XRefTable, obj types.Object, rootDict types.Dict) (types.Dict, int, error) { + d, err := xRefTable.DereferenceDict(obj) + if err != nil { + return nil, 0, err + } + + if d == nil { + return nil, 0, errors.New("pdfcpu: repairPagesDict: cannot dereference pageNodeDict") + } + + indRef, err := xRefTable.IndRefForNewObject(d) + if err != nil { + return nil, 0, err + } + + rootDict["Pages"] = *indRef + + objNr := indRef.ObjectNumber.Value() + + // Patch kids.parents + + kids := pagesDictKids(xRefTable, d) + if kids == nil { + return nil, 0, errors.New("pdfcpu: repairPagesDict: corrupt \"Kids\" entry") + } + + for i := range kids { + + o := kids[i] + + if o == nil { + continue + } + + ir, ok := o.(types.IndirectRef) + if !ok { + return nil, 0, errors.New("pdfcpu: repairPagesDict: missing indirect reference for kid") + } + + if log.ValidateEnabled() { + log.Validate.Printf("repairPagesDict: PageNode: %s\n", ir) + } + + objNumber := ir.ObjectNumber.Value() + if objNumber == 0 { + continue + } + + d, err := xRefTable.DereferenceDict(ir) + if err != nil { + return nil, 0, err + } + if d == nil { + return nil, 0, errors.New("pdfcpu: repairPagesDict: corrupt page node") + } + + d["Parent"] = *indRef + } + + return d, objNr, nil +} + +func validatePages(xRefTable *model.XRefTable, rootDict types.Dict) (types.Dict, error) { + obj, found := rootDict.Find("Pages") + if !found { + return nil, errors.New("pdfcpu: validatePages: missing \"Pages\"") + } + + var ( + objNr int + pageRoot types.Dict + err error + ) + + ir, ok := obj.(types.IndirectRef) + if !ok { + if xRefTable.ValidationMode != model.ValidationRelaxed { + return nil, errors.New("pdfcpu: validatePages: missing indirect reference \"Pages\"") + } + pageRoot, objNr, err = repairPagesDict(xRefTable, obj, rootDict) + if err != nil { + return nil, err + } + msg := "repaired: missing \"Pages\" indirect reference" + if log.DebugEnabled() { + log.Debug.Println("pdfcpu " + msg) + } + if log.CLIEnabled() { + log.CLI.Println(msg) + } + } + + if ok { + objNr = ir.ObjectNumber.Value() + + pageRoot, err = xRefTable.DereferenceDict(obj) + if err != nil { + return nil, err + } + + if pageRoot == nil { + return nil, errors.New("pdfcpu: validatePages: cannot dereference pageNodeDict") + } + } + + pageCount := pageRoot.IntEntry("Count") + if pageCount == nil { + return nil, errors.New("pdfcpu: validatePages: missing \"Count\" in page root dict") + } + + i := 0 + err = validatePagesDict(xRefTable, pageRoot, objNr, false, false, &i) + if err != nil { + return nil, err + } + + if i != *pageCount { + return nil, errors.New("pdfcpu: validatePages: page tree corrupted") + } + + return pageRoot, err +} diff --git a/pkg/pdfcpu/validate/pattern.go b/pkg/pdfcpu/validate/pattern.go new file mode 100644 index 0000000000000000000000000000000000000000..bb40c50cd7e0aacb15edf23690e3c53a97de2113 --- /dev/null +++ b/pkg/pdfcpu/validate/pattern.go @@ -0,0 +1,180 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 validate + +import ( + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +func validateTilingPatternDict(xRefTable *model.XRefTable, sd *types.StreamDict, sinceVersion model.Version) error { + + dictName := "tilingPatternDict" + + // Version check + err := xRefTable.ValidateVersion(dictName, sinceVersion) + if err != nil { + return err + } + + _, err = validateNameEntry(xRefTable, sd.Dict, dictName, "Type", OPTIONAL, sinceVersion, func(s string) bool { return s == "Pattern" }) + if err != nil { + return err + } + + _, err = validateIntegerEntry(xRefTable, sd.Dict, dictName, "PatternType", REQUIRED, sinceVersion, func(i int) bool { return i == 1 }) + if err != nil { + return err + } + + _, err = validateIntegerEntry(xRefTable, sd.Dict, dictName, "PaintType", REQUIRED, sinceVersion, nil) + if err != nil { + return err + } + + _, err = validateIntegerEntry(xRefTable, sd.Dict, dictName, "TilingType", REQUIRED, sinceVersion, nil) + if err != nil { + return err + } + + _, err = validateRectangleEntry(xRefTable, sd.Dict, dictName, "BBox", REQUIRED, sinceVersion, nil) + if err != nil { + return err + } + + _, err = validateNumberEntry(xRefTable, sd.Dict, dictName, "XStep", REQUIRED, sinceVersion, func(f float64) bool { return f != 0 }) + if err != nil { + return err + } + + _, err = validateNumberEntry(xRefTable, sd.Dict, dictName, "YStep", REQUIRED, sinceVersion, func(f float64) bool { return f != 0 }) + if err != nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, sd.Dict, dictName, "Matrix", OPTIONAL, sinceVersion, func(a types.Array) bool { return len(a) == 6 }) + if err != nil { + return err + } + + o, ok := sd.Find("Resources") + if !ok { + return errors.New("pdfcpu: validateTilingPatternDict: missing required entry Resources") + } + + _, err = validateResourceDict(xRefTable, o) + + return err +} + +func validateShadingPatternDict(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + + dictName := "shadingPatternDict" + + err := xRefTable.ValidateVersion(dictName, sinceVersion) + if err != nil { + return err + } + + _, err = validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, sinceVersion, func(s string) bool { return s == "Pattern" }) + if err != nil { + return err + } + + _, err = validateIntegerEntry(xRefTable, d, dictName, "PatternType", REQUIRED, sinceVersion, func(i int) bool { return i == 2 }) + if err != nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "Matrix", OPTIONAL, sinceVersion, func(a types.Array) bool { return len(a) == 6 }) + if err != nil { + return err + } + + d1, err := validateDictEntry(xRefTable, d, dictName, "ExtGState", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + if d1 != nil { + err = validateExtGStateDict(xRefTable, d1) + if err != nil { + return err + } + } + + // Shading: required, dict or stream dict. + o, ok := d.Find("Shading") + if !ok { + return errors.Errorf("pdfcpu: validateShadingPatternDict: missing required entry \"Shading\".") + } + + return validateShading(xRefTable, o) +} + +func validatePattern(xRefTable *model.XRefTable, o types.Object) error { + + o, err := xRefTable.Dereference(o) + if err != nil || o == nil { + return err + } + + switch o := o.(type) { + + case types.Dict: + err = validateShadingPatternDict(xRefTable, o, model.V13) + + case types.StreamDict: + err = validateTilingPatternDict(xRefTable, &o, model.V10) + + default: + err = errors.New("pdfcpu: validatePattern: corrupt obj typ, must be dict or stream dict") + + } + + return err +} + +func validatePatternResourceDict(xRefTable *model.XRefTable, o types.Object, sinceVersion model.Version) error { + + // see 8.7 Patterns + + // Version check + err := xRefTable.ValidateVersion("PatternResourceDict", sinceVersion) + if err != nil { + return err + } + + d, err := xRefTable.DereferenceDict(o) + if err != nil || d == nil { + return err + } + + // Iterate over pattern resource dictionary + for _, o := range d { + + // Process pattern + err = validatePattern(xRefTable, o) + if err != nil { + return err + } + + } + + return nil +} diff --git a/pkg/pdfcpu/validate/property.go b/pkg/pdfcpu/validate/property.go new file mode 100644 index 0000000000000000000000000000000000000000..a7a0352b5ef59f65601a7772bd9f5244141f54a8 --- /dev/null +++ b/pkg/pdfcpu/validate/property.go @@ -0,0 +1,120 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 validate + +import ( + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +func validatePropertiesDict(xRefTable *model.XRefTable, o types.Object) error { + // see 14.6.2 + // a dictionary containing private information meaningful to the conforming writer creating marked content. + // anything possible + + // empty dict ok + // Optional Metadata entry ok + // Optional Contents entry ok + // Optional Resources entry ok + // Optional content group /OCG see 8.11.2 + // Optional content membership dict. /OCMD see 8.11.2.2 + // Optional MCID integer entry + // Optional Alt since 1.5 see 14.9.3 + // Optional ActualText since 1.5 see 14.9.4 + // Optional E see since 1.4 14.9.5 + // Optional Lang string RFC 3066 see 14.9.2 + + logProp := func(qual, k string, v types.Object) { + if log.ValidateEnabled() { + log.Validate.Printf("validatePropertiesDict: %s key=%s val=%v\n", qual, k, v) + } + } + + d, err := xRefTable.DereferenceDict(o) + if err != nil || d == nil { + return err + } + + if err = validateMetadata(xRefTable, d, OPTIONAL, model.V14); err != nil { + return err + } + + for key, val := range d { + + switch key { + + case "Metadata": + logProp("known", key, val) + + case "Contents": + logProp("known", key, val) + if _, err = validateStreamDict(xRefTable, val); err != nil { + return err + } + + case "Resources": + logProp("known", key, val) + if _, err = validateResourceDict(xRefTable, val); err != nil { + return err + } + + case "OCG": + logProp("unsupported", key, val) + return errors.Errorf("validatePropertiesDict: unsupported key \"%s\"\n", key) + + case "OCMD": + logProp("unsupported", key, val) + return errors.Errorf("validatePropertiesDict: unsupported key \"%s\"\n", key) + + //case "MCID": -> default + //case "Alt": -> default + //case "ActualText": -> default + //case "E": -> default + //case "Lang": -> default + + default: + logProp("unknown", key, val) + if _, err = xRefTable.Dereference(val); err != nil { + return err + } + } + + } + + return nil +} + +func validatePropertiesResourceDict(xRefTable *model.XRefTable, o types.Object, sinceVersion model.Version) error { + if err := xRefTable.ValidateVersion("PropertiesResourceDict", sinceVersion); err != nil { + return err + } + + d, err := xRefTable.DereferenceDict(o) + if err != nil || d == nil { + return err + } + + // Iterate over properties resource dict + for _, o := range d { + if err = validatePropertiesDict(xRefTable, o); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/pdfcpu/validate/shading.go b/pkg/pdfcpu/validate/shading.go new file mode 100644 index 0000000000000000000000000000000000000000..ee0769556ea1d44904fbeedd6c62d3e7956b8321 --- /dev/null +++ b/pkg/pdfcpu/validate/shading.go @@ -0,0 +1,348 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 validate + +import ( + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +func validateBitsPerComponent(i int) bool { + return types.IntMemberOf(i, []int{1, 2, 4, 8, 12, 16}) +} + +func validateBitsPerCoordinate(i int) bool { + return types.IntMemberOf(i, []int{1, 2, 4, 8, 12, 16, 24, 32}) +} + +func validateBitsPerFlag(i int) bool { + return types.IntMemberOf(i, []int{2, 4, 8}) +} + +func validateShadingDictCommonEntries(xRefTable *model.XRefTable, dict types.Dict) (shadType int, err error) { + + dictName := "shadingDictCommonEntries" + + shadingType, err := validateIntegerEntry(xRefTable, dict, dictName, "ShadingType", REQUIRED, model.V10, func(i int) bool { return i >= 1 && i <= 7 }) + if err != nil { + return 0, err + } + + err = validateColorSpaceEntry(xRefTable, dict, dictName, "ColorSpace", OPTIONAL, ExcludePatternCS) + if err != nil { + return 0, err + } + + _, err = validateArrayEntry(xRefTable, dict, dictName, "Background", OPTIONAL, model.V10, nil) + if err != nil { + return 0, err + } + + _, err = validateRectangleEntry(xRefTable, dict, dictName, "BBox", OPTIONAL, model.V10, nil) + if err != nil { + return 0, err + } + + _, err = validateBooleanEntry(xRefTable, dict, dictName, "AntiAlias", OPTIONAL, model.V10, nil) + + return shadingType.Value(), err +} + +func validateFunctionBasedShadingDict(xRefTable *model.XRefTable, dict types.Dict) error { + + dictName := "functionBasedShadingDict" + + _, err := validateNumberArrayEntry(xRefTable, dict, dictName, "Domain", OPTIONAL, model.V10, func(a types.Array) bool { return len(a) == 4 }) + if err != nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, dict, dictName, "Matrix", OPTIONAL, model.V10, func(a types.Array) bool { return len(a) == 6 }) + if err != nil { + return err + } + + return validateFunctionOrArrayOfFunctionsEntry(xRefTable, dict, dictName, "Function", REQUIRED, model.V10) +} + +func validateAxialShadingDict(xRefTable *model.XRefTable, dict types.Dict) error { + + dictName := "axialShadingDict" + + _, err := validateNumberArrayEntry(xRefTable, dict, dictName, "Coords", REQUIRED, model.V10, func(a types.Array) bool { return len(a) == 4 }) + if err != nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, dict, dictName, "Domain", OPTIONAL, model.V10, func(a types.Array) bool { return len(a) == 2 }) + if err != nil { + return err + } + + err = validateFunctionOrArrayOfFunctionsEntry(xRefTable, dict, dictName, "Function", REQUIRED, model.V10) + if err != nil { + return err + } + + _, err = validateBooleanArrayEntry(xRefTable, dict, dictName, "Extend", OPTIONAL, model.V10, func(a types.Array) bool { return len(a) == 2 }) + + return err +} + +func validateRadialShadingDict(xRefTable *model.XRefTable, dict types.Dict) error { + + dictName := "radialShadingDict" + + _, err := validateNumberArrayEntry(xRefTable, dict, dictName, "Coords", REQUIRED, model.V10, func(a types.Array) bool { return len(a) == 6 }) + if err != nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, dict, dictName, "Domain", OPTIONAL, model.V10, func(a types.Array) bool { return len(a) == 2 }) + if err != nil { + return err + } + + err = validateFunctionOrArrayOfFunctionsEntry(xRefTable, dict, dictName, "Function", REQUIRED, model.V10) + if err != nil { + return err + } + + _, err = validateBooleanArrayEntry(xRefTable, dict, dictName, "Extend", OPTIONAL, model.V10, func(a types.Array) bool { return len(a) == 2 }) + + return err +} + +func validateShadingDict(xRefTable *model.XRefTable, dict types.Dict) error { + + // Shading 1-3 + + shadingType, err := validateShadingDictCommonEntries(xRefTable, dict) + if err != nil { + return err + } + + switch shadingType { + case 1: + err = validateFunctionBasedShadingDict(xRefTable, dict) + + case 2: + err = validateAxialShadingDict(xRefTable, dict) + + case 3: + err = validateRadialShadingDict(xRefTable, dict) + + default: + return errors.Errorf("validateShadingDict: unexpected shadingType: %d\n", shadingType) + } + + return err +} + +func validateFreeFormGouroudShadedTriangleMeshesDict(xRefTable *model.XRefTable, dict types.Dict) error { + + dictName := "freeFormGouraudShadedTriangleMeshesDict" + + _, err := validateIntegerEntry(xRefTable, dict, dictName, "BitsPerCoordinate", REQUIRED, model.V10, validateBitsPerCoordinate) + if err != nil { + return err + } + + _, err = validateIntegerEntry(xRefTable, dict, dictName, "BitsPerComponent", REQUIRED, model.V10, validateBitsPerComponent) + if err != nil { + return err + } + + _, err = validateIntegerEntry(xRefTable, dict, dictName, "BitsPerFlag", REQUIRED, model.V10, validateBitsPerFlag) + if err != nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, dict, dictName, "Decode", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + return validateFunctionOrArrayOfFunctionsEntry(xRefTable, dict, dictName, "Function", OPTIONAL, model.V10) +} + +func validateLatticeFormGouraudShadedTriangleMeshesDict(xRefTable *model.XRefTable, dict types.Dict) error { + + dictName := "latticeFormGouraudShadedTriangleMeshesDict" + + _, err := validateIntegerEntry(xRefTable, dict, dictName, "BitsPerCoordinate", REQUIRED, model.V10, validateBitsPerCoordinate) + if err != nil { + return err + } + + _, err = validateIntegerEntry(xRefTable, dict, dictName, "BitsPerComponent", REQUIRED, model.V10, validateBitsPerComponent) + if err != nil { + return err + } + + _, err = validateIntegerEntry(xRefTable, dict, dictName, "VerticesPerRow", REQUIRED, model.V10, func(i int) bool { return i >= 2 }) + if err != nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, dict, dictName, "Decode", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + return validateFunctionOrArrayOfFunctionsEntry(xRefTable, dict, dictName, "Function", OPTIONAL, model.V10) +} + +func validateCoonsPatchMeshesDict(xRefTable *model.XRefTable, dict types.Dict) error { + + dictName := "coonsPatchMeshesDict" + + _, err := validateIntegerEntry(xRefTable, dict, dictName, "BitsPerCoordinate", REQUIRED, model.V10, validateBitsPerCoordinate) + if err != nil { + return err + } + + _, err = validateIntegerEntry(xRefTable, dict, dictName, "BitsPerComponent", REQUIRED, model.V10, validateBitsPerComponent) + if err != nil { + return err + } + + _, err = validateIntegerEntry(xRefTable, dict, dictName, "BitsPerFlag", REQUIRED, model.V10, validateBitsPerFlag) + if err != nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, dict, dictName, "Decode", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + return validateFunctionOrArrayOfFunctionsEntry(xRefTable, dict, dictName, "Function", OPTIONAL, model.V10) +} + +func validateTensorProductPatchMeshesDict(xRefTable *model.XRefTable, dict types.Dict) error { + + dictName := "tensorProductPatchMeshesDict" + + _, err := validateIntegerEntry(xRefTable, dict, dictName, "BitsPerCoordinate", REQUIRED, model.V10, validateBitsPerCoordinate) + if err != nil { + return err + } + + _, err = validateIntegerEntry(xRefTable, dict, dictName, "BitsPerComponent", REQUIRED, model.V10, validateBitsPerComponent) + if err != nil { + return err + } + + _, err = validateIntegerEntry(xRefTable, dict, dictName, "BitsPerFlag", REQUIRED, model.V10, validateBitsPerFlag) + if err != nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, dict, dictName, "Decode", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + return validateFunctionOrArrayOfFunctionsEntry(xRefTable, dict, dictName, "Function", OPTIONAL, model.V10) +} + +func validateShadingStreamDict(xRefTable *model.XRefTable, sd *types.StreamDict) error { + + // Shading 4-7 + + dict := sd.Dict + + shadingType, err := validateShadingDictCommonEntries(xRefTable, dict) + if err != nil { + return err + } + + switch shadingType { + + case 4: + err = validateFreeFormGouroudShadedTriangleMeshesDict(xRefTable, dict) + + case 5: + err = validateLatticeFormGouraudShadedTriangleMeshesDict(xRefTable, dict) + + case 6: + err = validateCoonsPatchMeshesDict(xRefTable, dict) + + case 7: + err = validateTensorProductPatchMeshesDict(xRefTable, dict) + + default: + return errors.Errorf("pdfcpu: validateShadingStreamDict: unexpected shadingType: %d\n", shadingType) + } + + return err +} + +func validateShading(xRefTable *model.XRefTable, obj types.Object) error { + + // see 8.7.4.3 Shading Dictionaries + + obj, err := xRefTable.Dereference(obj) + if err != nil || obj == nil { + return err + } + + switch obj := obj.(type) { + + case types.Dict: + err = validateShadingDict(xRefTable, obj) + + case types.StreamDict: + err = validateShadingStreamDict(xRefTable, &obj) + + default: + return errors.New("pdfcpu: validateShading: corrupt obj typ, must be dict or stream dict") + + } + + return err +} + +func validateShadingResourceDict(xRefTable *model.XRefTable, obj types.Object, sinceVersion model.Version) error { + + // see 8.7.4.3 Shading Dictionaries + + // Version check + err := xRefTable.ValidateVersion("shadingResourceDict", sinceVersion) + if err != nil { + return err + } + + d, err := xRefTable.DereferenceDict(obj) + if err != nil || d == nil { + return err + } + + // Iterate over shading resource dictionary + for _, obj := range d { + + // Process shading + err = validateShading(xRefTable, obj) + if err != nil { + return err + } + } + + return nil +} diff --git a/pkg/pdfcpu/validate/structTree.go b/pkg/pdfcpu/validate/structTree.go new file mode 100644 index 0000000000000000000000000000000000000000..2d1c62f3d13457006280a9afdb6c62d59fd454b3 --- /dev/null +++ b/pkg/pdfcpu/validate/structTree.go @@ -0,0 +1,713 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 validate + +import ( + "strconv" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +func validateMarkedContentReferenceDict(xRefTable *model.XRefTable, d types.Dict) error { + + var err error + + // Pg: optional, indirect reference + // Page object representing a page on which the graphics object in the marked-content sequence shall be rendered. + if ir := d.IndirectRefEntry("Pg"); ir != nil { + err = processStructElementDictPgEntry(xRefTable, *ir) + if err != nil { + return err + } + } + + // Stm: optional, indirect reference + // The content stream containing the marked-content sequence. + if ir := d.IndirectRefEntry("Stm"); ir != nil { + _, err = xRefTable.Dereference(ir) + if err != nil { + return err + } + } + + // StmOwn: optional, indirect reference + // The PDF object owning the stream identified by Stems annotation to which an appearance stream belongs. + if ir := d.IndirectRefEntry("StmOwn"); ir != nil { + _, err = xRefTable.Dereference(ir) + if err != nil { + return err + } + } + + // MCID: required, integer + // The marked-content identifier of the marked-content sequence within its content stream. + + if d.IntEntry("MCID") == nil { + err = errors.Errorf("pdfcpu: validateMarkedContentReferenceDict: missing entry \"MCID\".") + } + + return err +} + +func validateObjectReferenceDict(xRefTable *model.XRefTable, d types.Dict) error { + + // Pg: optional, indirect reference + // Page object representing a page on which some or all of the content items designated by the K entry shall be rendered. + if ir := d.IndirectRefEntry("Pg"); ir != nil { + err := processStructElementDictPgEntry(xRefTable, *ir) + if err != nil { + return err + } + } + + // Obj: required, indirect reference + ir := d.IndirectRefEntry("Obj") + if xRefTable.ValidationMode == model.ValidationStrict && ir == nil { + return errors.New("pdfcpu: validateObjectReferenceDict: missing required entry \"Obj\"") + } + + if ir == nil { + return nil + } + + //obj, err := xRefTable.Dereference(*ir) + //if err != nil { + // return err + //} + // + //if obj == nil { + // return errors.Errorf("pdfcpu: validateObjectReferenceDict: missing obj#%s", ir.ObjectNumber) + //} + + // ignore obj is empty + _, err := xRefTable.Dereference(*ir) + if err != nil { + return err + } + + return nil +} + +func validateStructElementKArrayElement(xRefTable *model.XRefTable, o types.Object) error { + switch o := o.(type) { + + case types.Integer: + return nil + + case types.Dict: + + dictType := o.Type() + + if dictType == nil || *dictType == "StructElem" { + return validateStructElementDict(xRefTable, o) + } + + if *dictType == "MCR" { + return validateMarkedContentReferenceDict(xRefTable, o) + } + + if *dictType == "OBJR" { + return validateObjectReferenceDict(xRefTable, o) + } + + return errors.Errorf("validateStructElementKArrayElement: invalid dictType %s (should be \"StructElem\" or \"OBJR\" or \"MCR\")\n", *dictType) + + } + + return errors.New("validateStructElementKArrayElement: unsupported PDF object") +} + +func validateStructElementDictEntryKArray(xRefTable *model.XRefTable, a types.Array) error { + for _, o := range a { + + // Avoid recursion. + ir, ok := o.(types.IndirectRef) + if ok { + valid, err := xRefTable.IsValid(ir) + if err != nil { + return err + } + if valid { + continue + } + if err := xRefTable.SetValid(ir); err != nil { + return err + } + } + + o, err := xRefTable.Dereference(o) + if err != nil { + return err + } + + if o == nil { + continue + } + + if err := validateStructElementKArrayElement(xRefTable, o); err != nil { + return err + } + + } + + return nil +} + +func validateStructElementDictEntryK(xRefTable *model.XRefTable, o types.Object) error { + + // K: optional, the children of this structure element + // + // struct element dict + // marked content reference dict + // object reference dict + // marked content id int + // array of all above + + o, err := xRefTable.Dereference(o) + if err != nil || o == nil { + return err + } + + switch o := o.(type) { + + case types.Integer: + + case types.Dict: + dictType := o.Type() + + if dictType == nil || *dictType == "StructElem" { + err = validateStructElementDict(xRefTable, o) + if err != nil { + return err + } + break + } + + if *dictType == "MCR" { + err = validateMarkedContentReferenceDict(xRefTable, o) + if err != nil { + return err + } + break + } + + if *dictType == "OBJR" { + err = validateObjectReferenceDict(xRefTable, o) + if err != nil { + return err + } + break + } + + return errors.Errorf("pdfcpu: validateStructElementDictEntryK: invalid dictType %s (should be \"StructElem\" or \"OBJR\" or \"MCR\")\n", *dictType) + + case types.Array: + + err = validateStructElementDictEntryKArray(xRefTable, o) + if err != nil { + return err + } + + default: + return errors.New("pdfcpu: validateStructElementDictEntryK: unsupported PDF object") + + } + + return nil +} + +func processStructElementDictPgEntry(xRefTable *model.XRefTable, ir types.IndirectRef) error { + + // is this object a known page object? + + o, err := xRefTable.Dereference(ir) + if err != nil { + return errors.Errorf("pdfcpu: processStructElementDictPgEntry: Pg obj:#%d gen:%d unknown\n", ir.ObjectNumber, ir.GenerationNumber) + } + + //logInfoWriter.Printf("known object for Pg: %v %s\n", obj, obj) + + if xRefTable.ValidationMode == model.ValidationRelaxed && o == nil { + return nil + } + + pageDict, ok := o.(types.Dict) + if !ok { + return errors.Errorf("pdfcpu: processStructElementDictPgEntry: Pg object corrupt dict: %s\n", o) + } + + if t := pageDict.Type(); t == nil || *t != "Page" { + return errors.Errorf("pdfcpu: processStructElementDictPgEntry: Pg object no pageDict: %s\n", pageDict) + } + + return nil +} + +func validateStructElementDictEntryA(xRefTable *model.XRefTable, o types.Object) error { + + o, err := xRefTable.Dereference(o) + if err != nil || o == nil { + return err + } + + switch o := o.(type) { + + case types.Dict: // No further processing. + + case types.StreamDict: // No further processing. + + case types.Array: + + for _, o := range o { + + o, err := xRefTable.Dereference(o) + if err != nil { + return err + } + + if o == nil { + continue + } + + switch o.(type) { + + case types.Integer: + // Each array element may be followed by a revision number (int).sort + + case types.Dict: + // No further processing. + + case types.StreamDict: + // No further processing. + + default: + return errors.Errorf("pdfcpu: validateStructElementDictEntryA: unsupported PDF object: %v\n.", o) + } + } + + default: + return errors.Errorf("pdfcpu: validateStructElementDictEntryA: unsupported PDF object: %v\n.", o) + + } + + return nil +} + +func validateStructElementDictEntryC(xRefTable *model.XRefTable, o types.Object) error { + + o, err := xRefTable.Dereference(o) + if err != nil || o == nil { + return err + } + + switch o := o.(type) { + + case types.Name: + // No further processing. + + case types.Array: + + for _, o := range o { + + o, err := xRefTable.Dereference(o) + if err != nil { + return err + } + + if o == nil { + continue + } + + switch o.(type) { + + case types.Name: + // No further processing. + + case types.Integer: + // Each array element may be followed by a revision number. + + default: + return errors.New("pdfcpu: validateStructElementDictEntryC: unsupported PDF object") + + } + } + + default: + return errors.New("pdfcpu: validateStructElementDictEntryC: unsupported PDF object") + + } + + return nil +} + +func validateStructElementDictPart1(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // S: structure type, required, name, see 14.7.3 and Annex E. + _, err := validateNameEntry(xRefTable, d, dictName, "S", OPTIONAL, model.V10, nil) + if err != nil { + if xRefTable.ValidationMode == model.ValidationStrict { + return err + } + i, err := validateIntegerEntry(xRefTable, d, dictName, "S", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + if i != nil { + // "Repair" + d["S"] = types.Name(strconv.Itoa((*i).Value())) + } + } + + // P: immediate parent, required, indirect reference + ir := d.IndirectRefEntry("P") + if xRefTable.ValidationMode != model.ValidationRelaxed { + if ir == nil { + return errors.Errorf("pdfcpu: validateStructElementDict: missing entry P: %s\n", d) + } + + // Check if parent structure element exists. + if _, ok := xRefTable.FindTableEntryForIndRef(ir); !ok { + return errors.Errorf("pdfcpu: validateStructElementDict: unknown parent: %v\n", ir) + } + } + + // ID: optional, byte string + _, err = validateStringEntry(xRefTable, d, dictName, "ID", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // Pg: optional, indirect reference + // Page object representing a page on which some or all of the content items designated by the K entry shall be rendered. + if ir := d.IndirectRefEntry("Pg"); ir != nil { + err = processStructElementDictPgEntry(xRefTable, *ir) + if err != nil { + return err + } + } + + // K: optional, the children of this structure element. + if o, found := d.Find("K"); found { + err = validateStructElementDictEntryK(xRefTable, o) + if err != nil { + return err + } + } + + // A: optional, attribute objects: dict or stream dict or array of these. + if o, ok := d.Find("A"); ok { + err = validateStructElementDictEntryA(xRefTable, o) + } + + return err +} + +func validateStructElementDictPart2(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // C: optional, name or array + if o, ok := d.Find("C"); ok { + err := validateStructElementDictEntryC(xRefTable, o) + if err != nil { + return err + } + } + + // R: optional, integer >= 0 + _, err := validateIntegerEntry(xRefTable, d, dictName, "R", OPTIONAL, model.V10, func(i int) bool { return i >= 0 }) + if err != nil { + return err + } + + // T: optional, text string + _, err = validateStringEntry(xRefTable, d, dictName, "T", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // Lang: optional, text string, since 1.4 + sinceVersion := model.V14 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V13 + } + _, err = validateStringEntry(xRefTable, d, dictName, "Lang", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // Alt: optional, text string + _, err = validateStringEntry(xRefTable, d, dictName, "Alt", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // E: optional, text string, since 1.5 + sinceVersion = model.V15 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V14 + } + _, err = validateStringEntry(xRefTable, d, dictName, "E", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // ActualText: optional, text string, since 1.4 + _, err = validateStringEntry(xRefTable, d, dictName, "ActualText", OPTIONAL, model.V14, nil) + + return err +} + +func validateStructElementDict(xRefTable *model.XRefTable, d types.Dict) error { + + // See table 323 + + dictName := "StructElementDict" + + err := validateStructElementDictPart1(xRefTable, d, dictName) + if err != nil { + return err + } + + return validateStructElementDictPart2(xRefTable, d, dictName) +} + +func validateStructTreeRootDictEntryKArray(xRefTable *model.XRefTable, a types.Array) error { + + for _, o := range a { + + o, err := xRefTable.Dereference(o) + if err != nil { + return err + } + + if o == nil { + continue + } + + switch o := o.(type) { + + case types.Dict: + + dictType := o.Type() + + if dictType == nil || *dictType == "StructElem" { + err = validateStructElementDict(xRefTable, o) + if err != nil { + return err + } + break + } + + return errors.Errorf("pdfcpu: validateStructTreeRootDictEntryKArray: invalid dictType %s (should be \"StructElem\")\n", *dictType) + + default: + return errors.New("pdfcpu: validateStructTreeRootDictEntryKArray: unsupported PDF object") + + } + } + + return nil +} + +func validateStructTreeRootDictEntryK(xRefTable *model.XRefTable, o types.Object) error { + + // The immediate child or children of the structure tree root in the structure hierarchy. + // The value may be either a dictionary representing a single structure element or an array of such dictionaries. + + o, err := xRefTable.Dereference(o) + if err != nil || o == nil { + return err + } + + switch o := o.(type) { + + case types.Dict: + + dictType := o.Type() + + if dictType == nil || *dictType == "StructElem" { + err = validateStructElementDict(xRefTable, o) + if err != nil { + return err + } + break + } + + return errors.Errorf("validateStructTreeRootDictEntryK: invalid dictType %s (should be \"StructElem\")\n", *dictType) + + case types.Array: + + err = validateStructTreeRootDictEntryKArray(xRefTable, o) + if err != nil { + return err + } + + default: + return errors.New("pdfcpu: validateStructTreeRootDictEntryK: unsupported PDF object") + + } + + return nil +} + +func processStructTreeClassMapDict(xRefTable *model.XRefTable, d types.Dict) error { + + for _, o := range d { + + // Process dict or array of dicts. + + o, err := xRefTable.Dereference(o) + if err != nil { + return err + } + + if o == nil { + continue + } + + switch o := o.(type) { + + case types.Dict: + // no further processing. + + case types.Array: + + for _, o := range o { + + _, err = xRefTable.DereferenceDict(o) + if err != nil { + return err + } + + } + + default: + return errors.New("pdfcpu: processStructTreeClassMapDict: unsupported PDF object") + + } + + } + + return nil +} + +func validateStructTreeRootDictEntryParentTree(xRefTable *model.XRefTable, ir *types.IndirectRef) error { + + if xRefTable.ValidationMode == model.ValidationRelaxed { + + // Accept empty dict + d, err := xRefTable.DereferenceDict(*ir) + if err != nil { + return err + } + if d == nil || d.Len() == 0 { + return nil + } + } + + d, err := xRefTable.DereferenceDict(*ir) + if err != nil { + return err + } + + _, _, err = validateNumberTree(xRefTable, "StructTree", d, true) + return err +} + +func validateStructTreeRootDict(xRefTable *model.XRefTable, d types.Dict) error { + + dictName := "StructTreeRootDict" + + // required entry Type: name:StructTreeRoot + if d.Type() == nil || *d.Type() != "StructTreeRoot" { + return errors.New("pdfcpu: validateStructTreeRootDict: missing type") + } + + // Optional entry K: struct element dict or array of struct element dicts + if o, found := d.Find("K"); found { + err := validateStructTreeRootDictEntryK(xRefTable, o) + if err != nil { + return err + } + } + + // Optional entry IDTree: name tree, key=elementId value=struct element dict + // A name tree that maps element identifiers to the structure elements they denote. + ir := d.IndirectRefEntry("IDTree") + if ir != nil { + d, err := xRefTable.DereferenceDict(*ir) + if err != nil { + return err + } + if len(d) > 0 { + _, _, _, err = validateNameTree(xRefTable, "IDTree", d, true) + if err != nil { + return err + } + } + } + + // Optional entry ParentTree: number tree, value=indRef of struct element dict or array of struct element dicts + // A number tree used in finding the structure elements to which content items belong. + if ir = d.IndirectRefEntry("ParentTree"); ir != nil { + err := validateStructTreeRootDictEntryParentTree(xRefTable, ir) + if err != nil { + return err + } + } + + // Optional entry ParentTreeNextKey: integer + _, err := validateIntegerEntry(xRefTable, d, dictName, "ParentTreeNextKey", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // Optional entry RoleMap: dict + // A dictionary that shall map the names of structure used in the document + // to their approximate equivalents in the set of standard structure + _, err = validateDictEntry(xRefTable, d, dictName, "RoleMap", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // Optional entry ClassMap: dict + // A dictionary that shall map name objects designating attribute classes + // to the corresponding attribute objects or arrays of attribute objects. + d1, err := validateDictEntry(xRefTable, d, dictName, "ClassMap", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + if d1 != nil { + err = processStructTreeClassMapDict(xRefTable, d1) + } + + return err +} + +func validateStructTree(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) error { + + // 14.7.2 Structure Hierarchy + + d, err := validateDictEntry(xRefTable, rootDict, "RootDict", "StructTreeRoot", required, sinceVersion, nil) + if err != nil || d == nil { + return err + } + + return validateStructTreeRootDict(xRefTable, d) +} diff --git a/pkg/pdfcpu/validate/thread.go b/pkg/pdfcpu/validate/thread.go new file mode 100644 index 0000000000000000000000000000000000000000..4d66699eee18a2469d3d35d46c331df7d49b7f20 --- /dev/null +++ b/pkg/pdfcpu/validate/thread.go @@ -0,0 +1,250 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 validate + +import ( + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +func validateEntryV(xRefTable *model.XRefTable, d types.Dict, dictName string, required bool, sinceVersion model.Version, pBeadIndRef *types.IndirectRef, objNumber int) error { + + previousBeadIndRef, err := validateIndRefEntry(xRefTable, d, dictName, "V", required, sinceVersion) + if err != nil { + return err + } + + if *previousBeadIndRef != *pBeadIndRef { + return errors.Errorf("pdfcpu: validateEntryV: obj#%d invalid entry V, corrupt previous Bead indirect reference", objNumber) + } + + return nil +} + +func validateBeadDict(xRefTable *model.XRefTable, beadIndRef, threadIndRef, pBeadIndRef, lBeadIndRef *types.IndirectRef) error { + + objNumber := beadIndRef.ObjectNumber.Value() + + dictName := "beadDict" + sinceVersion := model.V10 + + d, err := xRefTable.DereferenceDict(*beadIndRef) + if err != nil { + return err + } + if d == nil { + return errors.Errorf("pdfcpu: validateBeadDict: obj#%d missing dict", objNumber) + } + + // Validate optional entry Type, must be "Bead". + _, err = validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, sinceVersion, func(s string) bool { return s == "Bead" }) + if err != nil { + return err + } + + // Validate entry T, must refer to threadDict. + indRefT, err := validateIndRefEntry(xRefTable, d, dictName, "T", OPTIONAL, sinceVersion) + if err != nil { + return err + } + if indRefT != nil && *indRefT != *threadIndRef { + return errors.Errorf("pdfcpu: validateBeadDict: obj#%d invalid entry T (backpointer to ThreadDict)", objNumber) + } + + // Validate required entry R, must be rectangle. + _, err = validateRectangleEntry(xRefTable, d, dictName, "R", REQUIRED, sinceVersion, nil) + if err != nil { + return err + } + + // Validate required entry P, must be indRef to pageDict. + err = validateEntryP(xRefTable, d, dictName, REQUIRED, sinceVersion) + if err != nil { + return err + } + + // Validate required entry V, must refer to previous bead. + err = validateEntryV(xRefTable, d, dictName, REQUIRED, sinceVersion, pBeadIndRef, objNumber) + if err != nil { + return err + } + + // Validate required entry N, must refer to last bead. + nBeadIndRef, err := validateIndRefEntry(xRefTable, d, dictName, "N", REQUIRED, sinceVersion) + if err != nil { + return err + } + + // Recurse until next bead equals last bead. + if *nBeadIndRef != *lBeadIndRef { + err = validateBeadDict(xRefTable, nBeadIndRef, threadIndRef, beadIndRef, lBeadIndRef) + if err != nil { + return err + } + } + + return nil +} + +func soleBeadDict(beadIndRef, pBeadIndRef, nBeadIndRef *types.IndirectRef) bool { + // if N and V reference this bead dict, must be the first and only one. + return *pBeadIndRef == *nBeadIndRef && *beadIndRef == *pBeadIndRef +} + +func validateBeadChainIntegrity(beadIndRef, pBeadIndRef, nBeadIndRef *types.IndirectRef) bool { + return *pBeadIndRef != *beadIndRef && *nBeadIndRef != *beadIndRef +} + +func validateFirstBeadDict(xRefTable *model.XRefTable, beadIndRef, threadIndRef *types.IndirectRef) error { + + dictName := "firstBeadDict" + sinceVersion := model.V10 + + d, err := xRefTable.DereferenceDict(*beadIndRef) + if err != nil { + return err + } + + if d == nil { + return errors.New("pdfcpu: validateFirstBeadDict: missing dict") + } + + _, err = validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, sinceVersion, func(s string) bool { return s == "Bead" }) + if err != nil { + return err + } + + indRefT, err := validateIndRefEntry(xRefTable, d, dictName, "T", REQUIRED, sinceVersion) + if err != nil { + return err + } + + if *indRefT != *threadIndRef { + return errors.New("pdfcpu: validateFirstBeadDict: invalid entry T (backpointer to ThreadDict)") + } + + _, err = validateRectangleEntry(xRefTable, d, dictName, "R", REQUIRED, sinceVersion, nil) + if err != nil { + return err + } + + err = validateEntryP(xRefTable, d, dictName, REQUIRED, sinceVersion) + if err != nil { + return err + } + + pBeadIndRef, err := validateIndRefEntry(xRefTable, d, dictName, "V", REQUIRED, sinceVersion) + if err != nil { + return err + } + + nBeadIndRef, err := validateIndRefEntry(xRefTable, d, dictName, "N", REQUIRED, sinceVersion) + if err != nil { + return err + } + + if soleBeadDict(beadIndRef, pBeadIndRef, nBeadIndRef) { + return nil + } + + if !validateBeadChainIntegrity(beadIndRef, pBeadIndRef, nBeadIndRef) { + return errors.New("pdfcpu: validateFirstBeadDict: corrupt chain of beads") + } + + return validateBeadDict(xRefTable, nBeadIndRef, threadIndRef, beadIndRef, pBeadIndRef) +} + +func validateThreadDict(xRefTable *model.XRefTable, o types.Object, sinceVersion model.Version) error { + + dictName := "threadDict" + + threadIndRef, ok := o.(types.IndirectRef) + if !ok { + return errors.New("pdfcpu: validateThreadDict: not an indirect ref") + } + + objNumber := threadIndRef.ObjectNumber.Value() + + d, err := xRefTable.DereferenceDict(threadIndRef) + if err != nil { + return err + } + + _, err = validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, sinceVersion, func(s string) bool { return s == "Thread" }) + if err != nil { + return err + } + + // Validate optional thread information dict entry. + o, found := d.Find("I") + if found && o != nil { + _, err = validateDocumentInfoDict(xRefTable, o) + if err != nil { + return err + } + } + + fBeadIndRef := d.IndirectRefEntry("F") + if fBeadIndRef == nil { + return errors.Errorf("pdfcpu: validateThreadDict: obj#%d required indirect entry \"F\" missing", objNumber) + } + + // Validate the list of beads starting with the first bead dict. + return validateFirstBeadDict(xRefTable, fBeadIndRef, &threadIndRef) +} + +func validateThreads(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) error { + + // => 12.4.3 Articles + + ir := rootDict.IndirectRefEntry("Threads") + if ir == nil { + if required { + return errors.New("pdfcpu: validateThreads: required entry \"Threads\" missing") + } + return nil + } + + a, err := xRefTable.DereferenceArray(*ir) + if err != nil { + return err + } + if a == nil { + return nil + } + + err = xRefTable.ValidateVersion("threads", sinceVersion) + if err != nil { + return err + } + + for _, o := range a { + + if o == nil { + continue + } + + err = validateThreadDict(xRefTable, o, sinceVersion) + if err != nil { + return err + } + + } + + return nil +} diff --git a/pkg/pdfcpu/validate/viewerPreferences.go b/pkg/pdfcpu/validate/viewerPreferences.go new file mode 100644 index 0000000000000000000000000000000000000000..2af573989a699ce7cf757a0997df6d485864f2a8 --- /dev/null +++ b/pkg/pdfcpu/validate/viewerPreferences.go @@ -0,0 +1,240 @@ +/* +Copyright 2023 The pdfcpu Authors. + +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 + + 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 validate + +import ( + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +func validatePageBoundaries(xRefTable *model.XRefTable, d types.Dict, dictName string, vp *model.ViewerPreferences) error { + validate := func(s string) bool { + return types.MemberOf(s, []string{"MediaBox", "CropBox", "BleedBox", "TrimBox", "ArtBox"}) + } + + n, err := validateNameEntry(xRefTable, d, dictName, "ViewArea", OPTIONAL, model.V14, validate) + if err != nil { + return err + } + if n != nil { + vp.ViewArea = model.PageBoundaryFor(n.String()) + } + + n, err = validateNameEntry(xRefTable, d, dictName, "PrintArea", OPTIONAL, model.V14, validate) + if err != nil { + return err + } + if n != nil { + vp.PrintArea = model.PageBoundaryFor(n.String()) + } + + n, err = validateNameEntry(xRefTable, d, dictName, "ViewClip", OPTIONAL, model.V14, validate) + if err != nil { + return err + } + if n != nil { + vp.ViewClip = model.PageBoundaryFor(n.String()) + } + + n, err = validateNameEntry(xRefTable, d, dictName, "PrintClip", OPTIONAL, model.V14, validate) + if err != nil { + return err + } + if n != nil { + vp.PrintClip = model.PageBoundaryFor(n.String()) + } + + return nil +} + +func validatePrintPageRange(xRefTable *model.XRefTable, d types.Dict, dictName string, vp *model.ViewerPreferences) error { + validate := func(arr types.Array) bool { + if len(arr) > 0 && len(arr)%2 > 0 { + return false + } + for i := 0; i < len(arr); i += 2 { + if arr[i].(types.Integer) >= arr[i+1].(types.Integer) { + return false + } + } + return true + } + + arr, err := validateIntegerArrayEntry(xRefTable, d, dictName, "PrintPageRange", OPTIONAL, model.V17, validate) + if err != nil { + return err + } + + if len(arr) > 0 { + vp.PrintPageRange = arr + } + + return nil +} + +func validateEnforcePrintScaling(xRefTable *model.XRefTable, d types.Dict, dictName string, vp *model.ViewerPreferences) error { + validate := func(arr types.Array) bool { + if len(arr) != 1 { + return false + } + return arr[0].String() == "PrintScaling" + } + + arr, err := validateNameArrayEntry(xRefTable, d, dictName, "Enforce", OPTIONAL, model.V20, validate) + if err != nil { + return err + } + + if len(arr) > 0 { + if vp.PrintScaling != nil && *vp.PrintScaling == model.PrintScalingAppDefault { + return errors.New("pdfcpu: viewpreference \"Enforce[\"PrintScaling\"]\" needs \"PrintScaling\" <> \"AppDefault\"") + } + vp.Enforce = types.NewNameArray("PrintScaling") + } + + return nil +} + +func validatePrinterPreferences(xRefTable *model.XRefTable, d types.Dict, dictName string, vp *model.ViewerPreferences) error { + sinceVersion := model.V16 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V13 + } + validate := func(s string) bool { + return types.MemberOf(s, []string{"None", "AppDefault"}) + } + n, err := validateNameEntry(xRefTable, d, dictName, "PrintScaling", OPTIONAL, sinceVersion, validate) + if err != nil { + return err + } + if n != nil { + vp.PrintScaling = model.PrintScalingFor(n.String()) + } + + validate = func(s string) bool { + return types.MemberOf(s, []string{"Simplex", "DuplexFlipShortEdge", "DuplexFlipLongEdge"}) + } + n, err = validateNameEntry(xRefTable, d, dictName, "Duplex", OPTIONAL, model.V17, validate) + if err != nil { + return err + } + if n != nil { + vp.Duplex = model.PaperHandlingFor(n.String()) + } + + vp.PickTrayByPDFSize, err = validateFlexBooleanEntry(xRefTable, d, dictName, "PickTrayByPDFSize", OPTIONAL, model.V17) + if err != nil { + return err + } + + vp.NumCopies, err = validateIntegerEntry(xRefTable, d, dictName, "NumCopies", OPTIONAL, model.V17, func(i int) bool { return i >= 1 }) + if err != nil { + return err + } + + if err := validatePrintPageRange(xRefTable, d, dictName, vp); err != nil { + return err + } + + return validateEnforcePrintScaling(xRefTable, d, dictName, vp) +} + +func validateViewerPreferencesFlags(xRefTable *model.XRefTable, d types.Dict, dictName string, vp *model.ViewerPreferences) error { + var err error + vp.HideToolbar, err = validateFlexBooleanEntry(xRefTable, d, dictName, "HideToolbar", OPTIONAL, model.V10) + if err != nil { + return err + } + + vp.HideMenubar, err = validateFlexBooleanEntry(xRefTable, d, dictName, "HideMenubar", OPTIONAL, model.V10) + if err != nil { + return err + } + + vp.HideWindowUI, err = validateFlexBooleanEntry(xRefTable, d, dictName, "HideWindowUI", OPTIONAL, model.V10) + if err != nil { + return err + } + + vp.FitWindow, err = validateFlexBooleanEntry(xRefTable, d, dictName, "FitWindow", OPTIONAL, model.V10) + if err != nil { + return err + } + + vp.CenterWindow, err = validateFlexBooleanEntry(xRefTable, d, dictName, "CenterWindow", OPTIONAL, model.V10) + if err != nil { + return err + } + + sinceVersion := model.V14 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V10 + } + vp.DisplayDocTitle, err = validateFlexBooleanEntry(xRefTable, d, dictName, "DisplayDocTitle", OPTIONAL, sinceVersion) + if err != nil { + return err + } + + return nil +} + +func validateViewerPreferences(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) error { + // => 12.2 Viewer Preferences + + dictName := "rootDict" + + d, err := validateDictEntry(xRefTable, rootDict, dictName, "ViewerPreferences", required, sinceVersion, nil) + if err != nil || d == nil { + return err + } + + vp := model.ViewerPreferences{} + xRefTable.ViewerPref = &vp + + dictName = "ViewerPreferences" + + if err := validateViewerPreferencesFlags(xRefTable, d, dictName, &vp); err != nil { + return err + } + + validate := func(s string) bool { + return types.MemberOf(s, []string{"UseNone", "UseOutlines", "UseThumbs", "UseOC"}) + } + n, err := validateNameEntry(xRefTable, d, dictName, "NonFullScreenPageMode", OPTIONAL, model.V10, validate) + if err != nil { + return err + } + if n != nil { + vp.NonFullScreenPageMode = (*model.NonFullScreenPageMode)(model.PageModeFor(n.String())) + } + + validate = func(s string) bool { return types.MemberOf(s, []string{"L2R", "R2L"}) } + n, err = validateNameEntry(xRefTable, d, dictName, "Direction", OPTIONAL, model.V13, validate) + if err != nil { + return err + } + if n != nil { + vp.Direction = model.DirectionFor(n.String()) + } + + if err := validatePageBoundaries(xRefTable, d, dictName, &vp); err != nil { + return err + } + + return validatePrinterPreferences(xRefTable, d, dictName, &vp) +} diff --git a/pkg/pdfcpu/validate/xObject.go b/pkg/pdfcpu/validate/xObject.go new file mode 100644 index 0000000000000000000000000000000000000000..cc1123bca213a3900a7ae42deb80897d1571c061 --- /dev/null +++ b/pkg/pdfcpu/validate/xObject.go @@ -0,0 +1,878 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 validate + +import ( + "github.com/pdfcpu/pdfcpu/pkg/filter" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +const ( + + // ExcludePatternCS ... + ExcludePatternCS = true + + // IncludePatternCS ... + IncludePatternCS = false + + isAlternateImageStreamDict = true + isNoAlternateImageStreamDict = false +) + +func validateReferenceDictPageEntry(xRefTable *model.XRefTable, o types.Object) error { + + o, err := xRefTable.Dereference(o) + if err != nil || o == nil { + return err + } + + switch o.(type) { + + case types.Integer, types.StringLiteral, types.HexLiteral: + // no further processing + + default: + return errors.New("pdfcpu: validateReferenceDictPageEntry: corrupt type") + + } + + return nil +} + +func validateReferenceDict(xRefTable *model.XRefTable, d types.Dict) error { + + // see 8.10.4 Reference XObjects + + dictName := "refDict" + + // F, file spec, required + _, err := validateFileSpecEntry(xRefTable, d, dictName, "F", REQUIRED, model.V10) + if err != nil { + return err + } + + // Page, integer or text string, required + o, ok := d.Find("Page") + if !ok { + return errors.New("pdfcpu: validateReferenceDict: missing required entry \"Page\"") + } + + err = validateReferenceDictPageEntry(xRefTable, o) + if err != nil { + return err + } + + // ID, string array, optional + _, err = validateStringArrayEntry(xRefTable, d, dictName, "ID", OPTIONAL, model.V10, func(a types.Array) bool { return len(a) == 2 }) + + return err +} + +func validateOPIDictV13Part1(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // Type, optional, name + _, err := validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, model.V10, func(s string) bool { return s == "OPI" }) + if err != nil { + return err + } + + // Version, required, number + _, err = validateNumberEntry(xRefTable, d, dictName, "Version", REQUIRED, model.V10, func(f float64) bool { return f == 1.3 }) + if err != nil { + return err + } + + // F, required, file specification + _, err = validateFileSpecEntry(xRefTable, d, dictName, "F", REQUIRED, model.V10) + if err != nil { + return err + } + + // ID, optional, byte string + _, err = validateStringEntry(xRefTable, d, dictName, "ID", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // Comments, optional, text string + _, err = validateStringEntry(xRefTable, d, dictName, "Comments", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // Size, required, array of integers, len 2 + _, err = validateIntegerArrayEntry(xRefTable, d, dictName, "Size", REQUIRED, model.V10, func(a types.Array) bool { return len(a) == 2 }) + if err != nil { + return err + } + + // CropRect, required, array of integers, len 4 + _, err = validateRectangleEntry(xRefTable, d, dictName, "CropRect", REQUIRED, model.V10, nil) + + if err != nil { + return err + } + + // CropFixed, optional, array of numbers, len 4 + _, err = validateRectangleEntry(xRefTable, d, dictName, "CropFixed", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // Position, required, array of numbers, len 8 + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "Position", REQUIRED, model.V10, func(a types.Array) bool { return len(a) == 8 }) + + return err +} + +func validateOPIDictV13Part2(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // Resolution, optional, array of numbers, len 2 + _, err := validateNumberArrayEntry(xRefTable, d, dictName, "Resolution", OPTIONAL, model.V10, func(a types.Array) bool { return len(a) == 2 }) + if err != nil { + return err + } + + // ColorType, optional, name + _, err = validateNameEntry(xRefTable, d, dictName, "ColorType", OPTIONAL, model.V10, func(s string) bool { return s == "Process" || s == "Spot" || s == "Separation" }) + if err != nil { + return err + } + + // Color, optional, array, len 5 + _, err = validateArrayEntry(xRefTable, d, dictName, "Color", OPTIONAL, model.V10, func(a types.Array) bool { return len(a) == 5 }) + if err != nil { + return err + } + + // Tint, optional, number + _, err = validateNumberEntry(xRefTable, d, dictName, "Tint", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // Overprint, optional, boolean + _, err = validateBooleanEntry(xRefTable, d, dictName, "Overprint", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // ImageType, optional, array of integers, len 2 + _, err = validateIntegerArrayEntry(xRefTable, d, dictName, "ImageType", OPTIONAL, model.V10, func(a types.Array) bool { return len(a) == 2 }) + if err != nil { + return err + } + + // GrayMap, optional, array of integers + _, err = validateIntegerArrayEntry(xRefTable, d, dictName, "GrayMap", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // Transparency, optional, boolean + _, err = validateBooleanEntry(xRefTable, d, dictName, "Transparency", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // Tags, optional, array + _, err = validateArrayEntry(xRefTable, d, dictName, "Tags", OPTIONAL, model.V10, nil) + + return err +} + +func validateOPIDictV13(xRefTable *model.XRefTable, d types.Dict) error { + + // 14.11.7 Open Prepresse interface (OPI) + + dictName := "opiDictV13" + + err := validateOPIDictV13Part1(xRefTable, d, dictName) + if err != nil { + return err + } + + return validateOPIDictV13Part2(xRefTable, d, dictName) +} + +func validateOPIDictInks(xRefTable *model.XRefTable, o types.Object) error { + + o, err := xRefTable.Dereference(o) + if err != nil || o == nil { + return err + } + + switch o := o.(type) { + + case types.Name: + if colorant := o.Value(); colorant != "full_color" && colorant != "registration" { + return errors.New("pdfcpu: validateOPIDictInks: corrupt colorant name") + } + + case types.Array: + // no further processing + + default: + return errors.New("pdfcpu: validateOPIDictInks: corrupt type") + + } + + return nil +} + +func validateOPIDictV20(xRefTable *model.XRefTable, d types.Dict) error { + + // 14.11.7 Open Prepresse interface (OPI) + + dictName := "opiDictV20" + + _, err := validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, model.V10, func(s string) bool { return s == "OPI" }) + if err != nil { + return err + } + + _, err = validateNumberEntry(xRefTable, d, dictName, "Version", REQUIRED, model.V10, func(f float64) bool { return f == 2.0 }) + if err != nil { + return err + } + + _, err = validateFileSpecEntry(xRefTable, d, dictName, "F", REQUIRED, model.V10) + if err != nil { + return err + } + + _, err = validateStringEntry(xRefTable, d, dictName, "MainImage", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + _, err = validateArrayEntry(xRefTable, d, dictName, "Tags", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, d, dictName, "Size", OPTIONAL, model.V10, func(a types.Array) bool { return len(a) == 2 }) + if err != nil { + return err + } + + _, err = validateRectangleEntry(xRefTable, d, dictName, "CropRect", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + _, err = validateBooleanEntry(xRefTable, d, dictName, "Overprint", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + if o, found := d.Find("Inks"); found { + err = validateOPIDictInks(xRefTable, o) + if err != nil { + return err + } + } + + _, err = validateIntegerArrayEntry(xRefTable, d, dictName, "IncludedImageDimensions", OPTIONAL, model.V10, func(a types.Array) bool { return len(a) == 2 }) + if err != nil { + return err + } + + _, err = validateIntegerEntry(xRefTable, d, dictName, "IncludedImageQuality", OPTIONAL, model.V10, func(i int) bool { return i >= 1 && i <= 3 }) + + return err +} + +func validateOPIVersionDict(xRefTable *model.XRefTable, d types.Dict) error { + + // 14.11.7 Open Prepresse interface (OPI) + + if d.Len() != 1 { + return errors.New("pdfcpu: validateOPIVersionDict: must have exactly one entry keyed 1.3 or 2.0") + } + + validateOPIVersion := func(s string) bool { return types.MemberOf(s, []string{"1.3", "2.0"}) } + + for opiVersion, obj := range d { + + if !validateOPIVersion(opiVersion) { + return errors.New("pdfcpu: validateOPIVersionDict: invalid OPI version") + } + + d, err := xRefTable.DereferenceDict(obj) + if err != nil || d == nil { + return err + } + + if opiVersion == "1.3" { + err = validateOPIDictV13(xRefTable, d) + } else { + err = validateOPIDictV20(xRefTable, d) + } + + if err != nil { + return err + } + + } + + return nil +} + +func validateMaskStreamDict(xRefTable *model.XRefTable, sd *types.StreamDict) error { + + if sd.Type() != nil && *sd.Type() != "XObject" { + return errors.New("pdfcpu: validateMaskStreamDict: corrupt imageStreamDict type") + } + + if sd.Subtype() == nil || *sd.Subtype() != "Image" { + return errors.New("pdfcpu: validateMaskStreamDict: corrupt imageStreamDict subtype") + } + + return validateImageStreamDict(xRefTable, sd, isNoAlternateImageStreamDict) +} + +func validateMaskEntry(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) error { + + // stream ("explicit masking", another Image XObject) or array of colors ("color key masking") + + o, err := validateEntry(xRefTable, d, dictName, entryName, required, sinceVersion) + if err != nil || o == nil { + return err + } + + switch o := o.(type) { + + case types.StreamDict: + err = validateMaskStreamDict(xRefTable, &o) + if err != nil { + return err + } + + case types.Array: + // no further processing + + default: + + return errors.Errorf("pdfcpu: validateMaskEntry: dict=%s corrupt entry \"%s\"\n", dictName, entryName) + + } + + return nil +} + +func validateAlternateImageStreamDicts(xRefTable *model.XRefTable, d types.Dict, dictName string, entryName string, required bool, sinceVersion model.Version) error { + + a, err := validateArrayEntry(xRefTable, d, dictName, entryName, required, sinceVersion, nil) + if err != nil { + return err + } + if a == nil { + if required { + return errors.Errorf("pdfcpu: validateAlternateImageStreamDicts: dict=%s required entry \"%s\" missing.", dictName, entryName) + } + return nil + } + + for _, o := range a { + + sd, err := validateStreamDict(xRefTable, o) + if err != nil { + return err + } + + if sd == nil { + continue + } + + err = validateImageStreamDict(xRefTable, sd, isAlternateImageStreamDict) + if err != nil { + return err + } + } + + return nil +} + +func validateImageStreamDictPart1(xRefTable *model.XRefTable, sd *types.StreamDict, dictName string) (isImageMask bool, err error) { + + // Width, integer, required + _, err = validateIntegerEntry(xRefTable, sd.Dict, dictName, "Width", REQUIRED, model.V10, nil) + if err != nil { + return false, err + } + + // Height, integer, required + _, err = validateIntegerEntry(xRefTable, sd.Dict, dictName, "Height", REQUIRED, model.V10, nil) + if err != nil { + return false, err + } + + // ImageMask, boolean, optional + imageMask, err := validateBooleanEntry(xRefTable, sd.Dict, dictName, "ImageMask", OPTIONAL, model.V10, nil) + if err != nil { + return false, err + } + + isImageMask = (imageMask != nil) && *imageMask == true + + // ColorSpace, name or array, required unless used filter is JPXDecode; not allowed for imagemasks. + if !isImageMask { + + required := REQUIRED + + if sd.HasSoleFilterNamed(filter.JPX) { + required = OPTIONAL + } + + if sd.HasSoleFilterNamed(filter.CCITTFax) && xRefTable.ValidationMode == model.ValidationRelaxed { + required = OPTIONAL + } + + err = validateColorSpaceEntry(xRefTable, sd.Dict, dictName, "ColorSpace", required, ExcludePatternCS) + if err != nil { + return false, err + } + + } + + return isImageMask, nil +} + +func validateImageStreamDictPart2(xRefTable *model.XRefTable, sd *types.StreamDict, dictName string, isImageMask, isAlternate bool) error { + + // BitsPerComponent, integer + required := REQUIRED + if sd.HasSoleFilterNamed(filter.JPX) || isImageMask { + required = OPTIONAL + } + // For imageMasks BitsPerComponent must be 1. + var validateBPC func(i int) bool + if isImageMask { + validateBPC = func(i int) bool { + return i == 1 + } + } + _, err := validateIntegerEntry(xRefTable, sd.Dict, dictName, "BitsPerComponent", required, model.V10, validateBPC) + if err != nil { + return err + } + + // Intent, name, optional, since V1.0 + validate := func(s string) bool { + return types.MemberOf(s, []string{"AbsoluteColorimetric", "RelativeColorimetric", "Saturation", "Perceptual"}) + } + _, err = validateNameEntry(xRefTable, sd.Dict, dictName, "Intent", OPTIONAL, model.V11, validate) + if err != nil { + return err + } + + // Mask, stream or array, optional since V1.3; not allowed for image masks. + if !isImageMask { + err = validateMaskEntry(xRefTable, sd.Dict, dictName, "Mask", OPTIONAL, model.V13) + if err != nil { + return err + } + } + + // Decode, array, optional + _, err = validateNumberArrayEntry(xRefTable, sd.Dict, dictName, "Decode", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // Interpolate, boolean, optional + _, err = validateBooleanEntry(xRefTable, sd.Dict, dictName, "Interpolate", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // Alternates, array, optional, since V1.3 + if !isAlternate { + err = validateAlternateImageStreamDicts(xRefTable, sd.Dict, dictName, "Alternates", OPTIONAL, model.V13) + } + + return err +} + +func validateImageStreamDict(xRefTable *model.XRefTable, sd *types.StreamDict, isAlternate bool) error { + dictName := "imageStreamDict" + var isImageMask bool + + isImageMask, err := validateImageStreamDictPart1(xRefTable, sd, dictName) + if err != nil { + return err + } + + err = validateImageStreamDictPart2(xRefTable, sd, dictName, isImageMask, isAlternate) + if err != nil { + return err + } + + // SMask, stream, optional, since V1.4 + sinceVersion := model.V14 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V12 + } + sd1, err := validateStreamDictEntry(xRefTable, sd.Dict, dictName, "SMask", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + if sd1 != nil { + err = validateImageStreamDict(xRefTable, sd1, isNoAlternateImageStreamDict) + if err != nil { + return err + } + } + + // SMaskInData, integer, optional + _, err = validateIntegerEntry(xRefTable, sd.Dict, dictName, "SMaskInData", OPTIONAL, model.V10, func(i int) bool { return i >= 0 && i <= 2 }) + if err != nil { + return err + } + + // Name, name, required for V10 + // Shall no longer be used. + // _, err = validateNameEntry(xRefTable, sd.Dict, dictName, "Name", xRefTable.Version() == model.V10, model.V10, nil) + // if err != nil { + // return err + // } + + // StructParent, integer, optional + _, err = validateIntegerEntry(xRefTable, sd.Dict, dictName, "StructParent", OPTIONAL, model.V13, nil) + if err != nil { + return err + } + + // ID, byte string, optional, since V1.3 + _, err = validateStringEntry(xRefTable, sd.Dict, dictName, "ID", OPTIONAL, model.V13, nil) + if err != nil { + return err + } + + // OPI, dict, optional since V1.2 + err = validateEntryOPI(xRefTable, sd.Dict, dictName, "OPI", OPTIONAL, model.V12) + if err != nil { + return err + } + + // Metadata, stream, optional since V1.4 + err = validateMetadata(xRefTable, sd.Dict, OPTIONAL, model.V14) + if err != nil { + return err + } + + // OC, dict, optional since V1.5 + return validateEntryOC(xRefTable, sd.Dict, dictName, "OC", OPTIONAL, model.V15) +} + +func validateFormStreamDictPart1(xRefTable *model.XRefTable, sd *types.StreamDict, dictName string) error { + var err error + if xRefTable.ValidationMode == model.ValidationRelaxed { + _, err = validateNumberEntry(xRefTable, sd.Dict, dictName, "FormType", OPTIONAL, model.V10, func(f float64) bool { return f == 1. }) + } else { + _, err = validateIntegerEntry(xRefTable, sd.Dict, dictName, "FormType", OPTIONAL, model.V10, func(i int) bool { return i == 1 }) + } + if err != nil { + return err + } + + _, err = validateRectangleEntry(xRefTable, sd.Dict, dictName, "BBox", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + _, err = validateNumberArrayEntry(xRefTable, sd.Dict, dictName, "Matrix", OPTIONAL, model.V10, func(a types.Array) bool { return len(a) == 6 }) + if err != nil { + return err + } + + // Resources, dict, optional, since V1.2 + if o, ok := sd.Find("Resources"); ok { + _, err = validateResourceDict(xRefTable, o) + if err != nil { + return err + } + } + + // Group, dict, optional, since V1.4 + err = validatePageEntryGroup(xRefTable, sd.Dict, OPTIONAL, model.V14) + if err != nil { + return err + } + + // Ref, dict, optional, since V1.4 + d, err := validateDictEntry(xRefTable, sd.Dict, dictName, "Ref", OPTIONAL, model.V14, nil) + if err != nil { + return err + } + if d != nil { + err = validateReferenceDict(xRefTable, d) + if err != nil { + return err + } + } + + // Metadata, stream, optional, since V1.4 + return validateMetadata(xRefTable, sd.Dict, OPTIONAL, model.V14) +} + +func validateEntryOC(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) error { + + d1, err := validateDictEntry(xRefTable, d, dictName, entryName, required, sinceVersion, nil) + if err != nil { + return err + } + + if d1 != nil { + err = validateOptionalContentGroupDict(xRefTable, d1, sinceVersion) + } + + return err +} + +func validateEntryOPI(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) error { + + d1, err := validateDictEntry(xRefTable, d, dictName, entryName, required, sinceVersion, nil) + if err != nil { + return err + } + + if d1 != nil { + err = validateOPIVersionDict(xRefTable, d1) + } + + return err +} + +func validateFormStreamDictPart2(xRefTable *model.XRefTable, d types.Dict, dictName string) error { + + // PieceInfo, dict, optional, since V1.3 + if xRefTable.ValidationMode != model.ValidationRelaxed { + hasPieceInfo, err := validatePieceInfo(xRefTable, d, dictName, "PieceInfo", OPTIONAL, model.V13) + if err != nil { + return err + } + + // LastModified, date, required if PieceInfo present, since V1.3 + lm, err := validateDateEntry(xRefTable, d, dictName, "LastModified", OPTIONAL, model.V13) + if err != nil { + return err + } + + if hasPieceInfo && lm == nil { + err = errors.New("pdfcpu: validateFormStreamDictPart2: missing \"LastModified\" (required by \"PieceInfo\")") + return err + } + } + + // StructParent, integer + sp, err := validateIntegerEntry(xRefTable, d, dictName, "StructParent", OPTIONAL, model.V13, nil) + if err != nil { + return err + } + + // StructParents, integer + sps, err := validateIntegerEntry(xRefTable, d, dictName, "StructParents", OPTIONAL, model.V13, nil) + if err != nil { + return err + } + if sp != nil && sps != nil { + return errors.New("pdfcpu: validateFormStreamDictPart2: only \"StructParent\" or \"StructParents\" allowed") + } + + // OPI, dict, optional, since V1.2 + err = validateEntryOPI(xRefTable, d, dictName, "OPI", OPTIONAL, model.V12) + if err != nil { + return err + } + + // OC, optional, content group dict or content membership dict, since V1.5 + // Specifying the optional content properties for the annotation. + sinceVersion := model.V15 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V13 + } + err = validateOptionalContent(xRefTable, d, dictName, "OC", OPTIONAL, sinceVersion) + if err != nil { + return err + } + + // Name, name, optional (required in 1.0) + required := xRefTable.Version() == model.V10 + _, err = validateNameEntry(xRefTable, d, dictName, "Name", required, model.V10, nil) + + return err +} + +func validateFormStreamDict(xRefTable *model.XRefTable, sd *types.StreamDict) error { + + // 8.10 Form XObjects + + dictName := "formStreamDict" + + err := validateFormStreamDictPart1(xRefTable, sd, dictName) + if err != nil { + return err + } + + return validateFormStreamDictPart2(xRefTable, sd.Dict, dictName) +} + +func validateXObjectType(xRefTable *model.XRefTable, sd *types.StreamDict) error { + ss := []string{"XObject"} + if xRefTable.ValidationMode == model.ValidationRelaxed { + ss = append(ss, "Xobject") + } + + n, err := validateNameEntry(xRefTable, sd.Dict, "xObjectStreamDict", "Type", OPTIONAL, model.V10, func(s string) bool { return types.MemberOf(s, ss) }) + if err != nil { + return err + } + + // Repair "Xobject" to "XObject". + if n != nil && *n == "Xobject" { + sd.Dict["Type"] = types.Name("XObject") + } + + return nil +} + +func validateXObjectStreamDict(xRefTable *model.XRefTable, o types.Object) error { + + // see 8.8 External Objects + + // Dereference stream dict and ensure it is validated exactly once in order + // to handle XObjects(forms) with recursive structures like produced by Microsoft. + sd, valid, err := xRefTable.DereferenceStreamDict(o) + if valid { + return nil + } + if err != nil || sd == nil { + return err + } + + dictName := "xObjectStreamDict" + + if err := validateXObjectType(xRefTable, sd); err != nil { + return err + } + + required := REQUIRED + if xRefTable.ValidationMode == model.ValidationRelaxed { + required = OPTIONAL + } + subtype, err := validateNameEntry(xRefTable, sd.Dict, dictName, "Subtype", required, model.V10, nil) + if err != nil { + return err + } + + if subtype == nil { + // relaxed + _, found := sd.Find("BBox") + if found { + return validateFormStreamDict(xRefTable, sd) + } + + // Relaxed for page Thumb + return validateImageStreamDict(xRefTable, sd, isNoAlternateImageStreamDict) + } + + switch *subtype { + + case "Form": + err = validateFormStreamDict(xRefTable, sd) + + case "Image": + err = validateImageStreamDict(xRefTable, sd, isNoAlternateImageStreamDict) + + case "PS": + err = errors.Errorf("pdfcpu: validateXObjectStreamDict: PostScript XObjects should not be used") + + default: + return errors.Errorf("pdfcpu: validateXObjectStreamDict: unknown Subtype: %s\n", *subtype) + + } + + return err +} + +func validateGroupAttributesDict(xRefTable *model.XRefTable, o types.Object) error { + + // see 11.6.6 Transparency Group XObjects + + d, err := xRefTable.DereferenceDict(o) + if err != nil || d == nil { + return err + } + + dictName := "groupAttributesDict" + + // Type, name, optional + _, err = validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, model.V10, func(s string) bool { return s == "Group" }) + if err != nil { + return err + } + + // S, name, required + _, err = validateNameEntry(xRefTable, d, dictName, "S", REQUIRED, model.V10, func(s string) bool { return s == "Transparency" }) + if err != nil { + return err + } + + // CS, colorSpace, optional + err = validateColorSpaceEntry(xRefTable, d, dictName, "CS", OPTIONAL, ExcludePatternCS) + if err != nil { + return err + } + + // I, boolean, optional + _, err = validateBooleanEntry(xRefTable, d, dictName, "I", OPTIONAL, model.V10, nil) + + return err +} + +func validateXObjectResourceDict(xRefTable *model.XRefTable, o types.Object, sinceVersion model.Version) error { + + // Version check + err := xRefTable.ValidateVersion("XObjectResourceDict", sinceVersion) + if err != nil { + return err + } + + d, err := xRefTable.DereferenceDict(o) + if err != nil || d == nil { + return err + } + + //fmt.Printf("XObjResDict:\n%s\n", d) + + // Iterate over XObject resource dictionary + for _, o := range d { + + // Process XObject dict + err = validateXObjectStreamDict(xRefTable, o) + if err != nil { + return err + } + } + + return nil +} diff --git a/pkg/pdfcpu/validate/xReftable.go b/pkg/pdfcpu/validate/xReftable.go new file mode 100644 index 0000000000000000000000000000000000000000..e912fdafab3a671c577f0f23a43ceac9538c5f7a --- /dev/null +++ b/pkg/pdfcpu/validate/xReftable.go @@ -0,0 +1,1023 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 validate implements validation against PDF 32000-1:2008. +package validate + +import ( + "fmt" + "net/http" + "net/url" + "sort" + "strconv" + "time" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// XRefTable validates a PDF cross reference table obeying the validation mode. +func XRefTable(xRefTable *model.XRefTable) error { + if log.InfoEnabled() { + log.Info.Println("validating") + } + if log.ValidateEnabled() { + log.Validate.Println("*** validateXRefTable begin ***") + } + + metaDataAuthoritative, err := metaDataModifiedAfterInfoDict(xRefTable) + if err != nil { + return err + } + + if metaDataAuthoritative { + // if both info dict and catalog metadata present and metadata modification date after infodict modification date + // validate document information dictionary before catalog metadata. + err := validateDocumentInfoObject(xRefTable) + if err != nil { + return err + } + } + + // Validate root object(aka the document catalog) and page tree. + err = validateRootObject(xRefTable) + if err != nil { + return err + } + + if !metaDataAuthoritative { + // Validate document information dictionary after catalog metadata. + err = validateDocumentInfoObject(xRefTable) + if err != nil { + return err + } + } + + // Validate offspec additional streams as declared in pdf trailer. + err = validateAdditionalStreams(xRefTable) + if err != nil { + return err + } + + xRefTable.Valid = true + + if log.ValidateEnabled() { + log.Validate.Println("*** validateXRefTable end ***") + } + + return nil +} + +func metaDataModifiedAfterInfoDict(xRefTable *model.XRefTable) (bool, error) { + rootDict, err := xRefTable.Catalog() + if err != nil { + return false, err + } + + xmpMeta, err := catalogMetaData(xRefTable, rootDict, OPTIONAL, model.V14) + if err != nil { + return false, err + } + + if xmpMeta == nil || xRefTable.Info == nil { + return false, nil + } + + xRefTable.CatalogXMPMeta = xmpMeta + + d, err := xRefTable.DereferenceDict(*xRefTable.Info) + if err != nil { + return false, err + } + if d == nil { + return true, nil + } + + modDate, ok := d["ModDate"] + if !ok { + return true, nil + } + + modTimestampInfoDict, err := timeOfDateObject(xRefTable, modDate, model.V10) + if err != nil { + return false, err + } + if modTimestampInfoDict == nil { + return true, nil + } + + modTimestampMetaData := time.Time(xmpMeta.RDF.Description.ModDate) + if modTimestampMetaData.IsZero() { + // xmlns:xap='http://ns.adobe.com/xap/1.0/ ...xap:ModifyDate='2006-06-05T21:58:13-05:00'> + //fmt.Println("metadata modificationDate is zero -> older than infodict") + return false, nil + } + + //fmt.Printf("infoDict: %s metaData: %s\n", modTimestampInfoDict, modTimestampMetaData) + + if *modTimestampInfoDict == modTimestampMetaData { + return false, nil + } + + infoDictOlderThanMetaDict := (*modTimestampInfoDict).Before(modTimestampMetaData) + + return infoDictOlderThanMetaDict, nil +} + +func validateRootVersion(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) error { + _, err := validateNameEntry(xRefTable, rootDict, "rootDict", "Version", OPTIONAL, sinceVersion, nil) + return err +} + +func validateExtensions(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) error { + // => 7.12 Extensions Dictionary + + _, err := validateDictEntry(xRefTable, rootDict, "rootDict", "Extensions", required, sinceVersion, nil) + + // No validation due to lack of documentation. + + return err +} + +func validatePageLabels(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) error { + // optional since PDF 1.3 + // => 7.9.7 Number Trees, 12.4.2 Page Labels + + // Dict or indirect ref to Dict + + ir := rootDict.IndirectRefEntry("PageLabels") + if ir == nil { + if required { + return errors.Errorf("validatePageLabels: required entry \"PageLabels\" missing") + } + return nil + } + + dictName := "PageLabels" + + // Version check + err := xRefTable.ValidateVersion(dictName, sinceVersion) + if err != nil { + return err + } + + d, err := xRefTable.DereferenceDict(*ir) + if err != nil { + return err + } + + _, _, err = validateNumberTree(xRefTable, "PageLabel", d, true) + + return err +} + +func validateNames(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) error { + // => 7.7.4 Name Dictionary + + d, err := validateDictEntry(xRefTable, rootDict, "rootDict", "Names", required, sinceVersion, nil) + if err != nil || d == nil { + return err + } + + validateNameTreeName := func(s string) bool { + return types.MemberOf(s, []string{"Dests", "AP", "JavaScript", "Pages", "Templates", "IDS", + "URLS", "EmbeddedFiles", "AlternatePresentations", "Renditions"}) + } + + for treeName, value := range d { + + if ok := validateNameTreeName(treeName); !ok { + if xRefTable.ValidationMode == model.ValidationStrict { + return errors.Errorf("validateNames: unknown name tree name: %s\n", treeName) + } + continue + } + + if xRefTable.Names[treeName] != nil { + // Already internalized. + continue + } + + d, err := xRefTable.DereferenceDict(value) + if err != nil { + return err + } + if len(d) == 0 { + continue + } + + _, _, tree, err := validateNameTree(xRefTable, treeName, d, true) + if err != nil { + return err + } + + if tree != nil { + // Internalize. + xRefTable.Names[treeName] = tree + } + + } + + return nil +} + +func validateNamedDestinations(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) error { + // => 12.3.2.3 Named Destinations + + // indRef or dict with destination array values. + + d, err := validateDictEntry(xRefTable, rootDict, "rootDict", "Dests", required, sinceVersion, nil) + if err != nil || d == nil { + return err + } + + for _, o := range d { + if _, err = validateDestination(xRefTable, o, false); err != nil { + return err + } + } + + return nil +} + +func pageLayoutValidator(v model.Version) func(s string) bool { + layouts := []string{"SinglePage", "OneColumn", "TwoColumnLeft", "TwoColumnRight"} + if v >= model.V15 { + layouts = append(layouts, "TwoPageLeft", "TwoPageRight") + } + validate := func(s string) bool { + return types.MemberOf(s, layouts) + } + return validate +} + +func validatePageLayout(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) error { + n, err := validateNameEntry(xRefTable, rootDict, "rootDict", "PageLayout", required, sinceVersion, pageLayoutValidator(xRefTable.Version())) + if err != nil { + return err + } + + if n != nil { + xRefTable.PageLayout = model.PageLayoutFor(n.String()) + } + + return nil +} + +func pageModeValidator(v model.Version) func(s string) bool { + // "None" is out of spec - but no need to repair. + modes := []string{"UseNone", "UseOutlines", "UseThumbs", "FullScreen", "None"} + if v >= model.V15 { + modes = append(modes, "UseOC") + } + if v >= model.V16 { + modes = append(modes, "UseAttachments") + } + return func(s string) bool { return types.MemberOf(s, modes) } +} + +func validatePageMode(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) error { + n, err := validateNameEntry(xRefTable, rootDict, "rootDict", "PageMode", required, sinceVersion, pageModeValidator(xRefTable.Version())) + if err != nil { + return err + } + + if n != nil { + xRefTable.PageMode = model.PageModeFor(n.String()) + } + + return nil +} + +func validateOpenAction(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) error { + // => 12.3.2 Destinations, 12.6 Actions + + // A value specifying a destination that shall be displayed + // or an action that shall be performed when the document is opened. + // The value shall be either an array defining a destination (see 12.3.2, "Destinations") + // or an action dictionary representing an action (12.6, "Actions"). + // + // If this entry is absent, the document shall be opened + // to the top of the first page at the default magnification factor. + + o, err := validateEntry(xRefTable, rootDict, "rootDict", "OpenAction", required, sinceVersion) + if err != nil || o == nil { + return err + } + + switch o := o.(type) { + + case types.Dict: + err = validateActionDict(xRefTable, o) + + case types.Array: + err = validateDestinationArray(xRefTable, o) + + default: + err = errors.New("pdfcpu: validateOpenAction: unexpected object") + } + + return err +} + +func validateURI(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) error { + // => 12.6.4.7 URI Actions + + // URI dict with one optional entry Base, ASCII string + + d, err := validateDictEntry(xRefTable, rootDict, "rootDict", "URI", required, sinceVersion, nil) + if err != nil || d == nil { + return err + } + + // Base, optional, ASCII string + _, err = validateStringEntry(xRefTable, d, "URIdict", "Base", OPTIONAL, model.V10, nil) + + return err +} + +func validateMarkInfo(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) error { + // => 14.7 Logical Structure + + d, err := validateDictEntry(xRefTable, rootDict, "rootDict", "MarkInfo", required, sinceVersion, nil) + if err != nil || d == nil { + return err + } + + var isTaggedPDF bool + + dictName := "markInfoDict" + + // Marked, optional, boolean + marked, err := validateBooleanEntry(xRefTable, d, dictName, "Marked", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + if marked != nil { + isTaggedPDF = *marked + } + + // Suspects: optional, since V1.6, boolean + sinceVersion = model.V16 + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V14 + } + suspects, err := validateBooleanEntry(xRefTable, d, dictName, "Suspects", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + if suspects != nil && *suspects { + isTaggedPDF = false + } + + xRefTable.Tagged = isTaggedPDF + + // UserProperties: optional, since V1.6, boolean + _, err = validateBooleanEntry(xRefTable, d, dictName, "UserProperties", OPTIONAL, model.V16, nil) + + return err +} + +func validateLang(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) error { + _, err := validateStringEntry(xRefTable, rootDict, "rootDict", "Lang", required, sinceVersion, nil) + return err +} + +func validateCaptureCommandDictArray(xRefTable *model.XRefTable, a types.Array) error { + for _, o := range a { + + d, err := xRefTable.DereferenceDict(o) + if err != nil { + return err + } + + if d == nil { + continue + } + + err = validateCaptureCommandDict(xRefTable, d) + if err != nil { + return err + } + + } + + return nil +} + +func validateWebCaptureInfoDict(xRefTable *model.XRefTable, d types.Dict) error { + dictName := "webCaptureInfoDict" + + // V, required, since V1.3, number + _, err := validateNumberEntry(xRefTable, d, dictName, "V", REQUIRED, model.V13, nil) + if err != nil { + return err + } + + // C, optional, since V1.3, array of web capture command dict indRefs + a, err := validateIndRefArrayEntry(xRefTable, d, dictName, "C", OPTIONAL, model.V13, nil) + if err != nil { + return err + } + + if a != nil { + err = validateCaptureCommandDictArray(xRefTable, a) + } + + return err +} + +func validateSpiderInfo(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) error { + // 14.10.2 Web Capture Information Dictionary + + d, err := validateDictEntry(xRefTable, rootDict, "rootDict", "SpiderInfo", required, sinceVersion, nil) + if err != nil || d == nil { + return err + } + + return validateWebCaptureInfoDict(xRefTable, d) +} + +func validateOutputIntentDict(xRefTable *model.XRefTable, d types.Dict) error { + dictName := "outputIntentDict" + + // Type, optional, name + _, err := validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, model.V10, func(s string) bool { return s == "OutputIntent" }) + if err != nil { + return err + } + + // S: required, name + _, err = validateNameEntry(xRefTable, d, dictName, "S", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + // OutputCondition, optional, text string + _, err = validateStringEntry(xRefTable, d, dictName, "OutputCondition", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // OutputConditionIdentifier, required, text string + _, err = validateStringEntry(xRefTable, d, dictName, "OutputConditionIdentifier", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + // RegistryName, optional, text string + _, err = validateStringEntry(xRefTable, d, dictName, "RegistryName", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // Info, optional, text string + _, err = validateStringEntry(xRefTable, d, dictName, "Info", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // DestOutputProfile, optional, streamDict + _, err = validateStreamDictEntry(xRefTable, d, dictName, "DestOutputProfile", OPTIONAL, model.V10, nil) + + return err +} + +func validateOutputIntents(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) error { + // => 14.11.5 Output Intents + + if xRefTable.ValidationMode == model.ValidationRelaxed { + sinceVersion = model.V13 + } + + a, err := validateArrayEntry(xRefTable, rootDict, "rootDict", "OutputIntents", required, sinceVersion, nil) + if err != nil || a == nil { + return err + } + + for _, o := range a { + + d, err := xRefTable.DereferenceDict(o) + if err != nil { + return err + } + + if d == nil { + continue + } + + err = validateOutputIntentDict(xRefTable, d) + if err != nil { + return err + } + } + + return nil +} + +func validatePieceDict(xRefTable *model.XRefTable, d types.Dict) error { + dictName := "pieceDict" + + for _, o := range d { + + d1, err := xRefTable.DereferenceDict(o) + if err != nil { + return err + } + + if d1 == nil { + continue + } + + required := REQUIRED + if xRefTable.ValidationMode == model.ValidationRelaxed { + required = OPTIONAL + } + _, err = validateDateEntry(xRefTable, d1, dictName, "LastModified", required, model.V10) + if err != nil { + return err + } + + _, err = validateEntry(xRefTable, d1, dictName, "Private", OPTIONAL, model.V10) + if err != nil { + return err + } + + } + + return nil +} + +func validateRootPieceInfo(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) error { + if xRefTable.ValidationMode == model.ValidationRelaxed { + return nil + } + + _, err := validatePieceInfo(xRefTable, rootDict, "rootDict", "PieceInfo", required, sinceVersion) + + return err +} + +func validatePieceInfo(xRefTable *model.XRefTable, d types.Dict, dictName, entryName string, required bool, sinceVersion model.Version) (hasPieceInfo bool, err error) { + // 14.5 Page-Piece Dictionaries + + pieceDict, err := validateDictEntry(xRefTable, d, dictName, entryName, required, sinceVersion, nil) + if err != nil || pieceDict == nil { + return false, err + } + + err = validatePieceDict(xRefTable, pieceDict) + + return hasPieceInfo, err +} + +// TODO implement +func validatePermissions(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) error { + // => 12.8.4 Permissions + + d, err := validateDictEntry(xRefTable, rootDict, "rootDict", "Permissions", required, sinceVersion, nil) + if err != nil || d == nil { + return err + } + + return errors.New("pdfcpu: validatePermissions: not supported") +} + +// TODO implement +func validateLegal(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) error { + // => 12.8.5 Legal Content Attestations + + d, err := validateDictEntry(xRefTable, rootDict, "rootDict", "Legal", required, sinceVersion, nil) + if err != nil || d == nil { + return err + } + + return errors.New("pdfcpu: validateLegal: not supported") +} + +func validateRequirementDict(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { + dictName := "requirementDict" + + // Type, optional, name, + _, err := validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, sinceVersion, func(s string) bool { return s == "Requirement" }) + if err != nil { + return err + } + + // S, required, name + _, err = validateNameEntry(xRefTable, d, dictName, "S", REQUIRED, sinceVersion, func(s string) bool { return s == "EnableJavaScripts" }) + if err != nil { + return err + } + + // The RH entry (requirement handler dicts) shall not be used in PDF 1.7. + + return nil +} + +func validateRequirements(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) error { + // => 12.10 Document Requirements + + a, err := validateArrayEntry(xRefTable, rootDict, "rootDict", "Requirements", required, sinceVersion, nil) + if err != nil || a == nil { + return err + } + + for _, o := range a { + + d, err := xRefTable.DereferenceDict(o) + if err != nil { + return err + } + + if d == nil { + continue + } + + err = validateRequirementDict(xRefTable, d, sinceVersion) + if err != nil { + return err + } + + } + + return nil +} + +func validateCollectionFieldDict(xRefTable *model.XRefTable, d types.Dict) error { + dictName := "colFlddict" + + _, err := validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, model.V10, func(s string) bool { return s == "CollectionField" }) + if err != nil { + return err + } + + // Subtype, required name + subTypes := []string{"S", "D", "N", "F", "Desc", "ModDate", "CreationDate", "Size"} + + if xRefTable.ValidationMode == model.ValidationRelaxed { + // See i659.pdf + subTypes = append(subTypes, "AFRelationship") + subTypes = append(subTypes, "CompressedSize") + } + + validateCollectionFieldSubtype := func(s string) bool { + return types.MemberOf(s, subTypes) + } + _, err = validateNameEntry(xRefTable, d, dictName, "Subtype", REQUIRED, model.V10, validateCollectionFieldSubtype) + if err != nil { + return err + } + + // N, required text string + _, err = validateStringEntry(xRefTable, d, dictName, "N", REQUIRED, model.V10, nil) + if err != nil { + return err + } + + // O, optional integer + _, err = validateIntegerEntry(xRefTable, d, dictName, "O", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // V, optional boolean + _, err = validateBooleanEntry(xRefTable, d, dictName, "V", OPTIONAL, model.V10, nil) + if err != nil { + return err + } + + // E, optional boolean + _, err = validateBooleanEntry(xRefTable, d, dictName, "E", OPTIONAL, model.V10, nil) + + return err +} + +func validateCollectionSchemaDict(xRefTable *model.XRefTable, d types.Dict) error { + for k, v := range d { + + if k == "Type" { + + var n types.Name + n, err := xRefTable.DereferenceName(v, model.V10, nil) + if err != nil { + return err + } + + if n != "CollectionSchema" { + return errors.New("pdfcpu: validateCollectionSchemaDict: invalid entry \"Type\"") + } + + continue + } + + d, err := xRefTable.DereferenceDict(v) + if err != nil { + return err + } + + if d == nil { + continue + } + + err = validateCollectionFieldDict(xRefTable, d) + if err != nil { + return err + } + + } + + return nil +} + +func validateCollectionSortDict(xRefTable *model.XRefTable, d types.Dict) error { + dictName := "colSortDict" + + // S, required name or array of names. + err := validateNameOrArrayOfNameEntry(xRefTable, d, dictName, "S", REQUIRED, model.V10) + if err != nil { + return err + } + + // A, optional boolean or array of booleans. + err = validateBooleanOrArrayOfBooleanEntry(xRefTable, d, dictName, "A", OPTIONAL, model.V10) + + return err +} + +func validateInitialView(s string) bool { return s == "D" || s == "T" || s == "H" } + +func validateCollection(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) error { + // => 12.3.5 Collections + + d, err := validateDictEntry(xRefTable, rootDict, "rootDict", "Collection", required, sinceVersion, nil) + if err != nil || d == nil { + return err + } + + dictName := "Collection" + + _, err = validateNameEntry(xRefTable, d, dictName, "Type", OPTIONAL, sinceVersion, func(s string) bool { return s == "Collection" }) + if err != nil { + return err + } + + // Schema, optional dict + d1, err := validateDictEntry(xRefTable, d, dictName, "Schema", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + if d1 != nil { + err = validateCollectionSchemaDict(xRefTable, d1) + if err != nil { + return err + } + } + + // D, optional string + _, err = validateStringEntry(xRefTable, d, dictName, "D", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + + // View, optional name + _, err = validateNameEntry(xRefTable, d, dictName, "View", OPTIONAL, sinceVersion, validateInitialView) + if err != nil { + return err + } + + // Sort, optional dict + d1, err = validateDictEntry(xRefTable, d, dictName, "Sort", OPTIONAL, sinceVersion, nil) + if err != nil { + return err + } + if d1 != nil { + err = validateCollectionSortDict(xRefTable, d1) + if err != nil { + return err + } + } + + return nil +} + +func validateNeedsRendering(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) error { + _, err := validateBooleanEntry(xRefTable, rootDict, "rootDict", "NeedsRendering", required, sinceVersion, nil) + return err +} + +func logURIError(xRefTable *model.XRefTable, pages []int) { + fmt.Println() + for _, page := range pages { + for uri, resp := range xRefTable.URIs[page] { + if resp != "" { + var s string + switch resp { + case "i": + s = "invalid url" + case "s": + s = "severe error" + default: + s = fmt.Sprintf("status=%s", resp) + } + if log.CLIEnabled() { + log.CLI.Printf("Page %d: %s %s\n", page, uri, s) + } + } + } + } +} + +func checkForBrokenLinks(xRefTable *model.XRefTable) error { + var httpErr bool + if log.CLIEnabled() { + log.CLI.Println("validating URIs..") + } + + pages := []int{} + for i := range xRefTable.URIs { + pages = append(pages, i) + } + sort.Ints(pages) + + client := http.Client{ + Timeout: 5 * time.Second, + } + + for _, page := range pages { + for uri := range xRefTable.URIs[page] { + if log.CLIEnabled() { + fmt.Printf(".") + } + _, err := url.ParseRequestURI(uri) + if err != nil { + httpErr = true + xRefTable.URIs[page][uri] = "i" + continue + } + res, err := client.Get(uri) + if err != nil { + httpErr = true + xRefTable.URIs[page][uri] = "s" + continue + } + defer res.Body.Close() + if res.StatusCode != 200 { + httpErr = true + xRefTable.URIs[page][uri] = strconv.Itoa(res.StatusCode) + continue + } + } + } + + if log.CLIEnabled() { + logURIError(xRefTable, pages) + } + + if httpErr { + return errors.New("broken links detected") + } + + return nil +} + +func validateRootObject(xRefTable *model.XRefTable) error { + if log.ValidateEnabled() { + log.Validate.Println("*** validateRootObject begin ***") + } + + // => 7.7.2 Document Catalog + + // Entry opt since type info + // ------------------------------------------------------------------------------------ + // Type n string "Catalog" + // Version y 1.4 name overrules header version if later + // Extensions y ISO 32000 dict => 7.12 Extensions Dictionary + // Pages n - (dict) => 7.7.3 Page Tree + // PageLabels y 1.3 number tree => 7.9.7 Number Trees, 12.4.2 Page Labels + // Names y 1.2 dict => 7.7.4 Name Dictionary + // Dests y only 1.1 (dict) => 12.3.2.3 Named Destinations + // ViewerPreferences y 1.2 dict => 12.2 Viewer Preferences + // PageLayout y - name /SinglePage, /OneColumn etc. + // PageMode y - name /UseNone, /FullScreen etc. + // Outlines y - (dict) => 12.3.3 Document Outline + // Threads y 1.1 (array) => 12.4.3 Articles + // OpenAction y 1.1 array or dict => 12.3.2 Destinations, 12.6 Actions + // AA y 1.4 dict => 12.6.3 Trigger Events + // URI y 1.1 dict => 12.6.4.7 URI Actions + // AcroForm y 1.2 dict => 12.7.2 Interactive Form Dictionary + // Metadata y 1.4 (stream) => 14.3.2 Metadata Streams + // StructTreeRoot y 1.3 dict => 14.7.2 Structure Hierarchy + // Markinfo y 1.4 dict => 14.7 Logical Structure + // Lang y 1.4 string + // SpiderInfo y 1.3 dict => 14.10.2 Web Capture Information Dictionary + // OutputIntents y 1.4 array => 14.11.5 Output Intents + // PieceInfo y 1.4 dict => 14.5 Page-Piece Dictionaries + // OCProperties y 1.5 dict => 8.11.4 Configuring Optional Content + // Perms y 1.5 dict => 12.8.4 Permissions + // Legal y 1.5 dict => 12.8.5 Legal Content Attestations + // Requirements y 1.7 array => 12.10 Document Requirements + // Collection y 1.7 dict => 12.3.5 Collections + // NeedsRendering y 1.7 boolean => XML Forms Architecture (XFA) Spec. + + // DSS y 2.0 dict => 12.8.4.3 Document Security Store TODO + // AF y 2.0 array of dicts => 14.3 Associated Files TODO + // DPartRoot y 2.0 dict => 14.12 Document parts TODO + + d, err := xRefTable.Catalog() + if err != nil { + return err + } + + // Type + _, err = validateNameEntry(xRefTable, d, "rootDict", "Type", REQUIRED, model.V10, func(s string) bool { return s == "Catalog" }) + if err != nil { + return err + } + + // Pages + rootPageNodeDict, err := validatePages(xRefTable, d) + if err != nil { + return err + } + + for _, f := range []struct { + validate func(xRefTable *model.XRefTable, d types.Dict, required bool, sinceVersion model.Version) (err error) + required bool + sinceVersion model.Version + }{ + {validateRootVersion, OPTIONAL, model.V14}, + {validateExtensions, OPTIONAL, model.V10}, + {validatePageLabels, OPTIONAL, model.V13}, + {validateNames, OPTIONAL, model.V12}, + {validateNamedDestinations, OPTIONAL, model.V11}, + {validateViewerPreferences, OPTIONAL, model.V12}, + {validatePageLayout, OPTIONAL, model.V10}, + {validatePageMode, OPTIONAL, model.V10}, + {validateOutlines, OPTIONAL, model.V10}, + {validateThreads, OPTIONAL, model.V11}, + {validateOpenAction, OPTIONAL, model.V11}, + {validateRootAdditionalActions, OPTIONAL, model.V14}, + {validateURI, OPTIONAL, model.V11}, + {validateForm, OPTIONAL, model.V12}, + {validateRootMetadata, OPTIONAL, model.V14}, + {validateStructTree, OPTIONAL, model.V13}, + {validateMarkInfo, OPTIONAL, model.V14}, + {validateLang, OPTIONAL, model.V10}, + {validateSpiderInfo, OPTIONAL, model.V13}, + {validateOutputIntents, OPTIONAL, model.V14}, + {validateRootPieceInfo, OPTIONAL, model.V14}, + {validateOCProperties, OPTIONAL, model.V15}, + {validatePermissions, OPTIONAL, model.V15}, + {validateLegal, OPTIONAL, model.V17}, + {validateRequirements, OPTIONAL, model.V17}, + {validateCollection, OPTIONAL, model.V17}, + {validateNeedsRendering, OPTIONAL, model.V17}, + } { + if !f.required && xRefTable.Version() < f.sinceVersion { + // Ignore optional fields if currentVersion < sinceVersion + // This is really a workaround for explicitly extending relaxed validation. + continue + } + err = f.validate(xRefTable, d, f.required, f.sinceVersion) + if err != nil { + return err + } + } + + // Validate remainder of annotations after AcroForm validation only. + _, err = validatePagesAnnotations(xRefTable, rootPageNodeDict, 0) + + if xRefTable.ValidateLinks && len(xRefTable.URIs) > 0 { + err = checkForBrokenLinks(xRefTable) + } + + if err == nil { + if log.ValidateEnabled() { + log.Validate.Println("*** validateRootObject end ***") + } + } + + return err +} + +func validateAdditionalStreams(xRefTable *model.XRefTable) error { + // Out of spec scope. + return nil +} diff --git a/pkg/pdfcpu/write.go b/pkg/pdfcpu/write.go new file mode 100644 index 0000000000000000000000000000000000000000..39b8f24b14ad62cc3bcb830de5fd2947d8c0effa --- /dev/null +++ b/pkg/pdfcpu/write.go @@ -0,0 +1,1085 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 pdfcpu + +import ( + "bufio" + "bytes" + "encoding/hex" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/filter" + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +func writeObjects(ctx *model.Context) error { + // Write root object(aka the document catalog) and page tree. + if err := writeRootObject(ctx); err != nil { + return err + } + + if log.WriteEnabled() { + log.Write.Printf("offset after writeRootObject: %d\n", ctx.Write.Offset) + } + + // Write document information dictionary. + if err := writeDocumentInfoDict(ctx); err != nil { + return err + } + + if log.WriteEnabled() { + log.Write.Printf("offset after writeInfoObject: %d\n", ctx.Write.Offset) + } + + // Write offspec additional streams as declared in pdf trailer. + if err := writeAdditionalStreams(ctx); err != nil { + return err + } + + return writeEncryptDict(ctx) +} + +// Write generates a PDF file for the cross reference table contained in Context. +func Write(ctx *model.Context) (err error) { + // Create a writer for dirname and filename if not already supplied. + if ctx.Write.Writer == nil { + + fileName := filepath.Join(ctx.Write.DirName, ctx.Write.FileName) + if log.CLIEnabled() { + log.CLI.Printf("writing to %s\n", fileName) + } + + file, err := os.Create(fileName) + if err != nil { + return errors.Wrapf(err, "can't create %s\n%s", fileName, err) + } + + ctx.Write.Writer = bufio.NewWriter(file) + + defer func() { + + // The underlying bufio.Writer has already been flushed. + + // Processing error takes precedence. + if err != nil { + file.Close() + return + } + + // Do not miss out on closing errors. + err = file.Close() + + }() + + } + + if err = prepareContextForWriting(ctx); err != nil { + return err + } + + // if exists metadata, update from info dict + // else if v2 create from scratch + // else nothing just write info dict + + // Since we support PDF Collections (since V1.7) for file attachments + // we need to generate V1.7 PDF files. + v := model.V17 + + if ctx.Version() == model.V20 { + v = model.V20 + } + + if err = writeHeader(ctx.Write, v); err != nil { + return err + } + + // Ensure there is no root version. + if ctx.RootVersion != nil { + ctx.RootDict.Delete("Version") + } + + if log.WriteEnabled() { + log.Write.Printf("offset after writeHeader: %d\n", ctx.Write.Offset) + } + + if err := writeObjects(ctx); err != nil { + return err + } + + // Mark redundant objects as free. + // eg. duplicate resources, compressed objects, linearization dicts.. + deleteRedundantObjects(ctx) + + if err = writeXRef(ctx); err != nil { + return err + } + + // Write pdf trailer. + if err = writeTrailer(ctx.Write); err != nil { + return err + } + + if err = setFileSizeOfWrittenFile(ctx.Write); err != nil { + return err + } + + if ctx.Read != nil { + ctx.Write.BinaryImageSize = ctx.Read.BinaryImageSize + ctx.Write.BinaryFontSize = ctx.Read.BinaryFontSize + logWriteStats(ctx) + } + + return nil +} + +// WriteIncrement writes a PDF increment.. +func WriteIncrement(ctx *model.Context) error { + // Write all modified objects that are part of this increment. + for _, i := range ctx.Write.ObjNrs { + if err := writeFlatObject(ctx, i); err != nil { + return err + } + } + + if err := writeXRef(ctx); err != nil { + return err + } + + return writeTrailer(ctx.Write) +} + +func prepareContextForWriting(ctx *model.Context) error { + if err := ensureInfoDictAndFileID(ctx); err != nil { + return err + } + + return handleEncryption(ctx) +} + +func writeAdditionalStreams(ctx *model.Context) error { + if ctx.AdditionalStreams == nil { + return nil + } + + if _, _, err := writeDeepObject(ctx, ctx.AdditionalStreams); err != nil { + return err + } + + return nil +} + +func ensureFileID(ctx *model.Context) error { + fid, err := fileID(ctx) + if err != nil { + return err + } + + if ctx.ID == nil { + // Ensure ctx.ID + ctx.ID = types.Array{fid, fid} + return nil + } + + // Update ctx.ID + a := ctx.ID + if len(a) != 2 { + return errors.New("pdfcpu: ID must be an array with 2 elements") + } + + a[1] = fid + + return nil +} + +func ensureInfoDictAndFileID(ctx *model.Context) error { + if ctx.Version() < model.V20 { + if err := ensureInfoDict(ctx); err != nil { + return err + } + } + + return ensureFileID(ctx) +} + +// Write root entry to disk. +func writeRootEntry(ctx *model.Context, d types.Dict, dictName, entryName string, statsAttr int) error { + o, err := writeEntry(ctx, d, dictName, entryName) + if err != nil { + return err + } + + if o != nil { + ctx.Stats.AddRootAttr(statsAttr) + } + + return nil +} + +// Write root entry to object stream. +func writeRootEntryToObjStream(ctx *model.Context, d types.Dict, dictName, entryName string, statsAttr int) error { + ctx.Write.WriteToObjectStream = true + + if err := writeRootEntry(ctx, d, dictName, entryName, statsAttr); err != nil { + return err + } + + return stopObjectStream(ctx) +} + +// Write page tree. +func writePages(ctx *model.Context, rootDict types.Dict) error { + // Page tree root (the top "Pages" dict) must be indirect reference. + indRef := rootDict.IndirectRefEntry("Pages") + if indRef == nil { + return errors.New("pdfcpu: writePages: missing indirect obj for pages dict") + } + + // Embed all page tree objects into objects stream. + ctx.Write.WriteToObjectStream = true + + // Write page tree. + p := 0 + if _, _, err := writePagesDict(ctx, indRef, &p); err != nil { + return err + } + + return stopObjectStream(ctx) +} + +func writeRootAttrsBatch1(ctx *model.Context, d types.Dict, dictName string) error { + for _, e := range []struct { + entryName string + statsAttr int + }{ + {"Extensions", model.RootExtensions}, + {"PageLabels", model.RootPageLabels}, + {"Names", model.RootNames}, + {"Dests", model.RootDests}, + {"ViewerPreferences", model.RootViewerPrefs}, + {"PageLayout", model.RootPageLayout}, + {"PageMode", model.RootPageMode}, + {"Outlines", model.RootOutlines}, + {"Threads", model.RootThreads}, + {"OpenAction", model.RootOpenAction}, + {"AA", model.RootAA}, + {"URI", model.RootURI}, + {"AcroForm", model.RootAcroForm}, + {"Metadata", model.RootMetadata}, + } { + if err := writeRootEntry(ctx, d, dictName, e.entryName, e.statsAttr); err != nil { + return err + } + } + + return nil +} + +func writeRootAttrsBatch2(ctx *model.Context, d types.Dict, dictName string) error { + for _, e := range []struct { + entryName string + statsAttr int + }{ + {"MarkInfo", model.RootMarkInfo}, + {"Lang", model.RootLang}, + {"SpiderInfo", model.RootSpiderInfo}, + {"OutputIntents", model.RootOutputIntents}, + {"PieceInfo", model.RootPieceInfo}, + {"OCProperties", model.RootOCProperties}, + {"Perms", model.RootPerms}, + {"Legal", model.RootLegal}, + {"Requirements", model.RootRequirements}, + {"Collection", model.RootCollection}, + {"NeedsRendering", model.RootNeedsRendering}, + } { + if err := writeRootEntry(ctx, d, dictName, e.entryName, e.statsAttr); err != nil { + return err + } + } + + return nil +} + +func writeRootObject(ctx *model.Context) error { + // => 7.7.2 Document Catalog + + xRefTable := ctx.XRefTable + catalog := *xRefTable.Root + objNumber := int(catalog.ObjectNumber) + genNumber := int(catalog.GenerationNumber) + + if log.WriteEnabled() { + log.Write.Printf("*** writeRootObject: begin offset=%d *** %s\n", ctx.Write.Offset, catalog) + } + + // Ensure corresponding and accurate name tree object graphs. + if !ctx.ApplyReducedFeatureSet() { + if err := ctx.BindNameTrees(); err != nil { + return err + } + } + + d, err := xRefTable.DereferenceDict(catalog) + if err != nil { + return err + } + + if d == nil { + return errors.Errorf("pdfcpu: writeRootObject: unable to dereference root dict") + } + + dictName := "rootDict" + + if ctx.ApplyReducedFeatureSet() { + log.Write.Println("writeRootObject - reducedFeatureSet:exclude complex entries.") + d.Delete("Names") + d.Delete("Dests") + d.Delete("Outlines") + d.Delete("OpenAction") + d.Delete("StructTreeRoot") + d.Delete("OCProperties") + } + + if err = writeDictObject(ctx, objNumber, genNumber, d); err != nil { + return err + } + + if log.WriteEnabled() { + log.Write.Printf("writeRootObject: %s\n", d) + log.Write.Printf("writeRootObject: new offset after rootDict = %d\n", ctx.Write.Offset) + } + + if err = writeRootEntry(ctx, d, dictName, "Version", model.RootVersion); err != nil { + return err + } + + if err = writePages(ctx, d); err != nil { + return err + } + + if err := writeRootAttrsBatch1(ctx, d, dictName); err != nil { + return err + } + + if err = writeRootEntryToObjStream(ctx, d, dictName, "StructTreeRoot", model.RootStructTreeRoot); err != nil { + return err + } + + if err := writeRootAttrsBatch2(ctx, d, dictName); err != nil { + return err + } + + if log.WriteEnabled() { + log.Write.Printf("*** writeRootObject: end offset=%d ***\n", ctx.Write.Offset) + } + + return nil +} + +func writeTrailerDict(ctx *model.Context) error { + if log.WriteEnabled() { + log.Write.Printf("writeTrailerDict begin\n") + } + + w := ctx.Write + xRefTable := ctx.XRefTable + + if _, err := w.WriteString("trailer"); err != nil { + return err + } + + if err := w.WriteEol(); err != nil { + return err + } + + d := types.NewDict() + d.Insert("Size", types.Integer(*xRefTable.Size)) + d.Insert("Root", *xRefTable.Root) + + if xRefTable.Info != nil { + d.Insert("Info", *xRefTable.Info) + } + + if ctx.Encrypt != nil && ctx.EncKey != nil { + d.Insert("Encrypt", *ctx.Encrypt) + } + + if xRefTable.ID != nil { + d.Insert("ID", xRefTable.ID) + } + + if ctx.Write.Increment { + d.Insert("Prev", types.Integer(*ctx.Write.OffsetPrevXRef)) + } + + if _, err := w.WriteString(d.PDFString()); err != nil { + return err + } + + if log.WriteEnabled() { + log.Write.Printf("writeTrailerDict end\n") + } + + return nil +} + +func writeXRefSubsection(ctx *model.Context, start int, size int) error { + if log.WriteEnabled() { + log.Write.Printf("writeXRefSubsection: start=%d size=%d\n", start, size) + } + + w := ctx.Write + + if _, err := w.WriteString(fmt.Sprintf("%d %d%s", start, size, w.Eol)); err != nil { + return err + } + + var lines []string + + for i := start; i < start+size; i++ { + + entry := ctx.XRefTable.Table[i] + + if entry.Compressed { + return errors.New("pdfcpu: writeXRefSubsection: compressed entries present") + } + + var s string + + if entry.Free { + s = fmt.Sprintf("%010d %05d f%2s", *entry.Offset, *entry.Generation, w.Eol) + } else { + var off int64 + writeOffset, found := ctx.Write.Table[i] + if found { + off = writeOffset + } + s = fmt.Sprintf("%010d %05d n%2s", off, *entry.Generation, w.Eol) + } + + lines = append(lines, fmt.Sprintf("%d: %s", i, s)) + + if _, err := w.WriteString(s); err != nil { + return err + } + } + + if log.WriteEnabled() { + log.Write.Printf("\n%s\n", strings.Join(lines, "")) + log.Write.Printf("writeXRefSubsection: end\n") + } + + return nil +} + +func deleteRedundantObject(ctx *model.Context, objNr int) { + if len(ctx.Write.SelectedPages) == 0 && + (ctx.Optimize.IsDuplicateFontObject(objNr) || ctx.Optimize.IsDuplicateImageObject(objNr)) { + ctx.FreeObject(objNr) + } + + if ctx.IsLinearizationObject(objNr) || ctx.Optimize.IsDuplicateInfoObject(objNr) || + ctx.Read.IsObjectStreamObject(objNr) { + ctx.FreeObject(objNr) + } + +} + +func detectLinearizationObjs(xRefTable *model.XRefTable, entry *model.XRefTableEntry, i int) { + if _, ok := entry.Object.(types.StreamDict); ok { + + if *entry.Offset == *xRefTable.OffsetPrimaryHintTable { + xRefTable.LinearizationObjs[i] = true + if log.WriteEnabled() { + log.Write.Printf("detectLinearizationObjs: primaryHintTable at obj #%d\n", i) + } + } + + if xRefTable.OffsetOverflowHintTable != nil && + *entry.Offset == *xRefTable.OffsetOverflowHintTable { + xRefTable.LinearizationObjs[i] = true + if log.WriteEnabled() { + log.Write.Printf("detectLinearizationObjs: overflowHintTable at obj #%d\n", i) + } + } + + } +} + +func deleteRedundantObjects(ctx *model.Context) { + if ctx.Optimize == nil { + return + } + + xRefTable := ctx.XRefTable + + if log.WriteEnabled() { + log.Write.Printf("deleteRedundantObjects begin: Size=%d\n", *xRefTable.Size) + } + + for i := 0; i < *xRefTable.Size; i++ { + + // Missing object remains missing. + entry, found := xRefTable.Find(i) + if !found { + continue + } + + // Free object + if entry.Free { + continue + } + + // Object written + if ctx.Write.HasWriteOffset(i) { + // Resources may be cross referenced from different objects + // eg. font descriptors may be shared by different font dicts. + // Try to remove this object from the list of the potential duplicate objects. + if log.WriteEnabled() { + log.Write.Printf("deleteRedundantObjects: remove duplicate obj #%d\n", i) + } + delete(ctx.Optimize.DuplicateFontObjs, i) + delete(ctx.Optimize.DuplicateImageObjs, i) + delete(ctx.Optimize.DuplicateInfoObjects, i) + continue + } + + // Object not written + + if ctx.Read.Linearized && entry.Offset != nil { + // This block applies to pre existing objects only. + // Since there is no type entry for stream dicts associated with linearization dicts + // we have to check every StreamDict that has not been written. + detectLinearizationObjs(xRefTable, entry, i) + } + + deleteRedundantObject(ctx, i) + } + + if log.WriteEnabled() { + log.Write.Println("deleteRedundantObjects end") + } +} + +func sortedWritableKeys(ctx *model.Context) []int { + var keys []int + + for i, e := range ctx.Table { + if !ctx.Write.Increment && e.Free || ctx.Write.HasWriteOffset(i) { + keys = append(keys, i) + } + } + + sort.Ints(keys) + + return keys +} + +// After inserting the last object write the cross reference table to disk. +func writeXRefTable(ctx *model.Context) error { + keys := sortedWritableKeys(ctx) + + objCount := len(keys) + if log.WriteEnabled() { + log.Write.Printf("xref has %d entries\n", objCount) + } + + if _, err := ctx.Write.WriteString("xref"); err != nil { + return err + } + + if err := ctx.Write.WriteEol(); err != nil { + return err + } + + start := keys[0] + size := 1 + + for i := 1; i < len(keys); i++ { + + if keys[i]-keys[i-1] > 1 { + + if err := writeXRefSubsection(ctx, start, size); err != nil { + return err + } + + start = keys[i] + size = 1 + continue + } + + size++ + } + + if err := writeXRefSubsection(ctx, start, size); err != nil { + return err + } + + if err := writeTrailerDict(ctx); err != nil { + return err + } + + if err := ctx.Write.WriteEol(); err != nil { + return err + } + + if _, err := ctx.Write.WriteString("startxref"); err != nil { + return err + } + + if err := ctx.Write.WriteEol(); err != nil { + return err + } + + if _, err := ctx.Write.WriteString(fmt.Sprintf("%d", ctx.Write.Offset)); err != nil { + return err + } + + return ctx.Write.WriteEol() +} + +// int64ToBuf returns a byte slice with length byteCount representing integer i. +func int64ToBuf(i int64, byteCount int) (buf []byte) { + j := 0 + var b []byte + + for k := i; k > 0; { + b = append(b, byte(k&0xff)) + k >>= 8 + j++ + } + + // Swap byte order + for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 { + b[i], b[j] = b[j], b[i] + } + + if j < byteCount { + buf = append(bytes.Repeat([]byte{0}, byteCount-j), b...) + } else { + buf = b + } + + return +} + +func createXRefStream(ctx *model.Context, i1, i2, i3 int, objNrs []int) ([]byte, *types.Array, error) { + if log.WriteEnabled() { + log.Write.Println("createXRefStream begin") + } + + xRefTable := ctx.XRefTable + + var ( + buf []byte + a types.Array + ) + + objCount := len(objNrs) + if log.WriteEnabled() { + log.Write.Printf("createXRefStream: xref has %d entries\n", objCount) + } + + start := objNrs[0] + size := 0 + + for i := 0; i < len(objNrs); i++ { + + j := objNrs[i] + entry := xRefTable.Table[j] + var s1, s2, s3 []byte + + if entry.Free { + + // unused + if log.WriteEnabled() { + log.Write.Printf("createXRefStream: unused i=%d nextFreeAt:%d gen:%d\n", j, int(*entry.Offset), int(*entry.Generation)) + } + + s1 = int64ToBuf(0, i1) + s2 = int64ToBuf(*entry.Offset, i2) + s3 = int64ToBuf(int64(*entry.Generation), i3) + + } else if entry.Compressed { + + // in use, compressed into object stream + if log.WriteEnabled() { + log.Write.Printf("createXRefStream: compressed i=%d at objstr %d[%d]\n", j, int(*entry.ObjectStream), int(*entry.ObjectStreamInd)) + } + + s1 = int64ToBuf(2, i1) + s2 = int64ToBuf(int64(*entry.ObjectStream), i2) + s3 = int64ToBuf(int64(*entry.ObjectStreamInd), i3) + + } else { + + off, found := ctx.Write.Table[j] + if !found { + return nil, nil, errors.Errorf("pdfcpu: createXRefStream: missing write offset for obj #%d\n", i) + } + + // in use, uncompressed + if log.WriteEnabled() { + log.Write.Printf("createXRefStream: used i=%d offset:%d gen:%d\n", j, int(off), int(*entry.Generation)) + } + + s1 = int64ToBuf(1, i1) + s2 = int64ToBuf(off, i2) + s3 = int64ToBuf(int64(*entry.Generation), i3) + + } + + if log.WriteEnabled() { + log.Write.Printf("createXRefStream: written: %x %x %x \n", s1, s2, s3) + } + + buf = append(buf, s1...) + buf = append(buf, s2...) + buf = append(buf, s3...) + + if i > 0 && (objNrs[i]-objNrs[i-1] > 1) { + + a = append(a, types.Integer(start)) + a = append(a, types.Integer(size)) + + start = objNrs[i] + size = 1 + continue + } + + size++ + } + + a = append(a, types.Integer(start)) + a = append(a, types.Integer(size)) + + if log.WriteEnabled() { + log.Write.Println("createXRefStream end") + } + + return buf, &a, nil +} + +// NewXRefStreamDict creates a new PDFXRefStreamDict object. +func newXRefStreamDict(ctx *model.Context) *types.XRefStreamDict { + sd := types.StreamDict{Dict: types.NewDict()} + sd.Insert("Type", types.Name("XRef")) + sd.Insert("Filter", types.Name(filter.Flate)) + sd.FilterPipeline = []types.PDFFilter{{Name: filter.Flate, DecodeParms: nil}} + sd.Insert("Root", *ctx.Root) + if ctx.Info != nil { + sd.Insert("Info", *ctx.Info) + } + if ctx.ID != nil { + sd.Insert("ID", ctx.ID) + } + if ctx.Encrypt != nil && ctx.EncKey != nil { + sd.Insert("Encrypt", *ctx.Encrypt) + } + if ctx.Write.Increment { + sd.Insert("Prev", types.Integer(*ctx.Write.OffsetPrevXRef)) + } + return &types.XRefStreamDict{StreamDict: sd} +} + +func writeXRefStream(ctx *model.Context) error { + if log.WriteEnabled() { + log.Write.Println("writeXRefStream begin") + } + + xRefTable := ctx.XRefTable + xRefStreamDict := newXRefStreamDict(ctx) + xRefTableEntry := model.NewXRefTableEntryGen0(*xRefStreamDict) + + // Reuse free objects (including recycled objects from this run). + objNumber, err := xRefTable.InsertAndUseRecycled(*xRefTableEntry) + if err != nil { + return err + } + + xRefStreamDict.Insert("Size", types.Integer(*xRefTable.Size)) + + // Include xref stream dict obj within xref stream dict. + offset := ctx.Write.Offset + ctx.Write.SetWriteOffset(objNumber) + + i2Base := int64(*ctx.Size) + if offset > i2Base { + i2Base = offset + } + + i1 := 1 // 0, 1 or 2 always fit into 1 byte. + + i2 := func(i int64) (byteCount int) { + for i > 0 { + i >>= 8 + byteCount++ + } + return byteCount + }(i2Base) + + i3 := 2 // scale for max objectstream index <= 0x ff ff + + wArr := types.Array{types.Integer(i1), types.Integer(i2), types.Integer(i3)} + xRefStreamDict.Insert("W", wArr) + + // Generate xRefStreamDict data = xref entries -> xRefStreamDict.Content + objNrs := sortedWritableKeys(ctx) + content, indArr, err := createXRefStream(ctx, i1, i2, i3, objNrs) + if err != nil { + return err + } + + xRefStreamDict.Content = content + xRefStreamDict.Insert("Index", *indArr) + + // Encode xRefStreamDict.Content -> xRefStreamDict.Raw + if err = xRefStreamDict.StreamDict.Encode(); err != nil { + return err + } + + if log.WriteEnabled() { + log.Write.Printf("writeXRefStream: xRefStreamDict: %s\n", xRefStreamDict) + } + + if err = writeStreamDictObject(ctx, objNumber, 0, xRefStreamDict.StreamDict); err != nil { + return err + } + + w := ctx.Write + + if _, err = w.WriteString("startxref"); err != nil { + return err + } + + if err = w.WriteEol(); err != nil { + return err + } + + if _, err = w.WriteString(fmt.Sprintf("%d", offset)); err != nil { + return err + } + + if err = w.WriteEol(); err != nil { + return err + } + + if log.WriteEnabled() { + log.Write.Println("writeXRefStream end") + } + + return nil +} + +func writeEncryptDict(ctx *model.Context) error { + // Bail out unless we really have to write encrypted. + if ctx.Encrypt == nil || ctx.EncKey == nil { + return nil + } + + indRef := *ctx.Encrypt + objNumber := int(indRef.ObjectNumber) + genNumber := int(indRef.GenerationNumber) + + d, err := ctx.DereferenceDict(indRef) + if err != nil { + return err + } + + return writeObject(ctx, objNumber, genNumber, d.PDFString()) +} + +func setupEncryption(ctx *model.Context) error { + var err error + + if ok := validateAlgorithm(ctx); !ok { + return errors.New("pdfcpu: unsupported encryption algorithm (PDF 2.0 assumes AES/256)") + } + + d := newEncryptDict( + ctx.Version(), + ctx.EncryptUsingAES, + ctx.EncryptKeyLength, + int16(ctx.Permissions), + ) + + if ctx.E, err = supportedEncryption(ctx, d); err != nil { + return err + } + + if ctx.ID == nil { + return errors.New("pdfcpu: encrypt: missing ID") + } + + if ctx.E.ID, err = ctx.IDFirstElement(); err != nil { + return err + } + + if err = calcOAndU(ctx, d); err != nil { + return err + } + + if err = writePermissions(ctx, d); err != nil { + return err + } + + xRefTableEntry := model.NewXRefTableEntryGen0(d) + + // Reuse free objects (including recycled objects from this run). + objNumber, err := ctx.InsertAndUseRecycled(*xRefTableEntry) + if err != nil { + return err + } + + ctx.Encrypt = types.NewIndirectRef(objNumber, 0) + + return nil +} + +func updateEncryption(ctx *model.Context) error { + if ctx.Encrypt == nil { + return errors.New("pdfcpu: This file is not encrypted - nothing written.") + } + + d, err := ctx.EncryptDict() + if err != nil { + return err + } + + if ctx.Cmd == model.SETPERMISSIONS { + //fmt.Printf("updating permissions to: %v\n", ctx.UserAccessPermissions) + ctx.E.P = int(ctx.Permissions) + d.Update("P", types.Integer(ctx.E.P)) + // and moving on, U is dependent on P + } + + // ctx.Cmd == CHANGEUPW or CHANGE OPW + + if ctx.UserPWNew != nil { + //fmt.Printf("change upw from <%s> to <%s>\n", ctx.UserPW, *ctx.UserPWNew) + ctx.UserPW = *ctx.UserPWNew + } + + if ctx.OwnerPWNew != nil { + //fmt.Printf("change opw from <%s> to <%s>\n", ctx.OwnerPW, *ctx.OwnerPWNew) + ctx.OwnerPW = *ctx.OwnerPWNew + } + + if ctx.E.R == 5 || ctx.E.R == 6 { + + if err = calcOAndU(ctx, d); err != nil { + return err + } + + // Calc Perms for rev 5, 6. + return writePermissions(ctx, d) + } + + //fmt.Printf("opw before: length:%d <%s>\n", len(ctx.E.O), ctx.E.O) + if ctx.E.O, err = o(ctx); err != nil { + return err + } + //fmt.Printf("opw after: length:%d <%s> %0X\n", len(ctx.E.O), ctx.E.O, ctx.E.O) + d.Update("O", types.HexLiteral(hex.EncodeToString(ctx.E.O))) + + //fmt.Printf("upw before: length:%d <%s>\n", len(ctx.E.U), ctx.E.U) + if ctx.E.U, ctx.EncKey, err = u(ctx); err != nil { + return err + } + //fmt.Printf("upw after: length:%d <%s> %0X\n", len(ctx.E.U), ctx.E.U, ctx.E.U) + //fmt.Printf("encKey = %0X\n", ctx.EncKey) + d.Update("U", types.HexLiteral(hex.EncodeToString(ctx.E.U))) + + return nil +} + +func handleEncryption(ctx *model.Context) error { + + if ctx.Cmd == model.ENCRYPT || ctx.Cmd == model.DECRYPT { + + if ctx.Cmd == model.DECRYPT { + + // Remove encryption. + ctx.EncKey = nil + + } else { + + if err := setupEncryption(ctx); err != nil { + return err + } + + alg := "RC4" + if ctx.EncryptUsingAES { + alg = "AES" + } + if log.CLIEnabled() { + log.CLI.Printf("using %s-%d\n", alg, ctx.EncryptKeyLength) + } + } + + } else if ctx.UserPWNew != nil || ctx.OwnerPWNew != nil || ctx.Cmd == model.SETPERMISSIONS { + + if err := updateEncryption(ctx); err != nil { + return err + } + + } + + // write xrefstream if using xrefstream only. + if ctx.Encrypt != nil && ctx.EncKey != nil && !ctx.Read.UsingXRefStreams { + ctx.WriteObjectStream = false + ctx.WriteXRefStream = false + } + + return nil +} + +func writeXRef(ctx *model.Context) error { + if ctx.WriteXRefStream { + // Write cross reference stream and generate objectstreams. + return writeXRefStream(ctx) + } + + // Write cross reference table section. + return writeXRefTable(ctx) +} + +func setFileSizeOfWrittenFile(w *model.WriteContext) error { + if err := w.Flush(); err != nil { + return err + } + + // If writing is Writer based then f is nil. + if w.Fp == nil { + return nil + } + + fileInfo, err := w.Fp.Stat() + if err != nil { + return err + } + + w.FileSize = fileInfo.Size() + + return nil +} diff --git a/pkg/pdfcpu/writeImage.go b/pkg/pdfcpu/writeImage.go new file mode 100644 index 0000000000000000000000000000000000000000..6d8e52022b5150e19ed2f73f74f91a6eef436bae --- /dev/null +++ b/pkg/pdfcpu/writeImage.go @@ -0,0 +1,994 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 pdfcpu + +import ( + "bytes" + "encoding/gob" + "image" + "image/color" + "image/png" + "io" + "os" + "strings" + + "github.com/hhrutter/tiff" + "github.com/pdfcpu/pdfcpu/pkg/filter" + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// Errors to be identified. +var ( + ErrUnsupported16BPC = errors.New("unsupported 16 bits per component") +) + +// colValRange defines a numeric range for color space component values that may be inverted. +type colValRange struct { + min, max float64 +} + +// PDFImage represents a XObject of subtype image. +type PDFImage struct { + objNr int + sd *types.StreamDict + comp int + bpc int + w, h int + softMask []byte + decode []colValRange + imageMask bool + thumb bool +} + +func decodeArr(a types.Array) []colValRange { + if a == nil { + return nil + } + + var decode []colValRange + var min, max, f64 float64 + + for i, f := range a { + switch o := f.(type) { + case types.Integer: + f64 = float64(o.Value()) + case types.Float: + f64 = o.Value() + } + if i%2 == 0 { + min = f64 + continue + } + max = f64 + decode = append(decode, colValRange{min, max}) + } + + return decode +} + +func pdfImage(xRefTable *model.XRefTable, sd *types.StreamDict, thumb bool, objNr int) (*PDFImage, error) { + comp, err := ColorSpaceComponents(xRefTable, sd) + if err != nil { + return nil, err + } + + bpc := *sd.IntEntry("BitsPerComponent") + + obj, ok := sd.Find("Width") + if !ok { + return nil, errors.Errorf("pdfcpu: missing image width obj#%d", objNr) + } + i, err := xRefTable.DereferenceInteger(obj) + if err != nil { + return nil, err + } + w := i.Value() + + obj, ok = sd.Find("Height") + if !ok { + return nil, errors.Errorf("pdfcpu: missing image height obj#%d", objNr) + } + i, err = xRefTable.DereferenceInteger(obj) + if err != nil { + return nil, err + } + h := i.Value() + + decode := decodeArr(sd.ArrayEntry("Decode")) + + var imgMask bool + if im := sd.BooleanEntry("ImageMask"); im != nil && *im { + imgMask = true + } + + sm, err := softMask(xRefTable, sd, w, h, objNr) + if err != nil { + return nil, err + } + + return &PDFImage{ + objNr: objNr, + sd: sd, + comp: comp, + bpc: bpc, + w: w, + h: h, + imageMask: imgMask, + softMask: sm, + decode: decode, + thumb: thumb, + }, nil +} + +// Identify the color lookup table for an Indexed color space. +func colorLookupTable(xRefTable *model.XRefTable, o types.Object) ([]byte, error) { + o, _ = xRefTable.Dereference(o) + + switch o := o.(type) { + + case types.StringLiteral: + return types.Unescape(o.Value()) + + case types.HexLiteral: + return o.Bytes() + + case types.StreamDict: + return streamBytes(&o) + + } + + return nil, nil +} + +func maxValForBits(bpc int) int { + return 1<> (8 - uint8(im.bpc)) + v := decodePixelValue(pix, im.bpc, cvr) + if im.bpc < 8 { + v = scaleToBPC8(v, im.bpc) + } + alpha := uint8(255) + if im.softMask != nil { + alpha = im.softMask[y*im.w+x] + } + img.Set(x, y, color.NRGBA{R: v, G: v, B: v, A: alpha}) + p <<= uint8(im.bpc) + x++ + } + i++ + } + } + + var buf bytes.Buffer + if err := png.Encode(&buf, img); err != nil { + return nil, "", err + } + + return &buf, "png", nil +} + +func renderDeviceRGBToPNG(im *PDFImage, resourceName string) (io.Reader, string, error) { + b := im.sd.Content + if log.DebugEnabled() { + log.Debug.Printf("renderDeviceRGBToPNG: objNr=%d w=%d h=%d bpc=%d buflen=%d\n", im.objNr, im.w, im.h, im.bpc, len(b)) + } + + // Validate buflen. + // Sometimes there is a trailing 0x0A in addition to the imagebytes. + if len(b) < (3*im.bpc*im.w*im.h+7)/8 { + return nil, "", errors.Errorf("pdfcpu: renderDeviceRGBToPNG: objNr=%d corrupt image object\n", im.objNr) + } + + // TODO Support bpc and decode. + img := image.NewNRGBA(image.Rect(0, 0, im.w, im.h)) + + i := 0 + for y := 0; y < im.h; y++ { + for x := 0; x < im.w; x++ { + alpha := uint8(255) + if im.softMask != nil { + alpha = im.softMask[y*im.w+x] + } + img.Set(x, y, color.NRGBA{R: b[i], G: b[i+1], B: b[i+2], A: alpha}) + i += 3 + } + } + + var buf bytes.Buffer + if err := png.Encode(&buf, img); err != nil { + return nil, "", err + } + + return &buf, "png", nil +} + +func renderCalRGBToPNG(im *PDFImage, resourceName string) (io.Reader, string, error) { + b := im.sd.Content + if log.DebugEnabled() { + log.Debug.Printf("renderCalRGBToPNG: objNr=%d w=%d h=%d bpc=%d buflen=%d\n", im.objNr, im.w, im.h, im.bpc, len(b)) + } + + if len(b) < (3*im.bpc*im.w*im.h+7)/8 { + return nil, "", errors.Errorf("pdfcpu:renderCalRGBToPNG: objNr=%d corrupt image object %v\n", im.objNr, *im.sd) + } + + // Optional int array "Range", length 2*N specifies min,max values of color components. + // This information can be validated against the iccProfile. + + // RGB + // TODO Support bpc, decode and softmask. + img := image.NewNRGBA(image.Rect(0, 0, im.w, im.h)) + i := 0 + for y := 0; y < im.h; y++ { + for x := 0; x < im.w; x++ { + img.Set(x, y, color.NRGBA{R: b[i], G: b[i+1], B: b[i+2], A: 255}) + i += 3 + } + } + + var buf bytes.Buffer + if err := png.Encode(&buf, img); err != nil { + return nil, "", err + } + + return &buf, "png", nil +} + +func renderICCBased(xRefTable *model.XRefTable, im *PDFImage, resourceName string, cs types.Array) (io.Reader, string, error) { + // Any ICC profile >= ICC.1:2004:10 is sufficient for any PDF version <= 1.7 + // If the embedded ICC profile version is newer than the one used by the Reader, substitute with Alternate color space. + + iccProfileStream, _, _ := xRefTable.DereferenceStreamDict(cs[1]) + + b := im.sd.Content + + if log.DebugEnabled() { + log.Debug.Printf("renderICCBasedToPNGFile: objNr=%d w=%d h=%d bpc=%d buflen=%d\n", im.objNr, im.w, im.h, im.bpc, len(b)) + } + + // 1,3 or 4 color components. + n := *iccProfileStream.IntEntry("N") + + if !types.IntMemberOf(n, []int{1, 3, 4}) { + return nil, "", errors.Errorf("pdfcpu: renderICCBasedToPNGFile: objNr=%d, N must be 1,3 or 4, got:%d\n", im.objNr, n) + } + + // TODO: Transform linear XYZ to RGB according to ICC profile. + // For now we fall back to appropriate color spaces for n + // regardless of a specified alternate color space. + + // Validate buflen. + // Sometimes there is a trailing 0x0A in addition to the imagebytes. + if len(b) < (n*im.bpc*im.w*im.h+7)/8 { + return nil, "", errors.Errorf("pdfcpu: renderICCBased: objNr=%d corrupt image object %v\n", im.objNr, *im.sd) + } + + switch n { + case 1: + // Gray + return renderDeviceGrayToPNG(im, resourceName) + + case 3: + // RGB + return renderDeviceRGBToPNG(im, resourceName) + + case 4: + // CMYK + return renderDeviceCMYKToTIFF(im, resourceName) + } + + return nil, "", nil +} + +func renderIndexedGrayToPNG(im *PDFImage, resourceName string, lookup []byte) (io.Reader, string, error) { + b := im.sd.Content + if log.DebugEnabled() { + log.Debug.Printf("renderIndexedGrayToPNG: objNr=%d w=%d h=%d bpc=%d buflen=%d\n", im.objNr, im.w, im.h, im.bpc, len(b)) + } + + // Validate buflen. + // For streams not using compression there is a trailing 0x0A in addition to the imagebytes. + if len(b) < (im.bpc*im.w*im.h+7)/8 { + return nil, "", errors.Errorf("pdfcpu: renderIndexedGrayToPNG: objNr=%d corrupt image object %v\n", im.objNr, *im.sd) + } + + cvr := colValRange{0, 1} + if im.decode != nil { + cvr = im.decode[0] + } + + img := image.NewGray(image.Rect(0, 0, im.w, im.h)) + + // TODO support softmask. + i := 0 + for y := 0; y < im.h; y++ { + for x := 0; x < im.w; { + p := b[i] + for j := 0; j < 8/im.bpc && x < im.w; j++ { + ind := p >> (8 - uint8(im.bpc)) + v := decodePixelValue(lookup[ind], im.bpc, cvr) + if im.bpc < 8 { + v = scaleToBPC8(v, im.bpc) + } + //fmt.Printf("x=%d y=%d pix=#%02x v=#%02x\n", x, y, pix, v) + img.Set(x, y, color.Gray{Y: v}) + p <<= uint8(im.bpc) + x++ + } + i++ + } + } + + var buf bytes.Buffer + if err := png.Encode(&buf, img); err != nil { + return nil, "", err + } + + return &buf, "png", nil +} + +func renderIndexedRGBToPNG(im *PDFImage, resourceName string, lookup []byte) (io.Reader, string, error) { + b := im.sd.Content + + img := image.NewNRGBA(image.Rect(0, 0, im.w, im.h)) + + i := 0 + // TODO: For (some) Runlength encoded images the line sequence is reversed. + for y := 0; y < im.h; y++ { + for x := 0; x < im.w; { + p := b[i] + for j := 0; j < 8/im.bpc && x < im.w; j++ { + ind := p >> (8 - uint8(im.bpc)) + //fmt.Printf("x=%d y=%d i=%d j=%d p=#%02x ind=#%02x\n", x, y, i, j, p, ind) + alpha := uint8(255) + if im.softMask != nil { + alpha = im.softMask[y*im.w+x] + } + l := 3 * int(ind) + img.Set(x, y, color.NRGBA{R: lookup[l], G: lookup[l+1], B: lookup[l+2], A: alpha}) + p <<= uint8(im.bpc) + x++ + } + i++ + } + } + + var buf bytes.Buffer + if err := png.Encode(&buf, img); err != nil { + return nil, "", err + } + + return &buf, "png", nil +} + +func imageForIndexedCMYKWithoutSoftMask(im *PDFImage, lookup []byte) image.Image { + + // Preserve CMYK color model for print applications. + + // TODO handle decode + + img := image.NewCMYK(image.Rect(0, 0, im.w, im.h)) + b := im.sd.Content + i := 0 + + for y := 0; y < im.h; y++ { + for x := 0; x < im.w; { + p := b[i] + for j := 0; j < 8/im.bpc && x < im.w; j++ { + ind := p >> (8 - uint8(im.bpc)) + //fmt.Printf("x=%d y=%d i=%d j=%d p=#%02x ind=#%02x\n", x, y, i, j, p, ind) + l := 4 * int(ind) + img.Set(x, y, color.CMYK{C: lookup[l], M: lookup[l+1], Y: lookup[l+2], K: lookup[l+3]}) + p <<= uint8(im.bpc) + x++ + } + i++ + } + } + + return img +} + +func imageForIndexedCMYKWithSoftMask(im *PDFImage, lookup []byte) image.Image { + + // TODO handle decode + + img := image.NewNRGBA(image.Rect(0, 0, im.w, im.h)) + b := im.sd.Content + i := 0 + + for y := 0; y < im.h; y++ { + for x := 0; x < im.w; { + p := b[i] + for j := 0; j < 8/im.bpc && x < im.w; j++ { + ind := p >> (8 - uint8(im.bpc)) + //fmt.Printf("x=%d y=%d i=%d j=%d p=#%02x ind=#%02x\n", x, y, i, j, p, ind) + l := 4 * int(ind) + cr, cg, cb := color.CMYKToRGB(lookup[l], lookup[l+1], lookup[l+2], lookup[l+3]) + alpha := im.softMask[y*im.w+x] + img.Set(x, y, color.NRGBA{cr, cg, cb, alpha}) + p <<= uint8(im.bpc) + x++ + } + i++ + } + } + + return img +} + +func renderIndexedCMYKToTIFF(im *PDFImage, resourceName string, lookup []byte) (io.Reader, string, error) { + + var img image.Image + if im.softMask != nil { + img = imageForIndexedCMYKWithSoftMask(im, lookup) + } else { + img = imageForIndexedCMYKWithoutSoftMask(im, lookup) + } + + var buf bytes.Buffer + if err := tiff.Encode(&buf, img, nil); err != nil { + return nil, "", err + } + + return &buf, "tif", nil +} + +func renderIndexedNameCS(im *PDFImage, resourceName string, cs types.Name, maxInd int, lookup []byte) (io.Reader, string, error) { + switch cs { + + case model.DeviceGrayCS: + if len(lookup) < 1*(maxInd+1) { + return nil, "", errors.Errorf("pdfcpu: renderIndexedNameCS: objNr=%d, corrupt DeviceGray lookup table\n", im.objNr) + } + return renderIndexedGrayToPNG(im, resourceName, lookup) + + case model.DeviceRGBCS: + if len(lookup) < 3*(maxInd+1) { + return nil, "", errors.Errorf("pdfcpu: renderIndexedNameCS: objNr=%d, corrupt DeviceRGB lookup table\n", im.objNr) + } + return renderIndexedRGBToPNG(im, resourceName, lookup) + + case model.DeviceCMYKCS: + if len(lookup) < 4*(maxInd+1) { + return nil, "", errors.Errorf("pdfcpu: renderIndexedNameCS: objNr=%d, corrupt DeviceCMYK lookup table\n", im.objNr) + } + return renderIndexedCMYKToTIFF(im, resourceName, lookup) + } + + if log.InfoEnabled() { + log.Info.Printf("renderIndexedNameCS: objNr=%d, unsupported base colorspace %s\n", im.objNr, cs.String()) + } + + return nil, "", nil +} + +func renderIndexedArrayCS(xRefTable *model.XRefTable, im *PDFImage, resourceName string, csa types.Array, maxInd int, lookup []byte) (io.Reader, string, error) { + b := im.sd.Content + + cs, _ := csa[0].(types.Name) + + switch cs { + + //case CalGrayCS: + + case model.CalRGBCS: + return renderIndexedRGBToPNG(im, resourceName, lookup) + + //case LabCS: + // return renderIndexedRGBToPNG(im, resourceName, lookup) + + case model.ICCBasedCS: + + iccProfileStream, _, _ := xRefTable.DereferenceStreamDict(csa[1]) + + // 1,3 or 4 color components. + n := *iccProfileStream.IntEntry("N") + if !types.IntMemberOf(n, []int{1, 3, 4}) { + return nil, "", errors.Errorf("pdfcpu: renderIndexedArrayCS: objNr=%d, N must be 1,3 or 4, got:%d\n", im.objNr, n) + } + + // Validate the lookup table. + if len(lookup) < n*(maxInd+1) { + return nil, "", errors.Errorf("pdfcpu: renderIndexedArrayCS: objNr=%d, corrupt ICCBased lookup table\n", im.objNr) + } + + // TODO: Transform linear XYZ to RGB according to ICC profile. + // For now we fall back to approriate color spaces for n + // regardless of a specified alternate color space. + + switch n { + case 1: + // Gray + // TODO use lookupTable! + // TODO handle bpc, decode and softmask. + img := image.NewGray(image.Rect(0, 0, im.w, im.h)) + i := 0 + for y := 0; y < im.h; y++ { + for x := 0; x < im.w; x++ { + img.Set(x, y, color.Gray{Y: b[i]}) + i++ + } + } + var buf bytes.Buffer + if err := png.Encode(&buf, img); err != nil { + return nil, "", err + } + return &buf, "png", nil + + case 3: + // RGB + return renderIndexedRGBToPNG(im, resourceName, lookup) + + case 4: + // CMYK + if log.DebugEnabled() { + log.Debug.Printf("renderIndexedArrayCS: CMYK objNr=%d w=%d h=%d bpc=%d buflen=%d\n", im.objNr, im.w, im.h, im.bpc, len(b)) + } + return renderIndexedCMYKToTIFF(im, resourceName, lookup) + } + } + + if log.InfoEnabled() { + log.Info.Printf("renderIndexedArrayCS: objNr=%d, unsupported base colorspace %s\n", im.objNr, csa) + } + + return nil, "", nil +} + +func renderIndexed(xRefTable *model.XRefTable, im *PDFImage, resourceName string, cs types.Array) (io.Reader, string, error) { + // Identify the base color space. + baseCS, _ := xRefTable.Dereference(cs[1]) + + // Identify the max index into the color lookup table. + maxInd, _ := xRefTable.DereferenceInteger(cs[2]) + + // Identify the color lookup table. + var lookup []byte + lookup, err := colorLookupTable(xRefTable, cs[3]) + if err != nil { + return nil, "", err + } + + if lookup == nil { + return nil, "", errors.Errorf("pdfcpu: renderIndexed: objNr=%d IndexedCS with corrupt lookup table %s\n", im.objNr, cs) + } + + b := im.sd.Content + + if log.DebugEnabled() { + log.Debug.Printf("renderIndexed: objNr=%d w=%d h=%d bpc=%d buflen=%d maxInd=%d\n", im.objNr, im.w, im.h, im.bpc, len(b), maxInd) + } + + // Validate buflen. + // The image data is a sequence of index values for pixels. + // Sometimes there is a trailing 0x0A. + if len(b) < (im.bpc*im.w*im.h+7)/8 { + return nil, "", errors.Errorf("pdfcpu: renderIndexed: objNr=%d corrupt image object %v\n", im.objNr, *im.sd) + } + + switch cs := baseCS.(type) { + case types.Name: + return renderIndexedNameCS(im, resourceName, cs, maxInd.Value(), lookup) + + case types.Array: + return renderIndexedArrayCS(xRefTable, im, resourceName, cs, maxInd.Value(), lookup) + } + + return nil, "", nil +} + +func renderDeviceN(xRefTable *model.XRefTable, im *PDFImage, resourceName string, cs types.Array) (io.Reader, string, error) { + if im.comp <= 4 { + switch im.comp { + case 1: + // Gray + return renderDeviceGrayToPNG(im, resourceName) + + case 3: + // RGB + return renderDeviceRGBToPNG(im, resourceName) + + case 4: + // CMYK + return renderDeviceCMYKToTIFF(im, resourceName) + } + } + + alternateCS, ok := cs[2].(types.Name) + if !ok { + return nil, "", nil + } + + switch alternateCS { + case model.DeviceGrayCS: + // Gray + return renderDeviceGrayToPNG(im, resourceName) + + case model.DeviceRGBCS: + // RGB + return renderDeviceRGBToPNG(im, resourceName) + + case model.DeviceCMYKCS: + // CMYK + return renderDeviceCMYKToTIFF(im, resourceName) + } + + return nil, "", nil +} + +func renderImage(xRefTable *model.XRefTable, sd *types.StreamDict, thumb bool, resourceName string, objNr int) (io.Reader, string, error) { + // If color space is CMYK then write .tif else write .png + + pdfImage, err := pdfImage(xRefTable, sd, thumb, objNr) + if err != nil { + return nil, "", err + } + + o, err := xRefTable.DereferenceDictEntry(sd.Dict, "ColorSpace") + if err != nil { + return nil, "", err + } + + switch cs := o.(type) { + + case types.Name: + switch cs { + + case model.DeviceGrayCS: + return renderDeviceGrayToPNG(pdfImage, resourceName) + + case model.DeviceRGBCS: + return renderDeviceRGBToPNG(pdfImage, resourceName) + + case model.DeviceCMYKCS: + return renderDeviceCMYKToTIFF(pdfImage, resourceName) + + default: + if log.InfoEnabled() { + log.Info.Printf("renderImage: objNr=%d, unsupported name colorspace %s\n", objNr, cs.String()) + } + } + + case types.Array: + csn, _ := cs[0].(types.Name) + + switch csn { + + case model.CalRGBCS: + return renderCalRGBToPNG(pdfImage, resourceName) + + case model.DeviceNCS: + return renderDeviceN(xRefTable, pdfImage, resourceName, cs) + + case model.ICCBasedCS: + return renderICCBased(xRefTable, pdfImage, resourceName, cs) + + case model.IndexedCS: + return renderIndexed(xRefTable, pdfImage, resourceName, cs) + + case model.SeparationCS: + return renderDeviceN(xRefTable, pdfImage, resourceName, cs) + + default: + if log.InfoEnabled() { + log.Info.Printf("renderImage: objNr=%d, unsupported array colorspace %s\n", objNr, csn) + } + } + + } + + return nil, "", nil +} + +func decodeCMYK(c, m, y, k uint8, decode []colValRange) (uint8, uint8, uint8, uint8) { + if len(decode) == 0 { + return c, m, y, k + } + c = decodePixelValue(c, 8, decode[0]) + m = decodePixelValue(m, 8, decode[1]) + y = decodePixelValue(y, 8, decode[2]) + k = decodePixelValue(k, 8, decode[3]) + return c, m, y, k +} + +func renderCMYKToPng(im *PDFImage, resourceName string) (io.Reader, string, error) { + bb := bytes.NewReader(im.sd.Content) + dec := gob.NewDecoder(bb) + + var img image.CMYK + if err := dec.Decode(&img); err != nil { + return nil, "", err + } + + img1 := image.NewRGBA(image.Rect(0, 0, im.w, im.h)) + + for y := 0; y < im.h; y++ { + for x := 0; x < im.w; x++ { + a := img.At(x, y).(color.CMYK) + cyan, mag, yel, blk := decodeCMYK(255-a.C, 255-a.M, 255-a.Y, 255-a.K, im.decode) + r, g, b := color.CMYKToRGB(cyan, mag, yel, blk) + // TODO Apply Decode array + img1.SetRGBA(x, y, color.RGBA{r, g, b, 255}) + } + } + + var buf bytes.Buffer + if err := png.Encode(&buf, img1); err != nil { + return nil, "", err + } + + return &buf, "png", nil +} + +func renderDCTToPNG(xRefTable *model.XRefTable, sd *types.StreamDict, thumb bool, resourceName string, objNr int) (io.Reader, string, error) { + im, err := pdfImage(xRefTable, sd, thumb, objNr) + if err != nil { + return nil, "", err + } + + return renderCMYKToPng(im, resourceName) +} + +// RenderImage returns a reader for a decoded image stream. +func RenderImage(xRefTable *model.XRefTable, sd *types.StreamDict, thumb bool, resourceName string, objNr int) (io.Reader, string, error) { + // Image compression is the last filter in the pipeline. + + if len(sd.FilterPipeline) == 0 { + return renderImage(xRefTable, sd, thumb, resourceName, objNr) + } + + f := sd.FilterPipeline[len(sd.FilterPipeline)-1].Name + + switch f { + + case filter.Flate, filter.CCITTFax, filter.RunLength: + return renderImage(xRefTable, sd, thumb, resourceName, objNr) + + case filter.DCT: + if sd.CSComponents == 4 { + return renderDCTToPNG(xRefTable, sd, thumb, resourceName, objNr) + } + return bytes.NewReader(sd.Content), "jpg", nil + + case filter.JPX: + return bytes.NewReader(sd.Content), "jpx", nil + } + + return nil, "", nil +} + +// WriteReader consumes r's content by writing it to a file at path. +func WriteReader(path string, r io.Reader) error { + w, err := os.Create(path) + if err != nil { + return err + } + if _, err = io.Copy(w, r); err != nil { + return err + } + return w.Close() +} + +// WriteImage writes a PDF image object to disk. +func WriteImage(xRefTable *model.XRefTable, fileName string, sd *types.StreamDict, thumb bool, objNr int) (string, error) { + r, _, err := RenderImage(xRefTable, sd, thumb, fileName, objNr) + if err != nil { + return "", err + } + if r == nil { + return "", errors.Errorf("pdfcpu: unable to extract image from obj#%d", objNr) + } + return fileName, WriteReader(fileName, r) +} diff --git a/pkg/pdfcpu/writeObjects.go b/pkg/pdfcpu/writeObjects.go new file mode 100644 index 0000000000000000000000000000000000000000..d65efc5816805ce9742b680a4fd71dd15c1208c2 --- /dev/null +++ b/pkg/pdfcpu/writeObjects.go @@ -0,0 +1,833 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 pdfcpu + +import ( + "fmt" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +const ( + + // ObjectStreamMaxObjects limits the number of objects within an object stream written. + ObjectStreamMaxObjects = 100 +) + +func writeCommentLine(w *model.WriteContext, comment string) (int, error) { + return w.WriteString(fmt.Sprintf("%%%s%s", comment, w.Eol)) +} + +func writeHeader(w *model.WriteContext, v model.Version) error { + i, err := writeCommentLine(w, "PDF-"+v.String()) + if err != nil { + return err + } + + j, err := writeCommentLine(w, "\xe2\xe3\xcf\xD3") + if err != nil { + return err + } + + w.Offset += int64(i + j) + + return nil +} + +func writeTrailer(w *model.WriteContext) error { + _, err := w.WriteString("%%EOF" + w.Eol) + return err +} + +func writeObjectHeader(w *model.WriteContext, objNumber, genNumber int) (int, error) { + return w.WriteString(fmt.Sprintf("%d %d obj%s", objNumber, genNumber, w.Eol)) +} + +func writeObjectTrailer(w *model.WriteContext) (int, error) { + return w.WriteString(fmt.Sprintf("%sendobj%s", w.Eol, w.Eol)) +} + +func startObjectStream(ctx *model.Context) error { + // See 7.5.7 Object streams + // When new object streams and compressed objects are created, they shall always be assigned new object numbers. + + if log.WriteEnabled() { + log.Write.Println("startObjectStream begin") + } + + objStreamDict := types.NewObjectStreamDict() + + objNr, err := ctx.InsertObject(*objStreamDict) + if err != nil { + return err + } + + ctx.Write.CurrentObjStream = &objNr + + if log.WriteEnabled() { + log.Write.Printf("startObjectStream end: %d\n", objNr) + } + + return nil +} + +func stopObjectStream(ctx *model.Context) error { + if log.WriteEnabled() { + log.Write.Println("stopObjectStream begin") + } + + xRefTable := ctx.XRefTable + + if !ctx.Write.WriteToObjectStream { + return errors.Errorf("stopObjectStream: Not writing to object stream.") + } + + if ctx.Write.CurrentObjStream == nil { + ctx.Write.WriteToObjectStream = false + if log.WriteEnabled() { + log.Write.Println("stopObjectStream end (no content)") + } + return nil + } + + entry, _ := xRefTable.FindTableEntry(*ctx.Write.CurrentObjStream, 0) + osd, _ := (entry.Object).(types.ObjectStreamDict) + + // When we are ready to write: append prolog and content + osd.Finalize() + + // Encode objStreamDict.Content -> objStreamDict.Raw + // and wipe (decoded) content to free up memory. + if err := osd.StreamDict.Encode(); err != nil { + return err + } + + // Release memory. + osd.Content = nil + + osd.StreamDict.Insert("First", types.Integer(osd.FirstObjOffset)) + osd.StreamDict.Insert("N", types.Integer(osd.ObjCount)) + + // for each objStream execute at the end right before xRefStreamDict gets written. + if log.WriteEnabled() { + log.Write.Printf("stopObjectStream: objStreamDict: %s\n", osd) + } + + if err := writeStreamDictObject(ctx, *ctx.Write.CurrentObjStream, 0, osd.StreamDict); err != nil { + return err + } + + // Release memory. + osd.Raw = nil + + ctx.Write.CurrentObjStream = nil + ctx.Write.WriteToObjectStream = false + + if log.WriteEnabled() { + log.Write.Println("stopObjectStream end") + } + + return nil +} + +func writeToObjectStream(ctx *model.Context, objNumber, genNumber int) (ok bool, err error) { + if log.WriteEnabled() { + log.Write.Printf("addToObjectStream begin, obj#:%d gen#:%d\n", objNumber, genNumber) + } + + w := ctx.Write + + if ctx.WriteXRefStream && // object streams assume an xRefStream to be generated. + ctx.WriteObjectStream && // signal for compression into object stream is on. + ctx.Write.WriteToObjectStream && // currently writing to object stream. + genNumber == 0 { + + if w.CurrentObjStream == nil { + // Create new objects stream on first write. + if err = startObjectStream(ctx); err != nil { + return false, err + } + } + + objStrEntry, _ := ctx.FindTableEntry(*ctx.Write.CurrentObjStream, 0) + objStreamDict, _ := (objStrEntry.Object).(types.ObjectStreamDict) + + // Get next free index in object stream. + i := objStreamDict.ObjCount + + // Locate the xref table entry for the object to be added to this object stream. + entry, _ := ctx.FindTableEntry(objNumber, genNumber) + + // Turn entry into a compressed entry located in object stream at index i. + entry.Compressed = true + entry.ObjectStream = ctx.Write.CurrentObjStream // ! + entry.ObjectStreamInd = &i + w.SetWriteOffset(objNumber) // for a compressed obj this is supposed to be a fake offset. value does not matter. + + // Append to prolog & content + s := entry.Object.PDFString() + if err = objStreamDict.AddObject(objNumber, s); err != nil { + return false, err + } + + objStrEntry.Object = objStreamDict + + if log.WriteEnabled() { + log.Write.Printf("writeObject end, obj#%d written to objectStream #%d\n", objNumber, *ctx.Write.CurrentObjStream) + } + + if objStreamDict.ObjCount == ObjectStreamMaxObjects { + if err = stopObjectStream(ctx); err != nil { + return false, err + } + w.WriteToObjectStream = true + } + + ok = true + + } + + if log.WriteEnabled() { + log.Write.Printf("addToObjectStream end, obj#:%d gen#:%d\n", objNumber, genNumber) + } + + return ok, nil +} + +func writeObject(ctx *model.Context, objNumber, genNumber int, s string) error { + if log.WriteEnabled() { + log.Write.Printf("writeObject begin, obj#:%d gen#:%d <%s>\n", objNumber, genNumber, s) + } + + w := ctx.Write + + // Cleanup entry (necessary for split command) + // TODO This is not the right place to check for an existing obj since we maybe writing NULL. + entry, ok := ctx.FindTableEntry(objNumber, genNumber) + if ok { + entry.Compressed = false + } + + // Set write-offset for this object. + w.SetWriteOffset(objNumber) + + written, err := writeObjectHeader(w, objNumber, genNumber) + if err != nil { + return err + } + + // Note: Lines that are not part of stream object data are limited to no more than 255 characters. + i, err := w.WriteString(s) + if err != nil { + return err + } + + j, err := writeObjectTrailer(w) + if err != nil { + return err + } + + // Write-offset for next object. + w.Offset += int64(written + i + j) + + if log.WriteEnabled() { + log.Write.Printf("writeObject end, %d bytes written\n", written+i+j) + } + + return nil +} + +func writePDFNullObject(ctx *model.Context, objNumber, genNumber int) error { + return writeObject(ctx, objNumber, genNumber, "null") +} + +func writeBooleanObject(ctx *model.Context, objNumber, genNumber int, boolean types.Boolean) error { + ok, err := writeToObjectStream(ctx, objNumber, genNumber) + if err != nil { + return err + } + + if ok { + return nil + } + + return writeObject(ctx, objNumber, genNumber, boolean.PDFString()) +} + +func writeNameObject(ctx *model.Context, objNumber, genNumber int, name types.Name) error { + ok, err := writeToObjectStream(ctx, objNumber, genNumber) + if err != nil { + return err + } + + if ok { + return nil + } + + return writeObject(ctx, objNumber, genNumber, name.PDFString()) +} + +func writeStringLiteralObject(ctx *model.Context, objNumber, genNumber int, sl types.StringLiteral) error { + ok, err := writeToObjectStream(ctx, objNumber, genNumber) + if err != nil { + return err + } + + if ok { + return nil + } + + if ctx.EncKey != nil { + sl1, err := encryptStringLiteral(sl, objNumber, genNumber, ctx.EncKey, ctx.AES4Strings, ctx.E.R) + if err != nil { + return err + } + + sl = *sl1 + } + + return writeObject(ctx, objNumber, genNumber, sl.PDFString()) +} + +func writeHexLiteralObject(ctx *model.Context, objNumber, genNumber int, hl types.HexLiteral) error { + ok, err := writeToObjectStream(ctx, objNumber, genNumber) + if err != nil { + return err + } + + if ok { + return nil + } + + if ctx.EncKey != nil { + hl1, err := encryptHexLiteral(hl, objNumber, genNumber, ctx.EncKey, ctx.AES4Strings, ctx.E.R) + if err != nil { + return err + } + + hl = *hl1 + } + + return writeObject(ctx, objNumber, genNumber, hl.PDFString()) +} + +func writeIntegerObject(ctx *model.Context, objNumber, genNumber int, integer types.Integer) error { + ok, err := writeToObjectStream(ctx, objNumber, genNumber) + if err != nil { + return err + } + + if ok { + return nil + } + + return writeObject(ctx, objNumber, genNumber, integer.PDFString()) +} + +func writeFloatObject(ctx *model.Context, objNumber, genNumber int, float types.Float) error { + ok, err := writeToObjectStream(ctx, objNumber, genNumber) + if err != nil { + return err + } + + if ok { + return nil + } + + return writeObject(ctx, objNumber, genNumber, float.PDFString()) +} + +func writeDictObject(ctx *model.Context, objNumber, genNumber int, d types.Dict) error { + ok, err := writeToObjectStream(ctx, objNumber, genNumber) + if err != nil { + return err + } + + if ok { + return nil + } + + if ctx.EncKey != nil { + _, err := encryptDeepObject(d, objNumber, genNumber, ctx.EncKey, ctx.AES4Strings, ctx.E.R) + if err != nil { + return err + } + } + + return writeObject(ctx, objNumber, genNumber, d.PDFString()) +} + +func writeArrayObject(ctx *model.Context, objNumber, genNumber int, a types.Array) error { + ok, err := writeToObjectStream(ctx, objNumber, genNumber) + if err != nil { + return err + } + + if ok { + return nil + } + + if ctx.EncKey != nil { + if _, err := encryptDeepObject(a, objNumber, genNumber, ctx.EncKey, ctx.AES4Strings, ctx.E.R); err != nil { + return err + } + } + + return writeObject(ctx, objNumber, genNumber, a.PDFString()) +} + +func writeStream(w *model.WriteContext, sd types.StreamDict) (int64, error) { + b, err := w.WriteString(fmt.Sprintf("%sstream%s", w.Eol, w.Eol)) + if err != nil { + return 0, errors.Wrapf(err, "writeStream: failed to write raw content") + } + + c, err := w.Write(sd.Raw) + if err != nil { + return 0, errors.Wrapf(err, "writeStream: failed to write raw content") + } + if int64(c) != *sd.StreamLength { + return 0, errors.Errorf("writeStream: failed to write raw content: %d bytes written - streamlength:%d", c, *sd.StreamLength) + } + + e, err := w.WriteString(fmt.Sprintf("%sendstream", w.Eol)) + if err != nil { + return 0, errors.Wrapf(err, "writeStream: failed to write raw content") + } + + written := int64(b+e) + *sd.StreamLength + + return written, nil +} + +func handleIndirectLength(ctx *model.Context, ir *types.IndirectRef) error { + objNr := int(ir.ObjectNumber) + genNr := int(ir.GenerationNumber) + + if ctx.Write.HasWriteOffset(objNr) { + if log.WriteEnabled() { + log.Write.Printf("*** handleIndirectLength: object #%d already written offset=%d ***\n", objNr, ctx.Write.Offset) + } + } else { + length, err := ctx.DereferenceInteger(*ir) + if err != nil || length == nil { + return err + } + if err = writeIntegerObject(ctx, objNr, genNr, *length); err != nil { + return err + } + } + + return nil +} + +func writeStreamObject(ctx *model.Context, objNr, genNr int, sd types.StreamDict, pdfString string) (int, int64, int, error) { + h, err := writeObjectHeader(ctx.Write, objNr, genNr) + if err != nil { + return 0, 0, 0, err + } + + // Note: Lines that are not part of stream object data are limited to no more than 255 characters. + if _, err = ctx.Write.WriteString(pdfString); err != nil { + return 0, 0, 0, err + } + + b, err := writeStream(ctx.Write, sd) + if err != nil { + return 0, 0, 0, err + } + + t, err := writeObjectTrailer(ctx.Write) + if err != nil { + return 0, 0, 0, err + } + + return h, b, t, nil +} + +func writeStreamDictObject(ctx *model.Context, objNr, genNr int, sd types.StreamDict) error { + if log.WriteEnabled() { + log.Write.Printf("writeStreamDictObject begin: object #%d\n%v", objNr, sd) + } + + var inObjStream bool + + if ctx.Write.WriteToObjectStream { + inObjStream = true + ctx.Write.WriteToObjectStream = false + } + + // Sometimes a streamDicts length is a reference. + if ir := sd.IndirectRefEntry("Length"); ir != nil { + if err := handleIndirectLength(ctx, ir); err != nil { + return err + } + } + + var err error + + // Unless the "Identity" crypt filter is used we have to encrypt. + isXRefStreamDict := sd.Type() != nil && *sd.Type() == "XRef" + if ctx.EncKey != nil && + !isXRefStreamDict && + !(len(sd.FilterPipeline) == 1 && sd.FilterPipeline[0].Name == "Crypt") { + + if sd.Raw, err = encryptStream(sd.Raw, objNr, genNr, ctx.EncKey, ctx.AES4Streams, ctx.E.R); err != nil { + return err + } + + l := int64(len(sd.Raw)) + sd.StreamLength = &l + sd.Update("Length", types.Integer(l)) + } + + ctx.Write.SetWriteOffset(objNr) + + pdfString := sd.PDFString() + + h, b, t, err := writeStreamObject(ctx, objNr, genNr, sd, pdfString) + if err != nil { + return err + } + + written := b + int64(h+len(pdfString)+t) + + ctx.Write.Offset += written + ctx.Write.BinaryTotalSize += *sd.StreamLength + + if inObjStream { + ctx.Write.WriteToObjectStream = true + } + + if log.WriteEnabled() { + log.Write.Printf("writeStreamDictObject end: object #%d written=%d\n", objNr, written) + } + + return nil +} + +func writeDirectObject(ctx *model.Context, o types.Object) error { + switch o := o.(type) { + + case types.Dict: + for k, v := range o { + if ctx.WritingPages && (k == "Dest" || k == "D") { + ctx.Dest = true + } + if _, _, err := writeDeepObject(ctx, v); err != nil { + return err + } + ctx.Dest = false + } + if log.WriteEnabled() { + log.Write.Printf("writeDirectObject: end offset=%d\n", ctx.Write.Offset) + } + + case types.Array: + for i, v := range o { + if ctx.Dest && i == 0 { + continue + } + if _, _, err := writeDeepObject(ctx, v); err != nil { + return err + } + } + if log.WriteEnabled() { + log.Write.Printf("writeDirectObject: end offset=%d\n", ctx.Write.Offset) + } + + default: + if log.WriteEnabled() { + log.Write.Printf("writeDirectObject: end, direct obj - nothing written: offset=%d\n%v\n", ctx.Write.Offset, o) + } + } + + return nil +} + +func writeNullObject(ctx *model.Context, objNumber, genNumber int) error { + // An indirect reference to nil is a corner case. + // Still, it is an object that will be written. + if err := writePDFNullObject(ctx, objNumber, genNumber); err != nil { + return err + } + + // Ensure no entry in free list. + return ctx.UndeleteObject(objNumber) +} + +func writeDeepDict(ctx *model.Context, d types.Dict, objNr, genNr int) error { + + if d.IsPage() { + valid, err := ctx.IsValidObj(objNr, genNr) + if err != nil { + return err + } + if !valid { + return nil + } + } + + if err := writeDictObject(ctx, objNr, genNr, d); err != nil { + return err + } + + for k, v := range d { + if ctx.WritingPages && (k == "Dest" || k == "D") { + ctx.Dest = true + } + if _, _, err := writeDeepObject(ctx, v); err != nil { + return err + } + ctx.Dest = false + } + + return nil +} + +func writeDeepStreamDict(ctx *model.Context, sd *types.StreamDict, objNr, genNr int) error { + if ctx.EncKey != nil { + if _, err := encryptDeepObject(*sd, objNr, genNr, ctx.EncKey, ctx.AES4Strings, ctx.E.R); err != nil { + return err + } + } + + if err := writeStreamDictObject(ctx, objNr, genNr, *sd); err != nil { + return err + } + + for _, v := range sd.Dict { + if _, _, err := writeDeepObject(ctx, v); err != nil { + return err + } + } + + return nil +} + +func writeDeepArray(ctx *model.Context, a types.Array, objNr, genNr int) error { + if err := writeArrayObject(ctx, objNr, genNr, a); err != nil { + return err + } + + for i, v := range a { + if ctx.Dest && i == 0 { + continue + } + if _, _, err := writeDeepObject(ctx, v); err != nil { + return err + } + } + + return nil +} + +func writeLazyObjectStreamObject(ctx *model.Context, objNr, genNr int, o types.LazyObjectStreamObject) error { + data, err := o.GetData() + if err != nil { + return err + } + + return writeObject(ctx, objNr, genNr, string(data)) +} + +func writeObjectGeneric(ctx *model.Context, o types.Object, objNr, genNr int) (err error) { + switch o := o.(type) { + + case types.Dict: + err = writeDeepDict(ctx, o, objNr, genNr) + + case types.StreamDict: + err = writeDeepStreamDict(ctx, &o, objNr, genNr) + + case types.Array: + err = writeDeepArray(ctx, o, objNr, genNr) + + case types.Integer: + err = writeIntegerObject(ctx, objNr, genNr, o) + + case types.Float: + err = writeFloatObject(ctx, objNr, genNr, o) + + case types.StringLiteral: + err = writeStringLiteralObject(ctx, objNr, genNr, o) + + case types.HexLiteral: + err = writeHexLiteralObject(ctx, objNr, genNr, o) + + case types.Boolean: + err = writeBooleanObject(ctx, objNr, genNr, o) + + case types.Name: + err = writeNameObject(ctx, objNr, genNr, o) + + case types.LazyObjectStreamObject: + err = writeLazyObjectStreamObject(ctx, objNr, genNr, o) + + default: + err = errors.Errorf("writeIndirectObject: undefined PDF object #%d %T\n", objNr, o) + } + + return err +} + +func writeIndirectObject(ctx *model.Context, ir types.IndirectRef) error { + objNr := int(ir.ObjectNumber) + genNr := int(ir.GenerationNumber) + + if ctx.Write.HasWriteOffset(objNr) { + if log.WriteEnabled() { + log.Write.Printf("writeIndirectObject end: object #%d already written.\n", objNr) + } + return nil + } + + o, err := ctx.DereferenceForWrite(ir) + if err != nil { + return errors.Wrapf(err, "writeIndirectObject: unable to dereference indirect object #%d", objNr) + } + + if log.WriteEnabled() { + log.Write.Printf("writeIndirectObject: object #%d gets writeoffset: %d\n", objNr, ctx.Write.Offset) + } + + if o == nil { + + if err = writeNullObject(ctx, objNr, genNr); err != nil { + return err + } + + if log.WriteEnabled() { + log.Write.Printf("writeIndirectObject: end, obj#%d resolved to nil, offset=%d\n", objNr, ctx.Write.Offset) + } + + return nil + } + + if err := writeObjectGeneric(ctx, o, objNr, genNr); err != nil { + return err + } + + return err +} + +func writeDeepObject(ctx *model.Context, objIn types.Object) (objOut types.Object, written bool, err error) { + if log.WriteEnabled() { + log.Write.Printf("writeDeepObject: begin offset=%d\n%s\n", ctx.Write.Offset, objIn) + } + + ir, ok := objIn.(types.IndirectRef) + if !ok { + return objIn, written, writeDirectObject(ctx, objIn) + } + + if err = writeIndirectObject(ctx, ir); err == nil { + written = true + if log.WriteEnabled() { + log.Write.Printf("writeDeepObject: end offset=%d\n", ctx.Write.Offset) + } + } + + return objOut, written, err +} + +func writeEntry(ctx *model.Context, d types.Dict, dictName, entryName string) (types.Object, error) { + o, found := d.Find(entryName) + if !found || o == nil { + if log.WriteEnabled() { + log.Write.Printf("writeEntry end: entry %s is nil\n", entryName) + } + return nil, nil + } + + if log.WriteEnabled() { + log.Write.Printf("writeEntry begin: dict=%s entry=%s offset=%d\n", dictName, entryName, ctx.Write.Offset) + } + + o, _, err := writeDeepObject(ctx, o) + if err != nil { + return nil, err + } + + if o == nil { + if log.WriteEnabled() { + log.Write.Printf("writeEntry end: dict=%s entry=%s resolved to nil, offset=%d\n", dictName, entryName, ctx.Write.Offset) + } + return nil, nil + } + + if log.WriteEnabled() { + log.Write.Printf("writeEntry end: dict=%s entry=%s offset=%d\n", dictName, entryName, ctx.Write.Offset) + } + + return o, nil +} + +func writeFlatObject(ctx *model.Context, objNr int) error { + e, ok := ctx.FindTableEntryLight(objNr) + if !ok { + return errors.Errorf("writeFlatObject: undefined PDF object #%d ", objNr) + } + + if e.Free { + ctx.Write.Table[objNr] = 0 + return nil + } + + o := e.Object + genNr := *e.Generation + var err error + + switch o := o.(type) { + + case types.Dict: + err = writeDictObject(ctx, objNr, genNr, o) + + case types.StreamDict: + err = writeDeepStreamDict(ctx, &o, objNr, genNr) + + case types.Array: + err = writeArrayObject(ctx, objNr, genNr, o) + + case types.Integer: + err = writeIntegerObject(ctx, objNr, genNr, o) + + case types.Float: + err = writeFloatObject(ctx, objNr, genNr, o) + + case types.StringLiteral: + err = writeStringLiteralObject(ctx, objNr, genNr, o) + + case types.HexLiteral: + err = writeHexLiteralObject(ctx, objNr, genNr, o) + + case types.Boolean: + err = writeBooleanObject(ctx, objNr, genNr, o) + + case types.Name: + err = writeNameObject(ctx, objNr, genNr, o) + + default: + err = errors.Errorf("writeFlatObject: unexpected PDF object #%d %T\n", objNr, o) + + } + + return err +} diff --git a/pkg/pdfcpu/writePages.go b/pkg/pdfcpu/writePages.go new file mode 100644 index 0000000000000000000000000000000000000000..3f74781e5a1872d80c357245295a4175855b82bc --- /dev/null +++ b/pkg/pdfcpu/writePages.go @@ -0,0 +1,297 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 pdfcpu + +import ( + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// Write page entry to disk. +func writePageEntry(ctx *model.Context, d types.Dict, dictName, entryName string, statsAttr int) error { + o, err := writeEntry(ctx, d, dictName, entryName) + if err != nil { + return err + } + + if o != nil { + ctx.Stats.AddPageAttr(statsAttr) + } + + return nil +} + +func writePageDict(ctx *model.Context, indRef *types.IndirectRef, pageDict types.Dict, pageNr int) error { + objNr := indRef.ObjectNumber.Value() + genNr := indRef.GenerationNumber.Value() + + if ctx.Write.HasWriteOffset(objNr) { + if log.WriteEnabled() { + log.Write.Printf("writePageDict: object #%d already written.\n", objNr) + } + return nil + } + + if log.WriteEnabled() { + log.Write.Printf("writePageDict: logical pageNr=%d object #%d gets writeoffset: %d\n", pageNr, objNr, ctx.Write.Offset) + } + + dictName := "pageDict" + + if err := writeDictObject(ctx, objNr, genNr, pageDict); err != nil { + return err + } + + if log.WriteEnabled() { + log.Write.Printf("writePageDict: new offset = %d\n", ctx.Write.Offset) + } + + if indRef := pageDict.IndirectRefEntry("Parent"); indRef == nil { + return errors.New("pdfcpu: writePageDict: missing parent") + } + + ctx.WritingPages = true + + for _, e := range []struct { + entryName string + statsAttr int + }{ + {"Contents", model.PageContents}, + {"Resources", model.PageResources}, + {"MediaBox", model.PageMediaBox}, + {"CropBox", model.PageCropBox}, + {"BleedBox", model.PageBleedBox}, + {"TrimBox", model.PageTrimBox}, + {"ArtBox", model.PageArtBox}, + {"BoxColorInfo", model.PageBoxColorInfo}, + {"PieceInfo", model.PagePieceInfo}, + {"LastModified", model.PageLastModified}, + {"Rotate", model.PageRotate}, + {"Group", model.PageGroup}, + {"Annots", model.PageAnnots}, + {"Thumb", model.PageThumb}, + {"B", model.PageB}, + {"Dur", model.PageDur}, + {"Trans", model.PageTrans}, + {"AA", model.PageAA}, + {"Metadata", model.PageMetadata}, + {"StructParents", model.PageStructParents}, + {"ID", model.PageID}, + {"PZ", model.PagePZ}, + {"SeparationInfo", model.PageSeparationInfo}, + {"Tabs", model.PageTabs}, + {"TemplateInstantiated", model.PageTemplateInstantiated}, + {"PresSteps", model.PagePresSteps}, + {"UserUnit", model.PageUserUnit}, + {"VP", model.PageVP}, + } { + if err := writePageEntry(ctx, pageDict, dictName, e.entryName, e.statsAttr); err != nil { + return err + } + } + + ctx.WritingPages = false + + if log.WriteEnabled() { + log.Write.Printf("*** writePageDict end: obj#%d offset=%d ***\n", objNr, ctx.Write.Offset) + } + + return nil +} + +func pageNodeDict(ctx *model.Context, o types.Object) (types.Dict, *types.IndirectRef, error) { + if o == nil { + if log.WriteEnabled() { + log.Write.Println("pageNodeDict: is nil") + } + return nil, nil, nil + } + + // Dereference next page node dict. + indRef, ok := o.(types.IndirectRef) + if !ok { + return nil, nil, errors.New("pdfcpu: pageNodeDict: missing indirect reference") + } + if log.WriteEnabled() { + log.Write.Printf("pageNodeDict: PageNode: %s\n", indRef) + } + + d, err := ctx.DereferenceDict(indRef) + if err != nil { + return nil, nil, errors.New("pdfcpu: pageNodeDict: cannot dereference, pageNodeDict") + } + if d == nil { + return nil, nil, errors.New("pdfcpu: pageNodeDict: pageNodeDict is null") + } + + dictType := d.Type() + if dictType == nil { + return nil, nil, errors.New("pdfcpu: pageNodeDict: missing pageNodeDict type") + } + + return d, &indRef, nil +} + +func writeKids(ctx *model.Context, a types.Array, pageNr *int) (types.Array, int, error) { + kids := types.Array{} + count := 0 + + for _, o := range a { + + d, ir, err := pageNodeDict(ctx, o) + if err != nil { + return nil, 0, err + } + if d == nil { + continue + } + + switch *d.Type() { + + case "Pages": + // Recurse over pagetree + skip, c, err := writePagesDict(ctx, ir, pageNr) + if err != nil { + return nil, 0, err + } + if !skip { + kids = append(kids, o) + count += c + } + + case "Page": + *pageNr++ + if len(ctx.Write.SelectedPages) > 0 { + // if log.WriteEnabled() { + // log.Write.Printf("selectedPages: %v\n", ctx.Write.SelectedPages) + // } + writePage := ctx.Write.SelectedPages[*pageNr] + if ctx.Cmd == model.REMOVEPAGES { + writePage = !writePage + } + if writePage { + if log.WriteEnabled() { + log.Write.Printf("writeKids: writing page:%d\n", *pageNr) + } + err = writePageDict(ctx, ir, d, *pageNr) + kids = append(kids, o) + count++ + } else { + if log.WriteEnabled() { + log.Write.Printf("writeKids: skipping page:%d\n", *pageNr) + } + } + } else { + if log.WriteEnabled() { + log.Write.Printf("writeKids: writing page anyway:%d\n", *pageNr) + } + err = writePageDict(ctx, ir, d, *pageNr) + kids = append(kids, o) + count++ + } + + default: + err = errors.Errorf("pdfcpu: writeKids: Unexpected dict type: %s", *d.Type()) + + } + + if err != nil { + return nil, 0, err + } + + } + + return kids, count, nil +} + +func writePageEntries(ctx *model.Context, d types.Dict, dictName string) error { + // TODO Check inheritance rules. + for _, e := range []struct { + entryName string + statsAttr int + }{ + {"Resources", model.PageResources}, + {"MediaBox", model.PageMediaBox}, + {"CropBox", model.PageCropBox}, + {"Rotate", model.PageRotate}, + } { + if err := writePageEntry(ctx, d, dictName, e.entryName, e.statsAttr); err != nil { + return err + } + } + + return nil +} + +func writePagesDict(ctx *model.Context, indRef *types.IndirectRef, pageNr *int) (skip bool, writtenPages int, err error) { + if log.WriteEnabled() { + log.Write.Printf("writePagesDict: begin pageNr=%d\n", *pageNr) + } + + dictName := "pagesDict" + objNr := int(indRef.ObjectNumber) + genNr := int(indRef.GenerationNumber) + + d, err := ctx.DereferenceDict(*indRef) + if err != nil { + return false, 0, errors.Wrapf(err, "writePagesDict: unable to dereference indirect object #%d", objNr) + } + + // Push count, kids. + countOrig, _ := d.Find("Count") + c := countOrig.(types.Integer).Value() + + if c == 0 { + // Ignore empty page tree. + return true, 0, nil + } + + kidsOrig := d.ArrayEntry("Kids") + + // Iterate over page tree. + kidsArray := d.ArrayEntry("Kids") + kidsNew, countNew, err := writeKids(ctx, kidsArray, pageNr) + if err != nil { + return false, 0, err + } + + d.Update("Kids", kidsNew) + d.Update("Count", types.Integer(countNew)) + if log.WriteEnabled() { + log.Write.Printf("writePagesDict: writing pageDict for obj=%d page=%d\n%s", objNr, *pageNr, d) + } + + if err = writeDictObject(ctx, objNr, genNr, d); err != nil { + return false, 0, err + } + + if err := writePageEntries(ctx, d, dictName); err != nil { + return false, 0, err + } + + // Pop kids, count. + d.Update("Kids", kidsOrig) + d.Update("Count", countOrig) + + if log.WriteEnabled() { + log.Write.Printf("writePagesDict: end pageNr=%d\n", *pageNr) + } + + return false, countNew, nil +} diff --git a/pkg/pdfcpu/writeStats.go b/pkg/pdfcpu/writeStats.go new file mode 100644 index 0000000000000000000000000000000000000000..89f93cc4db16dc7d09ac20ecaa7344ccb4b24230 --- /dev/null +++ b/pkg/pdfcpu/writeStats.go @@ -0,0 +1,264 @@ +/* +Copyright 2018 The pdfcpu Authors. + +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 + + 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 pdfcpu + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +func logWriteStats(ctx *model.Context) { + + xRefTable := ctx.XRefTable + + if len(xRefTable.Table) != *xRefTable.Size { + if count, mstr := xRefTable.MissingObjects(); count > 0 { + log.Stats.Printf("%d missing objects: %s\n", count, *mstr) + } + } + + var nonRefObjs []int + + for i := 0; i < *xRefTable.Size; i++ { + + entry, found := xRefTable.Find(i) + if !found || entry.Free || ctx.Write.HasWriteOffset(i) { + continue + } + + nonRefObjs = append(nonRefObjs, i) + + } + + // Non referenced objects + ctx.Optimize.NonReferencedObjs = nonRefObjs + l, str := ctx.Optimize.NonReferencedObjsString() + log.Stats.Printf("%d original empty xref entries:\n%s", l, str) + + // Duplicate font objects + l, str = ctx.Optimize.DuplicateFontObjectsString() + log.Stats.Printf("%d original redundant font entries: %s", l, str) + + // Duplicate image objects + l, str = ctx.Optimize.DuplicateImageObjectsString() + log.Stats.Printf("%d original redundant image entries: %s", l, str) + + // Duplicate info objects + l, str = ctx.Optimize.DuplicateInfoObjectsString() + log.Stats.Printf("%d original redundant info entries: %s", l, str) + + // ObjectStreams + l, str = ctx.Read.ObjectStreamsString() + log.Stats.Printf("%d original objectStream entries: %s", l, str) + + // Linearization objects + l, str = ctx.LinearizationObjsString() + log.Stats.Printf("%d original linearization entries: %s", l, str) +} + +func statsHeadLine() *string { + + hl := "name;version;author;creator;producer;src_size (bin|text);src_bin:imgs|fonts|other;dest_size (bin|text);dest_bin:imgs|fonts|other;" + hl += "linearized;hybrid;xrefstr;objstr;pages;objs;missing;garbage;" + hl += "R_Version;R_Extensions;R_PageLabels;R_Names;R_Dests;R_ViewerPrefs;R_PageLayout;R_PageMode;" + hl += "R_Outlines;R_Threads;R_OpenAction;R_AA;R_URI;R_AcroForm;R_Metadata;R_StructTreeRoot;R_MarkInfo;" + hl += "R_Lang;R_SpiderInfo;R_OutputIntents;R_PieceInfo;R_OCProperties;R_Perms;R_Legal;R_Requirements;" + hl += "R_Collection;R_NeedsRendering;" + hl += "P_LastModified;P_Resources;P_MediaBox;P_CropBox;P_BleedBox;P_TrimBox;P_ArtBox;" + hl += "P_BoxColorInfo;P_Contents;P_Rotate;P_Group;P_Thumb;P_B;P_Dur;P_Trans;P_Annots;" + hl += "P_AA;P_Metadata;P_PieceInfo;P_StructParents;P_ID;P_PZ;P_SeparationInfo;P_Tabs;" + hl += "P_TemplateInstantiated;P_PresSteps;P_UserUnit;P_VP;\n" + + return &hl +} + +func statsLine(ctx *model.Context) *string { + + xRefTable := ctx.XRefTable + + version := xRefTable.HeaderVersion.String() + if xRefTable.RootVersion != nil { + version = fmt.Sprintf("%s,%s", version, xRefTable.RootVersion.String()) + } + + sourceFileSize := ctx.Read.FileSize + sourceBinarySize := ctx.Read.BinaryTotalSize + sourceNonBinarySize := sourceFileSize - sourceBinarySize + + sourceSizeStats := fmt.Sprintf("%s (%4.1f%% | %4.1f%%)", + types.ByteSize(sourceFileSize), + float32(sourceBinarySize)/float32(sourceFileSize)*100, + float32(sourceNonBinarySize)/float32(sourceFileSize)*100) + + sourceBinaryImageSize := ctx.Read.BinaryImageSize + ctx.Read.BinaryImageDuplSize + sourceBinaryFontSize := ctx.Read.BinaryFontSize + ctx.Read.BinaryFontDuplSize + sourceBinaryOtherSize := sourceBinarySize - sourceBinaryImageSize - sourceBinaryFontSize + + sourceBinaryStats := fmt.Sprintf("%4.1f%% | %4.1f%% | %4.1f%%", + float32(sourceBinaryImageSize)/float32(sourceBinarySize)*100, + float32(sourceBinaryFontSize)/float32(sourceBinarySize)*100, + float32(sourceBinaryOtherSize)/float32(sourceBinarySize)*100) + + destFileSize := ctx.Write.FileSize + destBinarySize := ctx.Write.BinaryTotalSize + destNonBinarySize := destFileSize - destBinarySize + + destSizeStats := fmt.Sprintf("%s (%4.1f%% | %4.1f%%)", + types.ByteSize(destFileSize), + float32(destBinarySize)/float32(destFileSize)*100, + float32(destNonBinarySize)/float32(destFileSize)*100) + + destBinaryImageSize := ctx.Write.BinaryImageSize + destBinaryFontSize := ctx.Write.BinaryFontSize + destBinaryOtherSize := destBinarySize - destBinaryImageSize - destBinaryFontSize + + destBinaryStats := fmt.Sprintf("%4.1f%% | %4.1f%% | %4.1f%%", + float32(destBinaryImageSize)/float32(destBinarySize)*100, + float32(destBinaryFontSize)/float32(destBinarySize)*100, + float32(destBinaryOtherSize)/float32(destBinarySize)*100) + + var missingObjs string + if count, mstr := xRefTable.MissingObjects(); count > 0 { + missingObjs = fmt.Sprintf("%d:%s", count, *mstr) + } + + var nonreferencedObjs string + if len(ctx.Optimize.NonReferencedObjs) > 0 { + var s []string + for _, o := range ctx.Optimize.NonReferencedObjs { + s = append(s, fmt.Sprintf("%d", o)) + } + nonreferencedObjs = fmt.Sprintf("%d:%s", len(ctx.Optimize.NonReferencedObjs), strings.Join(s, ",")) + } + + line := fmt.Sprintf("%s;%s;%s;%s;%s;%s;%s;%s;%s;%v;%v;%v;%v;%d;%d;%s;%s;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v\n", + filepath.Base(ctx.Read.FileName), + version, + xRefTable.Author, + xRefTable.Creator, + xRefTable.Producer, + sourceSizeStats, + sourceBinaryStats, + destSizeStats, + destBinaryStats, + ctx.Read.Linearized, + ctx.Read.Hybrid, + ctx.Read.UsingXRefStreams, + ctx.Read.UsingObjectStreams, + xRefTable.PageCount, + *xRefTable.Size, + missingObjs, + nonreferencedObjs, + xRefTable.Stats.UsesRootAttr(model.RootVersion), + xRefTable.Stats.UsesRootAttr(model.RootExtensions), + xRefTable.Stats.UsesRootAttr(model.RootPageLabels), + xRefTable.Stats.UsesRootAttr(model.RootNames), + xRefTable.Stats.UsesRootAttr(model.RootDests), + xRefTable.Stats.UsesRootAttr(model.RootViewerPrefs), + xRefTable.Stats.UsesRootAttr(model.RootPageLayout), + xRefTable.Stats.UsesRootAttr(model.RootPageMode), + xRefTable.Stats.UsesRootAttr(model.RootOutlines), + xRefTable.Stats.UsesRootAttr(model.RootThreads), + xRefTable.Stats.UsesRootAttr(model.RootOpenAction), + xRefTable.Stats.UsesRootAttr(model.RootAA), + xRefTable.Stats.UsesRootAttr(model.RootURI), + xRefTable.Stats.UsesRootAttr(model.RootAcroForm), + xRefTable.Stats.UsesRootAttr(model.RootMetadata), + xRefTable.Stats.UsesRootAttr(model.RootStructTreeRoot), + xRefTable.Stats.UsesRootAttr(model.RootMarkInfo), + xRefTable.Stats.UsesRootAttr(model.RootLang), + xRefTable.Stats.UsesRootAttr(model.RootSpiderInfo), + xRefTable.Stats.UsesRootAttr(model.RootOutputIntents), + xRefTable.Stats.UsesRootAttr(model.RootPieceInfo), + xRefTable.Stats.UsesRootAttr(model.RootOCProperties), + xRefTable.Stats.UsesRootAttr(model.RootPerms), + xRefTable.Stats.UsesRootAttr(model.RootLegal), + xRefTable.Stats.UsesRootAttr(model.RootRequirements), + xRefTable.Stats.UsesRootAttr(model.RootCollection), + xRefTable.Stats.UsesRootAttr(model.RootNeedsRendering), + xRefTable.Stats.UsesPageAttr(model.PageLastModified), + xRefTable.Stats.UsesPageAttr(model.PageResources), + xRefTable.Stats.UsesPageAttr(model.PageMediaBox), + xRefTable.Stats.UsesPageAttr(model.PageCropBox), + xRefTable.Stats.UsesPageAttr(model.PageBleedBox), + xRefTable.Stats.UsesPageAttr(model.PageTrimBox), + xRefTable.Stats.UsesPageAttr(model.PageArtBox), + xRefTable.Stats.UsesPageAttr(model.PageBoxColorInfo), + xRefTable.Stats.UsesPageAttr(model.PageContents), + xRefTable.Stats.UsesPageAttr(model.PageRotate), + xRefTable.Stats.UsesPageAttr(model.PageGroup), + xRefTable.Stats.UsesPageAttr(model.PageThumb), + xRefTable.Stats.UsesPageAttr(model.PageB), + xRefTable.Stats.UsesPageAttr(model.PageDur), + xRefTable.Stats.UsesPageAttr(model.PageTrans), + xRefTable.Stats.UsesPageAttr(model.PageAnnots), + xRefTable.Stats.UsesPageAttr(model.PageAA), + xRefTable.Stats.UsesPageAttr(model.PageMetadata), + xRefTable.Stats.UsesPageAttr(model.PagePieceInfo), + xRefTable.Stats.UsesPageAttr(model.PageStructParents), + xRefTable.Stats.UsesPageAttr(model.PageID), + xRefTable.Stats.UsesPageAttr(model.PagePZ), + xRefTable.Stats.UsesPageAttr(model.PageSeparationInfo), + xRefTable.Stats.UsesPageAttr(model.PageTabs), + xRefTable.Stats.UsesPageAttr(model.PageTemplateInstantiated), + xRefTable.Stats.UsesPageAttr(model.PagePresSteps), + xRefTable.Stats.UsesPageAttr(model.PageUserUnit), + xRefTable.Stats.UsesPageAttr(model.PageVP)) + + return &line +} + +// AppendStatsFile appends a stats line for this xRefTable to the configured csv file name. +func AppendStatsFile(ctx *model.Context) error { + + fileName := ctx.StatsFileName + + // if file does not exist, create file + file, err := os.OpenFile(fileName, os.O_APPEND|os.O_WRONLY, 0600) + if err != nil { + + if os.IsExist(err) { + return errors.Errorf("can't open %s\n%s", fileName, err) + } + + file, err = os.OpenFile(fileName, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) + if err != nil { + return errors.Errorf("can't create %s\n%s", fileName, err) + } + + _, err = file.WriteString(*statsHeadLine()) + if err != nil { + return err + } + + } + + defer func() { + file.Close() + }() + + _, err = file.WriteString(*statsLine(ctx)) + + return err +} diff --git a/pkg/pdfcpu/zoom.go b/pkg/pdfcpu/zoom.go new file mode 100644 index 0000000000000000000000000000000000000000..379d2d750e2ac695c9675c6724ca687548196f92 --- /dev/null +++ b/pkg/pdfcpu/zoom.go @@ -0,0 +1,186 @@ +/* +Copyright 2024 The pdfcpu Authors. + +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 + + 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 pdfcpu + +import ( + "bytes" + "fmt" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/matrix" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// ParseZoomConfig parses a Zoom command string into an internal structure. +func ParseZoomConfig(s string, u types.DisplayUnit) (*model.Zoom, error) { + + if s == "" { + return nil, errors.New("pdfcpu: missing zoom configuration string") + } + + zoom := &model.Zoom{Unit: u} + + ss := strings.Split(s, ",") + + for _, s := range ss { + + ss1 := strings.Split(s, ":") + if len(ss1) != 2 { + return nil, errors.New("pdfcpu: Invalid zoom configuration string. Please consult pdfcpu help zoom") + } + + paramPrefix := strings.TrimSpace(ss1[0]) + paramValueStr := strings.TrimSpace(ss1[1]) + + if err := model.ZoomParamMap.Handle(paramPrefix, paramValueStr, zoom); err != nil { + return nil, err + } + } + + if zoom.Factor != 0 && (zoom.HMargin != 0 || zoom.VMargin != 0) { + return nil, errors.New("pdfcpu: please supply either zoom \"factor\" or \"hmargin\" or \"vmargin\"") + } + + return zoom, nil +} + +func handleZoomOutBgColAndBorder(cropBox *types.Rectangle, bb *[]byte, zoom *model.Zoom) { + if zoom.Factor < 1 && (zoom.BgColor != nil || zoom.Border) { + + var buf bytes.Buffer + + if zoom.BgColor != nil { + draw.FillRectNoBorder(&buf, types.RectForWidthAndHeight(cropBox.LL.X, cropBox.LL.Y, cropBox.Width(), zoom.VMargin), *zoom.BgColor) + draw.FillRectNoBorder(&buf, types.RectForWidthAndHeight(cropBox.LL.X, cropBox.Height()-zoom.VMargin, cropBox.Width(), zoom.VMargin), *zoom.BgColor) + draw.FillRectNoBorder(&buf, types.RectForWidthAndHeight(cropBox.LL.X, zoom.VMargin, zoom.HMargin, cropBox.Height()-2*zoom.VMargin), *zoom.BgColor) + draw.FillRectNoBorder(&buf, types.RectForWidthAndHeight(cropBox.Width()-zoom.HMargin, zoom.VMargin, zoom.HMargin, cropBox.Height()-2*zoom.VMargin), *zoom.BgColor) + } + + if zoom.Border { + r := types.RectForWidthAndHeight( + cropBox.LL.X+zoom.HMargin, + cropBox.LL.Y+zoom.VMargin, + cropBox.Width()-2*zoom.HMargin, + cropBox.Height()-2*zoom.VMargin) + draw.DrawRect(&buf, r, 1, &color.Black, nil) + } + + *bb = append(*bb, buf.Bytes()...) + } +} + +func zoomPage(ctx *model.Context, pageNr int, zoom *model.Zoom) error { + d, _, inhPAttrs, err := ctx.PageDict(pageNr, false) + if err != nil { + return err + } + + cropBox := inhPAttrs.MediaBox + if inhPAttrs.CropBox != nil { + cropBox = inhPAttrs.CropBox + } + + // Account for existing rotation. + if inhPAttrs.Rotate != 0 { + if types.IntMemberOf(inhPAttrs.Rotate, []int{+90, -90, +270, -270}) { + w := cropBox.Width() + cropBox.UR.X = cropBox.LL.X + cropBox.Height() + cropBox.UR.Y = cropBox.LL.Y + w + } + } + + if err := zoom.EnsureFactorAndMargins(cropBox.Width(), cropBox.Height()); err != nil { + return err + } + + sc := zoom.Factor + sin, cos := 0., 1. + dx := zoom.HMargin + dy := zoom.VMargin + + m := matrix.CalcTransformMatrix(sc, sc, sin, cos, dx, dy) + + var trans bytes.Buffer + fmt.Fprintf(&trans, "q %.5f %.5f %.5f %.5f %.5f %.5f cm ", m[0][0], m[0][1], m[1][0], m[1][1], m[2][0], m[2][1]) + + bb, err := ctx.PageContent(d) + if err == model.ErrNoContent { + return nil + } + if err != nil { + return err + } + + if inhPAttrs.Rotate != 0 { + bbInvRot := append([]byte(" q "), model.ContentBytesForPageRotation(inhPAttrs.Rotate, cropBox.Width(), cropBox.Height())...) + bb = append(bbInvRot, bb...) + bb = append(bb, []byte(" Q")...) + } + + bb = append(trans.Bytes(), bb...) + bb = append(bb, []byte(" Q ")...) + + handleZoomOutBgColAndBorder(cropBox, &bb, zoom) + + sd, _ := ctx.NewStreamDictForBuf(bb) + if err := sd.Encode(); err != nil { + return err + } + + ir, err := ctx.IndRefForNewObject(*sd) + if err != nil { + return err + } + + d["Contents"] = *ir + + d.Update("MediaBox", cropBox.Array()) + d.Delete("Rotate") + d.Delete("CropBox") + + return nil +} + +func Zoom(ctx *model.Context, selectedPages types.IntSet, zoom *model.Zoom) error { + if log.DebugEnabled() { + log.Debug.Printf("Zoom:\n%s\n", zoom) + } + + if len(selectedPages) == 0 { + selectedPages = types.IntSet{} + for i := 1; i <= ctx.PageCount; i++ { + selectedPages[i] = true + } + } + + for k, v := range selectedPages { + if v { + if err := zoomPage(ctx, k, zoom); err != nil { + return err + } + } + } + + ctx.EnsureVersionForWriting() + + return nil +} diff --git a/resources/4exp.png b/resources/4exp.png new file mode 100644 index 0000000000000000000000000000000000000000..d75959ad48ffbbc9fa414e9bf447980f4f9afcc4 Binary files /dev/null and b/resources/4exp.png differ diff --git a/resources/Go-Logo_Aqua.png b/resources/Go-Logo_Aqua.png new file mode 100644 index 0000000000000000000000000000000000000000..4f210b0d8fa38f00777e8435d56b5792a3cbcdf6 Binary files /dev/null and b/resources/Go-Logo_Aqua.png differ diff --git a/resources/book2A4p1.png b/resources/book2A4p1.png new file mode 100644 index 0000000000000000000000000000000000000000..5c2284b0ba03483b178e50888305db75fdb15bc6 Binary files /dev/null and b/resources/book2A4p1.png differ diff --git a/resources/book4A4p1.png b/resources/book4A4p1.png new file mode 100644 index 0000000000000000000000000000000000000000..f3c01efefd21dfb7a0ec78cef72dfa7b8f45432a Binary files /dev/null and b/resources/book4A4p1.png differ diff --git a/resources/cjkv.png b/resources/cjkv.png new file mode 100644 index 0000000000000000000000000000000000000000..d52485f3fcf101aaf8965ca91e482fc7a0f08a6c Binary files /dev/null and b/resources/cjkv.png differ diff --git a/resources/demo.png b/resources/demo.png new file mode 100644 index 0000000000000000000000000000000000000000..89bc581601b49c30450b0598b276f61d29d65124 Binary files /dev/null and b/resources/demo.png differ diff --git a/resources/form.png b/resources/form.png new file mode 100644 index 0000000000000000000000000000000000000000..a8192a04603b3e204aee315781b0210909cdb883 Binary files /dev/null and b/resources/form.png differ diff --git a/resources/gridimg.png b/resources/gridimg.png new file mode 100644 index 0000000000000000000000000000000000000000..cbc778e3bfc04c2da15d963f8cfe022cbfb7d41f Binary files /dev/null and b/resources/gridimg.png differ diff --git a/resources/gridpdf.png b/resources/gridpdf.png new file mode 100644 index 0000000000000000000000000000000000000000..8dce709f2467c03563d03ae7c894319f7c5af60e Binary files /dev/null and b/resources/gridpdf.png differ diff --git a/resources/hold3.png b/resources/hold3.png new file mode 100644 index 0000000000000000000000000000000000000000..19ca143cab58ec694ea016739e8ea7e02c1467da Binary files /dev/null and b/resources/hold3.png differ diff --git a/resources/imagebox.png b/resources/imagebox.png new file mode 100644 index 0000000000000000000000000000000000000000..5750b991ce3a7542e5b0f02478fea99757b4821f Binary files /dev/null and b/resources/imagebox.png differ diff --git a/resources/logoSmall.png b/resources/logoSmall.png new file mode 100644 index 0000000000000000000000000000000000000000..34cf3cf59c673ba75d3b782726abe8595b03f34d Binary files /dev/null and b/resources/logoSmall.png differ diff --git a/resources/nup16img.png b/resources/nup16img.png new file mode 100644 index 0000000000000000000000000000000000000000..d87e8fb6d9cfb47c708964ab553c9184412488d7 Binary files /dev/null and b/resources/nup16img.png differ diff --git a/resources/nup4img.png b/resources/nup4img.png new file mode 100644 index 0000000000000000000000000000000000000000..966abfb680452f62a92a7844d2352a773c79b74b Binary files /dev/null and b/resources/nup4img.png differ diff --git a/resources/nup4pdf.png b/resources/nup4pdf.png new file mode 100644 index 0000000000000000000000000000000000000000..584e18737d90f3cdffc4b6f9a5a9f306c58e2f21 Binary files /dev/null and b/resources/nup4pdf.png differ diff --git a/resources/nup9pdf.png b/resources/nup9pdf.png new file mode 100644 index 0000000000000000000000000000000000000000..9f3bd536c5fa3df757dc1363a2d94484db3862f4 Binary files /dev/null and b/resources/nup9pdf.png differ diff --git a/resources/pdfa.png b/resources/pdfa.png new file mode 100644 index 0000000000000000000000000000000000000000..afb3d69bf2149ba99456deb60474c508a3f94597 Binary files /dev/null and b/resources/pdfa.png differ diff --git a/resources/pdfchip3.png b/resources/pdfchip3.png new file mode 100644 index 0000000000000000000000000000000000000000..1b7d5d7bce67a7b0016116d468b39a6d29fda68b Binary files /dev/null and b/resources/pdfchip3.png differ diff --git a/resources/stRoundBorder.png b/resources/stRoundBorder.png new file mode 100644 index 0000000000000000000000000000000000000000..282c289eb5bebf44dfcfbe2a0bcf0d910b099bdd Binary files /dev/null and b/resources/stRoundBorder.png differ diff --git a/resources/sti.png b/resources/sti.png new file mode 100644 index 0000000000000000000000000000000000000000..b76bd98273d95aecf6792ebacf5c2f30c379cbdd Binary files /dev/null and b/resources/sti.png differ diff --git a/resources/stp.png b/resources/stp.png new file mode 100644 index 0000000000000000000000000000000000000000..3dae96868ca6b520eeadd54d70c840c5e2dd9f91 Binary files /dev/null and b/resources/stp.png differ diff --git a/resources/stt31.png b/resources/stt31.png new file mode 100644 index 0000000000000000000000000000000000000000..1824c93a5ebd6384c6924f0048b84f9f43a8f749 Binary files /dev/null and b/resources/stt31.png differ diff --git a/resources/table.png b/resources/table.png new file mode 100644 index 0000000000000000000000000000000000000000..a475daed7bdc3f364966771c65a40f834f9b49ab Binary files /dev/null and b/resources/table.png differ diff --git a/resources/wmImageSample.png b/resources/wmImageSample.png new file mode 100644 index 0000000000000000000000000000000000000000..dcf5fde27c40e75753fc1a3c48d3ae48ede0f47e Binary files /dev/null and b/resources/wmImageSample.png differ diff --git a/resources/wmPDFSample.jpg b/resources/wmPDFSample.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9f36bd5378de545371ff18101e85c16e0742b0a7 Binary files /dev/null and b/resources/wmPDFSample.jpg differ diff --git a/resources/wmText2Sample.png b/resources/wmText2Sample.png new file mode 100644 index 0000000000000000000000000000000000000000..8f41aabade35b7dfa830e274216d4a0e566f5b1e Binary files /dev/null and b/resources/wmText2Sample.png differ diff --git a/resources/wmTextSample.png b/resources/wmTextSample.png new file mode 100644 index 0000000000000000000000000000000000000000..1b87775767619b63af3f17ce1d34152e64f32f5d Binary files /dev/null and b/resources/wmTextSample.png differ diff --git a/resources/wmi1abs.png b/resources/wmi1abs.png new file mode 100644 index 0000000000000000000000000000000000000000..f6bec357c09b70e99fbf2773ca1db0d9f5dfb83f Binary files /dev/null and b/resources/wmi1abs.png differ diff --git a/resources/wmi4.png b/resources/wmi4.png new file mode 100644 index 0000000000000000000000000000000000000000..d7da9f57a8555743281d8c8989af81c41b3cbfc5 Binary files /dev/null and b/resources/wmi4.png differ